Coverage Report

Created: 2025-09-27 06:45

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/fixed_decimal-0.5.6/src/compact.rs
Line
Count
Source
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
use core::convert::TryFrom;
6
use core::fmt;
7
8
use core::str::FromStr;
9
10
use crate::Error;
11
use crate::FixedDecimal;
12
13
/// A struct containing a [`FixedDecimal`] significand together with an exponent, representing a
14
/// number written in compact notation (such as 1.2M).
15
/// This represents a _source number_, as defined
16
/// [in UTS #35](https://www.unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax).
17
/// The value exponent=0 represents a number in non-compact
18
/// notation (such as 1 200 000).
19
///
20
/// This is distinct from [`crate::ScientificDecimal`] because it does not represent leading 0s
21
/// nor a sign in the exponent, and behaves differently in pluralization.
22
#[derive(Debug, Clone, PartialEq)]
23
pub struct CompactDecimal {
24
    significand: FixedDecimal,
25
    exponent: u8,
26
}
27
28
impl CompactDecimal {
29
    /// Constructs a [`CompactDecimal`] from its significand and exponent.
30
0
    pub fn from_significand_and_exponent(significand: FixedDecimal, exponent: u8) -> Self {
31
0
        Self {
32
0
            significand,
33
0
            exponent,
34
0
        }
35
0
    }
36
37
    /// Returns a reference to the significand of `self`.
38
    /// ```
39
    /// # use fixed_decimal::CompactDecimal;
40
    /// # use fixed_decimal::FixedDecimal;
41
    /// # use std::str::FromStr;
42
    /// #
43
    /// assert_eq!(
44
    ///     CompactDecimal::from_str("+1.20c6").unwrap().significand(),
45
    ///     &FixedDecimal::from_str("+1.20").unwrap()
46
    /// );
47
    /// ```
48
0
    pub fn significand(&self) -> &FixedDecimal {
49
0
        &self.significand
50
0
    }
51
52
    /// Returns the significand of `self`.
53
    /// ```
54
    /// # use fixed_decimal::CompactDecimal;
55
    /// # use fixed_decimal::FixedDecimal;
56
    /// # use std::str::FromStr;
57
    /// #
58
    /// assert_eq!(
59
    ///     CompactDecimal::from_str("+1.20c6")
60
    ///         .unwrap()
61
    ///         .into_significand(),
62
    ///     FixedDecimal::from_str("+1.20").unwrap()
63
    /// );
64
    /// ```
65
0
    pub fn into_significand(self) -> FixedDecimal {
66
0
        self.significand
67
0
    }
68
69
    /// Returns the exponent of `self`.
70
    /// ```
71
    /// # use fixed_decimal::CompactDecimal;
72
    /// # use std::str::FromStr;
73
    /// #
74
    /// assert_eq!(CompactDecimal::from_str("+1.20c6").unwrap().exponent(), 6);
75
    /// assert_eq!(CompactDecimal::from_str("1729").unwrap().exponent(), 0);
76
    /// ```
77
0
    pub fn exponent(&self) -> u8 {
78
0
        self.exponent
79
0
    }
80
}
81
82
/// Render the [`CompactDecimal`] in sampleValue syntax.
83
/// The letter c is used, rather than the deprecated e.
84
///
85
/// # Examples
86
///
87
/// ```
88
/// # use fixed_decimal::CompactDecimal;
89
/// # use std::str::FromStr;
90
/// # use writeable::assert_writeable_eq;
91
/// #
92
/// assert_writeable_eq!(
93
///     CompactDecimal::from_str("+1.20c6").unwrap(),
94
///     "+1.20c6"
95
/// );
96
/// assert_writeable_eq!(CompactDecimal::from_str("+1729").unwrap(), "+1729");
97
/// ```
98
impl writeable::Writeable for CompactDecimal {
99
0
    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
100
0
        self.significand.write_to(sink)?;
101
0
        if self.exponent != 0 {
102
0
            sink.write_char('c')?;
103
0
            self.exponent.write_to(sink)?;
104
0
        }
105
0
        Ok(())
106
0
    }
