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

Implementation of string.strip_prefix and string.strip_suffix #780

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
90 changes: 90 additions & 0 deletions src/gleam/string.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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("[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)
29 changes: 28 additions & 1 deletion src/gleam_stdlib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
32 changes: 32 additions & 0 deletions src/gleam_stdlib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
76 changes: 76 additions & 0 deletions test/gleam/string_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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("[email protected]", "@gleam.run")
|> should.equal(Ok("lucy"))

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

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("[email protected]", "Lucy")
|> should.equal(Error(Nil))

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