use alloc::vec::Vec;
use crate::error::WriteError;
use crate::format::locale_key;
use crate::number::{
AffixPart, Grouping, OTHER, PH_CURRENCY, PH_PERCENT, PH_SIGN, Style, T_ARG0, T_ARG1,
TemplatePart,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct SymbolsSpec<'s> {
pub grouping: Grouping,
pub minimum_grouping_digits: u8,
pub decimal: &'s str,
pub group: &'s str,
pub minus: &'s str,
pub plus: &'s str,
pub percent: &'s str,
pub digits: Option<&'s str>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct PatternSpec<'s> {
pub grouping: Grouping,
pub positive: (Vec<AffixPart<'s>>, Vec<AffixPart<'s>>),
pub negative: Option<(Vec<AffixPart<'s>>, Vec<AffixPart<'s>>)>,
}
const SYMBOLS: WriteError = WriteError::LocaleEntry(locale_key::NUMBER_SYMBOLS);
const PATTERNS: WriteError = WriteError::LocaleEntry(locale_key::NUMBER_PATTERNS);
fn str8_any(s: &str, out: &mut Vec<u8>, err: &WriteError) -> Result<(), WriteError> {
let n = u8::try_from(s.len()).map_err(|_| err.clone())?;
out.push(n);
out.extend_from_slice(s.as_bytes());
Ok(())
}
pub(crate) fn str8(s: &str, out: &mut Vec<u8>, err: &WriteError) -> Result<(), WriteError> {
if s.bytes().any(|b| b < 0x20) {
return Err(err.clone());
}
str8_any(s, out, err)
}
pub(crate) fn template(
parts: &[TemplatePart<'_>],
out: &mut Vec<u8>,
err: &WriteError,
) -> Result<(), WriteError> {
let mut bytes = Vec::new();
for p in parts {
match *p {
TemplatePart::Text(t) => {
if t.bytes().any(|b| b < 0x20) {
return Err(err.clone());
}
bytes.extend_from_slice(t.as_bytes());
}
TemplatePart::Arg0 => bytes.push(T_ARG0),
TemplatePart::Arg1 => bytes.push(T_ARG1),
}
}
out.push(u8::try_from(bytes.len()).map_err(|_| err.clone())?);
out.extend_from_slice(&bytes);
Ok(())
}
pub(crate) fn forms<T>(
list: &[(u8, T)],
out: &mut Vec<u8>,
err: &WriteError,
mut value: impl FnMut(&T, &mut Vec<u8>) -> Result<(), WriteError>,
) -> Result<(), WriteError> {
let mut sorted: Vec<&(u8, T)> = list.iter().collect();
sorted.sort_by_key(|(c, _)| *c);
if sorted.windows(2).any(|w| matches!(w, [a, b] if a.0 == b.0))
|| sorted.iter().any(|(c, _)| *c > OTHER)
{
return Err(err.clone());
}
out.push(u8::try_from(sorted.len()).map_err(|_| err.clone())?);
for (c, v) in sorted {
out.push(*c);
value(v, out)?;
}
Ok(())
}
pub fn symbols(s: &SymbolsSpec<'_>) -> Result<Vec<u8>, WriteError> {
let grouping = s.grouping.to_byte().ok_or(SYMBOLS)?;
if !(1..=15).contains(&s.minimum_grouping_digits) {
return Err(SYMBOLS);
}
let mut out = Vec::new();
out.push(grouping);
out.push(s.minimum_grouping_digits);
for text in [s.decimal, s.group, s.minus, s.plus, s.percent] {
str8_any(text, &mut out, &SYMBOLS)?;
}
match s.digits {
None | Some("0123456789") => {}
Some(d) => {
let mut widths = d.chars().map(char::len_utf8);
let w = widths.next().ok_or(SYMBOLS)?;
if d.chars().count() != 10 || widths.any(|x| x != w) {
return Err(SYMBOLS);
}
out.extend_from_slice(d.as_bytes());
}
}
Ok(out)
}
fn affix(parts: &[AffixPart<'_>], out: &mut Vec<u8>) -> Result<(), WriteError> {
let mut bytes = Vec::new();
for p in parts {
match *p {
AffixPart::Text(t) => {
if t.bytes().any(|b| b < 0x20) {
return Err(PATTERNS);
}
bytes.extend_from_slice(t.as_bytes());
}
AffixPart::Sign => bytes.push(PH_SIGN),
AffixPart::Percent => bytes.push(PH_PERCENT),
AffixPart::Currency => bytes.push(PH_CURRENCY),
}
}
out.push(u8::try_from(bytes.len()).map_err(|_| PATTERNS)?);
out.extend_from_slice(&bytes);
Ok(())
}
pub(crate) fn body(p: &PatternSpec<'_>) -> Result<Vec<u8>, WriteError> {
let mut out = Vec::new();
out.push(p.grouping.to_byte().ok_or(PATTERNS)?);
affix(&p.positive.0, &mut out)?;
affix(&p.positive.1, &mut out)?;
if let Some((prefix, suffix)) = &p.negative {
affix(prefix, &mut out)?;
affix(suffix, &mut out)?;
}
Ok(out)
}
pub fn patterns(list: &[(Style, PatternSpec<'_>)]) -> Result<Vec<u8>, WriteError> {
let mut records: Vec<(Style, Vec<u8>)> = Vec::with_capacity(list.len());
for (style, p) in list {
records.push((*style, body(p)?));
}
records.sort_by_key(|r| r.0);
if records
.windows(2)
.any(|w| matches!(w, [a, b] if a.0 == b.0))
{
return Err(PATTERNS);
}
let mut out = Vec::new();
for (style, bytes) in &records {
let same_as_base = style.base().is_some_and(|b| {
records
.iter()
.any(|(s, base_bytes)| *s == b && base_bytes == bytes)
});
if same_as_base {
continue;
}
out.push(*style as u8);
out.push(u8::try_from(bytes.len()).map_err(|_| PATTERNS)?);
out.extend_from_slice(bytes);
}
Ok(out)
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use alloc::vec;
use super::{PatternSpec, SymbolsSpec, patterns, symbols};
use crate::number::{AffixPart, Grouping, Patterns, Style, Symbols};
const WEST: Grouping = Grouping {
primary: 3,
secondary: 3,
};
#[test]
fn symbols_round_trip_and_limits() {
let mut s = SymbolsSpec {
grouping: WEST,
minimum_grouping_digits: 1,
decimal: "٫",
group: "٬",
minus: "\u{61c}-",
plus: "\u{61c}+",
percent: "٪\u{61c}",
digits: Some("٠١٢٣٤٥٦٧٨٩"),
};
let bytes = symbols(&s).expect("encodes");
let v = Symbols::parse(&bytes).expect("parses");
assert_eq!(v.decimal(), "٫");
assert_eq!(v.digits().digit(3), "٣");
assert_eq!(v.digits().as_str(), "٠١٢٣٤٥٦٧٨٩");
s.digits = Some("0123456789");
assert!(
Symbols::parse(&symbols(&s).expect("ascii"))
.expect("parses")
.digits()
.is_ascii()
);
s.digits = Some("0١٢٣٤٥٦٧٨٩"); assert!(symbols(&s).is_err());
s.digits = None;
s.minimum_grouping_digits = 0;
assert!(symbols(&s).is_err());
s.minimum_grouping_digits = 1;
s.grouping.primary = 16;
assert!(symbols(&s).is_err());
}
#[test]
fn patterns_canonical() {
let std = PatternSpec {
grouping: WEST,
positive: (vec![AffixPart::Currency], vec![]),
negative: None,
};
let spaced = PatternSpec {
grouping: WEST,
positive: (vec![AffixPart::Currency, AffixPart::Text("\u{a0}")], vec![]),
negative: None,
};
let a = patterns(&[
(Style::AccountingAlpha, std.clone()),
(Style::Currency, std.clone()),
(Style::CurrencyAlpha, spaced.clone()),
(Style::Accounting, std.clone()),
])
.expect("encodes");
let p = Patterns::new(&a);
assert!(p.is_valid());
assert_eq!(
p.styles().collect::<alloc::vec::Vec<_>>(),
[Style::Currency, Style::CurrencyAlpha, Style::Accounting]
);
assert_eq!(p.resolve(Style::AccountingAlpha), p.get(Style::Accounting));
assert!(patterns(&[(Style::Currency, std.clone()), (Style::Currency, spaced)]).is_err());
let bad = PatternSpec {
grouping: WEST,
positive: (vec![AffixPart::Text("\u{1}")], vec![]),
negative: None,
};
assert!(patterns(&[(Style::Percent, bad)]).is_err());
let split = PatternSpec {
grouping: WEST,
positive: (vec![AffixPart::Text("a"), AffixPart::Text("b")], vec![]),
negative: None,
};
let joined = PatternSpec {
grouping: WEST,
positive: (vec![AffixPart::Text("ab")], vec![]),
negative: None,
};
assert_eq!(
patterns(&[(Style::Percent, split)]),
patterns(&[(Style::Percent, joined)])
);
}
}