/* * Copyright 2023 Stephen Tetley * * Use of this source code is governed by the Apache 2.0 license * that can be found in the LICENSE.md file. */////// Basic support for regular expression matching on Strings.////// This module uses the builtin Regex datatype representing a compiled regular expression and/// alternative implementations of functions from `String.flix` using regular expressions.////// Note - for regex matching all scanning is left-to-right and "chunked" by the matcher./// This differs from String.flix where scanning is character at a time and can be left-to-right/// or right-to-left. Hence this module provides analogue but not exactly equivalent of functions:/// e.g./// > String.indexOfLeft ~> Regex/Text.indexOfFirst/// > String.indexOfRight ~> Regex/Text.indexOfLast/// > String.breakOnLeft ~> Regex/Text.breakOnFirst/// > String.breakOnRight ~> Regex/Text.breakOnLast///pubmod Regex {import java.util.regex.Patternimport java.util.regex.PatternSyntaxExceptionimport java.util.regex.{Matcher => JMatcher}import java.lang.IllegalStateExceptionimport java.lang.IndexOutOfBoundsExceptionuse Regex.Flaguse Regex.Flag.{ CanonEq, CaseInsenstive, Comments, Dotall, Literal, Multiline, UnicodeCase, UnicodeCharacterClass, UnixLines }use Regex.Matcheruse Regex.Matcher.{Matcher}////// Represents a flag controlling the compilation of regular expression.///pubenumFlagwithEq, Order, ToString {case CanonEqcase CaseInsenstivecase Commentscase Dotallcase Literalcase Multilinecase UnicodeCasecase UnicodeCharacterClasscase UnixLines }////// Returns the int value of `flag`.////// Regex Pattern Flags predate `Enum` in Java so they are represented as an `Int32`/// and can be summed to make a bit mask.///deftoInt(flag: Flag): Int32 =matchflag {case CanonEq => Pattern.CANON_EQcase CaseInsenstive => Pattern.CASE_INSENSITIVEcase Comments => Pattern.COMMENTScase Dotall => Pattern.DOTALLcase Literal => Pattern.LITERALcase Multiline => Pattern.MULTILINEcase UnicodeCase => Pattern.UNICODE_CASEcase UnicodeCharacterClass => Pattern.UNICODE_CHARACTER_CLASScase UnixLines => Pattern.UNIX_LINES }////// Sum a list of flags to an `Int32` bit mask.///pubdefsumFlags(flags: Set[Flag]): Int32 =Set.foldLeft((ac, e) -> ac+toInt(e), 0, flags)////// Decompose an Int32 representing bit masks into a list of flags.///deflistFlags(x: Int32): List[Flag] =letcheck = y -> {lety1 = toInt(y);Int32.bitwiseAnd(x, y1) ==y1 };List.filter(check, CanonEq :: CaseInsenstive :: Comments :: Dotall :: Literal :: Multiline :: UnicodeCase :: UnicodeCharacterClass :: UnixLines :: Nil )////// Return the unmatchable Regex - a regular expression that will not match any input.///pubdefunmatchable(): Regex =try { Pattern.compile("^\\b$") } catch {case_: PatternSyntaxException => unreachable!() }////// Return the regular expression string that matches the literal string `s`.///pubdefquote(s: String): String = Pattern.quote(s)////// Return the regular expression string used to build this Regex.///pubdefpattern(rgx: Regex): String =rgx.pattern()////// Return the flags used to build the Regex `rgx`.///pubdefflags(rgx: Regex): Set[Flag] =leti = rgx.flags();listFlags(i) |> List.toSet////// Returns `true` if the entire string `s` is matched by the Regex `rgx`.///pubdefisMatch(rgx: Regex, s: String): Bool = regionrc {let Matcher(m1) = newMatcher(rc, rgx, s);unsafeIO { m1.matches() } }////// Returns `true` if the string `input` is matched by the Regex `rgx`/// at any position within the string `s`.///pubdefisSubmatch(rgx: Regex, s: String): Bool = regionrc {newMatcher(rc, rgx, s) |> find }////// Returns the positions of the all the occurrences of `substr` in `s`.////// If `substr` regexp matches the empty string, positions where an empty match/// has been recognized will be returned.///pubdefindicesOf(substr: Regex, s: String): Vector[Int32] = regionrc {letm = newMatcher(rc, substr, s);matchfoldSubmatches(Chain.snoc, Chain.empty(), matcherStart, m) {case Ok(c) => Chain.toList(c) |> List.toVectorcase Err(_) => Vector.empty() } }////// Returns the contents of the all the occurrences of `substr` in `s`.////// Returns `Nil` if `substr` is the empty string.///pubdefsubmatches(substr: Regex, s: String): List[String] = regionrc {letm = newMatcher(rc, substr, s);matchfoldSubmatches(Chain.snoc, Chain.empty(), matcherContent, m) {case Ok(c) => Chain.toList(c)case Err(_) => Nil } }////// Returns the contents of the all the capture groups of `substr` in `s`.////// Returns `Nil` if `substr` is the empty string or regex contains no groups.///pubdefcapture(rgx: Regex, s: String): List[String] = regionrc {letm = newMatcher(rc, rgx, s);if (find(m))letcnt = groupCount(m);matchList.foldRight( (a, x) ->forM (x2 <- x;a2 <- a ) yielda2 :: x2, Ok(Nil),List.map(matcherGroup(m), List.range(1, cnt+1)) ) {case Ok(l) => lcase Err(_) => Nil }else Nil }////// Count the occurrences of `substr` in string `s`.///pubdefcountSubmatches(substr: Regex, s: String): Int32 = regionrc {letm = newMatcher(rc, substr, s);matchfoldSubmatches((acc, _) -> acc+1, 0, matcherStart, m) {case Ok(n) => ncase Err(_) => 0 } }////// Splits the string `s` around matches of the Regex `rgx`.///pubdefsplit(rgx: Regex, s: String): List[String] =unsafeIO { Array.toList(rgx.split(s)) }////// Returns string `s` with every match of the Regex `src` replaced by the string `dst`.///pubdefreplace(src: {src = Regex}, dst: {dst = String}, s: String): String = regionrc {let Matcher(m1) = newMatcher(rc, src#src, s);unsafeIO { m1.replaceAll(dst#dst) } }////// Returns string `s` with the first match of the regular expression `src` replaced by the string `dst`.///pubdefreplaceFirst(src: {src = Regex}, dst: {dst = String}, s: String): String = regionrc {let Matcher(m1) = newMatcher(rc, src#src, s);unsafeIO { m1.replaceFirst(dst#dst) } }////// Returns `true` if the string `s` starts the Regex `prefix`.///pubdefstartsWith(prefix: Regex, s: String): Bool = regionrc {let Matcher(m1) = newMatcher(rc, prefix, s);unsafeIO { m1.lookingAt() } }////// Returns `true` if the string `input` ends the regular expression Regex `suffix`.////// This will be slower than `startsWith` because there is no primitive Java function/// to call, instead the matches of `patt` are iterated until the last one is found.///pubdefendsWith(suffix: Regex, s: String): Bool = regionrc { let m = newMatcher(rc, suffix, s); match lastSubmatch(matcherRange, m) { case Err(_) => false case Ok(rng) => String.length(s) == rng#end } }////// Returns `Some(prefix)` of string `s` if its prefix matches `substr`.///pubdefgetPrefix(substr: Regex, s: String): Option[String] = regionrc { let m = newMatcher(rc, substr, s); match firstSubmatch(matcherRange, m) { case Err(_) => None case Ok(r) => if (r#start == 0) Some(String.takeLeft(r#end, s)) else None } }////// Returns `Some(suffix)` of string `s` if its suffix matches `substr`.///pubdefgetSuffix(substr: Regex, s: String): Option[String] = regionrc { let m = newMatcher(rc, substr, s); match lastSubmatch(matcherRange, m) { case Err(_) => None case Ok(r) => if (r#end == String.length(s)) Some(String.dropLeft(r#start, s)) else None } }////// Returns `Some(suffix)` of string `s` if its prefix matches `substr`.///pubdefstripPrefix(substr: Regex, s: String): Option[String] = regionrc { let m = newMatcher(rc, substr, s); match firstSubmatch(matcherRange, m) { case Err(_) => None case Ok(r) => if (r#start == 0) Some(String.dropLeft(r#end, s)) else None } }////// Returns `Some(prefix)` of string `s` if its suffix matches `substr`.///pubdefstripSuffix(substr: Regex, s: String): Option[String] = regionrc { let m = newMatcher(rc, substr, s); match lastSubmatch(matcherRange, m) { case Err(_) => None case Ok(r) => if (r#end == String.length(s)) Some(String.takeLeft(r#start, s)) else None } }////// Return the index of the first occurence of `substr` in `s` from the left.////// If the Regex `substr` is not present in `s` return None.///pubdefindexOfFirst(substr: Regex, s: String): Option[Int32] = regionrc {letm = newMatcher(rc, substr, s);firstSubmatch(matcherStart, m) |> Result.toOption }////// Return the index of the last occurence of `substr` in `s` starting from the left.////// If the Regex `substr` is not present in `s` return None.///pubdefindexOfLast(substr: Regex, s: String): Option[Int32] = regionrc {letm = newMatcher(rc, substr, s);lastSubmatch(matcherStart, m) |> Result.toOption }////// Return the content of the first occurence of `substr` in `s` from the left.////// If the Regex `substr` is not present in `s` return None.///pubdefgetFirst(substr: Regex, s: String): Option[String] = regionrc {letm = newMatcher(rc, substr, s);firstSubmatch(matcherContent, m) |> Result.toOption }////// Return the content of the last occurence of `substr` in `s` from the left.////// If the Regex `substr` is not present in `s` return None.///pubdefgetLast(substr: Regex, s: String): Option[String] = regionrc {letm = newMatcher(rc, substr, s);lastSubmatch(matcherContent, m) |> Result.toOption }////// This is `indexOfFirst` with a start offset.////// If the Regex `substr` is not present in `s` return None.///pubdefindexOfFirstWithOffset(substr: Regex, offset: Int32, s: String): Option[Int32] = regionrc {letm = newMatcher(rc, substr, s);matchsetBounds(start = offset, end = String.length(s), m) {case Ok() => firstSubmatch(matcherStart, m) |> Result.toOptioncase Err(_) => None } }////// This is `indexOfLast` with a start offset.////// If the Regex `substr` is not present in `s` return None.///pubdefindexOfLastWithOffset(substr: Regex, offset: Int32, s: String): Option[Int32] = regionrc {letm = newMatcher(rc, substr, s);matchsetBounds(start = offset, end = String.length(s), m) {case Ok() => lastSubmatch(matcherStart, m) |> Result.toOptioncase Err(_) => None } }////// This is `getFirst` with a start offset.////// If the Regex `substr` is not present in `s` return None.///pubdefgetFirstWithOffset(substr: Regex, offset: Int32, s: String): Option[String] = regionrc {letm = newMatcher(rc, substr, s);matchsetBounds(start = offset, end = String.length(s), m) {case Ok() => firstSubmatch(matcherContent, m) |> Result.toOptioncase Err(_) => None } }////// This is `getLast` with a start offset.////// If the Regex `substr` is not present in `s` return None.///pubdefgetLastWithOffset(substr: Regex, offset: Int32, s: String): Option[String] = regionrc {letm = newMatcher(rc, substr, s);matchsetBounds(start = offset, end = String.length(s), m) {case Ok() => lastSubmatch(matcherContent, m) |> Result.toOptioncase Err(_) => None } }////// Find the first instance of Regex `substr` in string `s`, return a pair of the/// prefix of string `s` up to `sub` and the rest of string `s` including `sub`.///pubdefbreakOnFirst(substr: Regex, s: String): (String, String) = regionrc { let m = newMatcher(rc, substr, s); match firstSubmatch(matcherRange, m) { case Err(_) => (s, "") case Ok(r) => (String.sliceLeft(end = r#start, s), String.sliceRight(start = r#start, s)) } }////// Find the first instance of Regex `substr` in string `s`, return a pair of the/// prefix of string `s` up to and including `sub` and the rest of string `s` after `sub`.///pubdefbreakAfterFirst(substr: Regex, s: String): (String, String) = regionrc { let m = newMatcher(rc, substr, s); match firstSubmatch(matcherRange, m) { case Err(_) => (s, "") case Ok(r) => (String.sliceLeft(end = r#end, s), String.sliceRight(start = r#end, s)) } }////// Find the last instance of `substr` in string `s`, return a pair of the/// initial string including `substr` and suffix from `substr`.///pubdefbreakOnLast(substr: Regex, s: String): (String, String) = regionrc { let m = newMatcher(rc, substr, s); match lastSubmatch(matcherRange, m) { case Err(_) => (s, "") case Ok(r) => (String.sliceLeft(end = r#end, s), String.sliceRight(start = r#end, s)) } }////// Find the last instance of `substr` in string `s`, return a pair of the/// initial string including `substr` and suffix from `substr`.///pubdefbreakBeforeLast(substr: Regex, s: String): (String, String) = regionrc { let m = newMatcher(rc, substr, s); match lastSubmatch(matcherRange, m) { case Err(_) => (s, "") case Ok(r) => (String.sliceLeft(end = r#start, s), String.sliceRight(start = r#start, s)) } }////// Returns `true` if the entire string `s` is matched by the Regex `rgx`.////// Returns `false` if the entire string does not match or the bounds are invalid.///pubdefisMatchWithBounds(rgx: Regex, start: {start = Int32}, end: {end = Int32}, s: String): Bool = regionrc {matchnewMatcherWithBounds(rc, rgx, start, end, s) {case Ok(Matcher(m)) => unsafeIO { m.matches() }case Err(_) => false } }////// Returns `true` if the string `input` is matched by the Regex `rgx`/// at any position within the string `s` that is within the bounds `(start, end)`.////// Returns `false` if there is no match or the bounds are invalid.///pubdefisSubmatchWithBounds(rgx: Regex, start: {start = Int32}, end: {end = Int32}, s: String): Bool = regionrc {matchnewMatcherWithBounds(rc, rgx, start, end, s) {case Ok(m) => find(m)case Err(_) => false } }////// Returns the positions of the all the occurrences of `substr` in `s`/// within the bounds `(start, end)`.////// If `substr` regexp matches the empty string, positions where an empty match/// has been recognized will be returned.////// Returns an empty vector if there are no matches or the bounds are invalid.///pubdefindicesOfWithBounds(substr: Regex, start: {start = Int32}, end: {end = Int32}, s: String): Vector[Int32] = regionrc {matchnewMatcherWithBounds(rc, substr, start, end, s) {case Ok(m) => matchfoldSubmatches(Chain.snoc, Chain.empty(), matcherStart, m) {case Ok(c) => Chain.toList(c) |> List.toVectorcase Err(_) => Vector.empty() }case Err(_) => Vector.empty() } }////// Returns the contents of the all the occurrences of `substr` in `s`/// within the bounds `(start, end)`.////// Returns `Nil` if `substr` is the empty string or the bounds are invalid.///pubdefsubmatchesWithBounds(substr: Regex, start: {start = Int32}, end: {end = Int32}, s: String): List[String] = regionrc {matchnewMatcherWithBounds(rc, substr, start, end, s) {case Ok(m) => matchfoldSubmatches(Chain.snoc, Chain.empty(), matcherContent, m) {case Ok(c) => Chain.toList(c)case Err(_) => Nil }case Err(_) => Nil } }////// Count the occurrences of `substr` in string `s` within the bounds `(start, end)`.////// Returns 0 if the bounds are invalid.///pubdefcountSubmatchesWithBounds(substr: Regex, start: {start = Int32}, end: {end = Int32}, s: String): Int32 = regionrc {matchnewMatcherWithBounds(rc, substr, start, end, s) {case Ok(m) => matchfoldSubmatches((acc, _) -> acc+1, 0, matcherStart, m) {case Ok(n) => ncase Err(_) => 0 }case Err(_) => 0 } }////// Return the index of the first occurence of `substr` in `s` from the left within/// the bounds `(start, end)`.////// If the Regex `substr` is not present in `s` or the bounds are invalid return `None`.///pubdefindexOfFirstWithBounds(substr: Regex, start: {start = Int32}, end: {end = Int32}, s: String): Option[Int32] = regionrc {matchnewMatcherWithBounds(rc, substr, start, end, s) {case Ok(m) => firstSubmatch(matcherStart, m) |> Result.toOptioncase Err(_) => None } }////// Return the index of the last occurence of `substr` in `s` from the left within/// the bounds `(start, end)`.////// If the Regex `substr` is not present in `s` or the bounds are invalid return `None`.///pubdefindexOfLastWithBounds(substr: Regex, start: {start = Int32}, end: {end = Int32}, s: String): Option[Int32] = regionrc {matchnewMatcherWithBounds(rc, substr, start, end, s) {case Ok(m) => lastSubmatch(matcherStart, m) |> Result.toOptioncase Err(_) => None } }////// Return the content of the first occurence of `substr` in `s` from the left within/// the bounds `(start, end)`.////// If the Regex `substr` is not present in `s` or the bounds are invalid return `None`.///pubdefgetFirstWithBounds(substr: Regex, start: {start = Int32}, end: {end = Int32}, s: String): Option[String] = regionrc {matchnewMatcherWithBounds(rc, substr, start, end, s) {case Ok(m) => firstSubmatch(matcherContent, m) |> Result.toOptioncase Err(_) => None } }////// Return the content of the last occurence of `substr` in `s` from the left within/// the bounds `(start, end)`.////// If the Regex `substr` is not present in `s` or the bounds are invalid return `None`.///pubdefgetLastWithBounds(substr: Regex, start: {start = Int32}, end: {end = Int32}, s: String): Option[String] = regionrc {matchnewMatcherWithBounds(rc, substr, start, end, s) {case Ok(m) => lastSubmatch(matcherContent, m) |> Result.toOptioncase Err(_) => None } }///////////////////////////////////////////////////////////////////////////// Internal support for matching using java.util.regex.Matcher /////////////////////////////////////////////////////////////////////////////////// Matcher is imperative - it can be seen as a stepper through a stream/// of matches. The matcher is updated with the function find to move to/// the next match.///enumMatcher[_: Region](JMatcher)////// Create a Matcher for Regex `rgx` on the source String `input`.///defnewMatcher(_: Region[r], rgx: Regex, s: String): Matcher[r] \ r = Matcher(checked_ecast(rgx.matcher(s)))////// Create a Matcher for Regex `rgx` on the source String `input` with bounds `start` and `end`.////// Returns `Ok(matcher)` if the bounds are valid for the input String `s` otherwise returns `Err(_)`.///defnewMatcherWithBounds(rc: Region[r], rgx: Regex, start: {start = Int32}, end: {end = Int32}, s: String): Result[String, Matcher[r]] \ r =letm = newMatcher(rc, rgx, s);matchsetBounds(start, end, m) {case Ok() => Ok(m)case Err(msg) => Err(msg) }////// Attempt to find the next match. Returns `true` and moves to the next match/// if there is a next match otherwise returns `false`.////// The internal state of the matcher is updated.///deffind(m: Matcher[r]): Bool \ r =let Matcher(m1) = m;unsafeIOasr { m1.find() }////// Attempt to find the next match after the supplied position `pos`./// Returns `true` and moves to the next match if there is a next match/// otherwise returns `false`.////// The internal state of the matcher is updated.///deffindFrom(pos: Int32, m: Matcher[r]): Bool \ r =try {let Matcher(m1) = m;unsafeIOasr { m1.find(pos) } } catch {case_: IndexOutOfBoundsException => false }////// Attempt to find the last match by iterating through the input string.////// Returns `true` and moves the matcher to the start of the last match if there are/// one-or-more matches otherwise returns `false`.////// The internal state of the matcher is updated.////// Note - there are no primitive Java functions for right-to-left scanning provided/// by Java's JDK so the performance of `findLast` is disadvantaged compared to `find`.///deffindLast(m: Matcher[r]): Bool \ r =defloop(lastPos) = {matchfind(m) {casetrue => { letpos1 = matcherStart(m); loop(pos1) }casefalse => lastPos } };matchfind(m) {casefalse => falsecasetrue => {letpos = matcherStart(m);matchloop(pos) {case Err(_) => falsecase Ok(startOfLast) => findFrom(startOfLast, m) } } }////// Set the bounds of the matcher's region.///defsetBounds(start: {start = Int32}, end: {end = Int32}, m: Matcher[r]): Result[String, Unit] \ r =try {let Matcher(m1) = m;unsafeIOasr { discardm1.$region(start#start, end#end) }; Ok(()) } catch {casee: IndexOutOfBoundsException => Err(e.getMessage()) }////// Return the start position of the current match of Matcher `m`.///defmatcherStart(m: Matcher[r]): Result[String, Int32] \ r =try {let Matcher(m1) = m; Ok(unsafeIOasr { m1.start() }) } catch {casee: IllegalStateException => Err(e.getMessage()) }////// Return the end position of the current match of Matcher `m`.///defmatcherEnd(m: Matcher[r]): Result[String, Int32] \ r =try {let Matcher(m1) = m; Ok(unsafeIOasr { m1.end() }) } catch {casee: IllegalStateException => Err(e.getMessage()) }////// Return the start and end positions of the matcher `m`.///defmatcherRange(m: Matcher[r]): Result[String, {start = Int32, end = Int32}] \ r =forM (s <- matcherStart(m);e <- matcherEnd(m) ) yield ({start = s, end = e})////// Return the text content of the current match of matcher `m`.///defmatcherContent(m: Matcher[r]): Result[String, String] \ r =try {let Matcher(m1) = m; Ok(unsafeIOasr { m1.group() }) } catch {casee: IllegalStateException => Err(e.getMessage()) }////// Return the text content of the current groups in matcher `m`.///defmatcherGroup(m: Matcher[r], i: Int32): Result[String, String] \ r =try {let Matcher(m1) = m; Ok(unsafeIOasr { if (notObject.isNull(m1.group(i))) m1.group(i) else"" }) } catch {casee: IllegalStateException => Err(e.getMessage())casee: IndexOutOfBoundsException => Err(e.getMessage()) }////// Return the number of groups in matcher `m`.///defgroupCount(m: Matcher[r]): Int32 \ r =let Matcher(m1) = m;unsafeIOasr { m1.groupCount() }////// `MatchQuery` is a read query applied to a the current match of a `Matcher`.///pubtypealiasMatchQuery[a: Type, r: Region] = Matcher[r] -> Result[String, a] \ r////// Returns the first result found by applying the query `asks` to `m` going from left to right.////// If the query `asks` or no match is found `Err(_)` is returned.///deffirstSubmatch(asks: MatchQuery[a, r], m: Matcher[r]): Result[String, a] \ r =if (find(m))asks(m)else Err("No match")////// Returns the last result found by applying the query `asks` to `m` going from left to right.////// If the query `asks` or no match is found `Err(_)` is returned.///deflastSubmatch(asks: MatchQuery[a, r], m: Matcher[r]): Result[String, a] \ r =if (findLast(m))asks(m)else Err("No match")////// Fold on all the submatches going from left to right.////// If the query `asks` fails further processing is stopped and `Err(_)` is returned.////// `f` is applied to the result of applying the query `asks` and the folds accumulating value./// The initial accumulating value is `x`.///deffoldSubmatches(f: (b, a) -> b \ ef, x: b, asks: MatchQuery[a, r], m: Matcher[r]): Result[String, b] \ { r, ef } =defloop(acc) = {matchfind(m) {casetrue => matchasks(m) {case Ok(a) => loop(f(acc, a))case Err(msg) => Err(msg) }casefalse => Ok(acc) } };loop(x)}