locale_settings/currency.rs
1/*!
2Fetch locale-specific currency formatting settings.
3
4The formatting information here does include the currency symbol,
5_international symbol_, and precision all of which overlap with the
6`symbol`, `alphabetic_code`, and subdivision `exponent` from the
7`codes::currency` module. However, there is significantly more
8information here and this should be used wherever necessary to
9format currency values correctly.
10
11However, values from the currency code can be used, for example the
12name and subdivision name(s), when labeling currency values.
13
14## Example
15
16The following should display "19 US Dollars and 99 cents".
17
18```
19use std::str::FromStr;
20use locale_types::{Locale, LocaleString};
21use locale_codes::currency;
22use locale_settings::locale::{Category, set_locale};
23use locale_settings::currency::get_currency_format;
24
25let amount: f64 = 19.9950;
26let en_us = LocaleString::from_str("en_US.UTF-8").unwrap();
27if set_locale(&Locale::String(en_us), &Category::Currency) {
28 let format = get_currency_format();
29 let currency = currency::lookup_by_alpha(&format.international_format.unwrap().currency_symbol.trim()).unwrap();
30 let local = format.local_format.unwrap();
31 let subdiv = currency.subdivisions.get(0).unwrap();
32 let subdiv_name = &subdiv.name.clone().unwrap();
33 println!(
34 "{0} {2} and {1:.4$} {3}",
35 amount.trunc(),
36 amount.fract(),
37 currency.name,
38 subdiv_name,
39 local.decimal_precision
40 );
41}
42```
43*/
44
45use crate::ffi::localeconv;
46use crate::ffi::utils::*;
47use crate::numeric::NumericFormat;
48use crate::Category;
49use locale_types::{Locale, LocaleResult};
50use std::os::raw::c_char;
51
52// ------------------------------------------------------------------------------------------------
53// Public Types
54// ------------------------------------------------------------------------------------------------
55
56/// This enumeration defines the handling of sign placement and
57/// choice for either positive or negative numeric values. The
58/// examples for each use a value of minus 999.99.
59///
60/// Note that one format that cannot be easily represented here
61/// is the often used spreadsheet _accounting format_ where the
62/// numeric part of the value is right aligned but currency symbols
63/// are left aligned in a cell and parenthesis are used to denote
64/// negatives but again the opening parenthesis is left aligned and
65/// the closing parenthesis is right aligned.
66#[derive(Debug, Clone, PartialEq)]
67pub enum SignLocation {
68 /// Use parenthesis `($999.99)` around the value to denote sign.
69 UseParenthesis,
70 /// Place a symbol before the value `$-999.99`.
71 BeforeString,
72 /// Place a symbol after the value `$999.99-`.
73 AfterString,
74 /// Place a symbol before the currency sign `-$999.99`.
75 BeforeCurrencySymbol,
76 /// Place a symbol after the currency sign `$-999.99`.
77 AfterCurrencySymbol,
78}
79
80/// This denotes the complete handling of sign placement for
81/// a currency value.
82#[derive(Debug, Clone, PartialEq)]
83pub struct SignFormat {
84 /// The currency symbol precedes (or follows) the numeric value.
85 pub currency_symbol_precedes: bool,
86 /// A space separates the numeric value and currency symbol.
87 pub space_separated: bool,
88 /// Placement of the sign symbol.
89 pub sign_location: SignLocation,
90}
91
92/// The actual formatting specification for the currency-specific
93/// part of a value.
94#[derive(Debug, Clone, PartialEq)]
95pub struct CurrencyCellFormat {
96 /// currency symbol to use.
97 pub currency_symbol: String,
98 /// number of digits of precision typically used.
99 pub decimal_precision: usize,
100 /// The correct way to display positive values.
101 pub positive_sign_format: SignFormat,
102 /// The correct way to display negative values.
103 pub negative_sign_format: SignFormat,
104}
105
106/// The complete currency formatting rules.
107#[derive(Debug, Clone)]
108pub struct CurrencyFormat {
109 /// Numeric handling, we reuse this.
110 pub number_format: NumericFormat,
111 /// Formatting values in its native locale.
112 pub local_format: Option<CurrencyCellFormat>,
113 /// Formatting a value outside its native locale.
114 pub international_format: Option<CurrencyCellFormat>,
115}
116
117// ------------------------------------------------------------------------------------------------
118// Public Functions
119// ------------------------------------------------------------------------------------------------
120
121/// Fetch the current local-specific currency formatting rules.
122pub fn get_currency_format() -> CurrencyFormat {
123 unsafe {
124 let char_max = c_char::max_value();
125 let lconv = localeconv();
126 CurrencyFormat {
127 number_format: NumericFormat {
128 positive_sign: cstr_to_string((*lconv).positive_sign),
129 negative_sign: cstr_to_string((*lconv).negative_sign),
130 decimal_separator: cstr_to_string((*lconv).mon_decimal_point),
131 thousands_separator: cstr_to_string((*lconv).mon_thousands_sep),
132 grouping: grouping_vector((*lconv).mon_grouping),
133 },
134 local_format: if (*lconv).frac_digits == char_max {
135 None
136 } else {
137 Some(CurrencyCellFormat {
138 currency_symbol: cstr_to_string((*lconv).currency_symbol),
139 decimal_precision: (*lconv).frac_digits as usize,
140 positive_sign_format: SignFormat {
141 currency_symbol_precedes: ((*lconv).p_cs_precedes == 1),
142 space_separated: ((*lconv).p_sep_by_space == 1),
143 sign_location: match (*lconv).p_sign_posn {
144 0 => SignLocation::UseParenthesis,
145 1 => SignLocation::BeforeString,
146 2 => SignLocation::AfterString,
147 3 => SignLocation::BeforeCurrencySymbol,
148 4 => SignLocation::AfterCurrencySymbol,
149 _ => panic!("Bad sign location {}", (*lconv).p_sign_posn),
150 },
151 },
152 negative_sign_format: SignFormat {
153 currency_symbol_precedes: ((*lconv).n_cs_precedes == 1),
154 space_separated: ((*lconv).n_sep_by_space == 1),
155 sign_location: match (*lconv).n_sign_posn {
156 0 => SignLocation::UseParenthesis,
157 1 => SignLocation::BeforeString,
158 2 => SignLocation::AfterString,
159 3 => SignLocation::BeforeCurrencySymbol,
160 4 => SignLocation::AfterCurrencySymbol,
161 _ => panic!("Bad sign location {}", (*lconv).n_sign_posn),
162 },
163 },
164 })
165 },
166 international_format: if (*lconv).int_frac_digits == char_max {
167 None
168 } else {
169 Some(CurrencyCellFormat {
170 currency_symbol: cstr_to_string((*lconv).int_curr_symbol),
171 decimal_precision: (*lconv).int_frac_digits as usize,
172 positive_sign_format: SignFormat {
173 currency_symbol_precedes: ((*lconv).int_p_cs_precedes == 1),
174 space_separated: ((*lconv).int_p_sep_by_space == 1),
175 sign_location: match (*lconv).int_p_sign_posn {
176 0 => SignLocation::UseParenthesis,
177 1 => SignLocation::BeforeString,
178 2 => SignLocation::AfterString,
179 3 => SignLocation::BeforeCurrencySymbol,
180 4 => SignLocation::AfterCurrencySymbol,
181 _ => panic!("Bad sign location {}", (*lconv).int_p_sign_posn),
182 },
183 },
184 negative_sign_format: SignFormat {
185 currency_symbol_precedes: ((*lconv).int_n_cs_precedes == 1),
186 space_separated: ((*lconv).int_n_sep_by_space == 1),
187 sign_location: match (*lconv).int_n_sign_posn {
188 0 => SignLocation::UseParenthesis,
189 1 => SignLocation::BeforeString,
190 2 => SignLocation::AfterString,
191 3 => SignLocation::BeforeCurrencySymbol,
192 4 => SignLocation::AfterCurrencySymbol,
193 _ => panic!("Bad sign location {}", (*lconv).int_n_sign_posn),
194 },
195 },
196 })
197 },
198 }
199 }
200}
201
202/// Fetch the currency formatting rules for a specified `Locale`.
203///
204/// # Arguments
205///
206/// * `locale` - The locale to query.
207/// * `inherit_current` - Whether the specified locale should inherit
208/// from the current locale.
209///
210/// If `inherit_current` is `false` the `locale` specified will be treated
211/// as an entirely new and complete locale when calling the C
212/// [`newlocale`](https://man.openbsd.org/newlocale.3) function. If it is
213/// `true` the `locale` is assumed to be a partially specified one and inherits
214/// any unspecified components from the current locale. For example, if the
215/// current locale is `en_US.UTF-8` and the parameters passed are `_NZ` and
216/// `true` then the resulting locale will be `en_NZ.UTF-8`.
217pub fn get_currency_format_for_locale(
218 locale: Locale,
219 inherit_current: bool,
220) -> LocaleResult<CurrencyFormat> {
221 get_format_for_locale(
222 Category::Currency,
223 locale,
224 &get_currency_format,
225 inherit_current,
226 )
227}
228
229// ------------------------------------------------------------------------------------------------
230// Unit Tests
231// ------------------------------------------------------------------------------------------------
232
233#[cfg(test)]
234mod tests {
235 use crate::currency::{get_currency_format, get_currency_format_for_locale};
236 use crate::locale::{set_locale, Category};
237 use locale_types::{Locale, LocaleString};
238 use std::str::FromStr;
239
240 // --------------------------------------------------------------------------------------------
241 #[test]
242 fn test_currency_settings() {
243 if set_locale(&Locale::POSIX, &Category::Currency) {
244 let format = get_currency_format();
245 println!("{:#?}", format);
246 assert_eq!(format.number_format.decimal_separator, "");
247 assert_eq!(format.local_format, None);
248 assert_eq!(format.international_format, None);
249 } else {
250 panic!("set_locale returned false");
251 }
252 }
253
254 #[test]
255 fn test_currency_settings_us() {
256 let en_us = LocaleString::from_str("en_US.UTF-8").unwrap();
257 if set_locale(&Locale::String(en_us), &Category::Currency) {
258 let format = get_currency_format();
259 println!("{:#?}", format);
260 assert_eq!(format.number_format.decimal_separator, ".");
261 assert_eq!(format.local_format.unwrap().currency_symbol, "$");
262 assert_eq!(format.international_format.unwrap().currency_symbol, "USD ");
263 } else {
264 panic!("set_locale returned false");
265 }
266 }
267
268 // --------------------------------------------------------------------------------------------
269 #[test]
270 fn test_currency_settings_for_locale() {
271 if set_locale(&Locale::POSIX, &Category::Currency) {
272 let locale = Locale::from_str("ja_JP.SJIS").unwrap();
273 let format = get_currency_format_for_locale(locale, false).unwrap();
274 println!("{:#?}", format);
275 assert_eq!(format.number_format.decimal_separator, ".");
276 assert!(format.local_format.is_some());
277 assert!(format.international_format.is_some());
278 assert_eq!(
279 format.international_format.unwrap().currency_symbol,
280 "JPY ".to_string()
281 );
282 } else {
283 panic!("set_locale returned false");
284 }
285 }
286}