use crate::datenames::{self, Names};
use crate::value::Value;
#[derive(Clone, Copy, Debug)]
pub struct OutputLocale {
pub tag: &'static str,
pub decimal: char,
pub group: Option<&'static str>,
pub grouping: bool,
pub min_grouping: usize,
pub csv_delimiter: u8,
pub instant: &'static str,
pub names: &'static Names,
}
const SPACE: &str = "\u{a0}";
const NARROW: &str = "\u{202f}";
pub const SHIPPED: &[&str] = &[
"en", "zh", "es", "ar", "pt", "ru", "ja", "de", "fr", "tr", "it",
];
pub use quarb_time::language;
pub fn for_tag(tag: &str) -> Option<OutputLocale> {
let trimmed = tag.trim();
let lang = language(tag);
let names = datenames::names(&lang)?;
let (tag, decimal, group, min_grouping, instant): (
&'static str,
char,
Option<&'static str>,
usize,
&'static str,
) = match lang.as_str() {
"en" => ("en", '.', Some(","), 1, "%b %e, %Y, %I:%M:%S %p"),
"zh" => ("zh", '.', Some(","), 1, "%Y年%m月%d日 %H:%M:%S"),
"es" => ("es", ',', Some("."), 2, "%e %b %Y, %H:%M:%S"),
"ar" => (
"ar",
'.',
Some(","),
1,
"%d\u{200f}/%m\u{200f}/%Y, %H:%M:%S",
),
"pt" => ("pt", ',', Some("."), 1, "%e de %b de %Y %H:%M:%S"),
"ru" => ("ru", ',', Some(SPACE), 1, "%e %b %Y г., %H:%M:%S"),
"ja" => ("ja", '.', Some(","), 1, "%Y/%m/%d %H:%M:%S"),
"de" => ("de", ',', Some("."), 1, "%d.%m.%Y, %H:%M:%S"),
"fr" => ("fr", ',', Some(NARROW), 1, "%e %b %Y %H:%M:%S"),
"tr" => ("tr", ',', Some("."), 1, "%e %b %Y %H:%M:%S"),
"it" => ("it", ',', Some("."), 1, "%e %b %Y, %H:%M:%S"),
_ => return None,
};
let (decimal, group) = match crate::number::locale_convention(trimmed) {
Some(crate::number::Decimal::Point) if decimal == ',' => (
'.',
if trimmed
.to_ascii_lowercase()
.replace('_', "-")
.contains("-ch")
{
Some("\u{2019}")
} else {
Some(",")
},
),
Some(crate::number::Decimal::Comma) if decimal == '.' => (',', Some(SPACE)),
_ => (decimal, group),
};
Some(OutputLocale {
tag,
decimal,
group,
grouping: false,
min_grouping,
csv_delimiter: if decimal == ',' { b';' } else { b',' },
instant,
names,
})
}
pub fn is_canonical(tag: &str) -> bool {
let t = tag.trim();
t.is_empty() || t == "C" || t == "POSIX" || t.starts_with("C.") || t.starts_with("POSIX.")
}
impl OutputLocale {
pub fn number(&self, canonical: &str) -> String {
if canonical.contains(['e', 'E', 'N', 'i']) {
return canonical.to_string();
}
let (sign, rest) = match canonical.strip_prefix('-') {
Some(r) => ("-", r),
None => ("", canonical),
};
let (int, frac) = match rest.split_once('.') {
Some((i, f)) => (i, Some(f)),
None => (rest, None),
};
let mut out = String::from(sign);
match self.group {
Some(sep) if self.grouping && int.len() > 3 + (self.min_grouping - 1) => {
let first = int.len() % 3;
if first > 0 {
out.push_str(&int[..first]);
}
for (i, chunk) in int[first..].as_bytes().chunks(3).enumerate() {
if first > 0 || i > 0 {
out.push_str(sep);
}
out.push_str(std::str::from_utf8(chunk).unwrap());
}
}
_ => out.push_str(int),
}
if let Some(f) = frac {
out.push(self.decimal);
out.push_str(f);
}
out
}
pub fn instant(&self, secs: i64, nanos: u32, offset_min: Option<i16>) -> String {
let day = crate::temporal::strftime_in("%e", secs, nanos, offset_min, self.names);
let fmt = self.instant.replace("%e", day.trim());
crate::temporal::strftime_form(&fmt, secs, nanos, offset_min, self.names, true)
}
pub fn scalar(&self, v: &Value) -> String {
match v {
Value::Int(_) | Value::Float(_) => self.number(&v.to_string()),
Value::Instant {
secs,
nanos,
offset_min,
} => self.instant(*secs, *nanos, *offset_min),
Value::Quantity { .. } => {
let s = v.to_string();
match s.split_once(' ') {
Some((mag, unit)) => format!("{} {unit}", self.number(mag)),
None => s,
}
}
other => other.to_string(),
}
}
pub fn quarb(&self, v: &Value) -> String {
match v {
Value::List(l) => {
let inner: Vec<String> = l.iter().map(|x| self.quarb(x)).collect();
format!("@({})", inner.join("; "))
}
Value::Record(o) => {
let inner: Vec<String> = o
.iter()
.map(|(k, x)| format!("{} = {}", crate::value::quarb_key(k), self.quarb(x)))
.collect();
format!("%({})", inner.join("; "))
}
Value::Null | Value::Str(_) | Value::Bool(_) => v.to_quarb(),
other => self.scalar(other),
}
}
pub fn display(&self, v: &Value) -> String {
match v {
Value::List(_) | Value::Record(_) => self.quarb(v),
Value::Str(s) => s.clone(),
other => self.scalar(other),
}
}
pub fn cell(&self, v: &Value) -> String {
match v {
Value::Null => String::new(),
Value::Str(s) => s.clone(),
other => self.display(other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn grouped(tag: &str) -> OutputLocale {
OutputLocale {
grouping: true,
..for_tag(tag).unwrap()
}
}
#[test]
fn numbers_by_locale() {
let ru = for_tag("ru-RU").unwrap();
assert_eq!(ru.number("1234567.5"), "1234567,5");
assert_eq!(ru.number("1876"), "1876");
assert_eq!(
grouped("ru-RU").number("1234567.5"),
"1\u{a0}234\u{a0}567,5"
);
assert_eq!(grouped("ru-RU").number("-42"), "-42");
assert_eq!(grouped("es").number("1234"), "1234");
assert_eq!(grouped("es").number("12345.5"), "12.345,5");
assert_eq!(grouped("en-US").number("1234567.25"), "1,234,567.25");
assert_eq!(grouped("en-US").number("1e21"), "1e21");
assert_eq!(grouped("de-CH").number("1234.5"), "1\u{2019}234.5");
assert_eq!(for_tag("de-CH").unwrap().number("1234.5"), "1234.5");
assert!(for_tag("C").is_none() && for_tag("xx").is_none());
assert!(is_canonical("C.UTF-8") && is_canonical("") && !is_canonical("ru_RU.UTF-8"));
assert!(for_tag("ru_RU.UTF-8").is_some());
}
#[test]
fn values_by_locale() {
let ru = for_tag("ru").unwrap();
let (s, n, o) = crate::temporal::parse_iso("1899-05-06T14:30:00").unwrap();
assert_eq!(ru.instant(s, n, o), "6 мая 1899 г., 14:30:00");
let de = for_tag("de").unwrap();
assert_eq!(de.instant(s, n, o), "06.05.1899, 14:30:00");
let v = Value::Record(vec![
("n".into(), Value::Float(1234.5)),
("t".into(), Value::Str("x".into())),
]);
assert_eq!(ru.quarb(&v), "%(n = 1234,5; t = \"x\")");
assert_eq!(ru.display(&Value::Int(1000000)), "1000000");
assert_eq!(ru.cell(&Value::Null), "");
assert_eq!(ru.csv_delimiter, b';');
assert_eq!(for_tag("en").unwrap().csv_delimiter, b',');
}
}