#String Package Documentation

    This package provides comprehensive string manipulation utilities for MoonBit, including string creation, conversion, searching, and Unicode handling.

    #String Creation and Conversion

    Create strings from various sources:

    ///|
    test "string creation" {
    // From character array
    let chars : ReadOnlyArray[Char] = ['H', 'e', 'l', 'l', 'o']
    let str1 = String::from_array(chars)
    inspect(str1, content="Hello")

    // From character iterator
    let str2 = String::from_iter('W', 'o', 'r', 'l', 'd')
    inspect(str2, content="World")

    // Default empty string
    let empty = ""
    inspect(empty, content="")
    }

    #String Iteration

    Iterate over Unicode characters in strings:

    ///|
    test "string iteration" {
    let text = "Hello🌍"

    // Forward iteration
    let chars = text.iter().collect()
    debug_inspect(chars, content="['H', 'e', 'l', 'l', 'o', '🌍']")

    // Reverse iteration
    let reversed = text.rev_iter().collect()
    debug_inspect(reversed, content="['🌍', 'o', 'l', 'l', 'e', 'H']")

    // Iteration with indices - demonstrate iter2 functionality
    let mut count = 0
    let mut first_char = 'a'
    text
    .iter2()
    .each(fn(idx, char) {
    if idx == 0 {
    first_char = char
    }
    count = count + 1
    })
    inspect(first_char, content="H")
    inspect(count, content="6") // 6 Unicode characters
    }

    #String Conversion

    Convert strings to other formats:

    ///|
    test "string conversion" {
    let text = "Hello 你好"

    // Convert to character array
    let chars = text.to_array()
    debug_inspect(chars, content="['H', 'e', 'l', 'l', 'o', ' ', '你', '好']")

    // Convert to bytes (UTF-8 encoding)
    let bytes = @utf8.encode(text) // Use UTF-8 encoding
    inspect(bytes.length(), content="12")
    debug_inspect(chars, content="['H', 'e', 'l', 'l', 'o', ' ', '你', '好']")

    // Convert to bytes (UTF-16 LE encoding)
    let bytes = @utf16.encode(text)
    inspect(bytes.length(), content="16") // 8 code units * 2 bytes each
    }

    #Unicode Handling

    Work with Unicode characters and surrogate pairs:

    ///|
    test "unicode handling" {
    let emoji_text = "Hello🤣World"

    // Character count vs UTF-16 code unit count
    let char_count = emoji_text.iter().count()
    let code_unit_count = emoji_text.length()
    inspect(char_count, content="11") // Unicode characters
    inspect(code_unit_count, content="12") // UTF-16 code units

    // Find character offset
    let offset = emoji_text.offset_of_nth_char(5) // Position of emoji
    debug_inspect(offset, content="Some(5)")

    // Test character length
    let has_11_chars = emoji_text.char_length_eq(11)
    inspect(has_11_chars, content="true")
    }

    #String Comparison

    Strings are ordered using shortlex order by their UTF-16 code units:

    ///|
    test "string comparison" {
    let result1 = "apple".compare("banana")
    inspect(result1, content="-1") // apple < banana
    let result2 = "hello".compare("hello")
    inspect(result2, content="0") // equal
    let result3 = "zebra".compare("apple")
    inspect(result3, content="1") // zebra > apple
    }

    #String Views

    String views provide efficient substring operations without copying. A String stores UTF-16 code units, and a view is just a {str, start, end} window into those units. A character outside the Basic Multilingual Plane is stored as a surrogate pair, and the s[start:end] slice syntax panics rather than split one:

    direction: right str: "String \"a😀b\" — UTF-16 code units" { u0: "[0] 'a'" u1: "[1] 0xD83D high surrogate" u2: "[2] 0xDE00 low surrogate" u3: "[3] 'b'" } view: "StringView {str, start, end}\ns[1:3] is the full 😀 (ok)\ns[2:4] starts inside 😀 → panics" view -> str: "zero copy, indices are code units"

    s[i] returns the code unit at i, s.get_char(i) decodes a full Char, and for c in s iterates characters (decoding surrogate pairs):

    ///|
    test "string views" {
    let text = "Hello, World!"
    let view = text[:][7:12] // "World" - create view using slice notation

    // Views support similar operations as strings
    let chars = view.iter().collect()
    debug_inspect(chars, content="['W', 'o', 'r', 'l', 'd']")

    // Convert view back to string
    let substring = view.to_owned()
    inspect(substring, content="World")
    }

    #Practical Examples

    Common string manipulation tasks:

    ///|
    test "practical examples" {
    let text = "The quick brown fox"

    // Split into words (using whitespace) - returns Iter[View]
    let words = text.split(" ").collect()
    inspect(words.length(), content="4")
    inspect(words[0].to_owned(), content="The")
    inspect(words[3].to_owned(), content="fox")

    // Join words back together - convert views to strings first
    let word_strings = words.map(fn(v) { v.to_owned() })
    let mut result = ""
    for i, word in word_strings.iter2() {
    if i > 0 {
    result = result + "-"
    }
    result = result + word
    }
    inspect(result, content="The-quick-brown-fox")

    // Case conversion (works on views)
    let upper = text[:].to_upper().to_owned()
    inspect(upper, content="THE QUICK BROWN FOX")
    let lower = text[:].to_lower().to_owned()
    inspect(lower, content="the quick brown fox")
    }

    #Regular Expressions

    Use Regex for string regex matching, replacement, and splitting:

    • Syntax follows MoonBit lexmatch regex literals.
    • Supported constructs include ., character classes ([abc], [^abc], [a-z], [[:digit:]]), quantifiers (*, +, ?, {n}, {n,}, {n,m} with non-greedy forms), grouping/alternation ((...), (?:...) for non-capturing groups, (?<name>...), a|b), and assertions/modifiers (^, $, \b, \B, (?i:...)).
    • Common escapes include \n, \r, \t, \f, \v; Regex also supports Unicode escapes \uXXXX and \u{X...}; \xHH is not supported in Regex.
    • ^ and $ are non-multiline anchors: they match only the start/end of the whole input, not per-line boundaries.
    • \d, \D, \s, \S, \w, and \W are not supported; use POSIX character classes such as [[:digit:]], [[:space:]], [[:word:]].
    • See https://github.com/moonbitlang/lexmatch_spec for full grammar.

    ///|
    test "string regex basics" {
    let regex =

    guard regex.execute("id=42") is Some(m) else { fail("Expected match") }
    inspect(m.content(), content="42")

    let replaced = regex.replace_by("a1b22", _m => "#")
    inspect(replaced, content="a#b#")

    let replaced_limited = regex.replace_by("a1b22c333", _m => "#", limit=2)
    inspect(replaced_limited, content="a#b#c333")
    }

    #Parsing

    Parse primitive types from strings. Prefer the type-directed from_str API for ordinary parsing. All parsing APIs raise on invalid input.

    ///|
    test "from_str" {
    let i : Int = @string.from_str("42")
    inspect(i, content="42")
    let negative : Int = @string.from_str("-17")
    inspect(negative, content="-17")
    let u : UInt = @string.from_str("42")
    inspect(u, content="42")
    let i64 : Int64 = @string.from_str("9999999999")
    inspect(i64, content="9999999999")
    let u64 : UInt64 = @string.from_str("18446744073709551615")
    inspect(u64, content="18446744073709551615")
    let d : Double = @string.from_str("3.14")
    inspect(d, content="3.14")
    let b : Bool = @string.from_str("true")
    inspect(b, content="true")
    }

    The FromStr trait provides from_str() for Bool, Int, Int64, UInt, UInt64, Double, and @bigint.BigInt. Use concrete parsers when you need parser-specific options or an explicit parser name. For example, integer parsers accept an optional base:

    ///|
    test "parse integers with base" {
    inspect(@string.parse_int("ff", base=16), content="255")
    inspect(@string.parse_int("101", base=2), content="5")
    inspect(@string.parse_uint64("ff_ff", base=16), content="65535")
    }

    #Regex Match Results

    MatchResult gives access to the matched text, its context, and capture groups:

    ///|
    test "match result" {
    let re =
    guard re.execute("key=42") is Some(m) else { fail("no match") }
    inspect(m.content(), content="key=42")
    inspect(m.before(), content="")
    inspect(m.after(), content="")
    debug_inspect(m.group(1), content="Some(<StringView: \"key\">)")
    debug_inspect(m.group(2), content="Some(<StringView: \"42\">)")
    }

    Named capture groups via (?<name>...):

    ///|
    test "named groups" {
    let re =
    guard re.execute("age:30") is Some(m) else { fail("no match") }
    debug_inspect(m.named_group("name"), content="Some(<StringView: \"age\">)")
    debug_inspect(m.named_group("val"), content="Some(<StringView: \"30\">)")
    }

    #Regex Find & Split

    find() returns an iterator over all non-overlapping matches. split() splits a string at each match boundary.

    ///|
    test "find and split" {
    let digits =
    let matches = digits
    .find("a1b22c333")
    .map(fn(m) { m.content().to_owned() })
    .collect()
    debug_inspect(
    matches,
    content=(
    #|["1", "22", "333"]
    ),
    )
    let parts = digits.split("a1b22c333").map(fn(v) { v.to_owned() }).collect()
    debug_inspect(
    parts,
    content=(
    #|["a", "b", "c", ""]
    ),
    )
    }

    #Regex Combinators

    Build complex patterns programmatically with Regex::string(), Regex::repeat(), Regex::capture(), + (sequence), and | (alternation):

    ///|
    test "regex combinators" {
    // match "abc" literally
    let abc = @string.Regex::string("abc")
    inspect(abc.execute("xabcy") is Some(_), content="true")
    // repeat: match 2 to 4 digits
    let digits = .repeat(min=2, max=4)
    guard digits.execute("a12345") is Some(m) else { fail("no match") }
    inspect(m.content(), content="1234") // greedy: takes max
    // alternation with |
    let either = @string.Regex::string("cat") | @string.Regex::string("dog")
    inspect(either.execute("I have a dog") is Some(_), content="true")
    }

    #Performance Notes

    • Use StringBuilder or Buffer for building strings incrementally rather than repeated concatenation
    • String views are lightweight and don't copy the underlying data
    • Unicode iteration handles surrogate pairs correctly but is slower than UTF-16 code unit iteration
    • Character length operations (char_length_eq, char_length_ge) have O(n) complexity where n is the length passed in, not the whole string's length

    ToStringView

    Trait for values that can be viewed as StringView.

    Types implementing this trait provide zero-copy access to string-like data.

    View

    using @moonbitlang/core/builtin { type StringView as View }

    Type View used by this package APIs.

    FromStr

    pub(open) trait FromStr {
    #as_free_fn
    fn from_str(StringView) -> Self raise
    }

    Trait for parsing values from textual input.
    impl FromStr for Bool
    impl FromStr for Int
    impl FromStr for Int64
    impl FromStr for UInt
    impl FromStr for UInt64
    impl FromStr for Double
    impl FromStr for BigInt

    MatchResult

    type MatchResult derive(
    Debug
    )

    Result of one successful match produced by Regex::execute.

    MatchResult::after

    fn MatchResult::after(self : MatchResult) -> StringView

    Return match after view.

    MatchResult::before

    fn MatchResult::before(self : MatchResult) -> StringView

    Return match before view.

    MatchResult::content

    fn MatchResult::content(self : MatchResult) -> StringView

    Return match content view.

    MatchResult::group

    fn MatchResult::group(self : MatchResult, group_index : Int) -> StringView?

    Access capture group information.

    MatchResult::named_group

    fn MatchResult::named_group(self : MatchResult, name : String) -> StringView?

    Access capture named_group information.

    Regex

    pub struct Regex {
    // private fields
    }

    A compiled regular expression for string-oriented matching.
    impl Add for Regex
    impl BitOr for Regex

    Regex::Regex

    #alias(new, deprecated="Use `Regex()` instead")
    fn Regex::Regex(pattern : StringView) -> Regex raise

    Compiles a regex pattern string into a Regex object.

    The regex syntax follows MoonBit lexmatch regex literals. The following constructs are recognized:

    • . wildcard (matches any character, including newline)
    • Character classes: [abc], [^abc], [a-z], POSIX classes such as [[:digit:]], [[:alpha:]], [[:space:]], [[:word:]]
    • Quantifiers: *, +, ?, {n}, {n,}, {n,m} and non-greedy forms *?, +?, ??, {n}?, {n,}?, {n,m}?
    • Grouping and alternation: ( ... ), (?: ... ) (non-capturing), (?<name> ... ), a|b
    • Assertions and modifiers: ^, $, \b, \B, (?i: ... )

    Escape sequences include \n, \r, \t, \f, \v, and escaped metacharacters. In Regex, Unicode escapes are supported: \uXXXX and \u{X...}. \xHH is not supported in Regex.

    ^ and $ are non-multiline anchors: they match only the beginning and end of the whole input, not per-line boundaries.

    \d, \D, \s, \S, \w, and \W are not supported; use POSIX character classes instead.

    POSIX character classes are ASCII-based.

    In character classes, the dash - is used to specify ranges (e.g., [a-z]). To match a literal dash, it must be escaped as \-. Placing a dash at the start or end of a character class (e.g., [-a] or [a-]) is not supported.

    For full grammar and semantics, see: https://github.com/moonbitlang/lexmatch_spec

    Raises when pattern is not a valid regex pattern.

    Example:

    test {
    let regex =
    guard regex.execute("a12b") is Some(m) else { fail("Expected match") }
    inspect(m.content(), content="12")
    }

    Regex::add

    fn Regex::add(self : Regex, other : Regex) -> Regex

    Regex::capture

    fn Regex::capture(self : Regex, group_name : String) -> Regex

    Wraps this regex in a named capture group.

    Returns a new regex that captures the entire match of self with the specified group_name.

    Example:

    test {
    let digit = .capture("number")
    let regex = @string.Regex::string("ID: ") + digit
    guard regex.execute("ID: 12345") is Some(m) else { fail("Expected match") }
    debug_inspect(
    m.named_group("number"),
    content=(
    #|Some(<StringView: "12345">)
    ),
    )
    }

    test {
    let user = .capture("user")
    let domain = .capture("domain")
    let tld = .capture("tld")
    let email = user +
    @string.Regex::string("@") +
    domain +
    @string.Regex::string(".") +
    tld
    guard email.execute("john@example.com") is Some(m) else {
    fail("Expected match")
    }
    debug_inspect(
    m.named_group("user"),
    content=(
    #|Some(<StringView: "john">)
    ),
    )
    debug_inspect(
    m.named_group("domain"),
    content=(
    #|Some(<StringView: "example">)
    ),
    )
    debug_inspect(
    m.named_group("tld"),
    content=(
    #|Some(<StringView: "com">)
    ),
    )
    }

    Regex::execute

    fn Regex::execute(self : Regex, input : StringView, last_index? : Int) -> MatchResult?

    Executes this regex on input and returns the first match found.

    Search starts at last_index (default 0). The returned match, when present, starts at or after that index.

    last_index must satisfy 0 <= last_index <= input.length().

    For inputs containing supplementary Unicode characters, last_index must also be a valid UTF-16 character boundary (that is, not the second code unit of a surrogate pair).

    Passing a last_index in the middle of a surrogate pair may produce match offsets that later cause MatchResult::before, MatchResult::content, or MatchResult::after to panic when slicing.

    last_index only controls where searching starts. It does not change anchor semantics:

    • ^ still matches only the beginning of input
    • $ still matches only the end of input

    This parameter is needed by iterative operations such as Regex::find, Regex::replace_by, and Regex::split, which repeatedly resume searching from the end of the previous match while keeping anchor behavior relative to the full input.

    Returns None when there is no match from last_index to the end of input.

    Example:

    test {
    let regex =
    let input = "a12b34"

    guard regex.execute(input) is Some(first) else {
    fail("Expected first match")
    }
    inspect(first.content(), content="12")

    let next = first.before().length() + first.content().length()
    guard regex.execute(input, last_index=next) is Some(second) else {
    fail("Expected second match")
    }
    inspect(second.content(), content="34")
    }

    test {
    let anchored =
    inspect(anchored.execute("ab", last_index=0) is Some(_), content="true")
    inspect(anchored.execute("xaby", last_index=1) is Some(_), content="false")
    }

    Regex::find

    fn Regex::find(regex : Regex, str : StringView) -> Iter[MatchResult]

    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:

    test {
    let regex =
    let matches = regex.find("a12b3").to_array()
    inspect(matches.length(), content="2")
    inspect(matches[0].content(), content="12")
    inspect(matches[1].content(), content="3")
    }

    Regex::lor

    fn Regex::lor(self : Regex, other : Regex) -> Regex

    Regex::repeat

    fn Regex::repeat(self : Regex, min? : Int, max? : Int, greedy? : Bool) -> Regex

    Repeats this regex with a quantifier and returns a new regex.

    • min is the minimum number of repetitions (default 0)
    • max is the optional maximum number of repetitions
    • greedy controls whether matching is greedy or non-greedy
    • Panics if min < 0, min > 256, max < min, or max > 256

    Example:

    test {
    let greedy = .repeat(min=2, max=4)
    guard greedy.execute("a12345") is Some(m1) else { fail("Expected match") }
    inspect(m1.content(), content="1234")

    let nongreedy = .repeat(min=2, max=4, greedy=false)
    guard nongreedy.execute("a12345") is Some(m2) else { fail("Expected match") }
    inspect(m2.content(), content="12")
    }

    Regex::replace_by

    fn Regex::replace_by(regex : Regex, str : StringView, replacer : (MatchResult) -> StringView, limit? : Int) -> StringView

    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:

    test {
    let regex =
    let result = regex.replace_by("a12b3", m => "[\{m.content()}]")
    inspect(result, content="a[12]b[3]")
    }

    Regex::split

    fn Regex::split(regex : Regex, str : StringView) -> Iter[StringView]

    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:

    test {
    let regex =
    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")
    }

    Regex::string

    fn Regex::string(str : StringView) -> Regex

    Builds a regex that matches str literally.

    Every character of str is matched literally; regex metacharacters have no special meaning.

    Example:

    test {
    let regex = @string.Regex::string("a+b(c)")
    inspect(regex.execute("a+b(c)") is Some(_), content="true")
    inspect(regex.execute("abcc") is Some(_), content="false")
    }

    Regex::unsafe_from_string

    fn Regex::unsafe_from_string(pattern : StringView) -> Regex

    Compiles a regex pattern string into a Regex object, panicking on invalid patterns.

    This function is equivalent to Regex(pattern) but converts any compilation error into a panic instead of raising an exception.

    The regex syntax follows the same rules as Regex.

    Panics when pattern is not a valid regex pattern.

    Example:

    test {
    let regex = @string.Regex::unsafe_from_string("[[:digit:]]+")
    guard regex.execute("a12b") is Some(m) else { fail("Expected match") }
    inspect(m.content(), content="12")
    }

    parse_bigint

    fn parse_bigint(str : StringView, base? : Int) ->
    BigInt
    raise

    Parses a string into a BigInt using the specified base (2 to 36).

    Returns an error if the input is malformed or the base is invalid.

    Example

    test {
    inspect(@string.parse_bigint("12345"), content="12345")
    inspect(@string.parse_bigint("-ff", base=16), content="-255")
    }

    parse_bool

    fn parse_bool(str : StringView) -> Bool raise

    Parse a string and return the represented boolean value or an error.

    parse_double

    fn parse_double(str : StringView) -> Double raise

    Parse a string into a double precision floating point number. Except for the infinity and NaN spellings listed below, the string must contain at least one decimal digit, in:
    • An integer part (decimal digits), and/or
    • A fractional part (decimal digits) after a decimal point

    An optional exponent part ('e' or 'E' followed by an optional sign and decimal digits) may follow those digits, but is not sufficient on its own.

    The string may optionally start with a sign ('+' or '-'). For readability, underscores may appear between digits.

    The digit-free forms "inf", "infinity" and "nan" are also accepted, in any letter case and with an optional leading sign, yielding the corresponding infinity or NaN value.

    Examples:
    test {
    inspect(@strconv.parse_double("123"), content="123")
    inspect(@strconv.parse_double("12.34"), content="12.34")
    inspect(@strconv.parse_double(".123"), content="0.123")
    inspect(@strconv.parse_double("1e5"), content="100000")
    inspect(@strconv.parse_double("1.2e-3"), content="0.0012")
    inspect(@strconv.parse_double("1_234.5"), content="1234.5")
    }

    An exponent value exp scales the mantissa (significand) by 10^exp. For example, "1.23e2" represents 1.23 × 10² = 123.

    parse_int

    fn parse_int(str : StringView, base? : Int) -> Int raise

    Parse a string in the given base (0, 2 to 36), return a Int number or an error. If the base argument is 0, the base will be inferred by the prefix.

    parse_int64

    fn parse_int64(str : StringView, base? : Int) -> Int64 raise

    Parses a string into an Int64 number using the specified base, or returns an error. The base must be 0 or between 2 and 36 (inclusive). If base is 0, it will be inferred from the string prefix:
    • "0x" or "0X" for base 16 (hex)
    • "0o" or "0O" for base 8 (octal)
    • "0b" or "0B" for base 2 (binary)
    • Default is base 10 (decimal) For readability, underscores may appear after base prefixes or between digits. These underscores do not affect the value. Examples:
    test {
    inspect(@strconv.parse_int64("123"), content="123")
    inspect(@strconv.parse_int64("0xff", base=0), content="255")
    inspect(@strconv.parse_int64("0o10"), content="8")
    inspect(@strconv.parse_int64("0b1010"), content="10")
    inspect(@strconv.parse_int64("1_234"), content="1234")
    inspect(@strconv.parse_int64("-123"), content="-123")
    inspect(@strconv.parse_int64("ff", base=16), content="255")
    inspect(@strconv.parse_int64("zz", base=36), content="1295")
    }

    parse_uint

    fn parse_uint(str : StringView, base? : Int) -> UInt raise

    Parse a string in the given base (0, 2 to 36), return an UInt number or an error. If the base argument is 0, the base will be inferred by the prefix.

    parse_uint64

    fn parse_uint64(str : StringView, base? : Int) -> UInt64 raise

    Parses a string into a UInt64 number using the specified base, or returns an error. The base must be 0 or between 2 and 36 (inclusive). If base is 0, it will be inferred from the string prefix:
    • "0x" or "0X" for base 16 (hex)
    • "0o" or "0O" for base 8 (octal)
    • "0b" or "0B" for base 2 (binary)
    • Default is base 10 (decimal) For readability, underscores may appear after base prefixes or between digits. These underscores do not affect the value. Examples:
    test {
    inspect(@strconv.parse_uint64("123"), content="123")
    inspect(@strconv.parse_uint64("0xff", base=0), content="255")
    inspect(@strconv.parse_uint64("0o10"), content="8")
    inspect(@strconv.parse_uint64("0b1010"), content="10")
    inspect(@strconv.parse_uint64("1_234"), content="1234")
    inspect(@strconv.parse_uint64("ff", base=16), content="255")
    inspect(@strconv.parse_uint64("zz", base=36), content="1295")
    }