Skip to main content

perspective_viewer/config/
number_string_format.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13mod 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)]
20#[serde(rename_all = "camelCase", tag = "style")]
21pub enum NumberFormatStyle {
22    #[default]
23    Decimal,
24    Currency(CurrencyNumberFormatStyle),
25    Percent,
26    Unit(UnitNumberFormatStyle),
27}
28
29#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
30#[serde(rename_all = "camelCase")]
31pub enum CurrencyDisplay {
32    Code,
33    #[default]
34    Symbol,
35    NarrowSymbol,
36    Name,
37}
38
39#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
40#[serde(rename_all = "camelCase")]
41pub enum CurrencySign {
42    #[default]
43    Standard,
44    Accounting,
45}
46
47#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Clone, TS)]
48#[serde(rename_all = "camelCase")]
49pub struct CurrencyNumberFormatStyle {
50    #[serde(default)]
51    pub currency: CurrencyCode,
52
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub currency_display: Option<CurrencyDisplay>,
55
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub currency_sign: Option<CurrencySign>,
58}
59
60#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
61#[serde(rename_all = "camelCase")]
62pub enum UnitDisplay {
63    #[default]
64    Short,
65    Narrow,
66    Long,
67}
68
69#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, TS)]
70#[serde(rename_all = "camelCase")]
71pub struct UnitNumberFormatStyle {
72    #[serde(default)]
73    pub unit: Unit,
74
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub unit_display: Option<UnitDisplay>,
77}
78
79#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
80#[serde(rename_all = "camelCase")]
81pub enum RoundingPriority {
82    #[default]
83    Auto,
84    MorePrecision,
85    LessPrecision,
86}
87
88#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
89#[serde(rename_all = "camelCase")]
90pub enum RoundingMode {
91    Ceil,
92    Floor,
93    Expand,
94    Trunc,
95    HalfCeil,
96    HalfFloor,
97    #[default]
98    HalfExpand,
99    HalfTrunc,
100    HalfEven,
101}
102
103#[derive(Default, Debug, PartialEq, Clone, TS)]
104pub enum RoundingIncrement {
105    #[default]
106    Auto,
107    Custom(f64),
108}
109impl std::fmt::Display for RoundingIncrement {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            RoundingIncrement::Auto => f.write_str("Auto"),
113            RoundingIncrement::Custom(val) => f.write_fmt(format_args!("{val}")),
114        }
115    }
116}
117
118pub const ROUNDING_INCREMENTS: [f64; 15] = [
119    1., 2., 5., 10., 20., 25., 50., 100., 200., 250., 500., 1000., 2000., 2500., 5000.,
120];
121
122#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
123#[serde(rename_all = "camelCase")]
124pub enum TrailingZeroDisplay {
125    #[default]
126    Auto,
127    StripIfInteger,
128}
129
130#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, TS)]
131#[serde(rename_all = "camelCase", tag = "notation")]
132pub enum Notation {
133    #[default]
134    Standard,
135    Scientific,
136    Engineering,
137    Compact(CompactDisplay),
138}
139
140#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
141#[serde(rename_all = "camelCase", tag = "compactDisplay")]
142pub enum CompactDisplay {
143    #[default]
144    Short,
145    Long,
146}
147
148#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
149#[serde(rename_all = "snake_case")]
150pub enum UseGrouping {
151    Always,
152
153    #[default]
154    Auto,
155    Min2, // default if notation is compact
156
157    #[serde(untagged)]
158    False(bool),
159}
160
161#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Copy, EnumIter, Display, TS)]
162#[serde(rename_all = "camelCase")]
163pub enum SignDisplay {
164    #[default]
165    Auto,
166    Always,
167    ExceptZero,
168    Negative,
169    Never,
170}
171
172#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, TS)]
173#[serde(rename_all = "camelCase")]
174pub struct CustomNumberFormatConfig {
175    #[serde(flatten)]
176    #[ts(skip)]
177    pub _style: Option<NumberFormatStyle>,
178
179    // see Digit Options
180    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumintegerdigits
181    // these min/max props can all be specified but it results in possible conflicts
182    // may consider making them distinct options
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub minimum_integer_digits: Option<f64>,
185
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub minimum_fraction_digits: Option<f64>,
188
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub maximum_fraction_digits: Option<f64>,
191
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub minimum_significant_digits: Option<f64>,
194
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub maximum_significant_digits: Option<f64>,
197
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub rounding_priority: Option<RoundingPriority>,
200
201    // specific values https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingincrement
202    // Only available with automatic rounding priority
203    // Cannot be mixed with sigfig rounding. (Does this mean max/min sigfig must be unset?)
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub rounding_increment: Option<f64>,
206
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub rounding_mode: Option<RoundingMode>,
209
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub trailing_zero_display: Option<TrailingZeroDisplay>,
212
213    #[serde(flatten)]
214    #[ts(skip)]
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub _notation: Option<Notation>,
217
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub use_grouping: Option<UseGrouping>,
220
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub sign_display: Option<SignDisplay>,
223}
224
225impl CustomNumberFormatConfig {
226    pub fn filter_default(self, is_float: bool) -> Self {
227        let (frac_min, frac_max) = if is_float { (2., 2.) } else { (0., 0.) };
228        let rounding_increment = self.rounding_increment;
229        let use_grouping = self
230            .use_grouping
231            .filter(|val| *val != UseGrouping::default());
232
233        let mut minimum_fraction_digits =
234            self.minimum_fraction_digits.filter(|val| *val != frac_min);
235
236        let mut maximum_fraction_digits =
237            self.maximum_fraction_digits.filter(|val| *val != frac_max);
238
239        let mut show_frac = is_float
240            && (minimum_fraction_digits.is_some()
241                || maximum_fraction_digits.is_some()
242                || use_grouping.is_some()
243                || matches!(
244                    self._style,
245                    Some(NumberFormatStyle::Percent | NumberFormatStyle::Unit(_))
246                ))
247            || !is_float && matches!(self._style, Some(NumberFormatStyle::Currency(_)));
248
249        // Rounding increment does not work unless `minimum_fraction_digits`
250        // and `maximum_fraction_digits` are set to 0.
251        if rounding_increment.is_some() {
252            show_frac = true;
253            minimum_fraction_digits = Some(0.);
254            maximum_fraction_digits = Some(0.);
255        }
256
257        let minimum_significant_digits = self.minimum_significant_digits.filter(|val| *val != 1.);
258        let maximum_significant_digits = self.maximum_significant_digits.filter(|val| *val != 21.);
259        let show_sig = minimum_significant_digits.is_some() || maximum_significant_digits.is_some();
260        Self {
261            _style: self
262                ._style
263                .filter(|style| !matches!(style, NumberFormatStyle::Decimal)),
264            minimum_integer_digits: self.minimum_integer_digits.filter(|val| *val != 1.),
265            minimum_fraction_digits: show_frac
266                .then_some(minimum_fraction_digits.unwrap_or(frac_min)),
267            maximum_fraction_digits: show_frac
268                .then_some(maximum_fraction_digits.unwrap_or(frac_max)),
269            minimum_significant_digits: show_sig
270                .then_some(minimum_significant_digits.unwrap_or(1.)),
271            maximum_significant_digits: show_sig
272                .then_some(minimum_significant_digits.unwrap_or(21.)),
273            rounding_priority: self
274                .rounding_priority
275                .filter(|val| *val != RoundingPriority::default()),
276            rounding_increment,
277            rounding_mode: self
278                .rounding_mode
279                .filter(|val| *val != RoundingMode::default()),
280            trailing_zero_display: self
281                .trailing_zero_display
282                .filter(|val| *val != TrailingZeroDisplay::default()),
283            _notation: self
284                ._notation
285                .filter(|notation| !matches!(notation, Notation::Standard)),
286            use_grouping,
287            sign_display: self
288                .sign_display
289                .filter(|val| *val != SignDisplay::default()),
290        }
291    }
292}