107
108
0
    fn writeable_length_hint(&self) -> writeable::LengthHint {
109
0
        let mut result = self.significand.writeable_length_hint();
110
0
        if self.exponent != 0 {
111
0
            result += self.exponent.writeable_length_hint() + 1;
112
0
        }
113
0
        result
114
0
    }
115
}
116
117
writeable::impl_display_with_writeable!(CompactDecimal);
118
119
impl FromStr for CompactDecimal {
120
    type Err = Error;
121
0
    fn from_str(input_str: &str) -> Result<Self, Self::Err> {
122
0
        Self::try_from(input_str.as_bytes())
123
0
    }
124
}
125
126
/// The deprecated letter e is not accepted as a synonym for c.
127
impl TryFrom<&[u8]> for CompactDecimal {
128
    type Error = Error;
129
0
    fn try_from(input_str: &[u8]) -> Result<Self, Self::Error> {
130
0
        if input_str.iter().any(|&c| c == b'e' || c == b'E') {
131
0
            return Err(Error::Syntax);
132
0
        }
133
0
        let mut parts = input_str.split(|&c| c == b'c');
134
0
        let significand = FixedDecimal::try_from(parts.next().ok_or(Error::Syntax)?)?;
135
0
        match parts.next() {
136
0
            None => Ok(CompactDecimal {
137
0
                significand,
138
0
                exponent: 0,
139
0
            }),
140
0
            Some(exponent_str) => {
141
0
                let exponent_str = core::str::from_utf8(exponent_str).map_err(|_| Error::Syntax)?;
142
0
                if parts.next().is_some() {
143
0
                    return Err(Error::Syntax);
144
0
                }
145
0
                if exponent_str.is_empty()
146
0
                    || exponent_str.bytes().next() == Some(b'0')
147
0
                    || !exponent_str.bytes().all(|c| c.is_ascii_digit())
148
                {
149
0
                    return Err(Error::Syntax);
150
0
                }
151
0
                let exponent = exponent_str.parse().map_err(|_| Error::Limit)?;
152
0
                Ok(CompactDecimal {
153
0
                    significand,
154
0
                    exponent,
155
0
                })
156
            }
157
        }
158
0
    }
159
}
160
161
#[test]
162
fn test_compact_syntax_error() {
163
    #[derive(Debug)]
164
    struct TestCase {
165
        pub input_str: &'static str,
166
        pub expected_err: Option<Error>,
167
    }
168
    let cases = [
169
        TestCase {
170
            input_str: "-123e4",
171
            expected_err: Some(Error::Syntax),
172
        },
173
        TestCase {
174
            input_str: "-123c",
175
            expected_err: Some(Error::Syntax),
176
        },
177
        TestCase {
178
            input_str: "1c10",
179
            expected_err: None,
180
        },
181
        TestCase {
182
            input_str: "1E1c1",
183
            expected_err: Some(Error::Syntax),
184
        },
185
        TestCase {
186
            input_str: "1e1c1",
187
            expected_err: Some(Error::Syntax),
188
        },
189
        TestCase {
190
            input_str: "1c1e1",
191
            expected_err: Some(Error::Syntax),
192
        },
193
        TestCase {
194
            input_str: "1c1E1",
195
            expected_err: Some(Error::Syntax),
196
        },
197
        TestCase {
198
            input_str: "-1c01",
199
            expected_err: Some(Error::Syntax),
200
        },
201
        TestCase {
202
            input_str: "-1c-1",
203
            expected_err: Some(Error::Syntax),
204
        },
205
        TestCase {
206
            input_str: "-1c1",
207
            expected_err: None,
208
        },
209
    ];
210
    for cas in &cases {
211
        match CompactDecimal::from_str(cas.input_str) {
212
            Ok(dec) => {
213
                assert_eq!(cas.expected_err, None, "{cas:?}");
214
                assert_eq!(cas.input_str, dec.to_string(), "{cas:?}");
215
            }
216
            Err(err) => {
217
                assert_eq!(cas.expected_err, Some(err), "{cas:?}");
218
            }
219
        }
220
    }
221
}