diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc290db..22bb7077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.52.0 - 2024-12-27 + +- The `string` module gains the `strip_prefix` and `strip_suffix` functions. + ## v0.51.0 - 2024-12-22 - `dynamic/decode` now has its own error type. diff --git a/gleam.toml b/gleam.toml index 04ba258b..a05d27e7 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "gleam_stdlib" -version = "0.51.0" +version = "0.52.0" gleam = ">= 0.32.0" licences = ["Apache-2.0"] description = "A standard library for the Gleam programming language" diff --git a/src/gleam/string.gleam b/src/gleam/string.gleam index 8802f09c..4485cb15 100644 --- a/src/gleam/string.gleam +++ b/src/gleam/string.gleam @@ -942,3 +942,93 @@ 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) + +// let string_codepoints = to_utf_codepoints(string) +// let prefix_codepoints = to_utf_codepoints(prefix) + +// let prefix_size = list.length(prefix_codepoints) +// let string_start = list.sized_chunk(string_codepoints, prefix_size) + +// case string_start { +// [] -> Error(Nil) +// [x, ..] -> { +// case x == prefix_codepoints { +// True -> { +// string_codepoints +// |> list.drop(prefix_size) +// |> from_utf_codepoints +// |> Ok +// } +// False -> Error(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 3fda5df9..ab091d38 100644 --- a/src/gleam_stdlib.erl +++ b/src/gleam_stdlib.erl @@ -14,7 +14,8 @@ inspect/1, float_to_string/1, int_from_base_string/2, utf_codepoint_list_to_string/1, contains_string/2, crop_string/2, base16_encode/1, base16_decode/1, string_replace/3, slice/3, - bit_array_to_int_and_size/1, bit_array_pad_to_bytes/1 + bit_array_to_int_and_size/1, bit_array_pad_to_bytes/1, + string_strip_prefix/2, string_strip_suffix/2 ]). %% Taken from OTP's uri_string module @@ -527,3 +528,29 @@ slice(String, Index, Length) -> X when is_binary(X) -> X; X when is_list(X) -> unicode:characters_to_binary(X) end. + +string_strip_prefix(String, <<>>) -> + {ok, String}; +string_strip_prefix(<<>>, _) -> + {error, nil}; +string_strip_prefix(String, Prefix) when byte_size(Prefix) > byte_size(String) -> + {error, nil}; +string_strip_prefix(String, Prefix) -> + PrefixSize = byte_size(Prefix), + case Prefix == binary_part(String, 0, PrefixSize) of + true -> {ok, binary_part(String, PrefixSize, byte_size(String) - PrefixSize)}; + false -> {error, nil} + end. + +string_strip_suffix(String, <<>>) -> + {ok, String}; +string_strip_suffix(<<>>, _) -> + {error, nil}; +string_strip_suffix(String, Suffix) when byte_size(Suffix) > byte_size(String) -> + {error, nil}; +string_strip_suffix(String, Suffix) -> + SuffixSize = byte_size(Suffix), + 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. diff --git a/src/gleam_stdlib.mjs b/src/gleam_stdlib.mjs index e7088b46..d4e9f1e9 100644 --- a/src/gleam_stdlib.mjs +++ b/src/gleam_stdlib.mjs @@ -974,3 +974,35 @@ export function log(x) { export function exp(x) { return Math.exp(x); } + +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) +} diff --git a/test/gleam/string_test.gleam b/test/gleam/string_test.gleam index 4eddb9cc..a2d34c97 100644 --- a/test/gleam/string_test.gleam +++ b/test/gleam/string_test.gleam @@ -1396,3 +1396,79 @@ 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")) + + // Test over a strip prefix of a compound emoji. + 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) + + // The codepoints are tested instead of a string literal, because of the presence of a leading zero space joiner. + 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("")) + + // Test over a strip suffix of a compound emoji. + 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) + + // The codepoints are tested instead of a string literal, because of the presence of a trailing zero space joiner. + 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)) +}