Coverage Report

Created: 2026-09-04 06:48

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/hifitime-4.3.1/src/parser.rs
Line
Count
Source
1
/*
2
* Hifitime
3
* Copyright (C) 2017-onward Christopher Rabotin <christopher.rabotin@gmail.com> et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors)
4
* This Source Code Form is subject to the terms of the Mozilla Public
5
* License, v. 2.0. If a copy of the MPL was not distributed with this
6
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
*
8
* Documentation: https://nyxspace.com/
9
*/
10
11
use crate::{HifitimeError, ParsingError};
12
13
#[cfg_attr(kani, derive(kani::Arbitrary))]
14
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
15
pub(crate) enum Token {
16
    #[default]
17
    Year,
18
    YearShort,
19
    Month,
20
    Day,
21
    Hour,
22
    Minute,
23
    Second,
24
    Subsecond,
25
    OffsetHours,
26
    OffsetMinutes,
27
    Timescale,
28
    DayOfYearInteger,
29
    DayOfYear,
30
    Weekday,
31
    WeekdayShort,
32
    WeekdayDecimal,
33
    MonthName,
34
    MonthNameShort,
35
}
36
37
impl Token {
38
    // Check that the _integer_ value is valid at first sight.
39
24.2k
    pub fn value_ok(&self, val: i32) -> Result<(), HifitimeError> {
40
24.2k
        match &self {
41
6.75k
            Self::Year => Ok(()),      // No validation
42
0
            Self::YearShort => Ok(()), // No validation
43
            Self::Month => {
44
6.44k
                if !(0..=13).contains(&val) {
45
241
                    Err(HifitimeError::Parse {
46
241
                        source: ParsingError::ValueError,
47
241
                        details: "invalid month",
48
241
                    })
49
                } else {
50
6.20k
                    Ok(())
51
                }
52
            }
53
            Self::Day => {
54
6.12k
                if !(0..=31).contains(&val) {
55
201
                    Err(HifitimeError::Parse {
56
201
                        source: ParsingError::ValueError,
57
201
                        details: "invalid day",
58
201
                    })
59
                } else {
60
5.92k
                    Ok(())
61
                }
62
            }
63
            Self::Hour | Self::OffsetHours => {
64
2.17k
                if !(0..=23).contains(&val) {
65
225
                    Err(HifitimeError::Parse {
66
225
                        source: ParsingError::ValueError,
67
225
                        details: "invalid hour",
68
225
                    })
69
                } else {
70
1.95k
                    Ok(())
71
                }
72
            }
73
            Self::Minute | Self::OffsetMinutes => {
74
1.29k
                if !(0..=59).contains(&val) {
75
170
                    Err(HifitimeError::Parse {
76
170
                        source: ParsingError::ValueError,
77
170
                        details: "invalid minutes",
78
170
                    })
79
                } else {
80
1.12k
                    Ok(())
81
                }
82
            }
83
            Self::Second => {
84
955
                if !(0..=60).contains(&val) {
85
142
                    Err(HifitimeError::Parse {
86
142
                        source: ParsingError::ValueError,
87
142
                        details: "invalid seconds",
88
142
                    })
89
                } else {
90
813
                    Ok(())
91
                }
92
            }
93
            Self::Subsecond => {
94
478
                if val < 0 {
95
0
                    Err(HifitimeError::Parse {
96
0
                        source: ParsingError::ValueError,
97
0
                        details: "invalid subseconds",
98
0
                    })
99
                } else {
100
478
                    Ok(())
101
                }
102
            }
103
0
            Self::Timescale => Ok(()),
104
            Self::DayOfYearInteger => {
105
0
                if !(0..=366).contains(&val) {
106
0
                    Err(HifitimeError::Parse {
107
0
                        source: ParsingError::ValueError,
108
0
                        details: "invalid day of year",
109
0
                    })
110
                } else {
111
0
                    Ok(())
112
                }
113
            }
114
            Self::WeekdayDecimal => {
115
0
                if !(0..=6).contains(&val) {
116
0
                    Err(HifitimeError::Parse {
117
0
                        source: ParsingError::ValueError,
118
0
                        details: "invalid weekday decimal (must be 0-6)",
119
0
                    })
120
                } else {
121
0
                    Ok(())
122
                }
123
            }
124
            Self::Weekday
125
            | Self::WeekdayShort
126
            | Self::MonthName
127
            | Self::MonthNameShort
128
            | Self::DayOfYear => {
129
                // These cannot be parsed as integers
130
0
                Err(HifitimeError::Parse {
131
0
                    source: ParsingError::ValueError,
132
0
                    details: "invalid name or day of year",
133
0
                })
134
            }
135
        }
136
24.2k
    }
137
138
    /// Returns the position in the array for a Gregorian date for this token
139
25.8k
    pub(crate) fn gregorian_position(&self) -> Option<usize> {
140
25.8k
        match &self {
141
7.83k
            Token::Year | Token::YearShort => Some(0),
142
6.64k
            Token::Month => Some(1),
143
6.18k
            Token::Day => Some(2),
144
1.98k
            Token::Hour => Some(3),
145
1.25k
            Token::Minute => Some(4),
146
970
            Token::Second => Some(5),
147
501
            Token::Subsecond => Some(6),
148
343
            Token::OffsetHours => Some(7),
149
132
            Token::OffsetMinutes => Some(8),
150
0
            _ => None,
151
        }
152
25.8k
    }
153
154
    /// Updates the token to what it should be seeking next given the delimiting character
155
    /// and returns the position in the array where the parsed integer should live
156
20.6k
    pub fn advance_with(&mut self, ending_char: char) -> Result<(), HifitimeError> {
157
20.6k
        match &self {
158
            Token::Year | Token::YearShort => {
159
7.64k
                if ending_char == '-' {
160
6.69k
                    *self = Token::Month;
161
6.69k
                    Ok(())
162
                } else {
163
948
                    Err(HifitimeError::Parse {
164
948
                        source: ParsingError::UnknownFormat,
165
948
                        details: "invalid year",
166
948
                    })
167
                }
168
            }
169
            Token::Month => {
170
6.44k
                if ending_char == '-' {
171
6.27k
                    *self = Token::Day;
172
6.27k
                    Ok(())
173
                } else {
174
168
                    Err(HifitimeError::Parse {
175
168
                        source: ParsingError::UnknownFormat,
176
168
                        details: "invalid month",
177
168
                    })
178
                }
179
            }
180
            Token::Day => {
181
2.61k
                if ending_char == 'T' || ending_char == ' ' {
182
2.55k
                    *self = Token::Hour;
183
2.55k
                    Ok(())
184
                } else {
185
56
                    Err(HifitimeError::Parse {
186
56
                        source: ParsingError::UnknownFormat,
187
56
                        details: "invalid day",
188
56
                    })
189
                }
190
            }
191
            Token::Hour => {
192
1.42k
                if ending_char == ':' {
193
1.34k
                    *self = Token::Minute;
194
1.34k
                    Ok(())
195
                } else {
196
80
                    Err(HifitimeError::Parse {
197
80
                        source: ParsingError::UnknownFormat,
198
80
                        details: "invalid hour",
199
80
                    })
200
                }
201
            }
202
            Token::Minute => {
203
1.14k
                if ending_char == ':' {
204
1.05k
                    *self = Token::Second;
205
1.05k
                    Ok(())
206
                } else {
207
83
                    Err(HifitimeError::Parse {
208
83
                        source: ParsingError::UnknownFormat,
209
83
                        details: "invalid minutes",
210
83
                    })
211
                }
212
            }
213
            Token::Second => {
214
863
                if ending_char == '.' {
215
518
                    *self = Token::Subsecond;
216
518
                } else if ending_char == ' ' || ending_char == 'Z' {
217
154
                    // There are no subseconds here, only room for a time scale
218
154
                    *self = Token::Timescale;
219
191
                } else if ending_char == '-' || ending_char == '+' {
220
181
                    // There are no subseconds here, but we're seeing the start of an offset
221
181
                    *self = Token::OffsetHours;
222
181
                } else {
223
10
                    return Err(HifitimeError::Parse {
224
10
                        source: ParsingError::UnknownFormat,
225
10
                        details: "invalid seconds",
226
10
                    });
227
                }
228
853
                Ok(())
229
            }
230
            Token::Subsecond => {
231
339
                if ending_char == ' ' || ending_char == 'Z' {
232
96
                    // There are no subseconds here, only room for a time scale
233
96
                    *self = Token::Timescale;
234
243
                } else if ending_char == '-' || ending_char == '+' {
235
233
                    // There are no subseconds here, but we're seeing the start of an offset
236
233
                    *self = Token::OffsetHours;
237
233
                } else {
238
10
                    return Err(HifitimeError::Parse {
239
10
                        source: ParsingError::UnknownFormat,
240
10
                        details: "invalid subseconds",
241
10
                    });
242
                }
243
329
                Ok(())
244
            }
245
            Token::OffsetHours => {
246
195
                if ending_char == ':' {
247
147
                    *self = Token::OffsetMinutes;
248
147
                    Ok(())
249
                } else {
250
48
                    Err(HifitimeError::Parse {
251
48
                        source: ParsingError::UnknownFormat,
252
48
                        details: "invalid hours offset",
253
48
                    })
254
                }
255
            }
256
            Token::OffsetMinutes => {
257
28
                if ending_char == ' ' || ending_char == 'Z' {
258
                    // Only room for a time scale
259
20
                    *self = Token::Timescale;
260
20
                    Ok(())
261
                } else {
262
8
                    Err(HifitimeError::Parse {
263
8
                        source: ParsingError::UnknownFormat,
264
8
                        details: "invalid minutes offset",
265
8
                    })
266
                }
267
            }
268
0
            _ => Ok(()),
269
        }
270
20.6k
    }
271
272
0
    pub(crate) const fn is_numeric(self) -> bool {
273
0
        !matches!(
274
0
            self,
275
            Token::Timescale
276
                | Token::Weekday
277
                | Token::WeekdayShort
278
                | Token::MonthName
279
                | Token::MonthNameShort
280
        )
281
0
    }
282
283
0
    pub(crate) const fn fixed_length(self) -> Option<usize> {
284
0
        match self {
285
0
            Token::Year => Some(4),
286
            Token::YearShort
287
            | Token::Month
288
            | Token::Day
289
            | Token::Hour
290
            | Token::Minute
291
            | Token::Second
292
            | Token::OffsetHours
293
0
            | Token::OffsetMinutes => Some(2),
294
0
            Token::DayOfYearInteger => Some(3),
295
0
            Token::WeekdayDecimal => Some(1),
296
0
            _ => None,
297
        }
298
0
    }
299
}