// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
fn StringView::next_char_index(self : StringView, index : Int) -> Int {
if index < self.length() && self.unsafe_get(index).is_leading_surrogate() {
index + 2
} else {
index + 1
}
}
///|
/// Replaces all non-overlapping matches in `str` using `replacer`.
///
/// The `replacer` callback is called once for each match and receives the
/// corresponding `MatchResult`.
///
/// If `limit` is provided, at most `limit` matches are replaced.
/// When omitted, all matches are replaced.
///
/// Returns `str` unchanged when there is no match.
///
/// If the regex can match an empty string, replacement content is inserted at
/// the beginning, between characters, and at the end of `str`.
///
/// Example:
///
/// ```mbt check
/// test {
/// let regex = re"[[:digit:]]+"
/// let result = regex.replace_by("a12b3", m => "[\{m.content()}]")
/// inspect(result, content="a[12]b[3]")
/// }
/// ```
pub fn Regex::replace_by(
regex : Regex,
str : StringView,
replacer : (MatchResult) -> StringView,
limit? : Int,
) -> StringView {
// Delay buffer creation until we find the first match to avoid
// unnecessary allocation when the regex doesn't match at all.
let mut buf : StringBuilder? = None
let copy_index = for copy_index = 0, search_index = 0, replaced = 0; search_index <=
str.length(); {
if limit is Some(limit) && replaced >= limit {
break copy_index
}
match regex.execute(str, last_index=search_index) {
None => break copy_index
Some(m) => {
guard m.result.group(0) is Some((start, end)) else { break copy_index }
let b = match buf {
Some(b) => b
None => {
let b = StringBuilder()
buf = Some(b)
b
}
}
b.write_stringview(str[copy_index:start])
b.write_stringview(replacer(m))
let search_index = if start == end {
str.next_char_index(end)
} else {
end
}
continue end, search_index, replaced + 1
}
}
} nobreak {
copy_index
}
match buf {
None => str
Some(b) => {
b.write_stringview(str[copy_index:])
b.to_string()
}
}
}
///|
/// Returns an iterator over all non-overlapping matches in `str`.
///
/// Matches are produced from left to right.
///
/// If the regex can match an empty string, iteration still terminates: after
/// each empty match, search resumes at the next character.
///
/// Example:
///
/// ```mbt check
/// test {
/// let regex = re"[[:digit:]]+"
/// let matches = regex.find("a12b3").to_array()
/// inspect(matches.length(), content="2")
/// inspect(matches[0].content(), content="12")
/// inspect(matches[1].content(), content="3")
/// }
/// ```
pub fn Regex::find(regex : Regex, str : StringView) -> Iter[MatchResult] {
let mut search_index = 0
let len = str.length()
Iter::new(() => {
guard search_index <= len else { None }
match regex.execute(str, last_index=search_index) {
None => None
Some(m) => {
guard m.result.group(0) is Some((start, end)) else { return None }
search_index = if start == end { str.next_char_index(end) } else { end }
Some(m)
}
}
})
}
///|
/// Splits `str` into all segments separated by regex matches.
///
/// If there is no match, the returned iterator yields only `str`.
///
/// Consecutive matches and boundary matches produce empty segments.
///
/// If the regex can match an empty string, the result includes boundary empty
/// segments and per-character splits.
///
/// Example:
///
/// ```mbt check
/// test {
/// let regex = re","
/// let parts = regex.split("a,,b").to_array()
/// inspect(parts.length(), content="3")
/// inspect(parts[0], content="a")
/// inspect(parts[1], content="")
/// inspect(parts[2], content="b")
/// }
/// ```
pub fn Regex::split(regex : Regex, str : StringView) -> Iter[StringView] {
let mut copy_index = 0
let mut search_index = 0
let len = str.length()
let mut done = false
Iter::new(() => {
guard !done else { None }
while search_index <= len {
match regex.execute(str, last_index=search_index) {
None => {
done = true
return Some(str[copy_index:])
}
Some(m) => {
guard m.result.group(0) is Some((start, end)) else {
done = true
return Some(str[copy_index:])
}
let part = str[copy_index:start]
copy_index = end
search_index = if start == end {
str.next_char_index(end)
} else {
end
}
return Some(part)
}
}
}
done = true
Some(str[copy_index:])
})
}