use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::rc::Rc;
use icu_collator::options::{AlternateHandling, CaseLevel, CollatorOptions, Strength};
use icu_collator::{Collator, CollatorBorrowed, CollatorPreferences};
use icu_locale_core::extensions::unicode::{key, Key};
use icu_locale_core::Locale;
pub const CLDR_VERSION: &str = "48";
thread_local! {
static COLLATORS: RefCell<HashMap<String, Option<Rc<CollatorBorrowed<'static>>>>> =
RefCell::new(HashMap::new());
}
fn cached(tag: &str) -> Option<Rc<CollatorBorrowed<'static>>> {
COLLATORS.with(|cache| {
cache
.borrow_mut()
.entry(tag.to_string())
.or_insert_with(|| build(tag).map(Rc::new))
.clone()
})
}
fn build(tag: &str) -> Option<CollatorBorrowed<'static>> {
let locale: Locale = tag.parse().ok()?;
let mut options = CollatorOptions::default();
options.strength = Some(match keyword(&locale, key!("ks")).as_deref() {
None | Some("level3") => Strength::Tertiary,
Some("level1") => Strength::Primary,
Some("level2") => Strength::Secondary,
Some("identic") => Strength::Identical,
Some(_) => return None,
});
options.alternate_handling = Some(match keyword(&locale, key!("ka")).as_deref() {
None | Some("noignore") => AlternateHandling::NonIgnorable,
Some("shifted") => AlternateHandling::Shifted,
Some(_) => return None,
});
options.case_level = Some(match keyword(&locale, key!("kc")).as_deref() {
None | Some("false") => CaseLevel::Off,
Some("true") => CaseLevel::On,
Some(_) => return None,
});
Collator::try_new(CollatorPreferences::from(&locale), options).ok()
}
fn keyword(locale: &Locale, k: Key) -> Option<String> {
locale
.extensions
.unicode
.keywords
.get(&k)
.map(|v| v.to_string())
}
pub fn resolves(tag: &str) -> bool {
cached(tag).is_some()
}
pub fn compare(tag: &str, a: &str, b: &str) -> Option<Ordering> {
Some(cached(tag)?.compare(a, b))
}
pub(crate) fn range_ok(tag: &str, value: &str, lo: Option<&str>, hi: Option<&str>) -> Option<bool> {
let coll = cached(tag)?;
let above = lo.is_none_or(|l| coll.compare(l, value) != Ordering::Greater);
let below = hi.is_none_or(|h| coll.compare(value, h) != Ordering::Greater);
Some(above && below)
}
pub(crate) fn enum_has(tag: &str, value: &str, vs: &[String]) -> Option<bool> {
let coll = cached(tag)?;
Some(vs.iter().any(|v| coll.compare(v, value) == Ordering::Equal))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tertiary_default_orders_accents_with_base() {
assert_eq!(compare("fr-CA", "étude", "zebra"), Some(Ordering::Less));
assert_eq!(compare("fr-CA", "café", "cafe"), Some(Ordering::Greater));
}
#[test]
fn equality_crosses_normalization_forms() {
assert_eq!(
compare("fr", "caf\u{e9}", "cafe\u{301}"),
Some(Ordering::Equal)
);
}
#[test]
fn strength_overrides() {
assert_eq!(
compare("en-u-ks-level1", "resume", "résumé"),
Some(Ordering::Equal)
);
assert_eq!(
compare("en-u-ks-level2", "Cafe", "cafe"),
Some(Ordering::Equal)
);
assert_eq!(
compare("en-u-ks-level2", "café", "cafe"),
Some(Ordering::Greater)
);
assert_eq!(
compare("de-u-ks-level1", "straße", "strasse"),
Some(Ordering::Equal)
);
assert_ne!(compare("de", "straße", "strasse"), Some(Ordering::Equal));
}
#[test]
fn named_collation() {
assert_eq!(compare("de-u-co-phonebk", "än", "af"), Some(Ordering::Less));
assert_eq!(compare("de", "än", "af"), Some(Ordering::Greater));
}
#[test]
fn unresolvable_tags() {
assert!(!resolves("123"));
assert!(!resolves(""));
assert!(!resolves("not a tag"));
assert!(!resolves("en-u-ks-level4"));
assert!(resolves("zz"));
assert!(resolves("en-US"));
}
}