use crate::bytes::Cur;
use crate::format::locale_key;
use crate::reader::Catalog;
pub(crate) const PH_SIGN: u8 = 0x01;
pub(crate) const PH_PERCENT: u8 = 0x02;
pub(crate) const PH_CURRENCY: u8 = 0x03;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Grouping {
pub primary: u8,
pub secondary: u8,
}
impl Grouping {
pub const NONE: Grouping = Grouping {
primary: 0,
secondary: 0,
};
pub const fn from_byte(b: u8) -> Grouping {
Grouping {
primary: b & 0x0f,
secondary: b >> 4,
}
}
pub const fn to_byte(self) -> Option<u8> {
if self.primary > 15 || self.secondary > 15 {
return None;
}
Some(self.secondary << 4 | self.primary)
}
pub fn separator_after(self, m: u32) -> bool {
let p = u32::from(self.primary);
if p == 0 || m < p {
return false;
}
let s = if self.secondary == 0 {
p
} else {
u32::from(self.secondary)
};
(m - p).is_multiple_of(s)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Digits<'a> {
text: &'a str,
width: u8,
}
const ASCII_DIGITS: &str = "0123456789";
impl<'a> Digits<'a> {
pub const ASCII: Digits<'static> = Digits { text: "", width: 0 };
fn parse(b: &'a [u8]) -> Option<Digits<'a>> {
if b.is_empty() {
return Some(Digits::ASCII);
}
let n = b.len();
if !n.is_multiple_of(10) || n > 40 {
return None;
}
let text = core::str::from_utf8(b).ok()?;
let w = n / 10;
let mut at = 0;
while at < n {
if !text.is_char_boundary(at) {
return None;
}
at += w;
}
Some(Digits {
text,
width: u8::try_from(w).ok()?,
})
}
pub const fn is_ascii(&self) -> bool {
self.width == 0
}
pub fn digit(&self, d: u8) -> &'a str {
if d > 9 {
return "";
}
let (text, w) = if self.width == 0 {
(ASCII_DIGITS, 1)
} else {
(self.text, usize::from(self.width))
};
let at = usize::from(d) * w;
text.get(at..at + w).unwrap_or("")
}
pub fn as_str(&self) -> &'a str {
if self.width == 0 {
ASCII_DIGITS
} else {
self.text
}
}
}
pub(crate) fn str8<'a>(c: &mut Cur<'a>) -> Option<&'a str> {
let n = c.u8()?;
core::str::from_utf8(c.take(usize::from(n))?).ok()
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Symbols<'a> {
grouping: Grouping,
min_grouping: u8,
decimal: &'a str,
group: &'a str,
minus: &'a str,
plus: &'a str,
percent: &'a str,
digits: Digits<'a>,
}
impl<'a> Symbols<'a> {
pub fn parse(entry: &'a [u8]) -> Option<Symbols<'a>> {
let mut c = Cur::new(entry, 0);
let grouping = Grouping::from_byte(c.u8()?);
let min_grouping = c.u8()?;
let decimal = str8(&mut c)?;
let group = str8(&mut c)?;
let minus = str8(&mut c)?;
let plus = str8(&mut c)?;
let percent = str8(&mut c)?;
let digits = Digits::parse(entry.get(c.pos()..)?)?;
Some(Symbols {
grouping,
min_grouping,
decimal,
group,
minus,
plus,
percent,
digits,
})
}
pub fn of(catalog: &'a Catalog) -> Option<Symbols<'a>> {
Symbols::parse(catalog.locale_entry(locale_key::NUMBER_SYMBOLS)?)
}
pub const fn grouping(&self) -> Grouping {
self.grouping
}
pub const fn minimum_grouping_digits(&self) -> u8 {
self.min_grouping
}
pub const fn decimal(&self) -> &'a str {
self.decimal
}
pub const fn group(&self) -> &'a str {
self.group
}
pub const fn minus(&self) -> &'a str {
self.minus
}
pub const fn plus(&self) -> &'a str {
self.plus
}
pub const fn percent(&self) -> &'a str {
self.percent
}
pub const fn digits(&self) -> Digits<'a> {
self.digits
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[repr(u8)]
pub enum Style {
Percent = 1,
Currency = 2,
CurrencyAlpha = 3,
CurrencyNoSymbol = 4,
Accounting = 5,
AccountingAlpha = 6,
AccountingNoSymbol = 7,
}
impl Style {
pub const ALL: [Style; 7] = [
Style::Percent,
Style::Currency,
Style::CurrencyAlpha,
Style::CurrencyNoSymbol,
Style::Accounting,
Style::AccountingAlpha,
Style::AccountingNoSymbol,
];
pub const fn from_u8(b: u8) -> Option<Style> {
Some(match b {
1 => Style::Percent,
2 => Style::Currency,
3 => Style::CurrencyAlpha,
4 => Style::CurrencyNoSymbol,
5 => Style::Accounting,
6 => Style::AccountingAlpha,
7 => Style::AccountingNoSymbol,
_ => return None,
})
}
pub const fn base(self) -> Option<Style> {
match self {
Style::CurrencyAlpha => Some(Style::Currency),
Style::AccountingAlpha => Some(Style::Accounting),
_ => None,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Patterns<'a> {
entry: &'a [u8],
}
impl<'a> Patterns<'a> {
pub const fn new(entry: &'a [u8]) -> Patterns<'a> {
Patterns { entry }
}
pub fn of(catalog: &'a Catalog) -> Option<Patterns<'a>> {
catalog
.locale_entry(locale_key::NUMBER_PATTERNS)
.map(Patterns::new)
}
pub fn get(&self, style: Style) -> Option<Pattern<'a>> {
let want = style as u8;
let mut c = Cur::new(self.entry, 0);
let mut prev = 0u8;
while !c.at_end() {
let s = c.u8()?;
let len = c.u8()?;
let body = c.take(usize::from(len))?;
if s <= prev {
return None;
}
prev = s;
if s == want {
return Pattern::parse(body);
}
if s > want {
return None;
}
}
None
}
pub fn resolve(&self, style: Style) -> Option<Pattern<'a>> {
self.get(style)
.or_else(|| style.base().and_then(|b| self.get(b)))
}
pub fn is_valid(&self) -> bool {
let mut c = Cur::new(self.entry, 0);
let mut prev = 0u8;
while !c.at_end() {
let (Some(s), Some(len)) = (c.u8(), c.u8()) else {
return false;
};
let Some(body) = c.take(usize::from(len)) else {
return false;
};
if s <= prev || Style::from_u8(s).is_none() || Pattern::parse(body).is_none() {
return false;
}
prev = s;
}
true
}
pub fn styles(&self) -> impl Iterator<Item = Style> + 'a {
let mut c = Cur::new(self.entry, 0);
core::iter::from_fn(move || {
loop {
let s = c.u8()?;
let len = c.u8()?;
c.skip(usize::from(len))?;
if let Some(style) = Style::from_u8(s) {
return Some(style);
}
}
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Pattern<'a> {
grouping: Grouping,
positive: Affixes<'a>,
negative: Option<Affixes<'a>>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SignShown {
None,
Minus,
Plus,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Signed<'a> {
pub prefix: Affix<'a>,
pub suffix: Affix<'a>,
pub sign_first: bool,
}
impl<'a> Pattern<'a> {
pub(crate) fn parse(body: &'a [u8]) -> Option<Pattern<'a>> {
let mut c = Cur::new(body, 0);
let grouping = Grouping::from_byte(c.u8()?);
let positive = Affixes {
prefix: Affix::read(&mut c)?,
suffix: Affix::read(&mut c)?,
};
let negative = if c.at_end() {
None
} else {
Some(Affixes {
prefix: Affix::read(&mut c)?,
suffix: Affix::read(&mut c)?,
})
};
c.at_end().then_some(Pattern {
grouping,
positive,
negative,
})
}
pub const fn grouping(&self) -> Grouping {
self.grouping
}
pub const fn positive(&self) -> Affixes<'a> {
self.positive
}
pub const fn negative(&self) -> Option<Affixes<'a>> {
self.negative
}
pub fn signed(&self, sign: SignShown) -> Signed<'a> {
let pos = Signed {
prefix: self.positive.prefix,
suffix: self.positive.suffix,
sign_first: !matches!(sign, SignShown::None),
};
let Some(neg) = self.negative else {
return pos;
};
let neg = Signed {
prefix: neg.prefix,
suffix: neg.suffix,
sign_first: false,
};
match sign {
SignShown::Minus => neg,
SignShown::Plus if neg.prefix.has_sign() || neg.suffix.has_sign() => neg,
SignShown::None | SignShown::Plus => pos,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Affixes<'a> {
pub prefix: Affix<'a>,
pub suffix: Affix<'a>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Affix<'a>(&'a str);
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum AffixPart<'a> {
Text(&'a str),
Sign,
Percent,
Currency,
}
impl<'a> Affix<'a> {
pub const EMPTY: Affix<'static> = Affix("");
fn read(c: &mut Cur<'a>) -> Option<Affix<'a>> {
let text = str8(c)?;
if text
.bytes()
.any(|b| b < 0x20 && !matches!(b, PH_SIGN | PH_PERCENT | PH_CURRENCY))
{
return None;
}
Some(Affix(text))
}
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub const fn encoded(&self) -> &'a str {
self.0
}
pub fn has_sign(&self) -> bool {
self.0.bytes().any(|b| b == PH_SIGN)
}
pub fn has_currency(&self) -> bool {
self.0.bytes().any(|b| b == PH_CURRENCY)
}
pub fn parts(&self) -> AffixParts<'a> {
AffixParts { rest: self.0 }
}
}
#[derive(Clone, Debug)]
pub struct AffixParts<'a> {
rest: &'a str,
}
impl<'a> Iterator for AffixParts<'a> {
type Item = AffixPart<'a>;
fn next(&mut self) -> Option<AffixPart<'a>> {
let b = self.rest.as_bytes();
let first = *b.first()?;
let placeholder = match first {
PH_SIGN => Some(AffixPart::Sign),
PH_PERCENT => Some(AffixPart::Percent),
PH_CURRENCY => Some(AffixPart::Currency),
_ => None,
};
if let Some(p) = placeholder {
self.rest = self.rest.get(1..).unwrap_or("");
return Some(p);
}
let mut end = 1;
while let Some(&x) = b.get(end) {
if x < 0x20 {
break;
}
end += 1;
}
let text = self.rest.get(..end).unwrap_or("");
self.rest = self.rest.get(end..).unwrap_or("");
Some(AffixPart::Text(text))
}
}
pub(crate) const T_ARG0: u8 = 0x01;
pub(crate) const T_ARG1: u8 = 0x02;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Template<'a>(&'a str);
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TemplatePart<'a> {
Text(&'a str),
Arg0,
Arg1,
}
impl<'a> Template<'a> {
pub(crate) fn read(c: &mut Cur<'a>) -> Option<Template<'a>> {
let text = str8(c)?;
if text.bytes().any(|b| b < 0x20 && b != T_ARG0 && b != T_ARG1) {
return None;
}
Some(Template(text))
}
pub(crate) const fn trusted(s: &'a str) -> Template<'a> {
Template(s)
}
pub const fn encoded(&self) -> &'a str {
self.0
}
pub fn parts(&self) -> TemplateParts<'a> {
TemplateParts { rest: self.0 }
}
}
#[derive(Clone, Debug)]
pub struct TemplateParts<'a> {
rest: &'a str,
}
impl<'a> Iterator for TemplateParts<'a> {
type Item = TemplatePart<'a>;
fn next(&mut self) -> Option<TemplatePart<'a>> {
let b = self.rest.as_bytes();
let first = *b.first()?;
let arg = match first {
T_ARG0 => Some(TemplatePart::Arg0),
T_ARG1 => Some(TemplatePart::Arg1),
_ => None,
};
if let Some(a) = arg {
self.rest = self.rest.get(1..).unwrap_or("");
return Some(a);
}
let mut end = 1;
while let Some(&x) = b.get(end) {
if x < 0x20 {
break;
}
end += 1;
}
let text = self.rest.get(..end).unwrap_or("");
self.rest = self.rest.get(end..).unwrap_or("");
Some(TemplatePart::Text(text))
}
}
pub const OTHER: u8 = 5;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct Forms<'a> {
body: &'a [u8],
k: u8,
}
impl<'a> Forms<'a> {
pub(crate) const EMPTY: Forms<'static> = Forms { body: &[], k: 0 };
pub(crate) fn read(c: &mut Cur<'a>, templates: bool) -> Option<Forms<'a>> {
let k = c.u8()?;
let start = c.pos();
let mut prev: Option<u8> = None;
for _ in 0..k {
let cat = c.u8()?;
if cat > OTHER || prev.is_some_and(|p| cat <= p) {
return None;
}
prev = Some(cat);
if templates {
Template::read(c)?;
} else {
let s = str8(c)?;
if s.bytes().any(|b| b < 0x20) {
return None;
}
}
}
let body = c.bytes().get(start..c.pos())?;
Some(Forms { body, k })
}
pub(crate) fn get(&self, category: u8) -> Option<&'a str> {
let mut c = Cur::new(self.body, 0);
let mut other = None;
for _ in 0..self.k {
let cat = c.u8()?;
let s = str8(&mut c)?;
if cat == category {
return Some(s);
}
if cat == OTHER {
other = Some(s);
}
}
other
}
pub(crate) const fn is_empty(&self) -> bool {
self.k == 0
}
}
#[cfg(test)]
#[allow(clippy::indexing_slicing, clippy::expect_used)]
mod tests {
use super::{AffixPart, Grouping, Patterns, SignShown, Style, Symbols};
#[test]
fn grouping_separators() {
let western = Grouping {
primary: 3,
secondary: 3,
};
let indian = Grouping {
primary: 3,
secondary: 2,
};
let at = |g: Grouping| {
(0..10)
.filter(|&m| g.separator_after(m))
.collect::<alloc::vec::Vec<_>>()
};
assert_eq!(at(western), [3, 6, 9]);
assert_eq!(at(indian), [3, 5, 7, 9]);
assert!(at(Grouping::NONE).is_empty());
assert_eq!(Grouping::from_byte(0x23), indian);
assert_eq!(indian.to_byte(), Some(0x23));
}
#[test]
fn symbols_malformed() {
let en = [0x33, 1, 1, b'.', 1, b',', 1, b'-', 1, b'+', 1, b'%'];
let s = Symbols::parse(&en).expect("en");
assert_eq!(s.decimal(), ".");
assert!(s.digits().is_ascii());
assert_eq!(s.digits().digit(7), "7");
assert_eq!(s.digits().digit(10), "");
for len in 0..en.len() {
assert!(
Symbols::parse(en.get(..len).unwrap_or(&[])).is_none(),
"{len}"
);
}
let mut bad_digits = alloc::vec::Vec::from(en);
bad_digits.extend_from_slice(b"012345678"); assert!(Symbols::parse(&bad_digits).is_none());
let mut not_utf8 = alloc::vec::Vec::from(en);
not_utf8[3] = 0xff;
assert!(Symbols::parse(¬_utf8).is_none());
}
#[test]
fn patterns_lookup_and_sign_rules() {
let entry = [
1, 4, 0x33, 0, 1, 0x02, 5, 9, 0x33, 1, 0x03, 0, 2, b'(', 0x03, 1, b')', ];
let p = Patterns::new(&entry);
assert!(p.is_valid());
assert_eq!(
p.styles().collect::<alloc::vec::Vec<_>>(),
[Style::Percent, Style::Accounting]
);
let pct = p.get(Style::Percent).expect("percent");
let s = pct.signed(SignShown::Minus);
assert!(s.sign_first && s.prefix.is_empty());
assert_eq!(
s.suffix.parts().collect::<alloc::vec::Vec<_>>(),
[AffixPart::Percent]
);
assert!(p.get(Style::Currency).is_none());
assert!(p.resolve(Style::AccountingAlpha).is_some());
let acc = p.get(Style::Accounting).expect("accounting");
let neg = acc.signed(SignShown::Minus);
assert!(!neg.sign_first);
assert_eq!(
neg.prefix.parts().collect::<alloc::vec::Vec<_>>(),
[AffixPart::Text("("), AffixPart::Currency]
);
let plus = acc.signed(SignShown::Plus);
assert!(plus.sign_first);
assert!(plus.prefix.has_currency());
for len in (1..entry.len()).filter(|&l| l != 6) {
assert!(
!Patterns::new(entry.get(..len).unwrap_or(&[])).is_valid(),
"{len}"
);
}
let swapped = [5, 4, 0x33, 0, 1, 0x02, 1, 4, 0x33, 0, 1, 0x02];
assert!(!Patterns::new(&swapped).is_valid());
assert!(Patterns::new(&swapped).get(Style::Percent).is_none());
let bad = [1, 4, 0x33, 0, 1, 0x04];
assert!(!Patterns::new(&bad).is_valid());
}
#[test]
fn affix_parts_split_text() {
let affix = "\u{200f}\u{1}\u{3}\u{a0}";
let entry = {
let mut e = alloc::vec![2u8, 0, 0x33];
e.push(0);
e.push(u8::try_from(affix.len()).unwrap_or(0));
e.extend_from_slice(affix.as_bytes());
let len = u8::try_from(e.len() - 2).unwrap_or(0);
e[1] = len;
e
};
let p = Patterns::new(&entry);
assert!(p.is_valid());
let pat = p.get(Style::Currency).expect("currency");
assert_eq!(
pat.positive()
.suffix
.parts()
.collect::<alloc::vec::Vec<_>>(),
[
AffixPart::Text("\u{200f}"),
AffixPart::Sign,
AffixPart::Currency,
AffixPart::Text("\u{a0}")
]
);
}
}