use mf2_catalog::Dir;
#[cfg(feature = "host-std")]
#[rustfmt::skip]
mod cldr;
const THRESHOLD: u32 = 50;
const DEMOTION: u32 = 5;
const VARIABLE: u16 = 0x8000;
const NEGATED: u16 = 0x4000;
type ScriptPair = (u16, u32, u16, u32, u8);
type RegionRule = (u16, u32, u16, u16, u32, u16, u8, bool);
#[derive(Clone, Copy)]
pub struct LanguageMatching {
scripts: &'static [u32],
regions: &'static [u16],
languages: &'static [u16],
language_values: &'static [(u8, u8)],
tags: &'static [u64],
tag_values: &'static [(u8, u8)],
language_pairs: &'static [u32],
language_distances: &'static [u8],
script_pairs: &'static [ScriptPair],
region_rules: &'static [RegionRule],
variables: &'static [&'static [u16]],
paradigms: &'static [(u16, u32, u16)],
defaults: [u8; 3],
}
impl core::fmt::Debug for LanguageMatching {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LanguageMatching")
.field("languages", &self.languages.len())
.field("tags", &self.tags.len())
.field("language_pairs", &self.language_pairs.len())
.field("script_pairs", &self.script_pairs.len())
.field("region_rules", &self.region_rules.len())
.field("variables", &self.variables.len())
.field("paradigms", &self.paradigms.len())
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct Lsr {
language: u64,
script: u32,
region: u16,
}
const UND: u64 = packed(b"und", 0);
const ZZZZ: u64 = packed(b"zzzz", 0);
const ZZ: u64 = packed(b"zz", 0);
const POSIX: u64 = packed(b"posix", 0);
#[allow(clippy::cast_lossless, reason = "`u64::from` is not `const`")]
const fn packed(letters: &[u8], out: u64) -> u64 {
match letters {
[b, rest @ ..] => packed(rest, (out << 5) | (*b - b'a' + 1) as u64),
[] => out,
}
}
const fn tag_key(language: u16, script: u32, region: u16) -> u64 {
((language as u64) << 36) | ((script as u64) << 16) | region as u64
}
impl LanguageMatching {
#[doc(hidden)]
pub const EMPTY: LanguageMatching = LanguageMatching {
scripts: &[],
regions: &[],
languages: &[],
language_values: &[],
tags: &[],
tag_values: &[],
language_pairs: &[],
language_distances: &[],
script_pairs: &[],
region_rules: &[],
variables: &[],
paradigms: &[],
defaults: [80, 50, 4],
};
#[doc(hidden)]
#[must_use]
#[allow(clippy::too_many_arguments)]
pub const fn new(
scripts: &'static [u32],
regions: &'static [u16],
languages: &'static [u16],
language_values: &'static [(u8, u8)],
tags: &'static [u64],
tag_values: &'static [(u8, u8)],
language_pairs: &'static [u32],
language_distances: &'static [u8],
script_pairs: &'static [ScriptPair],
region_rules: &'static [RegionRule],
variables: &'static [&'static [u16]],
paradigms: &'static [(u16, u32, u16)],
defaults: [u8; 3],
) -> LanguageMatching {
LanguageMatching {
scripts,
regions,
languages,
language_values,
tags,
tag_values,
language_pairs,
language_distances,
script_pairs,
region_rules,
variables,
paradigms,
defaults,
}
}
#[doc(hidden)]
pub fn best_match<'d>(
&self,
desired: impl IntoIterator<Item = &'d str>,
supported: &[(&str, Dir)],
) -> Option<usize> {
self.best(&mut desired.into_iter(), supported)
}
fn best(
&self,
desired: &mut dyn Iterator<Item = &str>,
supported: &[(&str, Dir)],
) -> Option<usize> {
let mut best = None;
let mut best_distance = THRESHOLD;
let mut best_entry = 0;
let mut best_paradigm = false;
let mut entry = 0;
for tag in desired {
let demotion = entry * DEMOTION;
if demotion >= THRESHOLD {
break;
}
let Some(reader) = self.reader(tag) else {
continue;
};
entry += 1;
for (i, (tag, _)) in supported.iter().enumerate() {
let Some(own) = parse(tag) else {
continue;
};
let own = self.maximize(own);
let weighted = demotion + self.distance(reader, own);
let paradigm = weighted <= best_distance && self.is_paradigm(own);
if weighted < best_distance
|| (weighted == best_distance
&& best_entry == entry
&& paradigm
&& !best_paradigm)
{
best = Some(i);
best_distance = weighted;
best_entry = entry;
best_paradigm = paradigm;
}
}
}
best
}
fn reader(&self, tag: &str) -> Option<Lsr> {
let reader = parse(tag)?;
Some(if reader.language == UND {
reader
} else {
self.maximize(reader)
})
}
#[doc(hidden)]
#[must_use]
pub fn distance_of(&self, desired: &str, supported: &str) -> Option<u32> {
let reader = self.reader(desired)?;
Some(self.distance(reader, self.maximize(parse(supported)?)))
}
fn maximize(&self, tag: Lsr) -> Lsr {
let Ok(language) = u16::try_from(tag.language) else {
return tag;
};
let found = if tag.script != 0 && tag.region != 0 {
None
} else {
[(tag.script, 0), (0, tag.region)]
.into_iter()
.filter(|&(s, r)| s != 0 || r != 0)
.find_map(|(s, r)| lookup(self.tags, self.tag_values, tag_key(language, s, r)))
.or_else(|| lookup(self.languages, self.language_values, language))
};
match found {
Some((script, region)) => Lsr {
language: tag.language,
script: if tag.script == 0 {
self.scripts.get(usize::from(script)).copied().unwrap_or(0)
} else {
tag.script
},
region: if tag.region == 0 {
self.regions.get(usize::from(region)).copied().unwrap_or(0)
} else {
tag.region
},
},
None => tag,
}
}
fn distance(&self, reader: Lsr, own: Lsr) -> u32 {
let [language, script, region] = self.defaults.map(u32::from);
let pair = u16::try_from(reader.language)
.ok()
.zip(u16::try_from(own.language).ok());
let mut total = 0;
if reader.language != own.language {
total += pair
.and_then(|(d, s)| {
let key = (u32::from(d) << 16) | u32::from(s);
lookup(self.language_pairs, self.language_distances, key)
})
.map_or(language, u32::from);
}
if reader.script != own.script {
total += pair
.and_then(|(d, s)| {
self.script_pairs
.iter()
.find(|p| (p.0, p.1, p.2, p.3) == (d, reader.script, s, own.script))
})
.map_or(script, |p| u32::from(p.4));
}
if reader.region != own.region {
total += self
.region_rules
.iter()
.find(|&&(dl, ds, dr, sl, ss, sr, _, oneway)| {
let (d, s) = ((dl, ds, dr), (sl, ss, sr));
(self.fits(d, reader) && self.fits(s, own))
|| (!oneway && self.fits(d, own) && self.fits(s, reader))
})
.map_or(region, |rule| u32::from(rule.6));
}
total
}
fn fits(&self, (language, script, region): (u16, u32, u16), tag: Lsr) -> bool {
(language == 0 || u64::from(language) == tag.language)
&& (script == 0 || script == tag.script)
&& (region == 0
|| if region & VARIABLE == 0 {
region == tag.region
} else {
let inside = self
.variables
.get(usize::from(region & !(VARIABLE | NEGATED)))
.is_some_and(|regions| regions.binary_search(&tag.region).is_ok());
inside != (region & NEGATED != 0)
})
}
fn is_paradigm(&self, tag: Lsr) -> bool {
self.paradigms
.iter()
.any(|&(l, s, r)| u64::from(l) == tag.language && s == tag.script && r == tag.region)
}
}
fn lookup<K: Ord + Copy, V: Copy>(keys: &[K], values: &[V], key: K) -> Option<V> {
let at = keys.binary_search(&key).ok()?;
values.get(at).copied()
}
#[cfg(feature = "host-std")]
impl LanguageMatching {
#[doc(hidden)]
#[must_use]
pub fn cldr() -> &'static LanguageMatching {
&cldr::CLDR
}
}
fn parse(tag: &str) -> Option<Lsr> {
let bytes = tag.as_bytes();
let end = bytes
.iter()
.position(|&b| b == b'.' || b == b'@')
.unwrap_or(bytes.len());
let mut subtags = bytes.get(..end)?.split(|&b| b == b'-' || b == b'_');
let first = subtags.next()?;
let language = pack(first).filter(|&l| l != POSIX && matches!(first.len(), 2 | 3 | 5..=8))?;
let mut next = subtags.next();
let mut extlangs = 0;
while let Some(s) = next
&& extlangs < 3
&& s.len() == 3
&& pack(s).is_some()
{
extlangs += 1;
next = subtags.next();
}
let mut script = 0;
if let Some(s) = next
&& s.len() == 4
&& let Some(packed) = pack(s)
{
script = packed;
next = subtags.next();
}
let region = next.and_then(region).map_or(0, u64::from);
Some(Lsr {
language,
script: if script == ZZZZ {
0
} else {
u32::try_from(script).ok()?
},
region: if region == ZZ {
0
} else {
u16::try_from(region).ok()?
},
})
}
fn pack(letters: &[u8]) -> Option<u64> {
letters.iter().try_fold(0, |packed, &b| {
b.is_ascii_alphabetic()
.then(|| (packed << 5) | u64::from(b.to_ascii_lowercase() - b'a' + 1))
})
}
fn region(subtag: &[u8]) -> Option<u16> {
match subtag.len() {
2 => pack(subtag).and_then(|p| u16::try_from(p).ok()),
3 => subtag.iter().try_fold(1024, |n: u16, &b| {
b.is_ascii_digit()
.then(|| 1024 + (n - 1024) * 10 + u16::from(b - b'0'))
}),
_ => None,
}
}