1mod enums;
14pub use enums::*;
15use serde::{Deserialize, Serialize};
16use strum::{Display, EnumIter};
17use ts_rs::TS;
18
19#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, TS)]
25#[serde(rename_all = "camelCase", tag = "style")]
26pub enum NumberFormatStyle {
27 #[default]
28 Decimal,
29 Currency(CurrencyNumberFormatStyle),
30 Percent,
31 Unit(UnitNumberFormatStyle),
32}
33
34#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
35#[serde(rename_all = "camelCase")]
36pub enum CurrencyDisplay {
37 Code,
38 #[default]
39 Symbol,
40 NarrowSymbol,
41 Name,
42}
43
44#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
45#[serde(rename_all = "camelCase")]
46pub enum CurrencySign {
47 #[default]
48 Standard,
49 Accounting,
50}
51
52#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Clone, TS)]
53#[serde(rename_all = "camelCase")]
54pub struct CurrencyNumberFormatStyle {
55 #[serde(default)]
56 pub currency: CurrencyCode,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub currency_display: Option<CurrencyDisplay>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub currency_sign: Option<CurrencySign>,
63}
64
65#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
66#[serde(rename_all = "camelCase")]
67pub enum UnitDisplay {
68 #[default]
69 Short,
70 Narrow,
71 Long,
72}
73
74#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, TS)]
75#[serde(rename_all = "camelCase")]
76pub struct UnitNumberFormatStyle {
77 #[serde(default)]
78 pub unit: Unit,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub unit_display: Option<UnitDisplay>,
82}
83
84#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
85#[serde(rename_all = "camelCase")]
86pub enum RoundingPriority {
87 #[default]
88 Auto,
89 MorePrecision,
90 LessPrecision,
91}
92
93#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
94#[serde(rename_all = "camelCase")]
95pub enum RoundingMode {
96 Ceil,
97 Floor,
98 Expand,
99 Trunc,
100 HalfCeil,
101 HalfFloor,
102 #[default]
103 HalfExpand,
104 HalfTrunc,
105 HalfEven,
106}
107
108#[derive(Default, Debug, PartialEq, Clone, TS)]
109pub enum RoundingIncrement {
110 #[default]
111 Auto,
112 Custom(f64),
113}
114impl std::fmt::Display for RoundingIncrement {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 RoundingIncrement::Auto => f.write_str("Auto"),
118 RoundingIncrement::Custom(val) => f.write_fmt(format_args!("{val}")),
119 }
120 }
121}
122
123pub const ROUNDING_INCREMENTS: [f64; 15] = [
124 1., 2., 5., 10., 20., 25., 50., 100., 200., 250., 500., 1000., 2000., 2500., 5000.,
125];
126
127#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
128#[serde(rename_all = "camelCase")]
129pub enum TrailingZeroDisplay {
130 #[default]
131 Auto,
132 StripIfInteger,
133}
134
135#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, TS)]
141#[serde(rename_all = "camelCase", tag = "notation")]
142pub enum Notation {
143 #[default]
144 Standard,
145 Scientific,
146 Engineering,
147 Compact(CompactDisplay),
148}
149
150#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
151#[serde(rename_all = "camelCase", tag = "compactDisplay")]
152pub enum CompactDisplay {
153 #[default]
154 Short,
155 Long,
156}
157
158#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
159#[serde(rename_all = "snake_case")]
160pub enum UseGrouping {
161 Always,
162
163 #[default]
164 Auto,
165 Min2, #[serde(untagged)]
168 False(bool),
169}
170
171#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
172#[serde(rename_all = "camelCase")]
173pub enum SignDisplay {
174 #[default]
175 Auto,
176 Always,
177 ExceptZero,
178 Negative,
179 Never,
180}
181
182#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, TS)]
191#[serde(rename_all = "camelCase")]
192pub struct CustomNumberFormatConfig {
193 #[serde(flatten)]
194 #[ts(skip)]
195 pub _style: Option<NumberFormatStyle>,
196
197 #[serde(skip_serializing_if = "Option::is_none")]
202 #[ts(optional, as = "Option<_>")]
203 pub minimum_integer_digits: Option<f64>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
206 #[ts(optional, as = "Option<_>")]
207 pub minimum_fraction_digits: Option<f64>,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
210 #[ts(optional, as = "Option<_>")]
211 pub maximum_fraction_digits: Option<f64>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
214 #[ts(optional, as = "Option<_>")]
215 pub minimum_significant_digits: Option<f64>,
216
217 #[serde(skip_serializing_if = "Option::is_none")]
218 #[ts(optional, as = "Option<_>")]
219 pub maximum_significant_digits: Option<f64>,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
222 #[ts(optional, as = "Option<_>")]
223 pub rounding_priority: Option<RoundingPriority>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
229 #[ts(optional, as = "Option<_>")]
230 pub rounding_increment: Option<f64>,
231
232 #[serde(skip_serializing_if = "Option::is_none")]
233 #[ts(optional, as = "Option<_>")]
234 pub rounding_mode: Option<RoundingMode>,
235
236 #[serde(skip_serializing_if = "Option::is_none")]
237 #[ts(optional, as = "Option<_>")]
238 pub trailing_zero_display: Option<TrailingZeroDisplay>,
239
240 #[serde(flatten)]
241 #[ts(skip)]
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub _notation: Option<Notation>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
250 #[ts(optional, as = "Option<_>")]
251 pub use_grouping: Option<UseGrouping>,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
254 #[ts(optional, as = "Option<_>")]
255 pub sign_display: Option<SignDisplay>,
256}
257
258#[derive(Clone, Debug, PartialEq)]
261pub struct NumberFormatDefaults {
262 pub is_float: bool,
263 pub style: NumberFormatStyle,
264 pub notation: Notation,
265 pub minimum_integer_digits: f64,
266 pub fraction: (f64, f64),
267 pub significant: (f64, f64),
268 pub rounding_priority: RoundingPriority,
269 pub rounding_mode: RoundingMode,
270 pub trailing_zero_display: TrailingZeroDisplay,
271 pub use_grouping: UseGrouping,
272 pub sign_display: SignDisplay,
273}
274
275impl NumberFormatDefaults {
276 pub fn builtin(is_float: bool) -> Self {
277 Self {
278 is_float,
279 style: NumberFormatStyle::default(),
280 notation: Notation::default(),
281 minimum_integer_digits: 1.,
282 fraction: if is_float { (2., 2.) } else { (0., 0.) },
283 significant: (1., 21.),
284 rounding_priority: RoundingPriority::default(),
285 rounding_mode: RoundingMode::default(),
286 trailing_zero_display: TrailingZeroDisplay::default(),
287 use_grouping: UseGrouping::default(),
288 sign_display: SignDisplay::default(),
289 }
290 }
291
292 pub fn resolve(spec_default: Option<&CustomNumberFormatConfig>, is_float: bool) -> Self {
294 let mut out = Self::builtin(is_float);
295 let Some(d) = spec_default else {
296 return out;
297 };
298
299 if let Some(style) = &d._style {
300 out.style = style.clone();
301 }
302
303 if let Some(notation) = &d._notation {
304 out.notation = notation.clone();
305 }
306
307 if let Some(v) = d.minimum_integer_digits {
308 out.minimum_integer_digits = v;
309 }
310
311 if let Some(v) = d.minimum_fraction_digits {
312 out.fraction.0 = v;
313 }
314
315 if let Some(v) = d.maximum_fraction_digits {
316 out.fraction.1 = v;
317 }
318
319 if let Some(v) = d.minimum_significant_digits {
320 out.significant.0 = v;
321 }
322
323 if let Some(v) = d.maximum_significant_digits {
324 out.significant.1 = v;
325 }
326
327 if let Some(v) = d.rounding_priority {
328 out.rounding_priority = v;
329 }
330
331 if let Some(v) = d.rounding_mode {
332 out.rounding_mode = v;
333 }
334
335 if let Some(v) = d.trailing_zero_display {
336 out.trailing_zero_display = v;
337 }
338
339 if let Some(v) = d.use_grouping {
340 out.use_grouping = v;
341 }
342
343 if let Some(v) = d.sign_display {
344 out.sign_display = v;
345 }
346
347 out
348 }
349}
350
351impl CustomNumberFormatConfig {
352 pub fn filter_default(self, defaults: &NumberFormatDefaults) -> Self {
353 let (frac_min, frac_max) = defaults.fraction;
354 let rounding_increment = self.rounding_increment;
355 let use_grouping = self
356 .use_grouping
357 .filter(|val| *val != defaults.use_grouping);
358
359 let mut minimum_fraction_digits =
360 self.minimum_fraction_digits.filter(|val| *val != frac_min);
361
362 let mut maximum_fraction_digits =
363 self.maximum_fraction_digits.filter(|val| *val != frac_max);
364
365 let mut show_frac = defaults.is_float
366 && (minimum_fraction_digits.is_some()
367 || maximum_fraction_digits.is_some()
368 || use_grouping.is_some()
369 || matches!(
370 self._style,
371 Some(NumberFormatStyle::Percent | NumberFormatStyle::Unit(_))
372 ))
373 || !defaults.is_float && matches!(self._style, Some(NumberFormatStyle::Currency(_)));
374
375 if rounding_increment.is_some() {
378 show_frac = true;
379 minimum_fraction_digits = Some(0.);
380 maximum_fraction_digits = Some(0.);
381 }
382
383 let minimum_significant_digits = self
384 .minimum_significant_digits
385 .filter(|val| *val != defaults.significant.0);
386
387 let maximum_significant_digits = self
388 .maximum_significant_digits
389 .filter(|val| *val != defaults.significant.1);
390
391 let show_sig = minimum_significant_digits.is_some() || maximum_significant_digits.is_some();
392 Self {
393 _style: self._style.filter(|style| *style != defaults.style),
394 minimum_integer_digits: self
395 .minimum_integer_digits
396 .filter(|val| *val != defaults.minimum_integer_digits),
397 minimum_fraction_digits: show_frac
398 .then_some(minimum_fraction_digits.unwrap_or(frac_min)),
399 maximum_fraction_digits: show_frac
400 .then_some(maximum_fraction_digits.unwrap_or(frac_max)),
401 minimum_significant_digits: show_sig
402 .then_some(minimum_significant_digits.unwrap_or(defaults.significant.0)),
403 maximum_significant_digits: show_sig
404 .then_some(maximum_significant_digits.unwrap_or(defaults.significant.1)),
405 rounding_priority: self
406 .rounding_priority
407 .filter(|val| *val != defaults.rounding_priority),
408 rounding_increment,
409 rounding_mode: self
410 .rounding_mode
411 .filter(|val| *val != defaults.rounding_mode),
412 trailing_zero_display: self
413 .trailing_zero_display
414 .filter(|val| *val != defaults.trailing_zero_display),
415 _notation: self
416 ._notation
417 .filter(|notation| *notation != defaults.notation),
418 use_grouping,
419 sign_display: self
420 .sign_display
421 .filter(|val| *val != defaults.sign_display),
422 }
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 fn sig(min: f64, max: f64) -> CustomNumberFormatConfig {
431 CustomNumberFormatConfig {
432 minimum_significant_digits: Some(min),
433 maximum_significant_digits: Some(max),
434 ..Default::default()
435 }
436 }
437
438 #[test]
439 fn resolve_overlays_partial_default_on_builtin() {
440 let spec = CustomNumberFormatConfig {
441 _notation: Some(Notation::Compact(CompactDisplay::Short)),
442 minimum_fraction_digits: Some(0.),
443 maximum_fraction_digits: Some(1.),
444 ..Default::default()
445 };
446
447 let defaults = NumberFormatDefaults::resolve(Some(&spec), true);
448 assert_eq!(defaults.notation, Notation::Compact(CompactDisplay::Short));
449 assert_eq!(defaults.fraction, (0., 1.));
450 assert_eq!(defaults.style, NumberFormatStyle::Decimal);
451 assert_eq!(defaults.significant, (1., 21.));
452 assert_eq!(
453 NumberFormatDefaults::resolve(None, true),
454 NumberFormatDefaults::builtin(true)
455 );
456 }
457
458 #[test]
459 fn filter_preserves_max_significant_digits() {
460 let filtered = sig(1., 5.).filter_default(&NumberFormatDefaults::builtin(true));
461 assert_eq!(filtered.minimum_significant_digits, Some(1.));
462 assert_eq!(filtered.maximum_significant_digits, Some(5.));
463
464 let filtered = sig(3., 21.).filter_default(&NumberFormatDefaults::builtin(true));
465 assert_eq!(filtered.minimum_significant_digits, Some(3.));
466 assert_eq!(filtered.maximum_significant_digits, Some(21.));
467 }
468
469 #[test]
470 fn filter_elides_values_equal_to_resolved_defaults() {
471 let defaults = NumberFormatDefaults {
472 fraction: (0., 1.),
473 notation: Notation::Compact(CompactDisplay::Short),
474 ..NumberFormatDefaults::builtin(true)
475 };
476
477 let config = CustomNumberFormatConfig {
478 _notation: Some(Notation::Compact(CompactDisplay::Short)),
479 minimum_fraction_digits: Some(0.),
480 maximum_fraction_digits: Some(1.),
481 ..Default::default()
482 };
483
484 assert_eq!(
485 config.filter_default(&defaults),
486 CustomNumberFormatConfig::default()
487 );
488 }
489
490 #[test]
491 fn filter_serializes_builtin_values_under_override() {
492 let defaults = NumberFormatDefaults {
493 notation: Notation::Compact(CompactDisplay::Short),
494 ..NumberFormatDefaults::builtin(true)
495 };
496
497 let config = CustomNumberFormatConfig {
498 _notation: Some(Notation::Standard),
499 ..Default::default()
500 };
501
502 let filtered = config.filter_default(&defaults);
503 assert_eq!(filtered._notation, Some(Notation::Standard));
504 }
505
506 #[test]
507 fn filter_bakes_fraction_digits_for_percent_style() {
508 let config = CustomNumberFormatConfig {
509 _style: Some(NumberFormatStyle::Percent),
510 ..Default::default()
511 };
512
513 let filtered = config.filter_default(&NumberFormatDefaults::builtin(true));
514 assert_eq!(filtered._style, Some(NumberFormatStyle::Percent));
515 assert_eq!(filtered.minimum_fraction_digits, Some(2.));
516 assert_eq!(filtered.maximum_fraction_digits, Some(2.));
517 }
518}