Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

string.strip_prefix and string.strip_suffix implementation proposal #771

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## v0.50.0 - 2024-12-20

- The `string` module gains the `strip_prefix` and `strip_suffix` functions.
- Implementations of `starts_with` and `ends_with` from the `string` module are now
based on the `strip_prefix` and `strip_suffix` functions and no longer on Erlang or Javascript.

## v0.49.0 - 2024-12-19

- The `list` module gains the `max` function.
Expand Down
86 changes: 80 additions & 6 deletions src/gleam/string.gleam
GiregL marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,12 @@ pub fn contains(does haystack: String, contain needle: String) -> Bool
/// // -> False
/// ```
///
@external(erlang, "gleam_stdlib", "string_starts_with")
@external(javascript, "../gleam_stdlib.mjs", "starts_with")
pub fn starts_with(string: String, prefix: String) -> Bool
pub fn starts_with(string: String, prefix: String) -> Bool {
case strip_prefix(string, prefix) {
Ok(_) -> True
Error(_) -> False
}
}

/// Checks whether the first `String` ends with the second one.
///
Expand All @@ -331,9 +334,12 @@ pub fn starts_with(string: String, prefix: String) -> Bool
/// // -> True
/// ```
///
@external(erlang, "gleam_stdlib", "string_ends_with")
@external(javascript, "../gleam_stdlib.mjs", "ends_with")
pub fn ends_with(string: String, suffix: String) -> Bool
pub fn ends_with(string: String, suffix: String) -> Bool {
case strip_suffix(string, suffix) {
Ok(_) -> True
Error(_) -> False
}
}

/// Creates a list of `String`s by splitting a given string on a given substring.
///
Expand Down Expand Up @@ -942,3 +948,71 @@ fn do_inspect(term: anything) -> StringTree
@external(erlang, "erlang", "byte_size")
@external(javascript, "../gleam_stdlib.mjs", "byte_size")
pub fn byte_size(string: String) -> Int

/// Returns a `Result(String, Nil)` of the given string without the given prefix.
/// If the string does not start with the given prefix, the function returns `Error(Nil)`
///
/// If an empty prefix is given, the result is always `Ok` containing the whole string.
/// If an empty string is given with a non empty prefix, then the result is always `Error(Nil)`
///
/// The function does **not** removes zero width joiners (`\u200D`) codepoints when stripping an emoji.
/// A leading one may remain.
///
/// ## Examples
///
/// ```gleam
/// strip_prefix("https://gleam.run", "https://")
/// // -> Ok("gleam.run")
///
/// strip_prefix("https://gleam.run", "")
/// // -> Ok("https://gleam.run")
///
/// strip_prefix("", "")
/// // -> Ok("")
///
/// strip_prefix("https://gleam.run", "Lucy")
/// // -> Error(Nil)
///
/// strip_prefix("", "Lucy")
/// // -> Error(Nil)
/// ```
@external(erlang, "gleam_stdlib", "string_strip_prefix")
@external(javascript, "../gleam_stdlib.mjs", "string_strip_prefix")
pub fn strip_prefix(
string: String,
prefix prefix: String,
) -> Result(String, Nil)

/// Returns a `Result(String, Nil)` of the given string without the given suffix.
/// If the string does not end with the given suffix, the function returns `Error(Nil)`
///
/// If an empty suffix is given, the result is always `Ok` containing the whole string.
/// If an empty string is given with a non empty suffix, then the result is always `Error(Nil)`
///
/// The function does **not** removes zero width joiners (`\u200D`) codepoints when stripping an emoji.
/// A trailing one may remain.
///
/// ## Examples
///
/// ```gleam
/// strip_suffix("[email protected]", "@gleam.run")
/// // -> Ok("lucy")
///
/// strip_suffix("[email protected]", "")
/// // -> Ok("[email protected]")
///
/// strip_suffix("", "")
/// // -> Ok("")
///
/// strip_suffix("[email protected]", "Lucy")
/// // -> Error(Nil)
///
/// strip_suffix("", "Lucy")
/// // -> Error(Nil)
/// ```
@external(erlang, "gleam_stdlib", "string_strip_suffix")
@external(javascript, "../gleam_stdlib.mjs", "string_strip_suffix")
pub fn strip_suffix(
string: String,
suffix suffix: String,
) -> Result(String, Nil)
28 changes: 18 additions & 10 deletions src/gleam_stdlib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
map_get/2, iodata_append/2, identity/1, decode_int/1, decode_bool/1,
decode_float/1, decode_list/1, decode_option/2, decode_field/2, parse_int/1,
parse_float/1, less_than/2, string_pop_grapheme/1, string_pop_codeunit/1,
string_starts_with/2, wrap_list/1, string_ends_with/2, string_pad/4,
string_strip_prefix/2, wrap_list/1, string_strip_suffix/2, string_pad/4,
decode_map/1, uri_parse/1,
decode_result/1, bit_array_slice/3, decode_bit_array/1, compile_regex/2,
regex_scan/2, percent_encode/1, percent_decode/1, regex_check/2,
Expand Down Expand Up @@ -174,17 +174,25 @@ parse_float(String) ->
less_than(Lhs, Rhs) ->
Lhs < Rhs.

