diff --git a/src/gleam/regex.gleam b/src/gleam/regex.gleam index d1f8ef0a..a849ed68 100644 --- a/src/gleam/regex.gleam +++ b/src/gleam/regex.gleam @@ -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 diff --git a/src/gleam/string.gleam b/src/gleam/string.gleam index e530c8fa..e6d69f5e 100644 --- a/src/gleam/string.gleam +++ b/src/gleam/string.gleam @@ -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. /// diff --git a/src/gleam/string_builder.gleam b/src/gleam/string_builder.gleam index 24445a20..8c538f2f 100644 --- a/src/gleam/string_builder.gleam +++ b/src/gleam/string_builder.gleam @@ -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 diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl index 1fc15b2d..df33c7cf 100644 --- a/src/gleam_stdlib.erl +++ b/src/gleam_stdlib.erl @@ -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 @@ -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} @@ -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; diff --git a/src/gleam_stdlib.mjs b/src/gleam_stdlib.mjs index 50ebb46f..0ee2370e 100644 --- a/src/gleam_stdlib.mjs +++ b/src/gleam_stdlib.mjs @@ -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(""); } @@ -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 + 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(); } diff --git a/test/gleam/regex_test.gleam b/test/gleam/regex_test.gleam index e5175779..136ecd3d 100644 --- a/test/gleam/regex_test.gleam +++ b/test/gleam/regex_test.gleam @@ -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") +} diff --git a/test/gleam/string_test.gleam b/test/gleam/string_test.gleam index 6d3031e4..ac311fee 100644 --- a/test/gleam/string_test.gleam +++ b/test/gleam/string_test.gleam @@ -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")