From 1ee3d10b25d1e0e70d371994bed4ce754dfeafb6 Mon Sep 17 00:00:00 2001 From: Ryan Miville Date: Tue, 3 Dec 2024 23:18:26 -0500 Subject: [PATCH 1/6] Add replace_match function --- src/gleam/regexp.gleam | 8 ++++++++ src/gleam_regexp_ffi.erl | 8 +++++++- src/gleam_regexp_ffi.mjs | 12 ++++++++++++ test/gleam_regexp_test.gleam | 30 +++++++++++++++++++++++++++++- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/gleam/regexp.gleam b/src/gleam/regexp.gleam index ed3c1e2..7ca1c56 100644 --- a/src/gleam/regexp.gleam +++ b/src/gleam/regexp.gleam @@ -218,3 +218,11 @@ pub fn replace( in string: String, with substitute: String, ) -> String + +@external(erlang, "gleam_regexp_ffi", "replace_match") +@external(javascript, "../gleam_regexp_ffi.mjs", "replace_match") +pub fn replace_match( + each pattern: Regexp, + in string: String, + with substitute: fn(Match) -> String, +) -> String diff --git a/src/gleam_regexp_ffi.erl b/src/gleam_regexp_ffi.erl index 2ec2c57..6fb902b 100644 --- a/src/gleam_regexp_ffi.erl +++ b/src/gleam_regexp_ffi.erl @@ -1,6 +1,6 @@ -module(gleam_regexp_ffi). --export([compile/2, check/2, split/2, replace/3, scan/2]). +-export([compile/2, check/2, split/2, replace/3, scan/2, replace_match/3]). compile(String, Options) -> {options, Caseless, Multiline} = Options, @@ -44,3 +44,9 @@ scan(Regexp, String) -> replace(Regexp, Subject, Replacement) -> re:replace(Subject, Regexp, Replacement, [global, {return, binary}]). +replace_match(Regexp, Subject, Replacement) -> + Replacement1 = fun(Content, Submatches) -> + Submatches1 = gleam@list:map(Submatches, fun gleam@string:to_option/1), + Replacement({match, Content, Submatches1}) + end, + re:replace(Subject, Regexp, Replacement1, [global, {return, binary}]). diff --git a/src/gleam_regexp_ffi.mjs b/src/gleam_regexp_ffi.mjs index 0dfad81..1127f83 100644 --- a/src/gleam_regexp_ffi.mjs +++ b/src/gleam_regexp_ffi.mjs @@ -4,6 +4,7 @@ import { Match as RegexMatch, } from "./gleam/regexp.mjs"; import { Some, None } from "../gleam_stdlib/gleam/option.mjs"; +import * as $string from "../gleam_stdlib/gleam/string.mjs"; export function check(regex, string) { regex.lastIndex = 0; @@ -49,3 +50,14 @@ export function scan(regex, string) { export function replace(regex, original_string, replacement) { return original_string.replaceAll(regex, replacement); } + +export function replace_match(regex, original_string, replacement) { + let replace = (match, ...args) => { + const hasNamedGroups = typeof args.at(-1) === "object"; + const groups = args.slice(0, hasNamedGroups ? -3 : -2); + const submatches = groups.map($string.to_option); + let regexMatch = new RegexMatch(match, List.fromArray(submatches)); + return replacement(regexMatch); + }; + return original_string.replaceAll(regex, replace); +} diff --git a/test/gleam_regexp_test.gleam b/test/gleam_regexp_test.gleam index b6e7fef..d2f7a68 100644 --- a/test/gleam_regexp_test.gleam +++ b/test/gleam_regexp_test.gleam @@ -1,5 +1,5 @@ import gleam/option.{None, Some} -import gleam/regexp.{Match, Options} +import gleam/regexp.{type Match, Match, Options} import gleeunit import gleeunit/should @@ -190,3 +190,31 @@ pub fn replace_3_test() { regexp.replace(re, "🐈🐈 are great!", "🐕") |> should.equal("🐕🐕 are great!") } + +pub fn replace_match_test() { + let replace = fn(match: Match) { + case match { + Match("1", _) -> "one" + Match("2", _) -> "two" + Match("3", _) -> "three" + Match(n, _) -> n + } + } + let assert Ok(re) = regexp.from_string("1|2|3") + regexp.replace_match(re, "1, 2, 3", replace) + |> should.equal("one, two, three") +} + +pub fn replace_match_submatch_test() { + let replace = fn(match: Match) { + case match.submatches { + [Some("1")] -> "one" + [Some("2")] -> "two" + [Some("3")] -> "three" + _ -> match.content + } + } + let assert Ok(re) = regexp.from_string("'(1|2|3)'") + regexp.replace_match(re, "'1', '2', '3'", replace) + |> should.equal("one, two, three") +} From 672fcfd40363eef2338973c2c3903a9f2065528e Mon Sep 17 00:00:00 2001 From: Ryan Miville Date: Wed, 4 Dec 2024 12:48:47 -0500 Subject: [PATCH 2/6] Fix js and rename to replace_map --- src/gleam/regexp.gleam | 21 ++++++++++++++++++--- src/gleam_regexp_ffi.erl | 4 ++-- src/gleam_regexp_ffi.mjs | 19 ++++++++++++++++--- test/gleam_regexp_test.gleam | 35 ++++++++++++++++++++++++----------- 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/gleam/regexp.gleam b/src/gleam/regexp.gleam index 7ca1c56..3fbe0ae 100644 --- a/src/gleam/regexp.gleam +++ b/src/gleam/regexp.gleam @@ -219,9 +219,24 @@ pub fn replace( with substitute: String, ) -> String -@external(erlang, "gleam_regexp_ffi", "replace_match") -@external(javascript, "../gleam_regexp_ffi.mjs", "replace_match") -pub fn replace_match( +/// Creates a new `String` by replacing all substrings that match the regular +/// expression. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(re) = regexp.from_string("^[A-Z]|([a-z])([A-Z])") +/// +/// use match <- regexp.replace_map(re, "TheQuickBrownFox") +/// case match.submatches { +/// [Some(lower), Some(upper)] -> lower <> "_" <> string.lowercase(upper) +/// _ -> string.lowercase(match.content) +/// } +/// // -> "the_quick_brown_fox" +/// ``` +@external(erlang, "gleam_regexp_ffi", "replace_map") +@external(javascript, "../gleam_regexp_ffi.mjs", "replace_map") +pub fn replace_map( each pattern: Regexp, in string: String, with substitute: fn(Match) -> String, diff --git a/src/gleam_regexp_ffi.erl b/src/gleam_regexp_ffi.erl index 6fb902b..07b71a5 100644 --- a/src/gleam_regexp_ffi.erl +++ b/src/gleam_regexp_ffi.erl @@ -1,6 +1,6 @@ -module(gleam_regexp_ffi). --export([compile/2, check/2, split/2, replace/3, scan/2, replace_match/3]). +-export([compile/2, check/2, split/2, replace/3, scan/2, replace_map/3]). compile(String, Options) -> {options, Caseless, Multiline} = Options, @@ -44,7 +44,7 @@ scan(Regexp, String) -> replace(Regexp, Subject, Replacement) -> re:replace(Subject, Regexp, Replacement, [global, {return, binary}]). -replace_match(Regexp, Subject, Replacement) -> +replace_map(Regexp, Subject, Replacement) -> Replacement1 = fun(Content, Submatches) -> Submatches1 = gleam@list:map(Submatches, fun gleam@string:to_option/1), Replacement({match, Content, Submatches1}) diff --git a/src/gleam_regexp_ffi.mjs b/src/gleam_regexp_ffi.mjs index 1127f83..c31174d 100644 --- a/src/gleam_regexp_ffi.mjs +++ b/src/gleam_regexp_ffi.mjs @@ -51,13 +51,26 @@ export function replace(regex, original_string, replacement) { return original_string.replaceAll(regex, replacement); } -export function replace_match(regex, original_string, replacement) { +export function replace_map(regex, original_string, replacement) { let replace = (match, ...args) => { const hasNamedGroups = typeof args.at(-1) === "object"; const groups = args.slice(0, hasNamedGroups ? -3 : -2); - const submatches = groups.map($string.to_option); - let regexMatch = new RegexMatch(match, List.fromArray(submatches)); + let regexMatch = new RegexMatch(match, toSubmatches(groups)); return replacement(regexMatch); }; return original_string.replaceAll(regex, replace); } + +function toSubmatches(groups) { + const submatches = []; + for (let n = 0; n < groups.length; n++) { + if (groups[n]) { + submatches[n] = new Some(groups[n]); + continue; + } + if (submatches.length > 0) { + submatches[n] = new None(); + } + } + return List.fromArray(submatches); +} diff --git a/test/gleam_regexp_test.gleam b/test/gleam_regexp_test.gleam index d2f7a68..34c4733 100644 --- a/test/gleam_regexp_test.gleam +++ b/test/gleam_regexp_test.gleam @@ -1,5 +1,6 @@ import gleam/option.{None, Some} import gleam/regexp.{type Match, Match, Options} +import gleam/string import gleeunit import gleeunit/should @@ -191,21 +192,21 @@ pub fn replace_3_test() { |> should.equal("🐕🐕 are great!") } -pub fn replace_match_test() { +pub fn replace_map_0_test() { let replace = fn(match: Match) { - case match { - Match("1", _) -> "one" - Match("2", _) -> "two" - Match("3", _) -> "three" - Match(n, _) -> n + case match.content { + "1" -> "one" + "2" -> "two" + "3" -> "three" + n -> n } } let assert Ok(re) = regexp.from_string("1|2|3") - regexp.replace_match(re, "1, 2, 3", replace) - |> should.equal("one, two, three") + regexp.replace_map(re, "1, 2, 3, 4", replace) + |> should.equal("one, two, three, 4") } -pub fn replace_match_submatch_test() { +pub fn replace_map_1_test() { let replace = fn(match: Match) { case match.submatches { [Some("1")] -> "one" @@ -215,6 +216,18 @@ pub fn replace_match_submatch_test() { } } let assert Ok(re) = regexp.from_string("'(1|2|3)'") - regexp.replace_match(re, "'1', '2', '3'", replace) - |> should.equal("one, two, three") + regexp.replace_map(re, "'1', '2', '3', '4'", replace) + |> should.equal("one, two, three, '4'") +} + +pub fn replace_map_2_test() { + let assert Ok(re) = regexp.from_string("^[A-Z]|([a-z])([A-Z])") + let replace = fn(match: Match) { + case match.submatches { + [Some(lower), Some(upper)] -> lower <> "_" <> string.lowercase(upper) + _ -> string.lowercase(match.content) + } + } + regexp.replace_map(re, "TheQuickBrownFox", replace) + |> should.equal("the_quick_brown_fox") } From 577707d78dd898b25b7e70eccb2f3d53207019f6 Mon Sep 17 00:00:00 2001 From: Ryan Miville Date: Wed, 4 Dec 2024 12:55:17 -0500 Subject: [PATCH 3/6] reformat doc --- src/gleam/regexp.gleam | 11 ++++++----- src/gleam_regexp_ffi.mjs | 27 +++++++++++---------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/gleam/regexp.gleam b/src/gleam/regexp.gleam index 3fbe0ae..e841a9c 100644 --- a/src/gleam/regexp.gleam +++ b/src/gleam/regexp.gleam @@ -226,12 +226,13 @@ pub fn replace( /// /// ```gleam /// let assert Ok(re) = regexp.from_string("^[A-Z]|([a-z])([A-Z])") -/// -/// use match <- regexp.replace_map(re, "TheQuickBrownFox") -/// case match.submatches { -/// [Some(lower), Some(upper)] -> lower <> "_" <> string.lowercase(upper) -/// _ -> string.lowercase(match.content) +/// let snake_case = fn(match: Match) { +/// case match.submatches { +/// [Some(lower), Some(upper)] -> lower <> "_" <> string.lowercase(upper) +/// _ -> string.lowercase(match.content) +/// } /// } +/// regexp.replace_map(each: re, in: "TheQuickBrownFox", with: snake_case) /// // -> "the_quick_brown_fox" /// ``` @external(erlang, "gleam_regexp_ffi", "replace_map") diff --git a/src/gleam_regexp_ffi.mjs b/src/gleam_regexp_ffi.mjs index c31174d..b1e41e2 100644 --- a/src/gleam_regexp_ffi.mjs +++ b/src/gleam_regexp_ffi.mjs @@ -4,7 +4,6 @@ import { Match as RegexMatch, } from "./gleam/regexp.mjs"; import { Some, None } from "../gleam_stdlib/gleam/option.mjs"; -import * as $string from "../gleam_stdlib/gleam/string.mjs"; export function check(regex, string) { regex.lastIndex = 0; @@ -55,22 +54,18 @@ export function replace_map(regex, original_string, replacement) { let replace = (match, ...args) => { const hasNamedGroups = typeof args.at(-1) === "object"; const groups = args.slice(0, hasNamedGroups ? -3 : -2); - let regexMatch = new RegexMatch(match, toSubmatches(groups)); + const submatches = []; + for (let n = 0; n < groups.length; n++) { + if (groups[n]) { + submatches[n] = new Some(groups[n]); + continue; + } + if (submatches.length > 0) { + submatches[n] = new None(); + } + } + let regexMatch = new RegexMatch(match, List.fromArray(submatches)); return replacement(regexMatch); }; return original_string.replaceAll(regex, replace); } - -function toSubmatches(groups) { - const submatches = []; - for (let n = 0; n < groups.length; n++) { - if (groups[n]) { - submatches[n] = new Some(groups[n]); - continue; - } - if (submatches.length > 0) { - submatches[n] = new None(); - } - } - return List.fromArray(submatches); -} From 815f2e447214e04ad4fb33c9594aadaa587b7dfc Mon Sep 17 00:00:00 2001 From: Ryan Miville Date: Thu, 5 Dec 2024 14:57:14 -0500 Subject: [PATCH 4/6] simpler doc and remove redundant test --- src/gleam/regexp.gleam | 14 ++++---------- test/gleam_regexp_test.gleam | 13 ------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/gleam/regexp.gleam b/src/gleam/regexp.gleam index e841a9c..c88738e 100644 --- a/src/gleam/regexp.gleam +++ b/src/gleam/regexp.gleam @@ -220,20 +220,14 @@ pub fn replace( ) -> String /// Creates a new `String` by replacing all substrings that match the regular -/// expression. +/// expression with the result of applying the function to each match. /// /// ## Examples /// /// ```gleam -/// let assert Ok(re) = regexp.from_string("^[A-Z]|([a-z])([A-Z])") -/// let snake_case = fn(match: Match) { -/// case match.submatches { -/// [Some(lower), Some(upper)] -> lower <> "_" <> string.lowercase(upper) -/// _ -> string.lowercase(match.content) -/// } -/// } -/// regexp.replace_map(each: re, in: "TheQuickBrownFox", with: snake_case) -/// // -> "the_quick_brown_fox" +/// let assert Ok(re) = regexp.from_string("\\w+") +/// regexp.replace_map(re, "hello, joe!", fn(m) { string.capitalise(m.content) }) +/// // -> "Hello, Joe!" /// ``` @external(erlang, "gleam_regexp_ffi", "replace_map") @external(javascript, "../gleam_regexp_ffi.mjs", "replace_map") diff --git a/test/gleam_regexp_test.gleam b/test/gleam_regexp_test.gleam index 34c4733..877aa42 100644 --- a/test/gleam_regexp_test.gleam +++ b/test/gleam_regexp_test.gleam @@ -1,6 +1,5 @@ import gleam/option.{None, Some} import gleam/regexp.{type Match, Match, Options} -import gleam/string import gleeunit import gleeunit/should @@ -219,15 +218,3 @@ pub fn replace_map_1_test() { regexp.replace_map(re, "'1', '2', '3', '4'", replace) |> should.equal("one, two, three, '4'") } - -pub fn replace_map_2_test() { - let assert Ok(re) = regexp.from_string("^[A-Z]|([a-z])([A-Z])") - let replace = fn(match: Match) { - case match.submatches { - [Some(lower), Some(upper)] -> lower <> "_" <> string.lowercase(upper) - _ -> string.lowercase(match.content) - } - } - regexp.replace_map(re, "TheQuickBrownFox", replace) - |> should.equal("the_quick_brown_fox") -} From dbf9f640380bc8f6535d463213334be6a7c2a7dd Mon Sep 17 00:00:00 2001 From: Ryan Miville Date: Tue, 10 Dec 2024 12:54:29 -0500 Subject: [PATCH 5/6] refactor ffi and rename to --- src/gleam/regexp.gleam | 8 ++++---- src/gleam_regexp_ffi.erl | 6 +++--- src/gleam_regexp_ffi.mjs | 40 +++++++++++++++--------------------- test/gleam_regexp_test.gleam | 8 ++++---- 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/src/gleam/regexp.gleam b/src/gleam/regexp.gleam index c88738e..a7a99ec 100644 --- a/src/gleam/regexp.gleam +++ b/src/gleam/regexp.gleam @@ -226,12 +226,12 @@ pub fn replace( /// /// ```gleam /// let assert Ok(re) = regexp.from_string("\\w+") -/// regexp.replace_map(re, "hello, joe!", fn(m) { string.capitalise(m.content) }) +/// regexp.match_map(re, "hello, joe!", fn(m) { string.capitalise(m.content) }) /// // -> "Hello, Joe!" /// ``` -@external(erlang, "gleam_regexp_ffi", "replace_map") -@external(javascript, "../gleam_regexp_ffi.mjs", "replace_map") -pub fn replace_map( +@external(erlang, "gleam_regexp_ffi", "match_map") +@external(javascript, "../gleam_regexp_ffi.mjs", "match_map") +pub fn match_map( each pattern: Regexp, in string: String, with substitute: fn(Match) -> String, diff --git a/src/gleam_regexp_ffi.erl b/src/gleam_regexp_ffi.erl index 07b71a5..977b3d3 100644 --- a/src/gleam_regexp_ffi.erl +++ b/src/gleam_regexp_ffi.erl @@ -1,6 +1,6 @@ -module(gleam_regexp_ffi). --export([compile/2, check/2, split/2, replace/3, scan/2, replace_map/3]). +-export([compile/2, check/2, split/2, replace/3, scan/2, match_map/3]). compile(String, Options) -> {options, Caseless, Multiline} = Options, @@ -44,9 +44,9 @@ scan(Regexp, String) -> replace(Regexp, Subject, Replacement) -> re:replace(Subject, Regexp, Replacement, [global, {return, binary}]). -replace_map(Regexp, Subject, Replacement) -> +match_map(Regexp, Subject, Replacement) -> Replacement1 = fun(Content, Submatches) -> - Submatches1 = gleam@list:map(Submatches, fun gleam@string:to_option/1), + Submatches1 = lists:map(fun gleam@string:to_option/1, Submatches), Replacement({match, Content, Submatches1}) end, re:replace(Subject, Regexp, Replacement1, [global, {return, binary}]). diff --git a/src/gleam_regexp_ffi.mjs b/src/gleam_regexp_ffi.mjs index b1e41e2..726d635 100644 --- a/src/gleam_regexp_ffi.mjs +++ b/src/gleam_regexp_ffi.mjs @@ -31,17 +31,7 @@ export function split(regex, string) { export function scan(regex, string) { const matches = Array.from(string.matchAll(regex)).map((match) => { const content = match[0]; - const submatches = []; - for (let n = match.length - 1; n > 0; n--) { - if (match[n]) { - submatches[n - 1] = new Some(match[n]); - continue; - } - if (submatches.length > 0) { - submatches[n - 1] = new None(); - } - } - return new RegexMatch(content, List.fromArray(submatches)); + return new RegexMatch(content, submatches(match.slice(1))); }); return List.fromArray(matches); } @@ -50,22 +40,26 @@ export function replace(regex, original_string, replacement) { return original_string.replaceAll(regex, replacement); } -export function replace_map(regex, original_string, replacement) { +export function match_map(regex, original_string, replacement) { let replace = (match, ...args) => { const hasNamedGroups = typeof args.at(-1) === "object"; const groups = args.slice(0, hasNamedGroups ? -3 : -2); - const submatches = []; - for (let n = 0; n < groups.length; n++) { - if (groups[n]) { - submatches[n] = new Some(groups[n]); - continue; - } - if (submatches.length > 0) { - submatches[n] = new None(); - } - } - let regexMatch = new RegexMatch(match, List.fromArray(submatches)); + let regexMatch = new RegexMatch(match, submatches(groups)); return replacement(regexMatch); }; return original_string.replaceAll(regex, replace); } + +function submatches(groups) { + const submatches = []; + for (let n = groups.length - 1; n >= 0; n--) { + if (groups[n]) { + submatches[n] = new Some(groups[n]); + continue; + } + if (submatches.length > 0) { + submatches[n] = new None(); + } + } + return List.fromArray(submatches); +} diff --git a/test/gleam_regexp_test.gleam b/test/gleam_regexp_test.gleam index 877aa42..4f02dc6 100644 --- a/test/gleam_regexp_test.gleam +++ b/test/gleam_regexp_test.gleam @@ -191,7 +191,7 @@ pub fn replace_3_test() { |> should.equal("🐕🐕 are great!") } -pub fn replace_map_0_test() { +pub fn match_map_0_test() { let replace = fn(match: Match) { case match.content { "1" -> "one" @@ -201,11 +201,11 @@ pub fn replace_map_0_test() { } } let assert Ok(re) = regexp.from_string("1|2|3") - regexp.replace_map(re, "1, 2, 3, 4", replace) + regexp.match_map(re, "1, 2, 3, 4", replace) |> should.equal("one, two, three, 4") } -pub fn replace_map_1_test() { +pub fn match_map_1_test() { let replace = fn(match: Match) { case match.submatches { [Some("1")] -> "one" @@ -215,6 +215,6 @@ pub fn replace_map_1_test() { } } let assert Ok(re) = regexp.from_string("'(1|2|3)'") - regexp.replace_map(re, "'1', '2', '3', '4'", replace) + regexp.match_map(re, "'1', '2', '3', '4'", replace) |> should.equal("one, two, three, '4'") } From 6f4c2de9d2c247e7d0a82c28111b329b876dd2f0 Mon Sep 17 00:00:00 2001 From: Ryan Miville Date: Wed, 5 Feb 2025 10:58:11 -0500 Subject: [PATCH 6/6] bump to v1.1.0 and add changelog --- CHANGELOG.md | 5 +++++ gleam.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d884b46 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## v1.1.0 - 2025-02-05 + +- Added the `match_map` function. diff --git a/gleam.toml b/gleam.toml index 3858bb9..a42e333 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "gleam_regexp" -version = "1.0.0" +version = "1.1.0" gleam = ">= 1.0.0" licences = ["Apache-2.0"] description = "Regular expressions in Gleam!"