///|
fn char_in_ranges(ch : Char, ranges : ReadOnlyArray[Char]) -> Bool {
for left = 0, right = ranges.length() / 2 - 1; left <= right; {
// invariant : left >= 0 && right >= 0 && right < ranges.length() / 2
// decreases : right - left + 1
// assert : left <= right
let mid = (left + right) / 2
let start = ranges[mid * 2]
let end = ranges[mid * 2 + 1]
if ch < start {
continue left, mid - 1
} else if ch > end {
continue mid + 1, right
} else {
return true
}
} nobreak {
false
}
}
///|
fn unicode_category_has(ch : Char, category : String) -> Bool {
match category {
"L" => char_in_ranges(ch, unicode_ranges_L)
"Nl" => char_in_ranges(ch, unicode_ranges_NL)
"Lu" => char_in_ranges(ch, unicode_ranges_LU)
"Ll" => char_in_ranges(ch, unicode_ranges_LL)
"Lt" => char_in_ranges(ch, unicode_ranges_LT)
_ => false
}
}
///|
pub struct UnicodeChar {
value : Char
}
///|
/// Wrap a `Char` to access Unicode helpers with chaining.
///
/// # Example
/// ```mbt check
/// test "unicode char wrapper" {
/// inspect(UnicodeChar::new('A').is_uppercase(), content="true")
/// }
/// ```
pub fn UnicodeChar::new(ch : Char) -> UnicodeChar {
{ value: ch }
}
///|
pub struct UnicodeString {
value : String
}
///|
/// Wrap a `String` to access Unicode helpers with chaining.
///
/// # Example
/// ```mbt check
/// test "unicode string wrapper" {
/// inspect(
/// UnicodeString::new("e\u{301}").normalize_nfc().foldcase().into_string(),
/// content="\u{00e9}",
/// )
/// }
/// ```
pub fn UnicodeString::new(s : String) -> UnicodeString {
{ value: s }
}
///|
/// Unwrap a Unicode string wrapper back into a `String`.
///
/// # Example
/// ```mbt check
/// test "unicode string into_string" {
/// let wrapped = UnicodeString::new("Hi")
/// inspect(wrapped.into_string(), content="Hi")
/// }
/// ```
pub fn UnicodeString::into_string(self : UnicodeString) -> String {
self.value
}
///|
/// Return true if the character is alphabetic.
///
/// # Example
/// ```mbt check
/// test "alphabetic predicate" {
/// inspect(UnicodeChar::new('A').is_alphabetic(), content="true")
/// inspect(UnicodeChar::new('1').is_alphabetic(), content="false")
/// }
/// ```
pub fn UnicodeChar::is_alphabetic(self : UnicodeChar) -> Bool {
let ch = self.value
unicode_category_has(ch, "L") || unicode_category_has(ch, "Nl")
}
///|
/// Return true if the character is uppercase.
///
/// # Example
/// ```mbt check
/// test "uppercase predicate" {
/// inspect(UnicodeChar::new('A').is_uppercase(), content="true")
/// inspect(UnicodeChar::new('a').is_uppercase(), content="false")
/// }
/// ```
pub fn UnicodeChar::is_uppercase(self : UnicodeChar) -> Bool {
unicode_category_has(self.value, "Lu")
}
///|
/// Return true if the character is lowercase.
///
/// # Example
/// ```mbt check
/// test "lowercase predicate" {
/// inspect(UnicodeChar::new('a').is_lowercase(), content="true")
/// inspect(UnicodeChar::new('A').is_lowercase(), content="false")
/// }
/// ```
pub fn UnicodeChar::is_lowercase(self : UnicodeChar) -> Bool {
unicode_category_has(self.value, "Ll")
}
///|
let unicode_general_category_order : ReadOnlyArray[String] = [
"Lu", "Ll", "Lt", "Lm", "Lo", "Mn", "Mc", "Me", "Nd", "Nl", "No", "Pc", "Pd", "Ps",
"Pe", "Pi", "Pf", "Po", "Sm", "Sc", "Sk", "So", "Zs", "Zl", "Zp", "Cc", "Cf", "Cs",
"Co", "Cn",
]
///|
/// Return the general category tag for the character.
///
/// # Example
/// ```mbt check
/// test "general category" {
/// inspect(UnicodeChar::new('A').general_category(), content="Lu")
/// }
/// ```
pub fn UnicodeChar::general_category(self : UnicodeChar) -> String {
let ch = self.value
for category in unicode_general_category_order {
match general_category_ranges.get(category) {
Some(ranges) => if char_in_ranges(ch, ranges) { return category }
None => ()
}
}
"Cn"
}
///|
///|
fn unicode_case_equivalents(ch : Char) -> Array[Char] {
let out : Array[Char] = [ch]
for cur = ch; true; {
// invariant : out.length() >= 1
// TODO(decreases) : cycle detection uses out; possible bug
// assert : out.length() >= 1
match case_folding.get(cur) {
Some(next) => {
if out.contains(next) {
break
}
out.push(next)
continue next
}
None => break
}
} nobreak {
()
}
out
}
///|
fn unicode_pick_equivalent_category(
chars : Array[Char],
category : String,
) -> Char? {
let ranges = match category {
"Lu" => unicode_ranges_LU
"Ll" => unicode_ranges_LL
"Lt" => unicode_ranges_LT
_ => return None
}
for ch in chars {
if char_in_ranges(ch, ranges) {
return Some(ch)
}
}
None
}
///|
fn unicode_min_equivalent(chars : Array[Char], fallback : Char) -> Char {
if chars.length() == 0 {
return fallback
}
for i = 1, min = chars[0]; i < chars.length(); {
// invariant : i >= 1 && i <= chars.length()
// decreases : chars.length() - i
// assert : i < chars.length()
let ch = chars[i]
if ch < min {
continue i + 1, ch
}
continue i + 1, min
} nobreak {
min
}
}
///|
/// Uppercase a character using Unicode case mapping.
///
/// # Example
/// ```mbt check
/// test "char upcase" {
/// inspect(UnicodeChar::new('a').upcase(), content="A")
/// }
/// ```
pub fn UnicodeChar::upcase(self : UnicodeChar) -> Char {
let ch = self.value
let equiv = unicode_case_equivalents(ch)
match unicode_pick_equivalent_category(equiv, "Lu") {
Some(upper) => upper
None =>
match unicode_pick_equivalent_category(equiv, "Lt") {
Some(title) => title
None => ch
}
}
}
///|
/// Lowercase a character using Unicode case mapping.
///
/// # Example
/// ```mbt check
/// test "char downcase" {
/// inspect(UnicodeChar::new('A').downcase(), content="a")
/// }
/// ```
pub fn UnicodeChar::downcase(self : UnicodeChar) -> Char {
let ch = self.value
let equiv = unicode_case_equivalents(ch)
match unicode_pick_equivalent_category(equiv, "Ll") {
Some(lower) => lower
None => ch
}
}
///|
/// Case-fold a character for case-insensitive comparison.
///
/// # Example
/// ```mbt check
/// test "char foldcase" {
/// inspect(UnicodeChar::new('A').foldcase(), content="a")
/// }
/// ```
pub fn UnicodeChar::foldcase(self : UnicodeChar) -> Char {
let ch = self.value
let equiv = unicode_case_equivalents(ch)
match unicode_pick_equivalent_category(equiv, "Ll") {
Some(lower) => lower
None => unicode_min_equivalent(equiv, ch)
}
}
///|
/// Uppercase a string using Unicode case mapping.
///
/// # Example
/// ```mbt check
/// test "string upcase" {
/// inspect(UnicodeString::new("abc").upcase().into_string(), content="ABC")
/// }
/// ```
pub fn UnicodeString::upcase(self : UnicodeString) -> UnicodeString {
let buf = StringBuilder()
for ch in self.value {
buf.write_char(UnicodeChar::new(ch).upcase())
}
{ value: buf.to_string() }
}
///|
/// Lowercase a string using Unicode case mapping.
///
/// # Example
/// ```mbt check
/// test "string downcase" {
/// inspect(UnicodeString::new("ABC").downcase().into_string(), content="abc")
/// }
/// ```
pub fn UnicodeString::downcase(self : UnicodeString) -> UnicodeString {
let buf = StringBuilder()
for ch in self.value {
buf.write_char(UnicodeChar::new(ch).downcase())
}
{ value: buf.to_string() }
}
///|
/// Case-fold a string for case-insensitive comparison.
///
/// # Example
/// ```mbt check
/// test "string foldcase" {
/// inspect(UnicodeString::new("ABC").foldcase().into_string(), content="abc")
/// }
/// ```
pub fn UnicodeString::foldcase(self : UnicodeString) -> UnicodeString {
let buf = StringBuilder()
for ch in self.value {
buf.write_char(UnicodeChar::new(ch).foldcase())
}
{ value: buf.to_string() }
}