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

Add replace_first for string, string_builder, and regex #690

Open
wants to merge 6 commits 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
24 changes: 24 additions & 0 deletions src/gleam/regex.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,27 @@ pub fn replace(
in string: String,
with substitute: String,
) -> String

/// Creates a new `String` by replacing the first substring that matches the regular
/// expression.
///
/// ## Examples
///
/// ```gleam
/// let assert Ok(re) = regex.from_string("^https://")
/// replace_first(one_of: re, in: "https://example.com", with: "www.")
/// // -> "www.example.com"
/// ```
///
/// ```gleam
/// let assert Ok(re) = regex.from_string("[, +-]")
/// replace_first(one_of: re, in: "a,b-c d+e", with: "/")
/// // -> "a/b-c d+e"
/// ```
@external(erlang, "gleam_stdlib", "regex_replace_first")
@external(javascript, "../gleam_stdlib.mjs", "regex_replace_first")
pub fn replace_first(
one_of pattern: Regex,
in string: String,
with substitute: String,
) -> String
25 changes: 25 additions & 0 deletions src/gleam/string.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,31 @@ pub fn replace(
|> string_builder.to_string
}

/// Creates a new `String` by replacing the first occurrence of a given substring.
///
/// ## Examples
///
/// ```gleam
/// replace_first("www.example.com", each: ".", with: "-")
/// // -> "www-example.com"
/// ```
///
/// ```gleam
/// replace_first("a,b,c,d,e", each: ",", with: "/")
/// // -> "a/b,c,d,e"
/// ```
///
pub fn replace_first(
in string: String,
one_of pattern: String,
with substitute: String,
) -> String {
string
|> string_builder.from_string
|> string_builder.replace_first(one_of: pattern, with: substitute)
|> string_builder.to_string
}

/// Creates a new `String` with all the graphemes in the input `String` converted to
/// lowercase.
///
Expand Down
10 changes: 10 additions & 0 deletions src/gleam/string_builder.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ pub fn replace(
with substitute: String,
) -> StringBuilder

/// Replaces the first instance of a pattern with a given string substitute.
///
@external(erlang, "gleam_stdlib", "string_replace_first")
@external(javascript, "../gleam_stdlib.mjs", "string_replace_first")
pub fn replace_first(
in builder: StringBuilder,
one_of pattern: String,
with substitute: String,
) -> StringBuilder

/// Compares two builders to determine if they have the same textual content.
///
/// Comparing two iodata using the `==` operator may return `False` even if they
Expand Down
9 changes: 8 additions & 1 deletion src/gleam_stdlib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
decode_tuple5/1, decode_tuple6/1, tuple_get/2, classify_dynamic/1, print/1,
println/1, print_error/1, println_error/1, inspect/1, float_to_string/1,
int_from_base_string/2, utf_codepoint_list_to_string/1, contains_string/2,
crop_string/2, base16_decode/1, string_replace/3, regex_replace/3, slice/3, bit_array_to_int_and_size/1
crop_string/2, base16_decode/1, string_replace/3, regex_replace/3, slice/3,
bit_array_to_int_and_size/1, string_replace_first/3, regex_replace_first/3
]).

%% Taken from OTP's uri_string module
Expand Down Expand Up @@ -266,6 +267,9 @@ regex_scan(Regex, String) ->
regex_replace(Regex, Subject, Replacement) ->
re:replace(Subject, Regex, Replacement, [global, {return, binary}]).

regex_replace_first(Regex, Subject, Replacement) ->
re:replace(Subject, Regex, Replacement, [{return, binary}]).

base_decode64(S) ->
try {ok, base64:decode(S)}
catch error:_ -> {error, nil}
Expand Down Expand Up @@ -553,6 +557,9 @@ base16_decode(String) ->
string_replace(String, Pattern, Replacement) ->
string:replace(String, Pattern, Replacement, all).

string_replace_first(String, Pattern, Replacement) ->
string:replace(String, Pattern, Replacement).

slice(String, Index, Length) ->
case string:slice(String, Index, Length) of
X when is_binary(X) -> X;
Expand Down
11 changes: 11 additions & 0 deletions src/gleam_stdlib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ export function string_replace(string, target, substitute) {
);
}

export function string_replace_first(string, target, substitute) {
return string.replace(target, substitute);
}

export function string_reverse(string) {
return [...string].reverse().join("");
}
Expand Down Expand Up @@ -451,6 +455,13 @@ export function regex_replace(regex, original_string, replacement) {
return original_string.replaceAll(regex, replacement);
}

export function regex_replace_first(regex, original_string, replacement) {
// Forcibly strip the g flag from the regex, if it's present
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: All JS regexes created in Gleam are global by default so this should always be true. See the compile_regex function above.

Copy link
Author

Choose a reason for hiding this comment

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

This is why I have to forcibly strip the flag here, and why I confidently assume it will exist instead of checking for it.

let flags = regex.toString().split("/").pop().replace("g", "");
let match = new RegExp(regex, flags);
return original_string.replace(match, replacement);
}

export function new_map() {
return Dict.new();
}
Expand Down
12 changes: 12 additions & 0 deletions test/gleam/regex_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,15 @@ pub fn replace_3_test() {
regex.replace(re, "🐈🐈 are great!", "🐕")
|> should.equal("🐕🐕 are great!")
}

pub fn replace_first_test() {
let assert Ok(re) = regex.from_string("🐈")
regex.replace_first(in: "🐈🐈 are great!", one_of: re, with: "🐕")
|> should.equal("🐕🐈 are great!")
}

pub fn replace_first_of_many_test() {
let assert Ok(re) = regex.from_string("[, +-]")
regex.replace_first(one_of: re, in: "a,b-c d+e", with: "/")
|> should.equal("a/b-c d+e")
}
16 changes: 16 additions & 0 deletions test/gleam/string_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ pub fn replace_test() {
|> should.equal("Gleam++Erlang++Elixir")
}

pub fn replace_first_0_test() {
"Gleam,Erlang,Elixir"
|> string.replace_first(",", "++")
|> should.equal("Gleam++Erlang,Elixir")
}

pub fn replace_first_1_test() {
string.replace_first(in: "🐈🐈 are great!", one_of: "🐈", with: "🐕")
|> should.equal("🐕🐈 are great!")
}

pub fn replace_first_2_test() {
string.replace_first(one_of: ",", in: "a,b,c,d,e", with: "/")
|> should.equal("a/b,c,d,e")
}

pub fn append_test() {
"Test"
|> string.append(" Me")
Expand Down