string_starts_with(_, <<>>) -> true;
string_starts_with(String, Prefix) when byte_size(Prefix) > byte_size(String) -> false;
string_starts_with(String, Prefix) ->
string_strip_prefix(String, <<>>) when is_binary(String) -> {ok, String};
string_strip_prefix(String, _) when is_binary(String), String == <<>> -> {error, nil};
string_strip_prefix(String, Prefix) when is_binary(String), is_binary(Prefix), byte_size(Prefix) > byte_size(String) -> {error, nil};
string_strip_prefix(String, Prefix) when is_binary(String), is_binary(Prefix) ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove all the unneeded guards please 🙏

PrefixSize = byte_size(Prefix),
Prefix == binary_part(String, 0, PrefixSize).
case Prefix == binary_part(String, 0, PrefixSize) of
true -> {ok, binary_part(String, PrefixSize, byte_size(String) - PrefixSize)};
false -> {error, nil}
end.

string_ends_with(_, <<>>) -> true;
string_ends_with(String, Suffix) when byte_size(Suffix) > byte_size(String) -> false;
string_ends_with(String, Suffix) ->
string_strip_suffix(String, <<>>) when is_binary(String) -> {ok, String};
string_strip_suffix(String, _) when is_binary(String), String == <<>> -> {error, nil};
string_strip_suffix(String, Suffix) when is_binary(String), is_binary(Suffix), byte_size(Suffix) > byte_size(String) -> {error, nil};
string_strip_suffix(String, Suffix) when is_binary(String), is_binary(Suffix) ->
SuffixSize = byte_size(Suffix),
Suffix == binary_part(String, byte_size(String) - SuffixSize, SuffixSize).
case Suffix == binary_part(String, byte_size(String) - SuffixSize, SuffixSize) of
true -> {ok, binary_part(String, 0, byte_size(String) - SuffixSize)};
false -> {error, nil}
end.

string_pad(String, Length, Dir, PadString) ->
Chars = string:pad(String, Length, Dir, binary_to_list(PadString)),
Expand Down Expand Up @@ -569,4 +577,4 @@ slice(String, Index, Length) ->
case string:slice(String, Index, Length) of
X when is_binary(X) -> X;
X when is_list(X) -> unicode:characters_to_binary(X)
end.
end.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files have to have a newline at the end to be valid!

32 changes: 32 additions & 0 deletions src/gleam_stdlib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1010,3 +1010,35 @@ export function bit_array_starts_with(bits, prefix) {

return true;
}

export function string_strip_prefix(str, prefix) {
if (prefix == "") {
return new Ok(str)
}

if (str == "" && prefix.length != 0) {
return new Error(undefined)
}

if (str.startsWith(prefix)) {
return new Ok(str.substring(prefix.length))
}

return new Error(undefined)
}

export function string_strip_suffix(str, suffix) {
if (suffix == "") {
return new Ok(str)
}

if (str == "" && suffix.length != 0) {
return new Error(undefined)
}

if (str.endsWith(suffix)) {
return new Ok(str.substring(0, str.length - suffix.length))
}

return new Error(undefined)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files have to have a newline at the end to be valid!

72 changes: 72 additions & 0 deletions test/gleam/string_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -1392,3 +1392,75 @@ pub fn inspect_map_test() {
|> string.inspect
|> should.equal("dict.from_list([#(\"a\", 1), #(\"b\", 2)])")
}

pub fn strip_prefix_test() {
string.strip_prefix("https://gleam.run", "https://")
|> should.equal(Ok("gleam.run"))

string.strip_prefix("https://gleam.run", "")
|> should.equal(Ok("https://gleam.run"))

let assert Ok(top_right) = string.utf_codepoint(0x1F469)
let assert Ok(bot_left) = string.utf_codepoint(0x1F467)
let assert Ok(bot_right) = string.utf_codepoint(0x1F466)
let assert Ok(separator) = string.utf_codepoint(0x200D)

string.strip_prefix("👩‍👩‍👧‍👦", prefix: "👩")
|> should.equal(
Ok(
string.from_utf_codepoints([
separator,
top_right,
separator,
bot_left,
separator,
bot_right,
]),
),
)

string.strip_prefix("", "")
|> should.equal(Ok(""))

string.strip_prefix("https://gleam.run", "Lucy")
|> should.equal(Error(Nil))

string.strip_prefix("", "Lucy")
|> should.equal(Error(Nil))
}

pub fn strip_suffix_test() {
string.strip_suffix("[email protected]", "@gleam.run")
|> should.equal(Ok("lucy"))

string.strip_suffix("[email protected]", "")
|> should.equal(Ok("[email protected]"))

string.strip_suffix("", "")
|> should.equal(Ok(""))

let assert Ok(top_left) = string.utf_codepoint(0x1F468)
let assert Ok(top_right) = string.utf_codepoint(0x1F469)
let assert Ok(bot_left) = string.utf_codepoint(0x1F467)
let assert Ok(separator) = string.utf_codepoint(0x200D)

string.strip_suffix("👨‍👩‍👧‍👦", suffix: "👦")
|> should.equal(
Ok(
string.from_utf_codepoints([
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come this isn't a string literal?

top_left,
separator,
top_right,
separator,
bot_left,
separator,
]),
),
)

string.strip_suffix("[email protected]", "Lucy")
|> should.equal(Error(Nil))

string.strip_suffix("", "Lucy")
|> should.equal(Error(Nil))
}