Skip to main content

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}