README

#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'].iter())
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") // 5 chars * 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 Unicode code points:

///|
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, and Double. 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 character count

#
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::compile, Unicode escapes are supported: \uXXXX and \u{X...}. \xHH is not supported in Regex::compile.

^ 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.

This is equivalent to Regex(Regex::escape(str)).

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. The string must contain at least one of:
  • An integer part (decimal digits)
  • A decimal point followed by a fractional part (decimal digits)
  • An exponent part ('e' or 'E' followed by an optional sign and decimal digits)

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

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")
}