• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zbraniecki / icu4x / 13958601093

19 Mar 2025 04:17PM UTC coverage: 74.164% (-1.5%) from 75.71%
13958601093

push

github

web-flow
Clean up properties docs (#6315)

58056 of 78281 relevant lines covered (74.16%)

819371.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.96
/provider/source/src/decimal/decimal_pattern.rs
1
// This file is part of ICU4X. For terms of use, please see the file
2
// called LICENSE at the top level of the ICU4X source tree
3
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4

5
//! Functions for dealing with UTS-35 number patterns.
6
//!
7
//! Spec reference: <https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns>
8

9
use displaydoc::Display;
10
#[cfg(feature = "experimental")]
11
use icu_pattern::{DoublePlaceholderKey, PatternItemCow};
12
use itertools::Itertools;
13
use std::str::FromStr;
14

15
#[derive(Display, Debug, PartialEq)]
4✔
16
pub(crate) enum Error {
17
    #[displaydoc("No body in decimal subpattern")]
18
    NoBodyInSubpattern,
19
    #[displaydoc("Unknown decimal body: {0}")]
×
20
    UnknownPatternBody(String),
1✔
21
}
22

23
/// Representation of a UTS-35 number subpattern (part of a number pattern between ';'s).
24
#[derive(Debug, PartialEq)]
12✔
25
pub(crate) struct DecimalSubPattern {
26
    pub(crate) prefix: String,
6✔
27
    pub(crate) suffix: String,
6✔
28
    pub(crate) primary_grouping: u8,
6✔
29
    pub(crate) secondary_grouping: u8,
6✔
30
    pub(crate) min_fraction_digits: u8,
6✔
31
    pub(crate) max_fraction_digits: u8,
6✔
32
}
33

34
impl FromStr for DecimalSubPattern {
35
    type Err = Error;
36

37
    #[allow(clippy::many_single_char_names)]
38
    fn from_str(subpattern: &str) -> Result<Self, Self::Err> {
77✔
39
        // Split the subpattern into prefix, body, and suffix.
40
        // TODO(#567): Handle quoted literals in prefix and suffix.
41
        // i = boundary between prefix and body
42
        // j = boundary between body and suffix
43
        let i = subpattern.find(['#', '0', ',', '.']);
77✔
44
        let i = match i {
77✔
45
            Some(i) => i,
75✔
46
            None => return Err(Error::NoBodyInSubpattern),
2✔
47
        };
48
        let j = subpattern[i..]
150✔
49
            .find(|c: char| !matches!(c, '#' | '0' | ',' | '.'))
662✔
50
            .unwrap_or(subpattern.len() - i)
75✔
51
            + i;
52
        let prefix = &subpattern[..i];
75✔
53
        let body = &subpattern[i..j];
75✔
54
        let suffix = &subpattern[j..];
75✔
55

56
        // For now, we expect one of a handful of pattern bodies.
57
        // TODO(#567): Generalize this to support all of UTS 35.
58
        let (a, b, c, d) = match body {
74✔
59
            "#,##0.###" => (3, 3, 0, 3),
75✔
60
            "#,##,##0.###" => (3, 2, 0, 3),
48✔
61
            "0.######" => (0, 0, 0, 6),
44✔
62
            "#,##0.00" => (3, 3, 2, 2),
44✔
63
            "#,#0.###" => (2, 2, 0, 3),
3✔
64
            "#,##,##0.00" => (3, 2, 2, 2),
3✔
65
            "#,#0.00" => (2, 2, 2, 2),
1✔
66
            _ => return Err(Error::UnknownPatternBody(body.to_string())),
1✔
67
        };
68
        Ok(Self {
74✔
69
            prefix: prefix.into(),
74✔
70
            suffix: suffix.into(),
74✔
71
            primary_grouping: a,
72
            secondary_grouping: b,
73
            min_fraction_digits: c,
74
            max_fraction_digits: d,
75
        })
×
76
    }
77✔
77
}
78

79
impl DecimalSubPattern {
80
    #[cfg(feature = "experimental")]
81
    pub(crate) fn to_pattern_items(&self) -> Vec<PatternItemCow<DoublePlaceholderKey>> {
40✔
82
        use std::borrow::Cow;
83
        vec![
80✔
84
            PatternItemCow::Literal(Cow::Borrowed(&self.prefix)),
40✔
85
            PatternItemCow::Placeholder(DoublePlaceholderKey::Place0),
40✔
86
            PatternItemCow::Literal(Cow::Borrowed(&self.suffix)),
40✔
87
        ]
88
    }
40✔
89
}
90

91
/// Representation of a UTS-35 number pattern, including positive subpattern (required) and negative
92
/// subpattern (optional).
93
#[derive(Debug, PartialEq)]
10✔
94
pub(crate) struct DecimalPattern {
95
    pub(crate) positive: DecimalSubPattern,
5✔
96
    pub(crate) negative: Option<DecimalSubPattern>,
5✔
97
}
98

99
impl FromStr for DecimalPattern {
100
    type Err = Error;
101

102
    fn from_str(pattern: &str) -> Result<Self, Self::Err> {
73✔
103
        // Example patterns:
104
        // #,##0
105
        // #,##,##0.###
106
        // #,##0.00;#,##0.00-
107
        // 0;0-
108
        let (positive, negative) = match pattern.split(';').next_tuple() {
143✔
109
            Some((u, s)) => (u.parse()?, Some(s.parse()?)),
5✔
110
            None => (pattern.parse()?, None),
68✔
111
        };
112
        Ok(Self { positive, negative })
70✔
113
    }
73✔
114
}
115

116
impl DecimalPattern {
117
    // Returns affixes in the form (prefix, suffix)
118
    pub(crate) fn localize_sign(&self, sign_str: &str) -> (String, String) {
50✔
119
        // UTS 35: the absence of a negative pattern means a single prefixed sign
120
        let signed_affixes = self
50✔
121
            .negative
122
            .as_ref()
123
            .map(|subpattern| (subpattern.prefix.as_str(), subpattern.suffix.as_str()))
×
124
            .unwrap_or_else(|| ("-", ""));
50✔
125
        (
50✔
126
            signed_affixes.0.replace('-', sign_str),
50✔
127
            signed_affixes.1.replace('-', sign_str),
50✔
128
        )
×
129
    }
50✔
130
}
131

132
#[test]
133
fn test_basic() {
2✔
134
    #[derive(Debug)]
×
135
    struct TestCase<'s> {
136
        pub(crate) pattern: &'s str,
×
137
        pub(crate) expected: Result<DecimalPattern, Error>,
×
138
    }
139
    let cases = [
1✔
140
        TestCase {
1✔
141
            pattern: "#,##0.###",
142
            expected: Ok(DecimalPattern {
1✔
143
                positive: DecimalSubPattern {
1✔
144
                    prefix: "".into(),
1✔
145
                    suffix: "".into(),
1✔
146
                    primary_grouping: 3,
147
                    secondary_grouping: 3,
148
                    min_fraction_digits: 0,
149
                    max_fraction_digits: 3,
150
                },
×
151
                negative: None,
1✔
152
            }),
153
        },
154
        TestCase {
1✔
155
            pattern: "a#,##0.###",
156
            expected: Ok(DecimalPattern {
1✔
157
                positive: DecimalSubPattern {
1✔
158
                    prefix: "a".into(),
1✔
159
                    suffix: "".into(),
1✔
160
                    primary_grouping: 3,
161
                    secondary_grouping: 3,
162
                    min_fraction_digits: 0,
163
                    max_fraction_digits: 3,
164
                },
×
165
                negative: None,
1✔
166
            }),
167
        },
168
        TestCase {
1✔
169
            pattern: "#,##0.###b",
170
            expected: Ok(DecimalPattern {
1✔
171
                positive: DecimalSubPattern {
1✔
172
                    prefix: "".into(),
1✔
173
                    suffix: "b".into(),
1✔
174
                    primary_grouping: 3,
175
                    secondary_grouping: 3,
176
                    min_fraction_digits: 0,
177
                    max_fraction_digits: 3,
178
                },
×
179
                negative: None,
1✔
180
            }),
181
        },
182
        TestCase {
1✔
183
            pattern: "aaa#,##0.###bbb",
184
            expected: Ok(DecimalPattern {
1✔
185
                positive: DecimalSubPattern {
1✔
186
                    prefix: "aaa".into(),
1✔
187
                    suffix: "bbb".into(),
1✔
188
                    primary_grouping: 3,
189
                    secondary_grouping: 3,
190
                    min_fraction_digits: 0,
191
                    max_fraction_digits: 3,
192
                },
×
193
                negative: None,
1✔
194
            }),
195
        },
196
        TestCase {
1✔
197
            pattern: "aaa#,##0.###bbb;ccc#,##0.###ddd",
198
            expected: Ok(DecimalPattern {
1✔
199
                positive: DecimalSubPattern {
1✔
200
                    prefix: "aaa".into(),
1✔
201
                    suffix: "bbb".into(),
1✔
202
                    primary_grouping: 3,
203
                    secondary_grouping: 3,
204
                    min_fraction_digits: 0,
205
                    max_fraction_digits: 3,
206
                },
×
207
                negative: Some(DecimalSubPattern {
1✔
208
                    prefix: "ccc".into(),
1✔
209
                    suffix: "ddd".into(),
1✔
210
                    primary_grouping: 3,
211
                    secondary_grouping: 3,
212
                    min_fraction_digits: 0,
213
                    max_fraction_digits: 3,
214
                }),
×
215
            }),
×
216
        },
217
        TestCase {
1✔
218
            pattern: "xyz",
219
            expected: Err(Error::NoBodyInSubpattern),
1✔
220
        },
221
        TestCase {
1✔
222
            pattern: "xyz;abc",
223
            expected: Err(Error::NoBodyInSubpattern),
1✔
224
        },
225
        TestCase {
1✔
226
            pattern: "aaa#0#bbb",
227
            expected: Err(Error::UnknownPatternBody("#0#".to_string())),
1✔
228
        },
229
    ];
×
230
    for cas in &cases {
1✔
231
        let actual = DecimalPattern::from_str(cas.pattern);
8✔
232
        assert_eq!(cas.expected, actual, "Pattern: {}", cas.pattern);
8✔
233
    }
8✔
234
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc