Skip to content

Commit 983286a

Browse files
committed
feat(currency): Add accounting formatting and relax constructor validation
1 parent 8996c3e commit 983286a

6 files changed

Lines changed: 146 additions & 66 deletions

File tree

components/experimental/src/dimension/currency/compact_formatter.rs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ use super::{
2222
CurrencyCode,
2323
options::{CurrencyFormatterOptions, CurrencyUsage},
2424
};
25-
use icu_pattern::DoublePlaceholderPattern;
2625

2726
extern crate alloc;
2827

@@ -116,10 +115,6 @@ impl CompactCurrencyFormatter {
116115
})?
117116
.payload;
118117

119-
if essential.get().standard_pattern().is_none() {
120-
return Err(DataError::custom("missing standard pattern"));
121-
}
122-
123118
let decimal_formatter = DecimalFormatter::try_new((&prefs).into(), Default::default())?;
124119

125120
let compact_data = DataProvider::<icu_decimal::provider::DecimalCompactShortV1>::load(
@@ -202,10 +197,6 @@ impl CompactCurrencyFormatter {
202197
})?
203198
.payload;
204199

205-
if essential.get().standard_pattern().is_none() {
206-
return Err(DataError::custom("missing standard pattern"));
207-
}
208-
209200
let fractions = provider.load(Default::default())?.payload;
210201

