1use {
18 ecma402_traits, rust_icu_common as common, rust_icu_unumberformatter as unumf,
19 std::convert::TryInto, std::fmt,
20};
21
22#[derive(Debug)]
23pub struct NumberFormat {
24 rep: unumf::UNumberFormatter,
26}
27
28pub(crate) mod internal {
29 use {
30 ecma402_traits::numberformat, ecma402_traits::numberformat::options,
31 rust_icu_common as common,
32 };
33
34 pub fn skeleton_from(opts: &numberformat::Options) -> Result<String, common::Error> {
41 let mut skel: Vec<String> = vec![];
42 if let Some(ref c) = opts.compact_display {
43 match c {
44 options::CompactDisplay::Long => skel.push("compact-long".into()),
45 options::CompactDisplay::Short => skel.push("compact-short".into()),
46 }
47 }
48 match opts.style {
49 options::Style::Currency => {
50 match opts.currency {
51 None => {
52 return Err(common::Error::Wrapper(anyhow::anyhow!(
53 "currency not specified"
54 )));
55 }
56 Some(ref c) => {
57 skel.push(format!("currency/{}", &c.0));
58 }
59 }
60 match opts.currency_display {
61 options::CurrencyDisplay::Symbol => {
62 skel.push(format!("unit-width-short"));
63 }
64 options::CurrencyDisplay::NarrowSymbol => {
65 skel.push(format!("unit-width-narrow"));
66 }
67 options::CurrencyDisplay::Code => {
68 skel.push(format!("unit-width-iso-code"));
69 }
70 options::CurrencyDisplay::Name => {
71 skel.push(format!("unit-width-full-name"));
72 }
73 }
74 match opts.currency_sign {
75 options::CurrencySign::Accounting => {
76 skel.push(format!("sign-accounting"));
77 }
78 options::CurrencySign::Standard => {
79 }
81 }
82 }
83 options::Style::Unit => match opts.unit {
84 None => {
85 return Err(common::Error::Wrapper(anyhow::anyhow!(
86 "unit not specified"
87 )));
88 }
89 Some(ref u) => {
90 skel.push(format!("measure-unit/{}", &u.0));
91 }
92 },
93 options::Style::Percent => {
94 skel.push(format!("percent"));
95 }
96 options::Style::Decimal => {
97 }
99 }
100 match opts.notation {
101 options::Notation::Standard => {
102 }
104 options::Notation::Engineering => match opts.sign_display {
105 options::SignDisplay::Auto => {
106 skel.push(format!("scientific/*ee"));
107 }
108 options::SignDisplay::Always => {
109 skel.push(format!("scientific/*ee/sign-always"));
110 }
111 options::SignDisplay::Never => {
112 skel.push(format!("scientific/*ee/sign-never"));
113 }
114 options::SignDisplay::ExceptZero => {
115 skel.push(format!("scientific/*ee/sign-expect-zero"));
116 }
117 },
118 options::Notation::Scientific => {
119 skel.push(format!("scientific"));
120 }
121 options::Notation::Compact => {
122 skel.push(format!("compact-short"));
124 }
125 }
126 if let Some(ref n) = opts.numbering_system {
127 skel.push(format!("numbering-system/{}", &n.0));
128 }
129
130 if opts.notation != options::Notation::Engineering {
131 match opts.sign_display {
132 options::SignDisplay::Auto => {
133 skel.push("sign-auto".into());
134 }
135 options::SignDisplay::Never => {
136 skel.push("sign-never".into());
137 }
138 options::SignDisplay::Always => {
139 skel.push("sign-always".into());
140 }
141 options::SignDisplay::ExceptZero => {
142 skel.push("sign-always".into());
143 }
144 }
145 }
146
147 let minimum_integer_digits = opts.minimum_integer_digits.unwrap_or(1);
148 let minimum_fraction_digits = opts.minimum_fraction_digits.unwrap_or(match opts.style {
151 options::Style::Currency => 2,
152 _ => 0,
153 });
154 let maximum_fraction_digits = opts.maximum_fraction_digits.unwrap_or(match opts.style {
155 options::Style::Currency => std::cmp::max(2, minimum_fraction_digits),
156 _ => 3,
157 });
158 let minimum_significant_digits = opts.minimum_significant_digits.unwrap_or(1);
159 let maximum_significant_digits = opts.maximum_significant_digits.unwrap_or(21);
160
161 skel.push(integer_digits(minimum_integer_digits as usize));
163 skel.push(fraction_digits(
164 minimum_fraction_digits as usize,
165 maximum_fraction_digits as usize,
166 minimum_significant_digits as usize,
167 maximum_significant_digits as usize,
168 ));
169
170 Ok(skel.iter().map(|s| format!("{} ", s)).collect())
171 }
172
173 fn integer_digits(digits: usize) -> String {
177 let zeroes: String = std::iter::repeat("0").take(digits).collect();
178 #[cfg(feature = "icu_version_67_plus")]
179 return format!("integer-width/*{}", zeroes);
180 #[cfg(not(feature = "icu_version_67_plus"))]
181 return format!("integer-width/+{}", zeroes);
182 }
183
184 fn fraction_digits(min: usize, max: usize, min_sig: usize, max_sig: usize) -> String {
185 eprintln!(
186 "fraction_digits: min: {}, max: {} min_sig: {}, max_sig: {}",
187 min, max, min_sig, max_sig
188 );
189 assert!(min <= max, "fraction_digits: min: {}, max: {}", min, max);
190 let zeroes: String = std::iter::repeat("0").take(min).collect();
191 let hashes: String = std::iter::repeat("#").take(max - min).collect();
192
193 assert!(
194 min_sig <= max_sig,
195 "significant_digits: min: {}, max: {}",
196 min_sig,
197 max_sig
198 );
199 let ats: String = std::iter::repeat("@").take(min_sig).collect();
200 let hashes_sig: String = std::iter::repeat("#").take(max_sig - min_sig).collect();
201
202 return format!(".{}{}/{}{}", zeroes, hashes, ats, hashes_sig,);
203 }
204
205 #[cfg(test)]
206 mod testing {
207 use super::*;
208
209 #[test]
210 fn fraction_digits_skeleton_fragment() {
211 assert_eq!(fraction_digits(0, 3, 1, 21), ".###/@####################");
212 assert_eq!(fraction_digits(2, 2, 1, 21), ".00/@####################");
213 assert_eq!(fraction_digits(0, 0, 0, 0), "./");
214 assert_eq!(fraction_digits(0, 3, 3, 3), ".###/@@@");
215 }
216 }
217}
218
219impl ecma402_traits::numberformat::NumberFormat for NumberFormat {
220 type Error = common::Error;
221
222 fn try_new<L>(l: L, opts: ecma402_traits::numberformat::Options) -> Result<Self, Self::Error>
227 where
228 L: ecma402_traits::Locale,
229 Self: Sized,
230 {
231 let locale = format!("{}", l);
232 let skeleton: String = internal::skeleton_from(&opts)?;
233 let rep = unumf::UNumberFormatter::try_new(&skeleton, &locale)?;
234 Ok(NumberFormat { rep })
235 }
236
237 fn format<W>(&self, number: f64, writer: &mut W) -> fmt::Result
244 where
245 W: fmt::Write,
246 {
247 let result = self.rep.format_double(number).map_err(|e| e.into())?;
248 let result_str: String = result.try_into().map_err(|e: common::Error| e.into())?;
249 write!(writer, "{}", result_str)
250 }
251}
252
253#[cfg(test)]
254mod testing {
255
256 use super::*;
257 use ecma402_traits::numberformat;
258 use ecma402_traits::numberformat::NumberFormat;
259 use rust_icu_uloc as uloc;
260 use std::convert::TryFrom;
261
262 #[test]
263 fn formatting() {
264 #[derive(Debug, Clone)]
265 struct TestCase {
266 locale: &'static str,
267 opts: numberformat::Options,
268 numbers: Vec<f64>,
269 expected: Vec<&'static str>,
270 }
271 let tests = vec![
272 TestCase {
273 locale: "sr-RS",
274 opts: Default::default(),
275 numbers: vec![
276 0.0, 1.0, -1.0, 1.5, -1.5, 100.0, 1000.0, 10000.0, 123456.789,
277 ],
278 expected: vec![
279 "0",
280 "1",
281 "-1",
282 "1,5",
283 "-1,5",
284 "100",
285 "1.000",
286 "10.000",
287 "123.456,789",
288 ],
289 },
290 TestCase {
291 locale: "de-DE",
292 opts: numberformat::Options {
293 style: numberformat::options::Style::Currency,
294 currency: Some("EUR".into()),
295 ..Default::default()
296 },
297 numbers: vec![123456.789],
298 expected: vec!["123.456,79\u{a0}€"],
299 },
300 TestCase {
301 locale: "ja-JP",
302 opts: numberformat::Options {
303 style: numberformat::options::Style::Currency,
304 currency: Some("JPY".into()),
305 minimum_fraction_digits: Some(0),
308 maximum_fraction_digits: Some(0),
309 ..Default::default()
310 },
311 numbers: vec![123456.789],
312 expected: vec!["¥123,457"],
313 },
314 ];
325 for test in tests {
326 let locale = crate::Locale::FromULoc(
327 uloc::ULoc::try_from(test.locale).expect(&format!("locale exists: {:?}", &test)),
328 );
329 let format = crate::numberformat::NumberFormat::try_new(locale, test.clone().opts)
330 .expect(&format!("try_from should succeed: {:?}", &test));
331 let actual = test
332 .numbers
333 .iter()
334 .map(|n| {
335 let mut result = String::new();
336 format
337 .format(*n, &mut result)
338 .expect(&format!("formatting succeeded for: {:?}", &test));
339 result
340 })
341 .collect::<Vec<String>>();
342 assert_eq!(
343 test.expected, actual,
344 "\n\tfor test case: {:?},\n\tformat: {:?}",
345 &test, &format
346 );
347 }
348 }
349}