4
4
//! proleptic Gregorian Calendar (the *civil* calendar),
5
5
//! to create UTC dates.
6
6
7
- use core:: {
8
- fmt:: { Display , Formatter } ,
9
- time:: Duration ,
10
- } ;
7
+ use crate :: time:: { UTCDay , UTCTimestamp , UTCTransformations } ;
8
+ use crate :: util:: StrWriter ;
9
+ use core:: fmt:: { Display , Formatter , Write } ;
10
+ use core:: num:: ParseIntError ;
11
+ use core:: time:: Duration ;
11
12
12
- use anyhow:: { bail, Result } ;
13
+ #[ cfg( feature = "alloc" ) ]
14
+ use alloc:: { format, string:: String } ;
13
15
14
- use crate :: time:: { UTCDay , UTCTimestamp , UTCTransformations } ;
16
+ // TODO <https://github.com/rust-lang/rust/issues/103765>
17
+ #[ cfg( feature = "nightly" ) ]
18
+ use core:: error:: Error ;
19
+ #[ cfg( all( feature = "std" , not( feature = "nightly" ) ) ) ]
20
+ use std:: error:: Error ;
15
21
16
22
/// UTC Date.
17
23
///
@@ -45,11 +51,17 @@ use crate::time::{UTCDay, UTCTimestamp, UTCTransformations};
45
51
/// // Not available for #![no_std]
46
52
/// let iso_date = utc_date.as_iso_date();
47
53
/// assert_eq!(iso_date, "2023-06-15");
54
+ /// // Write ISO 8601 date str to a stack buffer
55
+ /// let mut buf = [0; UTCDate::ISO_DATE_LEN];
56
+ /// let _bytes_written = utc_date.write_iso_date(&mut buf).unwrap();
57
+ /// let iso_date_str = core::str::from_utf8(&buf).unwrap();
58
+ /// assert_eq!(iso_date_str, "2023-06-15");
48
59
/// ```
49
60
///
50
61
/// ## Safety
51
62
/// Unchecked methods are provided for use in hot paths requiring high levels of optimisation.
52
63
/// These methods assume valid input.
64
+ #[ cfg_attr( feature = "serde" , derive( serde:: Serialize , serde:: Deserialize ) ) ]
53
65
#[ derive( Debug , Clone , Copy , PartialEq , Eq , PartialOrd , Ord , Hash ) ]
54
66
pub struct UTCDate {
55
67
era : u32 ,
@@ -101,6 +113,9 @@ impl UTCDate {
101
113
/// The minimum year supported
102
114
pub const MIN_YEAR : u64 = 1970 ;
103
115
116
+ /// The length of an ISO date (in characters)
117
+ pub const ISO_DATE_LEN : usize = 10 ;
118
+
104
119
/// Unchecked method to create a UTC Date from provided year, month and day.
105
120
///
106
121
/// ## Safety
@@ -120,21 +135,21 @@ impl UTCDate {
120
135
}
121
136
122
137
/// Try to create a UTC Date from provided year, month and day.
123
- pub fn try_from_components ( year : u64 , month : u8 , day : u8 ) -> Result < Self > {
138
+ pub fn try_from_components ( year : u64 , month : u8 , day : u8 ) -> Result < Self , UTCDateError > {
124
139
if !( Self :: MIN_YEAR ..=Self :: MAX_YEAR ) . contains ( & year) {
125
- bail ! ( "Year out of range! (year: {:04})" , year) ;
140
+ return Err ( UTCDateError :: YearOutOfRange ( year) ) ;
126
141
}
127
142
if month == 0 || month > 12 {
128
- bail ! ( "Month out of range! (month: {:02})" , month) ;
143
+ return Err ( UTCDateError :: MonthOutOfRange ( month) ) ;
129
144
}
130
- // force create
145
+ // SAFETY: we have checked year and month are within range
131
146
let date = unsafe { Self :: from_components_unchecked ( year, month, day) } ;
132
- // then check
147
+ // Then check days
133
148
if date. day == 0 || date. day > date. days_in_month ( ) {
134
- bail ! ( "Day out of range! (date: { date}" ) ;
149
+ return Err ( UTCDateError :: DayOutOfRange ( date) ) ;
135
150
}
136
151
if date > UTCDate :: MAX {
137
- bail ! ( "Date out of range! (date: { date}" ) ;
152
+ return Err ( UTCDateError :: DateOutOfRange ( date) ) ;
138
153
}
139
154
Ok ( date)
140
155
}
@@ -176,6 +191,7 @@ impl UTCDate {
176
191
let doy = ( ( 153 * ( if m > 2 { m - 3 } else { m + 9 } ) + 2 ) / 5 ) + d - 1 ;
177
192
let doe = ( yoe * 365 ) + ( yoe / 4 ) - ( yoe / 100 ) + doy as u32 ;
178
193
let days = ( era as u64 * 146097 ) + doe as u64 - 719468 ;
194
+ // SAFETY: days is not exceeding UTCDay::MAX
179
195
unsafe { UTCDay :: from_u64_unchecked ( days) }
180
196
}
181
197
@@ -223,13 +239,16 @@ impl UTCDate {
223
239
}
224
240
}
225
241
226
- /// Try parse date from string in the format:
242
+ /// Try parse date from str in the format:
227
243
/// * `YYYY-MM-DD`
228
244
///
229
245
/// Conforms to ISO 8601:
230
246
/// <https://www.w3.org/TR/NOTE-datetime>
231
- #[ cfg( feature = "std" ) ]
232
- pub fn try_from_iso_date ( iso : & str ) -> Result < Self > {
247
+ pub fn try_from_iso_date ( iso : & str ) -> Result < Self , UTCDateError > {
248
+ let len = iso. len ( ) ;
249
+ if len != Self :: ISO_DATE_LEN {
250
+ return Err ( UTCDateError :: InvalidStrLen ( len) ) ;
251
+ }
233
252
// handle slice
234
253
let ( year_str, rem) = iso. split_at ( 4 ) ; // remainder = "-MM-DD"
235
254
let ( month_str, rem) = rem[ 1 ..] . split_at ( 2 ) ; // remainder = "-DD"
@@ -246,10 +265,38 @@ impl UTCDate {
246
265
///
247
266
/// Conforms to ISO 8601:
248
267
/// <https://www.w3.org/TR/NOTE-datetime>
249
- #[ cfg( feature = "std " ) ]
268
+ #[ cfg( feature = "alloc " ) ]
250
269
pub fn as_iso_date ( & self ) -> String {
251
270
format ! ( "{self}" )
252
271
}
272
+
273
+ /// Internal truncated buffer write
274
+ #[ inline]
275
+ pub ( crate ) fn _write_iso_date_trunc ( & self , w : & mut StrWriter ) {
276
+ // unwrap infallible
277
+ write ! ( w, "{self}" ) . unwrap ( ) ;
278
+ }
279
+
280
+ /// Write an ISO date to a buffer in the format:
281
+ /// * `YYYY-MM-DD`
282
+ ///
283
+ /// The buffer should have minimum length of [UTCDate::ISO_DATE_LEN] (10).
284
+ ///
285
+ /// A buffer of insufficient length will error ([UTCDateError::InvalidStrLen]).
286
+ ///
287
+ /// Returns number of UTF8 characters (bytes) written
288
+ ///
289
+ /// Conforms to ISO 8601:
290
+ /// <https://www.w3.org/TR/NOTE-datetime>
291
+ pub fn write_iso_date ( & self , buf : & mut [ u8 ] ) -> Result < usize , UTCDateError > {
292
+ let write_len = Self :: ISO_DATE_LEN ;
293
+ if write_len > buf. len ( ) {
294
+ return Err ( UTCDateError :: InvalidStrLen ( buf. len ( ) ) ) ;
295
+ }
296
+ let mut writer = StrWriter :: new ( & mut buf[ ..write_len] ) ;
297
+ self . _write_iso_date_trunc ( & mut writer) ;
298
+ Ok ( writer. written )
299
+ }
253
300
}
254
301
255
302
impl UTCTransformations for UTCDate {
@@ -316,3 +363,49 @@ impl From<UTCDay> for UTCDate {
316
363
Self :: from_day ( utc_day)
317
364
}
318
365
}
366
+
367
+ /// Error type for UTCDate methods
368
+ #[ derive( Debug , Clone ) ]
369
+ pub enum UTCDateError {
370
+ /// Error raised parsing int to string
371
+ ParseErr ( ParseIntError ) ,
372
+ /// Error raised due to out of range year
373
+ YearOutOfRange ( u64 ) ,
374
+ /// Error raised due to out of range month
375
+ MonthOutOfRange ( u8 ) ,
376
+ /// Error raised due to out of range day
377
+ DayOutOfRange ( UTCDate ) ,
378
+ /// Error raised due to out of range date
379
+ DateOutOfRange ( UTCDate ) ,
380
+ /// Error raised due to invalid ISO date length
381
+ InvalidStrLen ( usize ) ,
382
+ }
383
+
384
+ impl Display for UTCDateError {
385
+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> core:: fmt:: Result {
386
+ match self {
387
+ Self :: ParseErr ( e) => e. fmt ( f) ,
388
+ Self :: YearOutOfRange ( y) => write ! ( f, "year ({y}) out of range!" ) ,
389
+ Self :: MonthOutOfRange ( m) => write ! ( f, "month ({m}) out of range!" ) ,
390
+ Self :: DayOutOfRange ( d) => write ! ( f, "day ({d}) out of range!" ) ,
391
+ Self :: DateOutOfRange ( date) => write ! ( f, "date ({date}) out of range!" ) ,
392
+ Self :: InvalidStrLen ( l) => write ! ( f, "invalid ISO date str length ({l}), 10 required" ) ,
393
+ }
394
+ }
395
+ }
396
+
397
+ #[ cfg( any( feature = "std" , feature = "nightly" ) ) ]
398
+ impl Error for UTCDateError {
399
+ fn source ( & self ) -> Option < & ( dyn Error + ' static ) > {
400
+ match self {
401
+ Self :: ParseErr ( e) => e. source ( ) ,
402
+ _ => None ,
403
+ }
404
+ }
405
+ }
406
+
407
+ impl From < ParseIntError > for UTCDateError {
408
+ fn from ( value : ParseIntError ) -> Self {
409
+ Self :: ParseErr ( value)
410
+ }
411
+ }
0 commit comments