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!" diff --git a/src/gleam/regexp.gleam b/src/gleam/regexp.gleam index ed3c1e2..a7a99ec 100644 --- a/src/gleam/regexp.gleam +++ b/src/gleam/regexp.gleam @@ -218,3 +218,21 @@ pub fn replace( in string: String, with substitute: String, ) -> String + +/// Creates a new `String` by replacing all substrings that match the regular +/// expression with the result of applying the function to each match. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(re) = regexp.from_string("\\w+") +/// regexp.match_map(re, "hello, joe!", fn(m) { string.capitalise(m.content) }) +/// // -> "Hello, Joe!" +/// ``` +@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, +) -> String diff --git a/src/gleam_regexp_ffi.erl b/src/gleam_regexp_ffi.erl index 2ec2c57..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]). +-export([compile/2, check/2, split/2, replace/3, scan/2, match_map/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}]). +match_map(Regexp, Subject, Replacement) -> + Replacement1 = fun(Content, Submatches) -> + 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 0dfad81..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); } @@ -49,3 +39,27 @@ export function scan(regex, string) { export function replace(regex, original_string, replacement) { return original_string.replaceAll(regex, 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); + 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 b6e7fef..4f02dc6 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 match_map_0_test() { + let replace = fn(match: Match) { + case match.content { + "1" -> "one" + "2" -> "two" + "3" -> "three" + n -> n + } + } + let assert Ok(re) = regexp.from_string("1|2|3") + regexp.match_map(re, "1, 2, 3, 4", replace) + |> should.equal("one, two, three, 4") +} + +pub fn match_map_1_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.match_map(re, "'1', '2', '3', '4'", replace) + |> should.equal("one, two, three, '4'") +}