211202
Ok(Self {
@@ -243,18 +234,18 @@ impl CompactCurrencyFormatter {
243234
let (currency_placeholder, pos_pattern, neg_pattern, pattern_selection) = self
244235
.essential
245236
.get()
246-
.name_and_pattern(self.options.width, currency_code);
237+
.name_and_pattern(self.options.width, self.options.usage, currency_code);
247238

248239
let mut sign = value.sign;
249240
let pattern = if sign == Sign::Negative {
250241
if let Some(neg_pattern) = neg_pattern {
251242
sign = Sign::None;
252243
neg_pattern
253244
} else {
254-
pos_pattern.unwrap_or_else(|| <&DoublePlaceholderPattern>::default())
245+
pos_pattern
255246
}
256247
} else {
257-
pos_pattern.unwrap_or_else(|| <&DoublePlaceholderPattern>::default())
248+
pos_pattern
258249
};
259250

260251
let fraction_info = self.resolve_fraction_info(

components/experimental/src/dimension/currency/format.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,4 +396,42 @@ mod tests {
396396
"-$12K"
397397
);
398398
}
399+
400+
#[test]
401+
pub fn test_accounting_formatting() {
402+
let prefs: CurrencyFormatterPreferences = locale!("en").into();
403+
let currency_code = CurrencyCode(tinystr!(3, "USD"));
404+
let value = "-12345.67".parse().unwrap();
405+
406+
let fmt_accounting = CurrencyFormatter::<Decimal>::try_new_short(
407+
prefs,
408+
CurrencyFormatterOptions {
409+
usage: CurrencyUsage::Accounting,
410+
..Default::default()
411+
},
412+
&currency_code,
413+
)
414+
.unwrap();
415+
assert_writeable_eq!(fmt_accounting.format_fixed_decimal(&value), "($12,345.67)");
416+
}
417+
418+
#[test]
419+
pub fn test_compact_accounting_formatting() {
420+
let prefs: CompactCurrencyFormatterPreferences = locale!("en").into();
421+
let currency_code = CurrencyCode(tinystr!(3, "USD"));
422+
let value = "-12345.67".parse().unwrap();
423+
424+
let fmt_accounting = CompactCurrencyFormatter::try_new(
425+
prefs,
426+
CurrencyFormatterOptions {
427+
usage: CurrencyUsage::Accounting,
428+
..Default::default()
429+
},
430+
)
431+
.unwrap();
432+
assert_writeable_eq!(
433+
fmt_accounting.format_fixed_decimal(&value, &currency_code),
434+
"($12K)"
435+
);
436+
}
399437
}

components/experimental/src/dimension/currency/formatter.rs

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ use super::super::provider::currency::{
2323
use super::CurrencyCode;
2424
use super::options::{CurrencyFormatterOptions, CurrencyUsage, Width};
2525
use fixed_decimal::{RoundingIncrement, Sign, SignedRoundingMode, UnsignedRoundingMode};
26-
use icu_pattern::DoublePlaceholderPattern;
2726

2827
extern crate alloc;
2928

@@ -145,10 +144,6 @@ impl CurrencyFormatter<Decimal> {
145144
let essential =
146145
load_with_fallback::<CurrencyEssentialsV1>(&crate::provider::Baked, ids)?.payload;
147146

148-
if essential.get().standard_pattern().is_none() {
149-
return Err(DataError::custom("missing standard pattern"));
150-
}
151-
152147
let fractions = crate::provider::Baked.load(Default::default())?.payload;
153148

154149
Ok(Self {
@@ -184,10 +179,6 @@ impl CurrencyFormatter<Decimal> {
184179
let essential =
185180
load_with_fallback::<CurrencyEssentialsV1>(&crate::provider::Baked, ids)?.payload;
186181

187-
if essential.get().standard_pattern().is_none() {
188-
return Err(DataError::custom("missing standard pattern"));
189-
}
190-
191182
let fractions = crate::provider::Baked.load(Default::default())?.payload;
192183

193184
Ok(Self {
@@ -231,10 +222,6 @@ impl CurrencyFormatter<Decimal> {
231222
let ids = req_id.into_iter().chain(core::iter::once(default_id));
232223
let essential = load_with_fallback::<CurrencyEssentialsV1>(provider, ids)?.payload;
233224

234-
if essential.get().standard_pattern().is_none() {
235-
return Err(DataError::custom("missing standard pattern"));
236-
}
237-
238225
let fractions = provider.load(Default::default())?.payload;
239226

240227
Ok(Self {
@@ -274,10 +261,6 @@ impl CurrencyFormatter<Decimal> {
274261
let ids = req_id.into_iter().chain(core::iter::once(default_id));
275262
let essential = load_with_fallback::<CurrencyEssentialsV1>(provider, ids)?.payload;
276263

277-
if essential.get().standard_pattern().is_none() {
278-
return Err(DataError::custom("missing standard pattern"));
279-
}
280-
281264
let fractions = provider.load(Default::default())?.payload;
282265

283266
Ok(Self {
@@ -464,18 +447,18 @@ impl CurrencyFormatter<Decimal> {
464447
// an exact decimal/money representation.
465448
let (currency_str, pos_pattern, neg_pattern, pattern_selection) = essential
466449
.get()
467-
.name_and_pattern(self.options.width, &self.bound_currency);
450+
.name_and_pattern(self.options.width, self.options.usage, &self.bound_currency);
468451

469452
let mut sign = value.sign;
470453
let pattern = if sign == Sign::Negative {
471454
if let Some(neg_pattern) = neg_pattern {
472455
sign = Sign::None;
473456
neg_pattern
474457
} else {
475-
pos_pattern.unwrap_or_else(|| <&DoublePlaceholderPattern>::default())
458+
pos_pattern
476459
}
477460
} else {
478-
pos_pattern.unwrap_or_else(|| <&DoublePlaceholderPattern>::default())
461+
pos_pattern
479462
};
480463

481464
let fraction_info =

components/experimental/src/dimension/currency/options.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ pub enum CurrencyUsage {
3838
Standard,
3939
/// Cash currency formatting (may use different rounding).
4040
Cash,
41+
/// Accounting currency formatting (may use different patterns, e.g. parentheses for negative values).
42+
Accounting,
4143
}
4244

4345
#[derive(Default, Debug, Eq, PartialEq, Clone, Copy, Hash)]

components/experimental/src/dimension/provider/currency/essentials.rs

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use zerovec::{VarZeroVec, ZeroMap, ZeroVec};
1414
use icu_pattern::DoublePlaceholderPattern;
1515

1616
use crate::dimension::currency::CurrencyCode;
17-
use crate::dimension::currency::options::Width;
17+
use crate::dimension::currency::options::{CurrencyUsage, Width};
1818

1919
#[cfg(feature = "compiled_data")]
2020
/// Baked data
@@ -144,17 +144,26 @@ pub struct CurrencyPatternConfig {
144144
pub narrow_placeholder_value: Option<PlaceholderValue>,
145145
}
146146

147+
// Fallback pattern: "{0} {1}" (number followed by currency with space, e.g. "10 $")
148+
//
149+
// Even though the baked data handles the fallback at data generation time,
150+
// we have the fallback here for users feeding their own data without
151+
// handling the fallback logic in their data generation.
152+
const FALLBACK_PATTERN: &DoublePlaceholderPattern =
153+
DoublePlaceholderPattern::from_ref_store_unchecked("\x02\x05 ");
154+
147155
impl<'a> CurrencyEssentials<'a> {
148156
/// Returns the formatted currency name/symbol,
149157
/// the currency pattern for the given width and currency,
150158
/// and the pattern selection.
151159
pub(crate) fn name_and_pattern(
152160
&'a self,
153161
width: Width,
162+
usage: CurrencyUsage,
154163
currency: &'a CurrencyCode,
155164
) -> (
156165
&'a str,
157-
Option<&'a DoublePlaceholderPattern>,
166+
&'a DoublePlaceholderPattern,
158167
Option<&'a DoublePlaceholderPattern>,
159168
PatternSelection,
160169
) {
@@ -181,16 +190,28 @@ impl<'a> CurrencyEssentials<'a> {
181190
Width::Long => unreachable!("CurrencyEssentials does not support Long width"),
182191
};
183192

184-
let pos_pattern = match pattern_selection {
185-
PatternSelection::Standard => self.standard_pattern(),
186-
PatternSelection::StandardAlphaNextToNumber => {
193+
let pos_pattern = match (usage, pattern_selection) {
194+
(CurrencyUsage::Accounting, PatternSelection::Standard) => {
195+
self.accounting_positive_pattern()
196+
}
197+
(CurrencyUsage::Accounting, PatternSelection::StandardAlphaNextToNumber) => {
198+
self.accounting_alpha_next_to_number_positive_pattern()
199+
}
200+
(_, PatternSelection::Standard) => self.standard_pattern(),
201+
(_, PatternSelection::StandardAlphaNextToNumber) => {
187202
self.standard_alpha_next_to_number_pattern()
188203
}
189204
};
190205

191-
let neg_pattern = match pattern_selection {
192-
PatternSelection::Standard => self.standard_negative_pattern(),
193-
PatternSelection::StandardAlphaNextToNumber => {
206+
let neg_pattern = match (usage, pattern_selection) {
207+
(CurrencyUsage::Accounting, PatternSelection::Standard) => {
208+
self.accounting_negative_pattern()
209+
}
210+
(CurrencyUsage::Accounting, PatternSelection::StandardAlphaNextToNumber) => {
211+
self.accounting_alpha_next_to_number_negative_pattern()
212+
}
213+
(_, PatternSelection::Standard) => self.standard_negative_pattern(),
214+
(_, PatternSelection::StandardAlphaNextToNumber) => {
194215
self.standard_alpha_next_to_number_negative_pattern()
195216
}
196217
};
@@ -199,10 +220,23 @@ impl<'a> CurrencyEssentials<'a> {
199220
}
200221

201222
/// Returns the standard pattern.
202-
pub fn standard_pattern(&self) -> Option<&DoublePlaceholderPattern> {
223+
///
224+
/// If the standard pattern is missing (which should only happen in corrupt
225+
/// or incomplete custom data), this returns a default safety pattern `"{1}{0}"`.
226+
/// Note that this is a safety default, not a CLDR-defined fallback.
227+
pub fn standard_pattern(&self) -> &DoublePlaceholderPattern {
203228
self.patterns
204229
.get(self.indices.standard as usize)
205-
.or_else(|| self.patterns.get(0))
230+
.unwrap_or_else(|| {
231+
debug_assert!(
232+
false,
233+
"Standard pattern index {} is out of bounds for patterns of length {}",
234+
self.indices.standard,
235+
self.patterns.len()
236+
);
237+
// GIGO
238+
FALLBACK_PATTERN
239+
})
206240
}
207241

208242
/// Returns the standard negative pattern if specified.
@@ -212,14 +246,24 @@ impl<'a> CurrencyEssentials<'a> {
212246
.and_then(|idx| self.patterns.get(idx as usize))
213247
}
214248

215-
/// Returns the `standard_alpha_next_to_number` pattern, falling back to `standard_pattern` if not present.
216-
pub fn standard_alpha_next_to_number_pattern(&self) -> Option<&DoublePlaceholderPattern> {
249+
/// Returns the `standard_alpha_next_to_number` pattern.
250+
///
251+
/// Fallback hierarchy:
252+
/// `standard_alpha_next_to_number` -> `standard`
253+
///
254+
/// Even though the baked data handles the fallback at data generation time,
255+
/// we have the fallback here for users feeding their own data without
256+
/// handling the fallback logic in their data generation.
257+
pub fn standard_alpha_next_to_number_pattern(&self) -> &DoublePlaceholderPattern {
217258
self.patterns
218259
.get(self.indices.standard_alpha_next_to_number as usize)
219-
.or_else(|| self.standard_pattern())
260+
.unwrap_or_else(|| self.standard_pattern())
220261
}
221262

222263
/// Returns the `standard_alpha_next_to_number` negative pattern if specified, falling back to standard negative.
264+
///
265+
/// Fallback hierarchy:
266+
/// `standard_alpha_next_to_number_negative` -> `standard_negative`
223267
pub fn standard_alpha_next_to_number_negative_pattern(
224268
&self,
225269
) -> Option<&DoublePlaceholderPattern> {
@@ -229,30 +273,49 @@ impl<'a> CurrencyEssentials<'a> {
229273
.or_else(|| self.standard_negative_pattern())
230274
}
231275

232-
/// Returns the positive accounting pattern, falling back to `standard_pattern` if not present.
233-
pub fn accounting_positive_pattern(&self) -> Option<&DoublePlaceholderPattern> {
276+
/// Returns the positive accounting pattern.
277+
///
278+
/// Fallback hierarchy:
279+
/// `accounting_positive` -> `standard`
280+
///
281+
/// Even though the baked data handles the fallback at data generation time,
282+
/// we have the fallback here for users feeding their own data without
283+
/// handling the fallback logic in their data generation.
284+
pub fn accounting_positive_pattern(&self) -> &DoublePlaceholderPattern {
234285
self.patterns
235286
.get(self.indices.accounting_positive as usize)
236-
.or_else(|| self.standard_pattern())
287+
.unwrap_or_else(|| self.standard_pattern())
237288
}
238289

239-
/// Returns the negative accounting pattern if present.
290+
/// Returns the negative accounting pattern if present, falling back to standard negative.
291+
///
292+
/// Fallback hierarchy:
293+
/// `accounting_negative` -> `standard_negative`
240294
pub fn accounting_negative_pattern(&self) -> Option<&DoublePlaceholderPattern> {
241295
self.indices
242296
.accounting_negative
243297
.and_then(|idx| self.patterns.get(idx as usize))
298+
.or_else(|| self.standard_negative_pattern())
244299
}
245300

246-
/// Returns the positive `accounting_alpha_next_to_number` pattern, falling back to accounting or standard.
247-
pub fn accounting_alpha_next_to_number_positive_pattern(
248-
&self,
249-
) -> Option<&DoublePlaceholderPattern> {
301+
/// Returns the positive `accounting_alpha_next_to_number` pattern.
302+
///
303+
/// Fallback hierarchy:
304+
/// `accounting_alpha_next_to_number_positive` -> `standard_alpha_next_to_number` -> `standard`
305+
///
306+
/// Even though the baked data handles the fallback at data generation time,
307+
/// we have the fallback here for users feeding their own data without
308+
/// handling the fallback logic in their data generation.
309+
pub fn accounting_alpha_next_to_number_positive_pattern(&self) -> &DoublePlaceholderPattern {
250310
self.patterns
251311
.get(self.indices.accounting_alpha_next_to_number_positive as usize)
252-
.or_else(|| self.accounting_positive_pattern())
312+
.unwrap_or_else(|| self.standard_alpha_next_to_number_pattern())
253313
}
254314

255315
/// Returns the negative `accounting_alpha_next_to_number` pattern, falling back to `accounting_negative_pattern`.
316+
///
317+
/// Fallback hierarchy:
318+
/// `accounting_alpha_next_to_number_negative` -> `accounting_negative` -> `standard_negative`
256319
pub fn accounting_alpha_next_to_number_negative_pattern(
257320
&self,
258321
) -> Option<&DoublePlaceholderPattern> {
@@ -262,3 +325,14 @@ impl<'a> CurrencyEssentials<'a> {
262325
.or_else(|| self.accounting_negative_pattern())
263326
}
264327
}
328+
329+
#[cfg(test)]
330+
mod tests {
331+
use super::*;
332+
use writeable::assert_writeable_eq;
333+
334+
#[test]
335+
fn test_fallback_pattern() {
336+
assert_writeable_eq!(FALLBACK_PATTERN.interpolate(("10", "$")), "10 $");
337+
}
338+
}

0 commit comments

Comments
 (0)