1use std::sync::LazyLock;
18
19use indexmap::IndexMap;
20use serde::Deserialize;
21use serde_json::{Map, Value};
22
23use crate::generated::catalog as g;
24use crate::json;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum PluralCategory {
29 Zero,
30 One,
31 Two,
32 Few,
33 Many,
34 Other,
35}
36
37impl PluralCategory {
38 pub fn as_str(self) -> &'static str {
39 match self {
40 PluralCategory::Zero => "zero",
41 PluralCategory::One => "one",
42 PluralCategory::Two => "two",
43 PluralCategory::Few => "few",
44 PluralCategory::Many => "many",
45 PluralCategory::Other => "other",
46 }
47 }
48
49 pub fn parse(s: &str) -> Option<PluralCategory> {
50 Some(match s {
51 "zero" => PluralCategory::Zero,
52 "one" => PluralCategory::One,
53 "two" => PluralCategory::Two,
54 "few" => PluralCategory::Few,
55 "many" => PluralCategory::Many,
56 "other" => PluralCategory::Other,
57 _ => return None,
58 })
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum DateStyle {
65 Short,
66 #[default]
67 Medium,
68 Long,
69 Full,
70}
71
72impl DateStyle {
73 pub fn as_str(self) -> &'static str {
74 match self {
75 DateStyle::Short => "short",
76 DateStyle::Medium => "medium",
77 DateStyle::Long => "long",
78 DateStyle::Full => "full",
79 }
80 }
81
82 pub fn parse(s: &str) -> Option<DateStyle> {
83 Some(match s {
84 "short" => DateStyle::Short,
85 "medium" => DateStyle::Medium,
86 "long" => DateStyle::Long,
87 "full" => DateStyle::Full,
88 _ => return None,
89 })
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum RelativeUnit {
96 Second,
97 Minute,
98 Hour,
99 Day,
100 Week,
101 Month,
102 Year,
103}
104
105impl RelativeUnit {
106 pub fn as_str(self) -> &'static str {
107 match self {
108 RelativeUnit::Second => "second",
109 RelativeUnit::Minute => "minute",
110 RelativeUnit::Hour => "hour",
111 RelativeUnit::Day => "day",
112 RelativeUnit::Week => "week",
113 RelativeUnit::Month => "month",
114 RelativeUnit::Year => "year",
115 }
116 }
117
118 pub fn parse(s: &str) -> Option<RelativeUnit> {
119 Some(match s {
120 "second" => RelativeUnit::Second,
121 "minute" => RelativeUnit::Minute,
122 "hour" => RelativeUnit::Hour,
123 "day" => RelativeUnit::Day,
124 "week" => RelativeUnit::Week,
125 "month" => RelativeUnit::Month,
126 "year" => RelativeUnit::Year,
127 _ => return None,
128 })
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq)]
136pub struct NumberOptions {
137 pub decimals: Option<u32>,
138 pub grouping: bool,
139}
140
141impl Default for NumberOptions {
142 fn default() -> Self {
143 NumberOptions { decimals: None, grouping: true }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Default)]
150pub struct DateOptions {
151 pub format: Option<String>,
152 pub style: Option<DateStyle>,
153 pub time: bool,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq)]
160pub struct DateValue {
161 pub ms: f64,
162 pub date_only: bool,
163}
164
165pub trait Formatter: Send + Sync {
169 fn locale(&self) -> String;
171 fn number(&self, value: f64, options: NumberOptions) -> String;
172 fn currency(&self, value: f64, code: &str, options: NumberOptions) -> String;
174 fn percent(&self, value: f64, decimals: Option<u32>) -> String;
176 fn date(&self, value: DateValue, options: &DateOptions) -> String;
177 fn relative_time(&self, value: i64, unit: RelativeUnit) -> String;
179 fn plural(&self, value: f64) -> PluralCategory;
180 fn bytes(&self, value: f64, unit: ByteUnit) -> String {
185 let decimals = if value.fract() == 0.0 { 0 } else { 1 };
186 format!("{} {}", self.number(value, NumberOptions { decimals: Some(decimals), grouping: true }), unit.english())
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum ByteUnit {
193 Byte,
194 Kilobyte,
195 Megabyte,
196 Gigabyte,
197}
198
199impl ByteUnit {
200 pub fn as_str(self) -> &'static str {
202 match self {
203 ByteUnit::Byte => "byte",
204 ByteUnit::Kilobyte => "kilobyte",
205 ByteUnit::Megabyte => "megabyte",
206 ByteUnit::Gigabyte => "gigabyte",
207 }
208 }
209
210 pub fn english(self) -> &'static str {
212 match self {
213 ByteUnit::Byte => "byte",
214 ByteUnit::Kilobyte => "kB",
215 ByteUnit::Megabyte => "MB",
216 ByteUnit::Gigabyte => "GB",
217 }
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct DateNames {
229 pub months: Vec<String>,
230 pub months_short: Vec<String>,
231 pub weekdays: Vec<String>,
232 pub weekdays_short: Vec<String>,
233 pub day_periods: Vec<String>,
234}
235
236#[derive(Deserialize)]
237#[serde(rename_all = "camelCase")]
238struct RelativeData {
239 units: IndexMap<String, Vec<String>>,
240 auto: IndexMap<String, IndexMap<String, String>>,
241 past: String,
242 future: String,
243}
244
245#[derive(Deserialize)]
246#[serde(rename_all = "camelCase")]
247struct Fallback {
248 #[serde(flatten)]
249 names: DateNames,
250 currency_prefix: IndexMap<String, String>,
251 currency_digits: IndexMap<String, u32>,
252 relative: RelativeData,
253 unknown_currency: String,
254 date_patterns: IndexMap<String, String>,
255 time_pattern: String,
256 date_time_join: IndexMap<String, String>,
257}
258
259#[derive(Deserialize)]
260#[serde(rename_all = "camelCase")]
261struct LocaleFile {
262 english_fallback: Fallback,
263}
264
265static FALLBACK: LazyLock<Fallback> = LazyLock::new(|| serde_json::from_str::<LocaleFile>(g::LOCALE_JSON).expect("locale.json englishFallback").english_fallback);
266
267pub fn english_names() -> &'static DateNames {
269 &FALLBACK.names
270}
271
272pub const ENGLISH_DATE_PATTERNS: [(DateStyle, &str); 4] = [(DateStyle::Short, "M/d/yy"), (DateStyle::Medium, "MMM d, y"), (DateStyle::Long, "MMMM d, y"), (DateStyle::Full, "EEEE, MMMM d, y")];
274pub const ENGLISH_TIME_PATTERN: &str = "h:mm a";
275
276pub fn currency_digits(code: &str) -> u32 {
278 FALLBACK.currency_digits.get(&code.to_uppercase()).copied().unwrap_or(2)
279}
280
281pub fn display_string(value: &Value) -> String {
291 match value {
292 Value::String(s) => s.clone(),
293 Value::Number(n) => match n.as_f64() {
294 Some(f) if f.is_finite() => json::number_to_string(f),
295 _ => String::new(),
296 },
297 Value::Bool(b) => b.to_string(),
298 _ => String::new(),
299 }
300}
301
302pub fn display_opt(value: Option<&Value>) -> String {
304 value.map(display_string).unwrap_or_default()
305}
306
307#[derive(Debug, Clone)]
312struct Decimal {
313 int: String,
314 frac: String,
315}
316
317pub(crate) fn shortest_digits(abs: f64) -> (String, i32) {
320 if abs == 0.0 {
321 return ("0".into(), 1);
322 }
323 let e = format!("{abs:e}");
324 let (mantissa, exp) = e.split_once('e').unwrap_or((&e, "0"));
325 let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
326 let exp: i32 = exp.parse().unwrap_or(0);
327 (digits, exp + 1)
328}
329
330fn decimal_of(abs: f64) -> Decimal {
331 let (digits, point) = shortest_digits(abs);
332 let mut all = digits;
333 let mut point = point;
334 if point <= 0 {
335 all = "0".repeat((1 - point) as usize) + &all;
336 point = 1;
337 }
338 let point = point as usize;
339 if point > all.len() {
340 all.push_str(&"0".repeat(point - all.len()));
341 }
342 let int = all[..point].trim_start_matches('0');
343 let int = if int.is_empty() { "0" } else { int };
344 Decimal { int: int.to_string(), frac: all[point..].trim_end_matches('0').to_string() }
345}
346
347fn shift(d: &Decimal, places: usize) -> Decimal {
349 let mut frac = d.frac.clone();
350 if frac.len() < places {
351 frac.push_str(&"0".repeat(places - frac.len()));
352 }
353 let int = format!("{}{}", d.int, &frac[..places]);
354 let int = int.trim_start_matches('0');
355 Decimal { int: if int.is_empty() { "0".into() } else { int.to_string() }, frac: frac[places..].trim_end_matches('0').to_string() }
356}
357
358fn round(d: &Decimal, min: usize, max: usize) -> Decimal {
361 let mut int = d.int.clone();
362 let mut frac = d.frac.clone();
363 if frac.len() > max {
364 let up = frac.as_bytes()[max] >= b'5';
365 let mut kept: Vec<u8> = format!("{int}{}", &frac[..max]).into_bytes();
366 if up {
367 let mut i = kept.len();
368 let mut carried = true;
369 while i > 0 {
370 i -= 1;
371 if kept[i] == b'9' {
372 kept[i] = b'0';
373 } else {
374 kept[i] += 1;
375 carried = false;
376 break;
377 }
378 }
379 if carried {
380 kept.insert(0, b'1');
381 }
382 }
383 let kept = String::from_utf8(kept).unwrap_or_default();
384 let int_len = kept.len() - max;
385 int = if int_len == 0 { "0".into() } else { kept[..int_len].to_string() };
386 frac = kept[int_len..].trim_end_matches('0').to_string();
387 }
388 if frac.len() < min {
389 frac.push_str(&"0".repeat(min - frac.len()));
390 }
391 Decimal { int, frac }
392}
393
394fn group(int: &str, on: bool) -> String {
395 if !on || int.len() < 4 {
396 return int.to_string();
397 }
398 let mut out = String::with_capacity(int.len() + int.len() / 3);
399 for (i, c) in int.chars().enumerate() {
400 if i > 0 && (int.len() - i).is_multiple_of(3) {
401 out.push(',');
402 }
403 out.push(c);
404 }
405 out
406}
407
408fn english_number(value: f64, min: usize, max: usize, grouping: bool) -> (&'static str, String) {
409 let neg = value < 0.0 || (value == 0.0 && value.is_sign_negative());
410 if value.is_nan() {
411 return ("", "NaN".into());
412 }
413 if value.is_infinite() {
414 return (if neg { "-" } else { "" }, "∞".into());
415 }
416 let d = round(&decimal_of(value.abs()), min, max);
417 let body = if d.frac.is_empty() { group(&d.int, grouping) } else { format!("{}.{}", group(&d.int, grouping), d.frac) };
418 (if neg { "-" } else { "" }, body)
419}
420
421pub fn fixed_digits(decimals: f64) -> Option<u32> {
423 decimals.is_finite().then(|| decimals.trunc().clamp(0.0, 20.0) as u32)
424}
425
426const MS_SECOND: f64 = 1_000.0;
431const MS_MINUTE: f64 = 60_000.0;
432const MS_HOUR: f64 = 3_600_000.0;
433const MS_DAY: f64 = 86_400_000.0;
434
435fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
437 let y = if m <= 2 { y - 1 } else { y };
438 let era = y.div_euclid(400);
439 let yoe = y - era * 400;
440 let mp = (m + 9) % 12;
441 let doy = (153 * mp + 2) / 5 + d - 1;
442 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
443 era * 146_097 + doe - 719_468
444}
445
446fn civil_from_days(z: i64) -> (i64, u32, u32) {
448 let z = z + 719_468;
449 let era = z.div_euclid(146_097);
450 let doe = z - era * 146_097;
451 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
452 let y = yoe + era * 400;
453 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
454 let mp = (5 * doy + 2) / 153;
455 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
456 let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
457 (if m <= 2 { y + 1 } else { y }, m, d)
458}
459
460fn date_utc(y: i64, m: i64, d: i64, h: i64, mi: i64, s: i64, ms: f64) -> f64 {
463 let y = y + (m - 1).div_euclid(12);
464 let m = (m - 1).rem_euclid(12) + 1;
465 let days = days_from_civil(y, m, 1) + (d - 1);
466 days as f64 * MS_DAY + (h as f64) * MS_HOUR + (mi as f64) * MS_MINUTE + (s as f64) * MS_SECOND + ms
467}
468
469fn digits_n(s: &str, n: usize) -> Option<i64> {
470 (s.len() == n && s.bytes().all(|b| b.is_ascii_digit())).then(|| s.parse().ok()).flatten()
471}
472
473pub fn parse_date_value(value: &Value) -> Option<DateValue> {
477 match value {
478 Value::Number(n) => n.as_f64().filter(|f| f.is_finite()).map(|ms| DateValue { ms, date_only: false }),
479 Value::String(s) => parse_date_str(s),
480 _ => None,
481 }
482}
483
484fn parse_date_str(s: &str) -> Option<DateValue> {
485 if s.trim().is_empty() || !s.is_ascii() {
486 return None;
487 }
488 let ymd = |d: &str| -> Option<(i64, i64, i64)> {
489 let mut it = d.split('-');
490 let y = digits_n(it.next()?, 4)?;
491 let m = digits_n(it.next()?, 2)?;
492 let day = digits_n(it.next()?, 2)?;
493 it.next().is_none().then_some((y, m, day))
494 };
495 if s.len() == 10 {
496 let (y, m, d) = ymd(s)?;
497 return Some(DateValue { ms: date_utc(y, m, d, 0, 0, 0, 0.0), date_only: true });
498 }
499 let (date, rest) = s.split_once('T')?;
500 let (y, m, d) = ymd(date)?;
501 let (time, offset_min) = if let Some(t) = rest.strip_suffix('Z') {
503 (t, 0i64)
504 } else if rest.len() > 6 && matches!(rest.as_bytes()[rest.len() - 6], b'+' | b'-') {
505 let (t, z) = rest.split_at(rest.len() - 6);
506 let sign = if z.starts_with('-') { -1 } else { 1 };
507 let (hh, mm) = z[1..].split_once(':')?;
508 let (hh, mm) = (digits_n(hh, 2)?, digits_n(mm, 2)?);
509 if hh > 23 || mm > 59 {
510 return None;
511 }
512 (t, sign * (hh * 60 + mm))
513 } else {
514 (rest, 0)
515 };
516 let mut parts = time.split(':');
517 let h = digits_n(parts.next()?, 2)?;
518 let mi = digits_n(parts.next()?, 2)?;
519 let (sec, frac_ms) = match parts.next() {
520 None => (0, 0.0),
521 Some(sec) => {
522 let (whole, frac) = match sec.split_once('.') {
523 Some((w, f)) => {
524 if f.is_empty() || !f.bytes().all(|b| b.is_ascii_digit()) {
525 return None;
526 }
527 (w, f)
528 }
529 None => (sec, ""),
530 };
531 let whole = digits_n(whole, 2)?;
532 let ms: f64 = if frac.is_empty() { 0.0 } else { format!("{:0<3}", &frac[..frac.len().min(3)]).parse().unwrap_or(0.0) };
534 (whole, ms)
535 }
536 };
537 if parts.next().is_some() || !(1..=12).contains(&m) || !(1..=31).contains(&d) || mi > 59 || sec > 59 || h > 24 || (h == 24 && (mi > 0 || sec > 0 || frac_ms > 0.0)) {
538 return None;
539 }
540 let ms = date_utc(y, m, d, h, mi, sec, frac_ms) - (offset_min as f64) * MS_MINUTE;
541 ms.is_finite().then_some(DateValue { ms, date_only: false })
542}
543
544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
546pub struct DateFields {
547 pub year: i64,
548 pub month: u32,
550 pub day: u32,
551 pub weekday: u32,
553 pub hour: u32,
554 pub minute: u32,
555 pub second: u32,
556}
557
558pub fn utc_fields(ms: f64) -> DateFields {
560 let days = (ms / MS_DAY).floor() as i64;
561 let rem = (ms - days as f64 * MS_DAY).max(0.0);
562 let (year, month, day) = civil_from_days(days);
563 let secs = (rem / MS_SECOND).floor() as i64;
564 DateFields {
565 year,
566 month,
567 day,
568 weekday: (days + 4).rem_euclid(7) as u32,
569 hour: (secs / 3600) as u32,
570 minute: ((secs / 60) % 60) as u32,
571 second: (secs % 60) as u32,
572 }
573}
574
575fn pad(n: i64, width: usize) -> String {
576 if n < 0 {
577 return format!("-{:0>width$}", -n, width = width);
578 }
579 format!("{n:0>width$}")
580}
581
582pub fn format_pattern(pattern: &str, f: &DateFields, names: &DateNames) -> String {
586 let chars: Vec<char> = pattern.chars().collect();
587 let mut out = String::new();
588 let mut i = 0;
589 let name = |list: &Vec<String>, i: usize| list.get(i).cloned().unwrap_or_default();
590 while i < chars.len() {
591 let ch = chars[i];
592 if ch == '\'' {
593 if chars.get(i + 1) == Some(&'\'') {
594 out.push('\'');
595 i += 2;
596 continue;
597 }
598 i += 1;
599 while i < chars.len() {
600 if chars[i] == '\'' {
601 if chars.get(i + 1) == Some(&'\'') {
602 out.push('\'');
603 i += 2;
604 continue;
605 }
606 i += 1;
607 break;
608 }
609 out.push(chars[i]);
610 i += 1;
611 }
612 continue;
613 }
614 if !ch.is_ascii_alphabetic() {
615 out.push(ch);
616 i += 1;
617 continue;
618 }
619 let mut n = 1;
620 while chars.get(i + n) == Some(&ch) {
621 n += 1;
622 }
623 i += n;
624 let h12 = if f.hour.is_multiple_of(12) { 12 } else { f.hour % 12 };
625 match ch {
626 'y' => out.push_str(&if n == 2 { pad(f.year.rem_euclid(100), 2) } else { pad(f.year, n) }),
627 'M' => out.push_str(&match n {
628 4.. => name(&names.months, f.month as usize - 1),
629 3 => name(&names.months_short, f.month as usize - 1),
630 _ => pad(f.month as i64, n),
631 }),
632 'd' => out.push_str(&pad(f.day as i64, n)),
633 'E' => out.push_str(&if n >= 4 { name(&names.weekdays, f.weekday as usize) } else { name(&names.weekdays_short, f.weekday as usize) }),
634 'h' => out.push_str(&pad(h12 as i64, n)),
635 'H' => out.push_str(&pad(f.hour as i64, n)),
636 'm' => out.push_str(&pad(f.minute as i64, n)),
637 's' => out.push_str(&pad(f.second as i64, n)),
638 'a' => out.push_str(&name(&names.day_periods, if f.hour < 12 { 0 } else { 1 })),
639 _ => (0..n).for_each(|_| out.push(ch)),
640 }
641 }
642 out
643}
644
645pub fn relative_time_unit(delta_ms: f64) -> (i64, RelativeUnit) {
650 let abs = delta_ms.abs();
651 let t = |unit: f64| {
652 let v = (delta_ms / unit).trunc();
653 if v == 0.0 {
654 0
655 } else {
656 v as i64
657 }
658 };
659 if abs < MS_MINUTE {
660 (t(MS_SECOND), RelativeUnit::Second)
661 } else if abs < MS_HOUR {
662 (t(MS_MINUTE), RelativeUnit::Minute)
663 } else if abs < MS_DAY {
664 (t(MS_HOUR), RelativeUnit::Hour)
665 } else if abs < 7.0 * MS_DAY {
666 (t(MS_DAY), RelativeUnit::Day)
667 } else if abs < 30.0 * MS_DAY {
668 (t(7.0 * MS_DAY), RelativeUnit::Week)
669 } else if abs < 365.0 * MS_DAY {
670 (t(30.0 * MS_DAY), RelativeUnit::Month)
671 } else {
672 (t(365.0 * MS_DAY), RelativeUnit::Year)
673 }
674}
675
676#[derive(Debug, Clone, Copy, Default)]
684pub struct EnglishFormatter;
685
686impl Formatter for EnglishFormatter {
687 fn locale(&self) -> String {
688 "en-US".into()
689 }
690
691 fn number(&self, value: f64, options: NumberOptions) -> String {
692 let (min, max) = options.decimals.map(|d| (d as usize, d as usize)).unwrap_or((0, 3));
693 let (sign, body) = english_number(value, min, max, options.grouping);
694 format!("{sign}{body}")
695 }
696
697 fn currency(&self, value: f64, code: &str, options: NumberOptions) -> String {
698 let code = code.to_uppercase();
699 let digits = options.decimals.unwrap_or_else(|| currency_digits(&code)) as usize;
700 let (sign, body) = english_number(value, digits, digits, options.grouping);
701 let prefix = FALLBACK.currency_prefix.get(&code).cloned().unwrap_or_else(|| FALLBACK.unknown_currency.replace("{code}", &code));
702 format!("{sign}{prefix}{body}")
703 }
704
705 fn percent(&self, value: f64, decimals: Option<u32>) -> String {
706 let digits = decimals.unwrap_or(0) as usize;
707 if !value.is_finite() {
708 let (sign, body) = english_number(value, 0, 0, true);
709 return format!("{sign}{body}%");
710 }
711 let neg = value < 0.0 || (value == 0.0 && value.is_sign_negative());
712 let d = round(&shift(&decimal_of(value.abs()), 2), digits, digits);
713 let body = if d.frac.is_empty() { group(&d.int, true) } else { format!("{}.{}", group(&d.int, true), d.frac) };
714 format!("{}{body}%", if neg { "-" } else { "" })
715 }
716
717 fn date(&self, value: DateValue, options: &DateOptions) -> String {
718 english_date(value, options, 0)
719 }
720
721 fn relative_time(&self, value: i64, unit: RelativeUnit) -> String {
722 if let Some(auto) = FALLBACK.relative.auto.get(unit.as_str()).and_then(|m| m.get(&value.to_string())) {
723 return auto.clone();
724 }
725 let names = &FALLBACK.relative.units[unit.as_str()];
726 let count = english_number(value.unsigned_abs() as f64, 0, 0, true).1;
727 let unit_name = if value.unsigned_abs() == 1 { &names[0] } else { &names[1] };
728 let template = if value < 0 { &FALLBACK.relative.past } else { &FALLBACK.relative.future };
729 template.replace("{value}", &count).replace("{unit}", unit_name)
730 }
731
732 fn plural(&self, value: f64) -> PluralCategory {
733 if value.abs() == 1.0 {
734 PluralCategory::One
735 } else {
736 PluralCategory::Other
737 }
738 }
739}
740
741fn english_date(value: DateValue, options: &DateOptions, offset_minutes: i32) -> String {
744 let ms = if value.date_only { value.ms } else { value.ms + offset_minutes as f64 * 60_000.0 };
745 let f = utc_fields(ms);
746 let names = english_names();
747 if let Some(pattern) = &options.format {
748 return format_pattern(pattern, &f, names);
749 }
750 let style = options.style.unwrap_or_default();
751 let pattern = FALLBACK.date_patterns.get(style.as_str()).map(String::as_str).unwrap_or("MMM d, y");
752 let date = format_pattern(pattern, &f, names);
753 if !options.time {
754 return date;
755 }
756 let time = format_pattern(&FALLBACK.time_pattern, &f, names);
757 let join = FALLBACK.date_time_join.get(style.as_str()).map(String::as_str).unwrap_or("{date}, {time}");
758 join.replace("{date}", &date).replace("{time}", &time)
759}
760
761pub type ZoneOffset = std::sync::Arc<dyn Fn(f64) -> i32 + Send + Sync>;
766
767#[derive(Clone)]
770pub struct ZonedEnglishFormatter {
771 offset: ZoneOffset,
772}
773
774impl ZonedEnglishFormatter {
775 pub fn new(offset: ZoneOffset) -> Self {
776 ZonedEnglishFormatter { offset }
777 }
778
779 pub fn fixed(minutes: i32) -> Self {
781 ZonedEnglishFormatter { offset: std::sync::Arc::new(move |_| minutes) }
782 }
783}
784
785impl std::fmt::Debug for ZonedEnglishFormatter {
786 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787 f.debug_struct("ZonedEnglishFormatter").finish_non_exhaustive()
788 }
789}
790
791impl Formatter for ZonedEnglishFormatter {
792 fn locale(&self) -> String {
793 ENGLISH.locale()
794 }
795 fn number(&self, value: f64, options: NumberOptions) -> String {
796 ENGLISH.number(value, options)
797 }
798 fn currency(&self, value: f64, code: &str, options: NumberOptions) -> String {
799 ENGLISH.currency(value, code, options)
800 }
801 fn percent(&self, value: f64, decimals: Option<u32>) -> String {
802 ENGLISH.percent(value, decimals)
803 }
804 fn date(&self, value: DateValue, options: &DateOptions) -> String {
805 english_date(value, options, (self.offset)(value.ms))
806 }
807 fn relative_time(&self, value: i64, unit: RelativeUnit) -> String {
808 ENGLISH.relative_time(value, unit)
809 }
810 fn plural(&self, value: f64) -> PluralCategory {
811 ENGLISH.plural(value)
812 }
813}
814
815pub static ENGLISH: EnglishFormatter = EnglishFormatter;
817
818pub fn parse_number(formatter: &dyn Formatter, text: &str) -> Option<f64> {
824 let digits: Vec<char> = formatter.number(9_876_543_210.0, NumberOptions { decimals: Some(0), grouping: false }).chars().filter(|c| c.is_numeric()).collect();
826 let digit_of = |c: char| -> Option<char> {
827 if digits.len() == 10 {
828 digits.iter().position(|d| *d == c).map(|i| char::from(b'9' - i as u8))
829 } else {
830 None
831 }
832 };
833 let sample = formatter.number(12_345.6, NumberOptions { decimals: Some(1), grouping: true });
835 let mut seps: Vec<String> = Vec::new();
837 let mut run = String::new();
838 let mut seen_digit = false;
839 for c in sample.chars() {
840 if c.is_numeric() {
841 if !run.is_empty() {
842 seps.push(std::mem::take(&mut run));
843 }
844 seen_digit = true;
845 } else if seen_digit {
846 run.push(c);
847 }
848 }
849 let (group, decimal) = match seps.as_slice() {
850 [d] => (None, d.trim().to_string()),
851 [g, .., d] => (Some(g.clone()), d.trim().to_string()),
852 [] => (None, String::new()),
853 };
854 let decimal = if decimal.is_empty() { ".".to_string() } else { decimal };
855 let ascii: String = text.chars().map(|c| if c.is_ascii_digit() { c } else { digit_of(c).unwrap_or(c) }).collect();
856 let mut clean = ascii.trim().to_string();
857 if let Some(g) = group.as_deref().filter(|g| !g.trim().is_empty()) {
858 clean = clean.replace(g, "");
859 }
860 clean.retain(|c| !c.is_whitespace() && !matches!(c, '\u{200e}' | '\u{200f}' | '\u{61c}'));
861 let clean = clean.replacen(&decimal, ".", 1).replace(['−', '–'], "-");
862 if clean.is_empty() || clean == "-" {
863 return None;
864 }
865 clean.parse::<f64>().ok().filter(|v| v.is_finite())
866}
867
868pub fn format_date_value(formatter: &dyn Formatter, value: &Value, options: &DateOptions) -> String {
870 parse_date_value(value).map(|v| formatter.date(v, options)).unwrap_or_default()
871}
872
873pub fn format_relative_value(formatter: &dyn Formatter, value: &Value, now_ms: f64) -> String {
875 match parse_date_value(value) {
876 Some(v) => {
877 let (n, unit) = relative_time_unit(v.ms - now_ms);
878 formatter.relative_time(n, unit)
879 }
880 None => String::new(),
881 }
882}
883
884pub const FORMAT_FUNCTION_NAMES: &[&str] = g::FORMAT_FUNCTION_NAMES;
892
893pub fn is_format_function(name: &str) -> bool {
894 FORMAT_FUNCTION_NAMES.contains(&name)
895}
896
897pub fn now_ms() -> f64 {
899 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as f64).unwrap_or(0.0)
900}
901
902fn to_num(v: Option<&Value>) -> f64 {
904 match v {
905 Some(Value::Number(n)) => n.as_f64().unwrap_or(f64::NAN),
906 Some(Value::String(s)) => js_number_from_str(s),
907 _ => f64::NAN,
908 }
909}
910
911fn js_number_from_str(s: &str) -> f64 {
915 let s = s.trim_matches([' ', '\t', '\n', '\r']);
916 let b = s.as_bytes();
917 let mut i = usize::from(matches!(b.first(), Some(b'+' | b'-')));
918 let digits = |i: &mut usize| {
919 let start = *i;
920 while b.get(*i).is_some_and(u8::is_ascii_digit) {
921 *i += 1;
922 }
923 *i - start
924 };
925 let int = digits(&mut i);
926 let frac = if b.get(i) == Some(&b'.') {
927 i += 1;
928 digits(&mut i)
929 } else {
930 0
931 };
932 if int == 0 && frac == 0 {
933 return f64::NAN;
934 }
935 if matches!(b.get(i), Some(b'e' | b'E')) {
936 i += 1;
937 if matches!(b.get(i), Some(b'+' | b'-')) {
938 i += 1;
939 }
940 if digits(&mut i) == 0 {
941 return f64::NAN;
942 }
943 }
944 if i != b.len() {
945 return f64::NAN;
946 }
947 s.parse::<f64>().unwrap_or(f64::NAN)
948}
949
950fn opt_digits(v: Option<&Value>) -> Option<u32> {
951 match v {
952 None | Some(Value::Null) => None,
953 Some(v) => fixed_digits(to_num(Some(v))),
954 }
955}
956
957fn grouping(v: Option<&Value>) -> bool {
958 !matches!(v, Some(Value::Bool(false)))
959}
960
961pub fn call_format_function(name: &str, args: &Map<String, Value>, formatter: &dyn Formatter, now: f64) -> Option<Option<Value>> {
965 let a = |k: &str| args.get(k);
966 let text = |s: String| Some(Value::String(s));
967 let number_or = |f: &dyn Fn(f64) -> String| {
968 let n = to_num(a("value"));
969 text(if n.is_finite() { f(n) } else { String::new() })
970 };
971 Some(match name {
972 "formatNumber" => number_or(&|n| formatter.number(n, NumberOptions { decimals: opt_digits(a("decimals")), grouping: grouping(a("grouping")) })),
973 "formatCurrency" => number_or(&|n| match a("currency").and_then(Value::as_str) {
974 Some(code) if code.len() == 3 && code.bytes().all(|b| b.is_ascii_alphabetic()) => formatter.currency(n, &code.to_uppercase(), NumberOptions { decimals: opt_digits(a("decimals")), grouping: grouping(a("grouping")) }),
975 _ => String::new(),
976 }),
977 "formatPercent" => number_or(&|n| formatter.percent(n, opt_digits(a("decimals")))),
978 "formatDate" => {
979 let options = DateOptions {
980 format: a("format").and_then(Value::as_str).filter(|f| !f.is_empty()).map(str::to_string),
981 style: a("style").and_then(Value::as_str).and_then(DateStyle::parse),
982 time: a("time") == Some(&Value::Bool(true)),
983 };
984 text(format_date_value(formatter, a("value").unwrap_or(&Value::Null), &options))
985 }
986 "formatRelativeTime" => {
987 let at = match a("now") {
988 Some(Value::Number(n)) => n.as_f64().unwrap_or(now),
989 Some(v) => parse_date_value(v).map(|d| d.ms).unwrap_or(now),
990 None => now,
991 };
992 text(format_relative_value(formatter, a("value").unwrap_or(&Value::Null), at))
993 }
994 "pluralize" => {
995 let arm = |k: &str| a(k).filter(|v| !v.is_null()).cloned();
996 let n = to_num(a("value"));
997 if !n.is_finite() {
998 arm("other")
999 } else if n == 0.0 && a("zero").is_some() {
1000 a("zero").cloned()
1001 } else {
1002 arm(formatter.plural(n).as_str()).or_else(|| arm("other"))
1003 }
1004 }
1005 _ => return None,
1006 })
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use super::*;
1012 use serde_json::json;
1013
1014 #[test]
1015 fn display_strings_follow_ecmascript() {
1016 for (v, s) in [(json!(412), "412"), (json!(1.5), "1.5"), (json!(0.30000000000000004), "0.30000000000000004"), (json!(1e-7), "1e-7"), (json!(1e21), "1e+21"), (json!(-0.0), "0"), (json!(true), "true"), (json!(null), ""), (json!([1]), "")] {
1017 assert_eq!(display_string(&v), s, "{v}");
1018 }
1019 }
1020
1021 #[test]
1022 fn dates_round_trip_through_the_civil_calendar() {
1023 let f = utc_fields(946_684_799_000.0);
1024 assert_eq!((f.year, f.month, f.day, f.weekday, f.hour, f.minute, f.second), (1999, 12, 31, 5, 23, 59, 59));
1025 assert_eq!(parse_date_value(&json!("2026-10-14T00:30:00+02:00")).map(|d| utc_fields(d.ms).day), Some(13));
1026 assert!(parse_date_value(&json!("next tuesday")).is_none());
1027 let f = utc_fields(-1.0);
1028 assert_eq!((f.year, f.month, f.day, f.hour), (1969, 12, 31, 23));
1029 }
1030
1031 #[test]
1032 fn a_host_formatter_replaces_the_fallback() {
1033 struct Loud;
1034 impl Formatter for Loud {
1035 fn locale(&self) -> String {
1036 "x".into()
1037 }
1038 fn number(&self, v: f64, _: NumberOptions) -> String {
1039 format!("#{v}")
1040 }
1041 fn currency(&self, _: f64, _: &str, _: NumberOptions) -> String {
1042 String::new()
1043 }
1044 fn percent(&self, _: f64, _: Option<u32>) -> String {
1045 String::new()
1046 }
1047 fn date(&self, _: DateValue, _: &DateOptions) -> String {
1048 String::new()
1049 }
1050 fn relative_time(&self, _: i64, _: RelativeUnit) -> String {
1051 String::new()
1052 }
1053 fn plural(&self, _: f64) -> PluralCategory {
1054 PluralCategory::Other
1055 }
1056 }
1057 let args = json!({"value": 3}).as_object().cloned().unwrap();
1058 assert_eq!(call_format_function("formatNumber", &args, &Loud, 0.0), Some(Some(json!("#3"))));
1059 let args = json!({"value": 1000}).as_object().cloned().unwrap();
1060 assert_eq!(call_format_function("formatRelativeTime", &args, &ENGLISH, 61_000.0), Some(Some(json!("1 minute ago"))));
1061 }
1062
1063 struct Seps(&'static str, &'static str, bool);
1066 impl Formatter for Seps {
1067 fn locale(&self) -> String {
1068 "x".into()
1069 }
1070 fn number(&self, v: f64, o: NumberOptions) -> String {
1071 let en = ENGLISH.number(v, o).replace(',', "\u{0}").replace('.', self.1).replace('\u{0}', self.0);
1072 if self.2 {
1073 en.chars().map(|c| c.to_digit(10).map(|d| char::from_u32(0x660 + d).unwrap()).unwrap_or(c)).collect()
1074 } else {
1075 en
1076 }
1077 }
1078 fn currency(&self, _: f64, _: &str, _: NumberOptions) -> String {
1079 String::new()
1080 }
1081 fn percent(&self, _: f64, _: Option<u32>) -> String {
1082 String::new()
1083 }
1084 fn date(&self, _: DateValue, _: &DateOptions) -> String {
1085 String::new()
1086 }
1087 fn relative_time(&self, _: i64, _: RelativeUnit) -> String {
1088 String::new()
1089 }
1090 fn plural(&self, _: f64) -> PluralCategory {
1091 PluralCategory::Other
1092 }
1093 }
1094
1095 #[test]
1096 fn typed_numbers_parse_in_the_formatters_separators_and_digits() {
1097 assert_eq!(parse_number(&ENGLISH, "1,234.50"), Some(1234.5));
1098 assert_eq!(parse_number(&ENGLISH, " −2.5 "), Some(-2.5));
1099 assert_eq!(parse_number(&ENGLISH, ""), None);
1100 assert_eq!(parse_number(&ENGLISH, "-"), None);
1101 assert_eq!(parse_number(&ENGLISH, "abc"), None);
1102 let de = Seps(".", ",", false);
1103 assert_eq!(de.number(1234.5, NumberOptions { decimals: Some(2), grouping: true }), "1.234,50");
1104 assert_eq!(parse_number(&de, "2,50"), Some(2.5));
1105 assert_eq!(parse_number(&de, "1.234,50"), Some(1234.5));
1106 let fr = Seps("\u{202f}", ",", false);
1107 assert_eq!(parse_number(&fr, "1\u{202f}234,5"), Some(1234.5));
1108 let ar = Seps("٬", "٫", true);
1109 assert_eq!(parse_number(&ar, "١٬٢٣٤٫٥"), Some(1234.5));
1110 assert_eq!(parse_number(&ar, "\u{61c}-٧٫٠"), Some(-7.0));
1111 }
1112
1113 #[test]
1114 fn numeric_strings_follow_the_reference_pattern() {
1115 for (text, want) in [("12", 12.0), (" -1.5e3\n", -1500.0), ("+.5", 0.5), ("1.", 1.0), ("2E-2", 0.02)] {
1116 assert_eq!(js_number_from_str(text), want, "{text:?}");
1117 }
1118 for text in ["", " ", "0x10", "Infinity", "1,000", "1e", ".", "-", "\u{a0}1", "1 2", "e5"] {
1119 assert!(js_number_from_str(text).is_nan(), "{text:?}");
1120 }
1121 }
1122
1123 #[test]
1124 fn the_zoned_fallback_shifts_instants_never_days() {
1125 let berlin = ZonedEnglishFormatter::fixed(120);
1126 let at = parse_date_value(&json!("2026-10-14T22:30:00Z")).unwrap();
1127 assert_eq!(berlin.date(at, &DateOptions { format: Some("yyyy-MM-dd HH:mm".into()), ..Default::default() }), "2026-10-15 00:30");
1128 let day = parse_date_value(&json!("2026-10-14")).unwrap();
1129 assert_eq!(ZonedEnglishFormatter::fixed(840).date(day, &DateOptions { style: Some(DateStyle::Long), ..Default::default() }), "October 14, 2026");
1130 assert_eq!(ENGLISH.currency(1.0, "XYZ", NumberOptions::default()), "XYZ\u{a0}1.00");
1131 }
1132}