1 | /*! |
2 | This module defines the internal core time data types. |
3 | |
4 | This includes physical time (i.e., a timestamp) and civil time. |
5 | |
6 | These types exist to provide a home for the core algorithms in a datetime |
7 | crate. For example, converting from a timestamp to a Gregorian calendar date |
8 | and clock time. |
9 | |
10 | These routines are specifically implemented on simple primitive integer types |
11 | and implicitly assume that the inputs are valid (i.e., within Jiff's minimum |
12 | and maximum ranges). |
13 | |
14 | These exist to provide `const` capabilities, and also to provide a small |
15 | reusable core of important algorithms that can be shared between `jiff` and |
16 | `jiff-static`. |
17 | |
18 | # Naming |
19 | |
20 | The types in this module are prefixed with letter `I` to make it clear that |
21 | they are internal types. Specifically, to distinguish them from Jiff's public |
22 | types. For example, `Date` versus `IDate`. |
23 | */ |
24 | |
25 | use super::error::{err, Error}; |
26 | |
27 | #[derive (Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] |
28 | pub(crate) struct ITimestamp { |
29 | pub(crate) second: i64, |
30 | pub(crate) nanosecond: i32, |
31 | } |
32 | |
33 | impl ITimestamp { |
34 | const MIN: ITimestamp = |
35 | ITimestamp { second: -377705023201, nanosecond: 0 }; |
36 | const MAX: ITimestamp = |
37 | ITimestamp { second: 253402207200, nanosecond: 999_999_999 }; |
38 | |
39 | /// Creates an `ITimestamp` from a Unix timestamp in seconds. |
40 | #[inline ] |
41 | pub(crate) const fn from_second(second: i64) -> ITimestamp { |
42 | ITimestamp { second, nanosecond: 0 } |
43 | } |
44 | |
45 | /// Converts a Unix timestamp with an offset to a Gregorian datetime. |
46 | /// |
47 | /// The offset should correspond to the number of seconds required to |
48 | /// add to this timestamp to get the local time. |
49 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
50 | pub(crate) const fn to_datetime(&self, offset: IOffset) -> IDateTime { |
51 | let ITimestamp { mut second, mut nanosecond } = *self; |
52 | second += offset.second as i64; |
53 | let mut epoch_day = second.div_euclid(86_400) as i32; |
54 | second = second.rem_euclid(86_400); |
55 | if nanosecond < 0 { |
56 | if second > 0 { |
57 | second -= 1; |
58 | nanosecond += 1_000_000_000; |
59 | } else { |
60 | epoch_day -= 1; |
61 | second += 86_399; |
62 | nanosecond += 1_000_000_000; |
63 | } |
64 | } |
65 | |
66 | let date = IEpochDay { epoch_day }.to_date(); |
67 | let mut time = ITimeSecond { second: second as i32 }.to_time(); |
68 | time.subsec_nanosecond = nanosecond; |
69 | IDateTime { date, time } |
70 | } |
71 | } |
72 | |
73 | #[derive (Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] |
74 | pub(crate) struct IOffset { |
75 | pub(crate) second: i32, |
76 | } |
77 | |
78 | impl IOffset { |
79 | pub(crate) const UTC: IOffset = IOffset { second: 0 }; |
80 | } |
81 | |
82 | #[derive (Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] |
83 | pub(crate) struct IDateTime { |
84 | pub(crate) date: IDate, |
85 | pub(crate) time: ITime, |
86 | } |
87 | |
88 | impl IDateTime { |
89 | const MIN: IDateTime = IDateTime { date: IDate::MIN, time: ITime::MIN }; |
90 | const MAX: IDateTime = IDateTime { date: IDate::MAX, time: ITime::MAX }; |
91 | |
92 | /// Converts a Gregorian datetime and its offset to a Unix timestamp. |
93 | /// |
94 | /// The offset should correspond to the number of seconds required to |
95 | /// subtract from this datetime in order to get to UTC. |
96 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
97 | pub(crate) fn to_timestamp(&self, offset: IOffset) -> ITimestamp { |
98 | let epoch_day = self.date.to_epoch_day().epoch_day; |
99 | let mut second = (epoch_day as i64) * 86_400 |
100 | + (self.time.to_second().second as i64); |
101 | let mut nanosecond = self.time.subsec_nanosecond; |
102 | second -= offset.second as i64; |
103 | if epoch_day < 0 && nanosecond != 0 { |
104 | second += 1; |
105 | nanosecond -= 1_000_000_000; |
106 | } |
107 | ITimestamp { second, nanosecond } |
108 | } |
109 | |
110 | /// Converts a Gregorian datetime and its offset to a Unix timestamp. |
111 | /// |
112 | /// If the timestamp would overflow Jiff's timestamp range, then this |
113 | /// returns `None`. |
114 | /// |
115 | /// The offset should correspond to the number of seconds required to |
116 | /// subtract from this datetime in order to get to UTC. |
117 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
118 | pub(crate) fn to_timestamp_checked( |
119 | &self, |
120 | offset: IOffset, |
121 | ) -> Option<ITimestamp> { |
122 | let ts = self.to_timestamp(offset); |
123 | if !(ITimestamp::MIN <= ts && ts <= ITimestamp::MAX) { |
124 | return None; |
125 | } |
126 | Some(ts) |
127 | } |
128 | |
129 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
130 | pub(crate) fn saturating_add_seconds(&self, seconds: i32) -> IDateTime { |
131 | self.checked_add_seconds(seconds).unwrap_or_else(|_| { |
132 | if seconds < 0 { |
133 | IDateTime::MIN |
134 | } else { |
135 | IDateTime::MAX |
136 | } |
137 | }) |
138 | } |
139 | |
140 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
141 | pub(crate) fn checked_add_seconds( |
142 | &self, |
143 | seconds: i32, |
144 | ) -> Result<IDateTime, Error> { |
145 | let day_second = |
146 | self.time.to_second().second.checked_add(seconds).ok_or_else( |
147 | || err!("adding ` {seconds}s` to datetime overflowed" ), |
148 | )?; |
149 | let days = day_second.div_euclid(86400); |
150 | let second = day_second.rem_euclid(86400); |
151 | let date = self.date.checked_add_days(days)?; |
152 | let time = ITimeSecond { second }.to_time(); |
153 | Ok(IDateTime { date, time }) |
154 | } |
155 | } |
156 | |
157 | #[derive (Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] |
158 | pub(crate) struct IEpochDay { |
159 | pub(crate) epoch_day: i32, |
160 | } |
161 | |
162 | impl IEpochDay { |
163 | const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 }; |
164 | const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 }; |
165 | |
166 | /// Converts days since the Unix epoch to a Gregorian date. |
167 | /// |
168 | /// This is Neri-Schneider. There's no branching or divisions. |
169 | /// |
170 | /// Ref: <https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L40C3-L40C34> |
171 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
172 | #[allow (non_upper_case_globals, non_snake_case)] // to mimic source |
173 | pub(crate) const fn to_date(&self) -> IDate { |
174 | const s: u32 = 82; |
175 | const K: u32 = 719468 + 146097 * s; |
176 | const L: u32 = 400 * s; |
177 | |
178 | let N_U = self.epoch_day as u32; |
179 | let N = N_U.wrapping_add(K); |
180 | |
181 | let N_1 = 4 * N + 3; |
182 | let C = N_1 / 146097; |
183 | let N_C = (N_1 % 146097) / 4; |
184 | |
185 | let N_2 = 4 * N_C + 3; |
186 | let P_2 = 2939745 * (N_2 as u64); |
187 | let Z = (P_2 / 4294967296) as u32; |
188 | let N_Y = (P_2 % 4294967296) as u32 / 2939745 / 4; |
189 | let Y = 100 * C + Z; |
190 | |
191 | let N_3 = 2141 * N_Y + 197913; |
192 | let M = N_3 / 65536; |
193 | let D = (N_3 % 65536) / 2141; |
194 | |
195 | let J = N_Y >= 306; |
196 | let year = Y.wrapping_sub(L).wrapping_add(J as u32) as i16; |
197 | let month = (if J { M - 12 } else { M }) as i8; |
198 | let day = (D + 1) as i8; |
199 | IDate { year, month, day } |
200 | } |
201 | |
202 | /// Returns the day of the week for this epoch day. |
203 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
204 | pub(crate) const fn weekday(&self) -> IWeekday { |
205 | // Based on Hinnant's approach here, although we use ISO weekday |
206 | // numbering by default. Basically, this works by using the knowledge |
207 | // that 1970-01-01 was a Thursday. |
208 | // |
209 | // Ref: http://howardhinnant.github.io/date_algorithms.html |
210 | IWeekday::from_monday_zero_offset( |
211 | (self.epoch_day + 3).rem_euclid(7) as i8 |
212 | ) |
213 | } |
214 | |
215 | /// Add the given number of days to this epoch day. |
216 | /// |
217 | /// If this would overflow an `i32` or result in an out-of-bounds epoch |
218 | /// day, then this returns an error. |
219 | #[inline ] |
220 | pub(crate) fn checked_add(&self, amount: i32) -> Result<IEpochDay, Error> { |
221 | let epoch_day = self.epoch_day; |
222 | let sum = epoch_day.checked_add(amount).ok_or_else(|| { |
223 | err!("adding ` {amount}` to epoch day ` {epoch_day}` overflowed i32" ) |
224 | })?; |
225 | let ret = IEpochDay { epoch_day: sum }; |
226 | if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) { |
227 | return Err(err!( |
228 | "adding ` {amount}` to epoch day ` {epoch_day}` \ |
229 | resulted in ` {sum}`, which is not in the required \ |
230 | epoch day range of ` {min}..= {max}`" , |
231 | min = IEpochDay::MIN.epoch_day, |
232 | max = IEpochDay::MAX.epoch_day, |
233 | )); |
234 | } |
235 | Ok(ret) |
236 | } |
237 | } |
238 | |
239 | #[derive (Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] |
240 | pub(crate) struct IDate { |
241 | pub(crate) year: i16, |
242 | pub(crate) month: i8, |
243 | pub(crate) day: i8, |
244 | } |
245 | |
246 | impl IDate { |
247 | const MIN: IDate = IDate { year: -9999, month: 1, day: 1 }; |
248 | const MAX: IDate = IDate { year: 9999, month: 12, day: 31 }; |
249 | |
250 | /// Fallibly builds a new date. |
251 | /// |
252 | /// This checks that the given day is valid for the given year/month. |
253 | /// |
254 | /// No other conditions are checked. This assumes `year` and `month` are |
255 | /// valid, and that `day >= 1`. |
256 | #[inline ] |
257 | pub(crate) fn try_new( |
258 | year: i16, |
259 | month: i8, |
260 | day: i8, |
261 | ) -> Result<IDate, Error> { |
262 | if day > 28 { |
263 | let max_day = days_in_month(year, month); |
264 | if day > max_day { |
265 | return Err(err!( |
266 | "day= {day} is out of range for year= {year} \ |
267 | and month= {month}, must be in range 1..= {max_day}" , |
268 | )); |
269 | } |
270 | } |
271 | Ok(IDate { year, month, day }) |
272 | } |
273 | |
274 | /// Returns the date corresponding to the day of the given year. The day |
275 | /// of the year should be a value in `1..=366`, with `366` only being valid |
276 | /// if `year` is a leap year. |
277 | /// |
278 | /// This assumes that `year` is valid, but returns an error if `day` is |
279 | /// not in the range `1..=366`. |
280 | #[inline ] |
281 | pub(crate) fn from_day_of_year( |
282 | year: i16, |
283 | day: i16, |
284 | ) -> Result<IDate, Error> { |
285 | if !(1 <= day && day <= 366) { |
286 | return Err(err!( |
287 | "day-of-year= {day} is out of range for year= {year}, \ |
288 | must be in range 1..= {max_day}" , |
289 | max_day = days_in_year(year), |
290 | )); |
291 | } |
292 | let start = IDate { year, month: 1, day: 1 }.to_epoch_day(); |
293 | let end = start |
294 | .checked_add(i32::from(day) - 1) |
295 | .map_err(|_| { |
296 | err!( |
297 | "failed to find date for \ |
298 | year= {year} and day-of-year= {day}: \ |
299 | adding ` {day}` to ` {start}` overflows \ |
300 | Jiff's range" , |
301 | start = start.epoch_day, |
302 | ) |
303 | })? |
304 | .to_date(); |
305 | // If we overflowed into the next year, then `day` is too big. |
306 | if year != end.year { |
307 | // Can only happen given day=366 and this is a leap year. |
308 | debug_assert_eq!(day, 366); |
309 | debug_assert!(!is_leap_year(year)); |
310 | return Err(err!( |
311 | "day-of-year= {day} is out of range for year= {year}, \ |
312 | must be in range 1..= {max_day}" , |
313 | max_day = days_in_year(year), |
314 | )); |
315 | } |
316 | Ok(end) |
317 | } |
318 | |
319 | /// Returns the date corresponding to the day of the given year. The day |
320 | /// of the year should be a value in `1..=365`, with February 29 being |
321 | /// completely ignored. That is, it is guaranteed that Febraury 29 will |
322 | /// never be returned by this function. It is impossible. |
323 | /// |
324 | /// This assumes that `year` is valid, but returns an error if `day` is |
325 | /// not in the range `1..=365`. |
326 | #[inline ] |
327 | pub(crate) fn from_day_of_year_no_leap( |
328 | year: i16, |
329 | mut day: i16, |
330 | ) -> Result<IDate, Error> { |
331 | if !(1 <= day && day <= 365) { |
332 | return Err(err!( |
333 | "day-of-year= {day} is out of range for year= {year}, \ |
334 | must be in range 1..=365" , |
335 | )); |
336 | } |
337 | if day >= 60 && is_leap_year(year) { |
338 | day += 1; |
339 | } |
340 | // The boundary check above guarantees this always succeeds. |
341 | Ok(IDate::from_day_of_year(year, day).unwrap()) |
342 | } |
343 | |
344 | /// Converts a Gregorian date to days since the Unix epoch. |
345 | /// |
346 | /// This is Neri-Schneider. There's no branching or divisions. |
347 | /// |
348 | /// Ref: https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L83 |
349 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
350 | #[allow (non_upper_case_globals, non_snake_case)] // to mimic source |
351 | pub(crate) const fn to_epoch_day(&self) -> IEpochDay { |
352 | const s: u32 = 82; |
353 | const K: u32 = 719468 + 146097 * s; |
354 | const L: u32 = 400 * s; |
355 | |
356 | let year = self.year as u32; |
357 | let month = self.month as u32; |
358 | let day = self.day as u32; |
359 | |
360 | let J = month <= 2; |
361 | let Y = year.wrapping_add(L).wrapping_sub(J as u32); |
362 | let M = if J { month + 12 } else { month }; |
363 | let D = day - 1; |
364 | let C = Y / 100; |
365 | |
366 | let y_star = 1461 * Y / 4 - C + C / 4; |
367 | let m_star = (979 * M - 2919) / 32; |
368 | let N = y_star + m_star + D; |
369 | |
370 | let N_U = N.wrapping_sub(K); |
371 | let epoch_day = N_U as i32; |
372 | IEpochDay { epoch_day } |
373 | } |
374 | |
375 | /// Returns the day of the week for this date. |
376 | #[inline ] |
377 | pub(crate) const fn weekday(&self) -> IWeekday { |
378 | self.to_epoch_day().weekday() |
379 | } |
380 | |
381 | /// Returns the `nth` weekday of the month represented by this date. |
382 | /// |
383 | /// `nth` must be non-zero and otherwise in the range `-5..=5`. If it |
384 | /// isn't, an error is returned. |
385 | /// |
386 | /// This also returns an error if `abs(nth)==5` and there is no "5th" |
387 | /// weekday of this month. |
388 | #[inline ] |
389 | pub(crate) fn nth_weekday_of_month( |
390 | &self, |
391 | nth: i8, |
392 | weekday: IWeekday, |
393 | ) -> Result<IDate, Error> { |
394 | if nth == 0 || !(-5 <= nth && nth <= 5) { |
395 | return Err(err!( |
396 | "got nth weekday of ` {nth}`, but \ |
397 | must be non-zero and in range `-5..=5`" , |
398 | )); |
399 | } |
400 | if nth > 0 { |
401 | let first_weekday = self.first_of_month().weekday(); |
402 | let diff = weekday.since(first_weekday); |
403 | let day = diff + 1 + (nth - 1) * 7; |
404 | IDate::try_new(self.year, self.month, day) |
405 | } else { |
406 | let last = self.last_of_month(); |
407 | let last_weekday = last.weekday(); |
408 | let diff = last_weekday.since(weekday); |
409 | let day = last.day - diff - (nth.abs() - 1) * 7; |
410 | // Our math can go below 1 when nth is -5 and there is no "5th from |
411 | // last" weekday in this month. Since this is outside the bounds |
412 | // of `Day`, we can't let this boundary condition escape. So we |
413 | // check it here. |
414 | if day < 1 { |
415 | return Err(err!( |
416 | "day= {day} is out of range for year= {year} \ |
417 | and month= {month}, must be in range 1..= {max_day}" , |
418 | year = self.year, |
419 | month = self.month, |
420 | max_day = days_in_month(self.year, self.month), |
421 | )); |
422 | } |
423 | IDate::try_new(self.year, self.month, day) |
424 | } |
425 | } |
426 | |
427 | /// Returns the day before this date. |
428 | #[inline ] |
429 | pub(crate) fn yesterday(self) -> Result<IDate, Error> { |
430 | if self.day == 1 { |
431 | if self.month == 1 { |
432 | let year = self.year - 1; |
433 | if year <= -10000 { |
434 | return Err(err!( |
435 | "returning yesterday for -9999-01-01 is not \ |
436 | possible because it is less than Jiff's supported |
437 | minimum date" , |
438 | )); |
439 | } |
440 | return Ok(IDate { year, month: 12, day: 31 }); |
441 | } |
442 | let month = self.month - 1; |
443 | let day = days_in_month(self.year, month); |
444 | return Ok(IDate { month, day, ..self }); |
445 | } |
446 | Ok(IDate { day: self.day - 1, ..self }) |
447 | } |
448 | |
449 | /// Returns the day after this date. |
450 | #[inline ] |
451 | pub(crate) fn tomorrow(self) -> Result<IDate, Error> { |
452 | if self.day >= 28 && self.day == days_in_month(self.year, self.month) { |
453 | if self.month == 12 { |
454 | let year = self.year + 1; |
455 | if year >= 10000 { |
456 | return Err(err!( |
457 | "returning tomorrow for 9999-12-31 is not \ |
458 | possible because it is greater than Jiff's supported |
459 | maximum date" , |
460 | )); |
461 | } |
462 | return Ok(IDate { year, month: 1, day: 1 }); |
463 | } |
464 | let month = self.month + 1; |
465 | return Ok(IDate { month, day: 1, ..self }); |
466 | } |
467 | Ok(IDate { day: self.day + 1, ..self }) |
468 | } |
469 | |
470 | /// Returns the year one year before this date. |
471 | #[inline ] |
472 | pub(crate) fn prev_year(self) -> Result<i16, Error> { |
473 | let year = self.year - 1; |
474 | if year <= -10_000 { |
475 | return Err(err!( |
476 | "returning previous year for {year:04}- {month:02}- {day:02} is \ |
477 | not possible because it is less than Jiff's supported \ |
478 | minimum date" , |
479 | year = self.year, |
480 | month = self.month, |
481 | day = self.day, |
482 | )); |
483 | } |
484 | Ok(year) |
485 | } |
486 | |
487 | /// Returns the year one year from this date. |
488 | #[inline ] |
489 | pub(crate) fn next_year(self) -> Result<i16, Error> { |
490 | let year = self.year + 1; |
491 | if year >= 10_000 { |
492 | return Err(err!( |
493 | "returning next year for {year:04}- {month:02}- {day:02} is \ |
494 | not possible because it is greater than Jiff's supported \ |
495 | maximum date" , |
496 | year = self.year, |
497 | month = self.month, |
498 | day = self.day, |
499 | )); |
500 | } |
501 | Ok(year) |
502 | } |
503 | |
504 | /// Add the number of days to this date. |
505 | #[inline ] |
506 | pub(crate) fn checked_add_days( |
507 | &self, |
508 | amount: i32, |
509 | ) -> Result<IDate, Error> { |
510 | match amount { |
511 | 0 => Ok(*self), |
512 | -1 => self.yesterday(), |
513 | 1 => self.tomorrow(), |
514 | n => self.to_epoch_day().checked_add(n).map(|d| d.to_date()), |
515 | } |
516 | } |
517 | |
518 | #[inline ] |
519 | fn first_of_month(&self) -> IDate { |
520 | IDate { day: 1, ..*self } |
521 | } |
522 | |
523 | #[inline ] |
524 | fn last_of_month(&self) -> IDate { |
525 | IDate { day: days_in_month(self.year, self.month), ..*self } |
526 | } |
527 | |
528 | #[cfg (test)] |
529 | pub(crate) fn at( |
530 | &self, |
531 | hour: i8, |
532 | minute: i8, |
533 | second: i8, |
534 | subsec_nanosecond: i32, |
535 | ) -> IDateTime { |
536 | let time = ITime { hour, minute, second, subsec_nanosecond }; |
537 | IDateTime { date: *self, time } |
538 | } |
539 | } |
540 | |
541 | /// Represents a clock time. |
542 | /// |
543 | /// This uses units of hours, minutes, seconds and fractional seconds (to |
544 | /// nanosecond precision). |
545 | #[derive (Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] |
546 | pub(crate) struct ITime { |
547 | pub(crate) hour: i8, |
548 | pub(crate) minute: i8, |
549 | pub(crate) second: i8, |
550 | pub(crate) subsec_nanosecond: i32, |
551 | } |
552 | |
553 | impl ITime { |
554 | pub(crate) const ZERO: ITime = |
555 | ITime { hour: 0, minute: 0, second: 0, subsec_nanosecond: 0 }; |
556 | pub(crate) const MIN: ITime = |
557 | ITime { hour: 0, minute: 0, second: 0, subsec_nanosecond: 0 }; |
558 | pub(crate) const MAX: ITime = ITime { |
559 | hour: 23, |
560 | minute: 59, |
561 | second: 59, |
562 | subsec_nanosecond: 999_999_999, |
563 | }; |
564 | |
565 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
566 | pub(crate) const fn to_second(&self) -> ITimeSecond { |
567 | let mut second: i32 = 0; |
568 | second += (self.hour as i32) * 3600; |
569 | second += (self.minute as i32) * 60; |
570 | second += self.second as i32; |
571 | ITimeSecond { second } |
572 | } |
573 | |
574 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
575 | pub(crate) const fn to_nanosecond(&self) -> ITimeNanosecond { |
576 | let mut nanosecond: i64 = 0; |
577 | nanosecond += (self.hour as i64) * 3_600_000_000_000; |
578 | nanosecond += (self.minute as i64) * 60_000_000_000; |
579 | nanosecond += (self.second as i64) * 1_000_000_000; |
580 | nanosecond += self.subsec_nanosecond as i64; |
581 | ITimeNanosecond { nanosecond } |
582 | } |
583 | } |
584 | |
585 | /// Represents a single point in the day, to second precision. |
586 | #[derive (Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] |
587 | pub(crate) struct ITimeSecond { |
588 | pub(crate) second: i32, |
589 | } |
590 | |
591 | impl ITimeSecond { |
592 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
593 | pub(crate) const fn to_time(&self) -> ITime { |
594 | let mut second: i32 = self.second; |
595 | let mut time: ITime = ITime::ZERO; |
596 | if second != 0 { |
597 | time.hour = (second / 3600) as i8; |
598 | second %= 3600; |
599 | if second != 0 { |
600 | time.minute = (second / 60) as i8; |
601 | time.second = (second % 60) as i8; |
602 | } |
603 | } |
604 | time |
605 | } |
606 | } |
607 | |
608 | /// Represents a single point in the day, to nanosecond precision. |
609 | #[derive (Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] |
610 | pub(crate) struct ITimeNanosecond { |
611 | pub(crate) nanosecond: i64, |
612 | } |
613 | |
614 | impl ITimeNanosecond { |
615 | #[cfg_attr (feature = "perf-inline" , inline(always))] |
616 | pub(crate) const fn to_time(&self) -> ITime { |
617 | let mut nanosecond: i64 = self.nanosecond; |
618 | let mut time: ITime = ITime::ZERO; |
619 | if nanosecond != 0 { |
620 | time.hour = (nanosecond / 3_600_000_000_000) as i8; |
621 | nanosecond %= 3_600_000_000_000; |
622 | if nanosecond != 0 { |
623 | time.minute = (nanosecond / 60_000_000_000) as i8; |
624 | nanosecond %= 60_000_000_000; |
625 | if nanosecond != 0 { |
626 | time.second = (nanosecond / 1_000_000_000) as i8; |
627 | time.subsec_nanosecond = |
628 | (nanosecond % 1_000_000_000) as i32; |
629 | } |
630 | } |
631 | } |
632 | time |
633 | } |
634 | } |
635 | |
636 | /// Represents a weekday. |
637 | #[derive (Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] |
638 | pub(crate) struct IWeekday { |
639 | /// Range is `1..=6` with `1=Monday`. |
640 | offset: i8, |
641 | } |
642 | |
643 | impl IWeekday { |
644 | /// Creates a weekday assuming the week starts on Monday and Monday is at |
645 | /// offset `0`. |
646 | #[inline ] |
647 | pub(crate) const fn from_monday_zero_offset(offset: i8) -> IWeekday { |
648 | assert!(0 <= offset && offset <= 6); |
649 | IWeekday::from_monday_one_offset(offset + 1) |
650 | } |
651 | |
652 | /// Creates a weekday assuming the week starts on Monday and Monday is at |
653 | /// offset `1`. |
654 | #[inline ] |
655 | pub(crate) const fn from_monday_one_offset(offset: i8) -> IWeekday { |
656 | assert!(1 <= offset && offset <= 7); |
657 | IWeekday { offset } |
658 | } |
659 | |
660 | /// Creates a weekday assuming the week starts on Sunday and Sunday is at |
661 | /// offset `0`. |
662 | #[inline ] |
663 | pub(crate) const fn from_sunday_zero_offset(offset: i8) -> IWeekday { |
664 | assert!(0 <= offset && offset <= 6); |
665 | IWeekday::from_monday_zero_offset((offset - 1).rem_euclid(7)) |
666 | } |
667 | |
668 | /// Creates a weekday assuming the week starts on Sunday and Sunday is at |
669 | /// offset `1`. |
670 | #[cfg (test)] // currently dead code |
671 | #[inline ] |
672 | pub(crate) const fn from_sunday_one_offset(offset: i8) -> IWeekday { |
673 | assert!(1 <= offset && offset <= 7); |
674 | IWeekday::from_sunday_zero_offset(offset - 1) |
675 | } |
676 | |
677 | /// Returns this weekday as an offset in the range `0..=6` where |
678 | /// `0=Monday`. |
679 | #[inline ] |
680 | pub(crate) const fn to_monday_zero_offset(self) -> i8 { |
681 | self.to_monday_one_offset() - 1 |
682 | } |
683 | |
684 | /// Returns this weekday as an offset in the range `1..=7` where |
685 | /// `1=Monday`. |
686 | #[inline ] |
687 | pub(crate) const fn to_monday_one_offset(self) -> i8 { |
688 | self.offset |
689 | } |
690 | |
691 | /// Returns this weekday as an offset in the range `0..=6` where |
692 | /// `0=Sunday`. |
693 | #[cfg (test)] // currently dead code |
694 | #[inline ] |
695 | pub(crate) const fn to_sunday_zero_offset(self) -> i8 { |
696 | (self.to_monday_zero_offset() + 1) % 7 |
697 | } |
698 | |
699 | /// Returns this weekday as an offset in the range `1..=7` where |
700 | /// `1=Sunday`. |
701 | #[cfg (test)] // currently dead code |
702 | #[inline ] |
703 | pub(crate) const fn to_sunday_one_offset(self) -> i8 { |
704 | self.to_sunday_zero_offset() + 1 |
705 | } |
706 | |
707 | #[inline ] |
708 | pub(crate) const fn since(self, other: IWeekday) -> i8 { |
709 | (self.to_monday_zero_offset() - other.to_monday_zero_offset()) |
710 | .rem_euclid(7) |
711 | } |
712 | } |
713 | |
714 | #[derive (Clone, Copy, Debug, Eq, PartialEq)] |
715 | pub(crate) enum IAmbiguousOffset { |
716 | Unambiguous { offset: IOffset }, |
717 | Gap { before: IOffset, after: IOffset }, |
718 | Fold { before: IOffset, after: IOffset }, |
719 | } |
720 | |
721 | /// Returns true if and only if the given year is a leap year. |
722 | /// |
723 | /// A leap year is a year with 366 days. Typical years have 365 days. |
724 | #[inline ] |
725 | pub(crate) const fn is_leap_year(year: i16) -> bool { |
726 | // From: https://github.com/BurntSushi/jiff/pull/23 |
727 | let d: i16 = if year % 25 != 0 { 4 } else { 16 }; |
728 | (year % d) == 0 |
729 | } |
730 | |
731 | /// Return the number of days in the given year. |
732 | #[inline ] |
733 | pub(crate) const fn days_in_year(year: i16) -> i16 { |
734 | if is_leap_year(year) { |
735 | 366 |
736 | } else { |
737 | 365 |
738 | } |
739 | } |
740 | |
741 | /// Return the number of days in the given month. |
742 | #[inline ] |
743 | pub(crate) const fn days_in_month(year: i16, month: i8) -> i8 { |
744 | // From: https://github.com/BurntSushi/jiff/pull/23 |
745 | if month == 2 { |
746 | if is_leap_year(year) { |
747 | 29 |
748 | } else { |
749 | 28 |
750 | } |
751 | } else { |
752 | 30 | (month ^ month >> 3) |
753 | } |
754 | } |
755 | |
756 | #[cfg (test)] |
757 | mod tests { |
758 | use super::*; |
759 | |
760 | #[test ] |
761 | fn roundtrip_epochday_date() { |
762 | for year in -9999..=9999 { |
763 | for month in 1..=12 { |
764 | for day in 1..=days_in_month(year, month) { |
765 | let date = IDate { year, month, day }; |
766 | let epoch_day = date.to_epoch_day(); |
767 | let date_roundtrip = epoch_day.to_date(); |
768 | assert_eq!(date, date_roundtrip); |
769 | } |
770 | } |
771 | } |
772 | } |
773 | |
774 | #[test ] |
775 | fn roundtrip_second_time() { |
776 | for second in 0..=86_399 { |
777 | let second = ITimeSecond { second }; |
778 | let time = second.to_time(); |
779 | let second_roundtrip = time.to_second(); |
780 | assert_eq!(second, second_roundtrip); |
781 | } |
782 | } |
783 | |
784 | #[test ] |
785 | fn roundtrip_nanosecond_time() { |
786 | for second in 0..=86_399 { |
787 | for nanosecond in |
788 | [0, 250_000_000, 500_000_000, 750_000_000, 900_000_000] |
789 | { |
790 | let nanosecond = ITimeNanosecond { |
791 | nanosecond: (second * 1_000_000_000 + nanosecond), |
792 | }; |
793 | let time = nanosecond.to_time(); |
794 | let nanosecond_roundtrip = time.to_nanosecond(); |
795 | assert_eq!(nanosecond, nanosecond_roundtrip); |
796 | } |
797 | } |
798 | } |
799 | |
800 | #[test ] |
801 | fn nth_weekday() { |
802 | let d1 = IDate { year: 2017, month: 3, day: 1 }; |
803 | let wday = IWeekday::from_sunday_zero_offset(5); |
804 | let d2 = d1.nth_weekday_of_month(2, wday).unwrap(); |
805 | assert_eq!(d2, IDate { year: 2017, month: 3, day: 10 }); |
806 | |
807 | let d1 = IDate { year: 2024, month: 3, day: 1 }; |
808 | let wday = IWeekday::from_sunday_zero_offset(4); |
809 | let d2 = d1.nth_weekday_of_month(-1, wday).unwrap(); |
810 | assert_eq!(d2, IDate { year: 2024, month: 3, day: 28 }); |
811 | |
812 | let d1 = IDate { year: 2024, month: 3, day: 25 }; |
813 | let wday = IWeekday::from_sunday_zero_offset(1); |
814 | assert!(d1.nth_weekday_of_month(5, wday).is_err()); |
815 | assert!(d1.nth_weekday_of_month(-5, wday).is_err()); |
816 | |
817 | let d1 = IDate { year: 1998, month: 1, day: 1 }; |
818 | let wday = IWeekday::from_sunday_zero_offset(6); |
819 | let d2 = d1.nth_weekday_of_month(5, wday).unwrap(); |
820 | assert_eq!(d2, IDate { year: 1998, month: 1, day: 31 }); |
821 | } |
822 | |
823 | #[test ] |
824 | fn weekday() { |
825 | let wday = IWeekday::from_sunday_zero_offset(0); |
826 | assert_eq!(wday.to_monday_one_offset(), 7); |
827 | |
828 | let wday = IWeekday::from_monday_one_offset(7); |
829 | assert_eq!(wday.to_sunday_zero_offset(), 0); |
830 | |
831 | let wday = IWeekday::from_sunday_one_offset(1); |
832 | assert_eq!(wday.to_monday_zero_offset(), 6); |
833 | |
834 | let wday = IWeekday::from_monday_zero_offset(6); |
835 | assert_eq!(wday.to_sunday_one_offset(), 1); |
836 | } |
837 | |
838 | #[test ] |
839 | fn weekday_since() { |
840 | let wday1 = IWeekday::from_sunday_zero_offset(0); |
841 | let wday2 = IWeekday::from_sunday_zero_offset(6); |
842 | assert_eq!(wday2.since(wday1), 6); |
843 | assert_eq!(wday1.since(wday2), 1); |
844 | } |
845 | |
846 | #[test ] |
847 | fn leap_year() { |
848 | assert!(!is_leap_year(1900)); |
849 | assert!(is_leap_year(2000)); |
850 | assert!(!is_leap_year(2001)); |
851 | assert!(!is_leap_year(2002)); |
852 | assert!(!is_leap_year(2003)); |
853 | assert!(is_leap_year(2004)); |
854 | } |
855 | |
856 | #[test ] |
857 | fn number_of_days_in_month() { |
858 | assert_eq!(days_in_month(2024, 1), 31); |
859 | assert_eq!(days_in_month(2024, 2), 29); |
860 | assert_eq!(days_in_month(2024, 3), 31); |
861 | assert_eq!(days_in_month(2024, 4), 30); |
862 | assert_eq!(days_in_month(2024, 5), 31); |
863 | assert_eq!(days_in_month(2024, 6), 30); |
864 | assert_eq!(days_in_month(2024, 7), 31); |
865 | assert_eq!(days_in_month(2024, 8), 31); |
866 | assert_eq!(days_in_month(2024, 9), 30); |
867 | assert_eq!(days_in_month(2024, 10), 31); |
868 | assert_eq!(days_in_month(2024, 11), 30); |
869 | assert_eq!(days_in_month(2024, 12), 31); |
870 | |
871 | assert_eq!(days_in_month(2025, 1), 31); |
872 | assert_eq!(days_in_month(2025, 2), 28); |
873 | assert_eq!(days_in_month(2025, 3), 31); |
874 | assert_eq!(days_in_month(2025, 4), 30); |
875 | assert_eq!(days_in_month(2025, 5), 31); |
876 | assert_eq!(days_in_month(2025, 6), 30); |
877 | assert_eq!(days_in_month(2025, 7), 31); |
878 | assert_eq!(days_in_month(2025, 8), 31); |
879 | assert_eq!(days_in_month(2025, 9), 30); |
880 | assert_eq!(days_in_month(2025, 10), 31); |
881 | assert_eq!(days_in_month(2025, 11), 30); |
882 | assert_eq!(days_in_month(2025, 12), 31); |
883 | |
884 | assert_eq!(days_in_month(1900, 2), 28); |
885 | assert_eq!(days_in_month(2000, 2), 29); |
886 | } |
887 | |
888 | #[test ] |
889 | fn yesterday() { |
890 | let d1 = IDate { year: 2025, month: 4, day: 7 }; |
891 | let d2 = d1.yesterday().unwrap(); |
892 | assert_eq!(d2, IDate { year: 2025, month: 4, day: 6 }); |
893 | |
894 | let d1 = IDate { year: 2025, month: 4, day: 1 }; |
895 | let d2 = d1.yesterday().unwrap(); |
896 | assert_eq!(d2, IDate { year: 2025, month: 3, day: 31 }); |
897 | |
898 | let d1 = IDate { year: 2025, month: 1, day: 1 }; |
899 | let d2 = d1.yesterday().unwrap(); |
900 | assert_eq!(d2, IDate { year: 2024, month: 12, day: 31 }); |
901 | |
902 | let d1 = IDate { year: -9999, month: 1, day: 1 }; |
903 | assert_eq!(d1.yesterday().ok(), None); |
904 | } |
905 | |
906 | #[test ] |
907 | fn tomorrow() { |
908 | let d1 = IDate { year: 2025, month: 4, day: 7 }; |
909 | let d2 = d1.tomorrow().unwrap(); |
910 | assert_eq!(d2, IDate { year: 2025, month: 4, day: 8 }); |
911 | |
912 | let d1 = IDate { year: 2025, month: 3, day: 31 }; |
913 | let d2 = d1.tomorrow().unwrap(); |
914 | assert_eq!(d2, IDate { year: 2025, month: 4, day: 1 }); |
915 | |
916 | let d1 = IDate { year: 2025, month: 12, day: 31 }; |
917 | let d2 = d1.tomorrow().unwrap(); |
918 | assert_eq!(d2, IDate { year: 2026, month: 1, day: 1 }); |
919 | |
920 | let d1 = IDate { year: 9999, month: 12, day: 31 }; |
921 | assert_eq!(d1.tomorrow().ok(), None); |
922 | } |
923 | } |
924 | |