diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b1cc96..c24f40cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/gleam/string.gleam b/src/gleam/string.gleam index 8802f09c..9d2ecfb6 100644 --- a/src/gleam/string.gleam +++ b/src/gleam/string.gleam @@ -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. /// @@ -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. /// @@ -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("lucy@gleam.run", "@gleam.run") +/// // -> Ok("lucy") +/// +/// strip_suffix("lucy@gleam.run", "") +/// // -> Ok("lucy@gleam.run") +/// +/// strip_suffix("", "") +/// // -> Ok("") +/// +/// strip_suffix("lucy@gleam.run", "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) diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl index 5c93fd4c..de3bbfdc 100644 --- a/src/gleam_stdlib.erl +++ b/src/gleam_stdlib.erl @@ -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, @@ -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) -> 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)), @@ -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. \ No newline at end of file diff --git a/src/gleam_stdlib.mjs b/src/gleam_stdlib.mjs index 4f804f78..ccf2dfa8 100644 --- a/src/gleam_stdlib.mjs +++ b/src/gleam_stdlib.mjs @@ -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) +} \ No newline at end of file diff --git a/test/gleam/string_test.gleam b/test/gleam/string_test.gleam index ffd2f9b9..4a6e6c7d 100644 --- a/test/gleam/string_test.gleam +++ b/test/gleam/string_test.gleam @@ -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("lucy@gleam.run", "@gleam.run") + |> should.equal(Ok("lucy")) + + string.strip_suffix("lucy@gleam.run", "") + |> should.equal(Ok("lucy@gleam.run")) + + 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([ + top_left, + separator, + top_right, + separator, + bot_left, + separator, + ]), + ), + ) + + string.strip_suffix("lucy@gleam.run", "Lucy") + |> should.equal(Error(Nil)) + + string.strip_suffix("", "Lucy") + |> should.equal(Error(Nil)) +}