// UPPER and LOWER: Unicode default case conversion (full mappings,
// locale-independent; spec M4d1 ยง5).
///|
/// The General_Category values case conversion needs, by the ordinals of
/// `@general_category.lookup_general_category` (UAX #44 order: Lu 0, Ll 1,
/// Lt 2, Lm 3, Lo 4, Mn 5, Mc 6, Me 7, ..., Sk 23, ..., Cf 26).
priv enum CaseCategory {
Lt
Lm
Mn
Me
Sk
Cf
}
///|
fn case_category(c : Char) -> CaseCategory? {
match @general_category.lookup_general_category(c) {
2 => Some(Lt)
3 => Some(Lm)
5 => Some(Mn)
7 => Some(Me)
23 => Some(Sk)
26 => Some(Cf)
_ => None
}
}
///|
/// Word_Break MidLetter, MidNumLet and Single_Quote (WordBreakProperty.txt,
/// Unicode 18.0.0).
let word_break_mid : Array[Char] = [
'\u{0027}', '\u{002E}', '\u{003A}', '\u{00B7}', '\u{0387}', '\u{055F}', '\u{05F4}',
'\u{2018}', '\u{2019}', '\u{2024}', '\u{2027}', '\u{FE13}', '\u{FE52}', '\u{FE55}',
'\u{FF07}', '\u{FF0E}', '\u{FF1A}',
]
///|
/// Cased (Unicode 3.13): Lowercase, Uppercase or General_Category Lt.
fn is_cased(c : Char) -> Bool {
@lowercase.is_lowercase(c) ||
@uppercase.is_uppercase(c) ||
case_category(c) is Some(Lt)
}
///|
/// Case_Ignorable (Unicode 3.13).
fn is_case_ignorable(c : Char) -> Bool {
case_category(c) is Some(Mn | Me | Cf | Lm | Sk) || word_break_mid.contains(c)
}
///|
/// Whether a cased letter is reached from `at` by stepping `step` (-1 or +1)
/// over case-ignorable characters only.
fn cased_near(cs : Array[Char], at : Int32, step : Int32) -> Bool {
let mut i = at + step
while i >= 0 && i < cs.length() {
if is_cased(cs[i]) {
return true
}
if !is_case_ignorable(cs[i]) {
return false
}
i += step
}
false
}
///|
/// `UPPER(s)`.
pub fn upper(args : Array[@value.Value]) -> @value.Value raise OpFailure {
apply("UPPER", [Text], args, xs => {
match xs {
[String(v)] => {
let b = StringBuilder()
for c in v {
b.write_string(@case_mapping.full_uppercase(c))
}
@value.Value::from_string(b.to_string())
}
_ => raise TypeFailure("UPPER of a value that is not a string")
}
})
}
///|
/// `LOWER(s)`, with the Final_Sigma context: U+03A3 becomes U+03C2 after a
/// cased letter and not before one (case-ignorable characters skipped).
pub fn lower(args : Array[@value.Value]) -> @value.Value raise OpFailure {
apply("LOWER", [Text], args, xs => {
match xs {
[String(v)] => {
let cs = chars_of(v)
let b = StringBuilder()
for i, c in cs {
if c == '\u{03A3}' {
let final_ = cased_near(cs, i, -1) && !cased_near(cs, i, 1)
b.write_char(if final_ { '\u{03C2}' } else { '\u{03C3}' })
} else {
b.write_string(@case_mapping.full_lowercase(c))
}
}
@value.Value::from_string(b.to_string())
}
_ => raise TypeFailure("LOWER of a value that is not a string")
}
})
}