Skip to main content

ferroui_base/utilities/
date_time.rs

1//! An instant in time as a date and time of day (.NET `System.DateTime`).
2//!
3//! The value is a count of 100-nanosecond ticks since 0001-01-01T00:00:00 in
4//! the proleptic Gregorian calendar, together with a [`DateTimeKind`]. The
5//! range is 0001-01-01T00:00:00 ..= 9999-12-31T23:59:59.9999999. Operations
6//! whose result would leave that range panic (.NET throws
7//! `ArgumentOutOfRangeException`); the `try_` forms return `None` instead.
8//!
9//! The calendar arithmetic (dates from day numbers, month lengths, leap
10//! years, days of the week) is the one of the `time` crate; the tick count,
11//! the kind, the range and the rules of the `add_` members are the .NET ones.
12
13use super::date_time_format::{self, Parsed};
14use super::{
15    CultureInfo, DateTimeFormatProvider, DateTimeKind, DateTimeStyles, DayOfWeek, FormatError, TimeZoneInfo,
16};
17use crate::animation::TimeSpan;
18use crate::platform::{IRuntimePlatform, StandardRuntimePlatform};
19use crate::{FerroLocator, LocatorExtensions};
20use std::cell::RefCell;
21use std::cmp::Ordering;
22use std::fmt;
23use std::hash::{Hash, Hasher};
24use std::ops::{Add, AddAssign, Sub, SubAssign};
25use std::rc::Rc;
26use std::str::FromStr;
27
28/// Gives the current UTC time.
29pub type UtcNowProvider = Rc<dyn Fn() -> DateTime>;
30
31crate::ferro_thread_local! {
32    // CLOCK-SEAM: tests pin "now" here. The wall clock itself is `IRuntimePlatform::get_utc_now`.
33    static UTC_NOW_PROVIDER: RefCell<Option<UtcNowProvider>> = const { RefCell::new(None) };
34}
35
36const TICKS_PER_DAY: i64 = TimeSpan::TICKS_PER_DAY;
37/// The Julian day number of 0001-01-01 (proleptic Gregorian).
38const JULIAN_DAY_OF_YEAR_ONE: i32 = 1_721_426;
39/// Days from 0001-01-01 to 10000-01-01.
40const DAYS_TO_10000: i64 = 3_652_059;
41/// Days from 0001-01-01 to 1970-01-01.
42const DAYS_TO_1970: i64 = 719_162;
43const MAX_TICKS: i64 = DAYS_TO_10000 * TICKS_PER_DAY - 1;
44const MAX_MONTHS: i32 = 120_000;
45
46const OUT_OF_RANGE: &str = "The added or subtracted value results in an un-representable DateTime.";
47
48/// A date and time of day with a resolution of 100 nanoseconds.
49///
50/// Equality, ordering and hashing consider the ticks only; the kind is ignored,
51/// as in .NET.
52#[derive(Clone, Copy)]
53pub struct DateTime {
54    ticks: i64,
55    kind: DateTimeKind,
56}
57
58impl DateTime {
59    /// The smallest value, 0001-01-01T00:00:00.
60    pub const MIN_VALUE: DateTime = DateTime { ticks: 0, kind: DateTimeKind::Unspecified };
61    /// The largest value, 9999-12-31T23:59:59.9999999.
62    pub const MAX_VALUE: DateTime = DateTime { ticks: MAX_TICKS, kind: DateTimeKind::Unspecified };
63    /// 1970-01-01T00:00:00 UTC.
64    pub const UNIX_EPOCH: DateTime = DateTime { ticks: DAYS_TO_1970 * TICKS_PER_DAY, kind: DateTimeKind::Utc };
65
66    /// The date `year`-`month`-`day` at midnight, of unspecified kind.
67    /// Panics when the arguments do not name a date in 0001..=9999.
68    #[track_caller]
69    pub fn new(year: i32, month: i32, day: i32) -> DateTime {
70        Self::try_new(year, month, day).expect("Year, Month, and Day parameters describe an un-representable DateTime.")
71    }
72
73    /// The given date and time of day, of unspecified kind. Panics when the
74    /// arguments do not name a valid date and time.
75    #[track_caller]
76    pub fn new_with_time(year: i32, month: i32, day: i32, hour: i32, minute: i32, second: i32) -> DateTime {
77        Self::try_new_with_time(year, month, day, hour, minute, second)
78            .expect("The parameters describe an un-representable DateTime.")
79    }
80
81    /// The given date and time of day with milliseconds, of unspecified kind.
82    /// Panics when the arguments do not name a valid date and time.
83    #[track_caller]
84    pub fn new_with_millisecond(
85        year: i32,
86        month: i32,
87        day: i32,
88        hour: i32,
89        minute: i32,
90        second: i32,
91        millisecond: i32,
92    ) -> DateTime {
93        assert!((0..1000).contains(&millisecond), "Valid values are between 0 and 999, inclusive. (Parameter 'millisecond')");
94        let value = Self::new_with_time(year, month, day, hour, minute, second);
95        DateTime { ticks: value.ticks + millisecond as i64 * TimeSpan::TICKS_PER_MILLISECOND, kind: value.kind }
96    }
97
98    /// The date `year`-`month`-`day` at midnight; `None` when the arguments
99    /// do not name a date in 0001..=9999.
100    pub fn try_new(year: i32, month: i32, day: i32) -> Option<DateTime> {
101        if !(1..=9999).contains(&year) {
102            return None;
103        }
104        let date = time::Date::from_calendar_date(year, month_of(month)?, u8::try_from(day).ok()?).ok()?;
105        let days = (date.to_julian_day() - JULIAN_DAY_OF_YEAR_ONE) as i64;
106        Some(DateTime { ticks: days * TICKS_PER_DAY, kind: DateTimeKind::Unspecified })
107    }
108
109    /// The given date and time of day; `None` when the arguments do not name
110    /// a valid date and time.
111    pub fn try_new_with_time(year: i32, month: i32, day: i32, hour: i32, minute: i32, second: i32) -> Option<DateTime> {
112        if !(0..24).contains(&hour) || !(0..60).contains(&minute) || !(0..60).contains(&second) {
113            return None;
114        }
115        let date = Self::try_new(year, month, day)?;
116        let time = hour as i64 * TimeSpan::TICKS_PER_HOUR
117            + minute as i64 * TimeSpan::TICKS_PER_MINUTE
118            + second as i64 * TimeSpan::TICKS_PER_SECOND;
119        Some(DateTime { ticks: date.ticks + time, kind: DateTimeKind::Unspecified })
120    }
121
122    /// The value with the given number of ticks, of unspecified kind. Panics
123    /// when `ticks` is outside the range of the type.
124    #[track_caller]
125    pub const fn from_ticks(ticks: i64) -> DateTime {
126        Self::from_ticks_kind(ticks, DateTimeKind::Unspecified)
127    }
128
129    /// The value with the given number of ticks and kind. Panics when `ticks`
130    /// is outside the range of the type.
131    #[track_caller]
132    pub const fn from_ticks_kind(ticks: i64, kind: DateTimeKind) -> DateTime {
133        assert!(
134            ticks >= 0 && ticks <= MAX_TICKS,
135            "Ticks must be between DateTime.MinValue.Ticks and DateTime.MaxValue.Ticks."
136        );
137        DateTime { ticks, kind }
138    }
139
140    /// The value with the given number of ticks; `None` when out of range.
141    pub const fn try_from_ticks(ticks: i64, kind: DateTimeKind) -> Option<DateTime> {
142        if ticks >= 0 && ticks <= MAX_TICKS {
143            Some(DateTime { ticks, kind })
144        } else {
145            None
146        }
147    }
148
149    /// The same ticks with another kind (.NET `DateTime.SpecifyKind`).
150    #[inline]
151    pub const fn specify_kind(self, kind: DateTimeKind) -> DateTime {
152        DateTime { ticks: self.ticks, kind }
153    }
154
155    // --- the clock ---
156
157    /// Installs (or with `None` removes) the source of the current UTC time
158    /// of the current thread and returns the previous one. Tests use it to
159    /// pin [`now`](Self::now), [`utc_now`](Self::utc_now) and [`today`](Self::today).
160    pub fn set_utc_now_provider(provider: Option<UtcNowProvider>) -> Option<UtcNowProvider> {
161        UTC_NOW_PROVIDER.with(|current| current.replace(provider))
162    }
163
164    /// The current date and time in UTC (kind [`DateTimeKind::Utc`]): the
165    /// time of the pinned source of this thread
166    /// ([`set_utc_now_provider`](Self::set_utc_now_provider)) when there is
167    /// one, else the wall clock of the registered
168    /// [`IRuntimePlatform`], else the one of the standard runtime platform.
169    pub fn utc_now() -> DateTime {
170        let provider = UTC_NOW_PROVIDER.with(|current| current.borrow().clone());
171        if let Some(provider) = provider {
172            return provider().to_universal_time();
173        }
174        let now = match FerroLocator::current().get_service::<dyn IRuntimePlatform>() {
175            Some(platform) => platform.get_utc_now(),
176            None => StandardRuntimePlatform::new().get_utc_now(),
177        };
178        now.specify_kind(DateTimeKind::Utc)
179    }
180
181    /// The current date and time in local time (kind [`DateTimeKind::Local`]).
182    pub fn now() -> DateTime {
183        Self::utc_now().to_local_time()
184    }
185
186    /// The current local date at midnight (kind [`DateTimeKind::Local`]).
187    pub fn today() -> DateTime {
188        Self::now().date()
189    }
190
191    // --- components ---
192
193    /// The number of ticks since 0001-01-01T00:00:00.
194    #[inline]
195    pub const fn ticks(self) -> i64 {
196        self.ticks
197    }
198
199    /// The kind of time.
200    #[inline]
201    pub const fn kind(self) -> DateTimeKind {
202        self.kind
203    }
204
205    /// The date at midnight, with the same kind.
206    #[inline]
207    pub const fn date(self) -> DateTime {
208        DateTime { ticks: self.ticks - self.ticks % TICKS_PER_DAY, kind: self.kind }
209    }
210
211    /// The time elapsed since midnight.
212    #[inline]
213    pub const fn time_of_day(self) -> TimeSpan {
214        TimeSpan::from_ticks(self.ticks % TICKS_PER_DAY)
215    }
216
217    /// The calendar date of the value.
218    pub(crate) fn to_date(self) -> time::Date {
219        time::Date::from_julian_day((self.ticks / TICKS_PER_DAY) as i32 + JULIAN_DAY_OF_YEAR_ONE)
220            .expect("every DateTime is a date of the years 1 to 9999")
221    }
222
223    /// The year, month, day of the month and day of the year (the latter two 1-based).
224    pub(crate) fn date_parts(self) -> (i32, i32, i32, i32) {
225        let date = self.to_date();
226        (date.year(), date.month() as u8 as i32, date.day() as i32, date.ordinal() as i32)
227    }
228
229    /// The year, 1..=9999.
230    pub fn year(self) -> i32 {
231        self.date_parts().0
232    }
233
234    /// The month, 1..=12.
235    pub fn month(self) -> i32 {
236        self.date_parts().1
237    }
238
239    /// The day of the month, 1..=31.
240    pub fn day(self) -> i32 {
241        self.date_parts().2
242    }
243
244    /// The day of the year, 1..=366.
245    pub fn day_of_year(self) -> i32 {
246        self.date_parts().3
247    }
248
249    /// The day of the week.
250    pub fn day_of_week(self) -> DayOfWeek {
251        DayOfWeek::ALL[self.to_date().weekday().number_days_from_sunday() as usize]
252    }
253
254    /// The hour, 0..=23.
255    #[inline]
256    pub const fn hour(self) -> i32 {
257        (self.ticks / TimeSpan::TICKS_PER_HOUR % 24) as i32
258    }
259
260    /// The minute, 0..=59.
261    #[inline]
262    pub const fn minute(self) -> i32 {
263        (self.ticks / TimeSpan::TICKS_PER_MINUTE % 60) as i32
264    }
265
266    /// The second, 0..=59.
267    #[inline]
268    pub const fn second(self) -> i32 {
269        (self.ticks / TimeSpan::TICKS_PER_SECOND % 60) as i32
270    }
271
272    /// The millisecond, 0..=999.
273    #[inline]
274    pub const fn millisecond(self) -> i32 {
275        (self.ticks / TimeSpan::TICKS_PER_MILLISECOND % 1000) as i32
276    }
277
278    // --- calendar facts ---
279
280    /// Whether `year` (1..=9999) is a leap year. Panics for other years.
281    #[track_caller]
282    pub fn is_leap_year(year: i32) -> bool {
283        assert!((1..=9999).contains(&year), "Year must be between 1 and 9999. (Parameter 'year')");
284        time::util::is_leap_year(year)
285    }
286
287    /// The number of days of `month` in `year`. Panics when the month is not
288    /// 1..=12 or the year not 1..=9999.
289    #[track_caller]
290    pub fn days_in_month(year: i32, month: i32) -> i32 {
291        let month = month_of(month).expect("Month must be between one and twelve. (Parameter 'month')");
292        assert!((1..=9999).contains(&year), "Year must be between 1 and 9999. (Parameter 'year')");
293        time::util::days_in_month(month, year) as i32
294    }
295
296    // --- arithmetic ---
297
298    /// The value `ticks` later; `None` when out of range.
299    pub const fn try_add_ticks(self, ticks: i64) -> Option<DateTime> {
300        match self.ticks.checked_add(ticks) {
301            Some(ticks) => Self::try_from_ticks(ticks, self.kind),
302            None => None,
303        }
304    }
305
306    /// The value `ticks` later. Panics when out of range.
307    #[track_caller]
308    pub fn add_ticks(self, ticks: i64) -> DateTime {
309        self.try_add_ticks(ticks).expect(OUT_OF_RANGE)
310    }
311
312    /// The value `value` later. Panics when out of range.
313    #[track_caller]
314    pub fn add(self, value: TimeSpan) -> DateTime {
315        self.add_ticks(value.ticks())
316    }
317
318    /// The value `value` later; `None` when out of range.
319    pub fn try_add(self, value: TimeSpan) -> Option<DateTime> {
320        self.try_add_ticks(value.ticks())
321    }
322
323    /// The value `value` earlier. Panics when out of range.
324    #[track_caller]
325    pub fn subtract(self, value: TimeSpan) -> DateTime {
326        value.ticks().checked_neg().and_then(|ticks| self.try_add_ticks(ticks)).expect(OUT_OF_RANGE)
327    }
328
329    /// The interval from `other` to this value.
330    #[inline]
331    pub fn subtract_date_time(self, other: DateTime) -> TimeSpan {
332        TimeSpan::from_ticks(self.ticks - other.ticks)
333    }
334
335    fn try_add_units(self, value: f64, ticks_per_unit: i64) -> Option<DateTime> {
336        if !value.is_finite() || value.abs() > (MAX_TICKS / ticks_per_unit) as f64 {
337            return None;
338        }
339        let integral = value.trunc();
340        let fractional = value - integral;
341        let ticks = integral as i64 * ticks_per_unit + (fractional * ticks_per_unit as f64) as i64;
342        self.try_add_ticks(ticks)
343    }
344
345    /// The value `value` (whole and fractional) days later; `None` when out of range.
346    pub fn try_add_days(self, value: f64) -> Option<DateTime> {
347        self.try_add_units(value, TICKS_PER_DAY)
348    }
349
350    /// The value `value` (whole and fractional) days later. Panics when out of range.
351    #[track_caller]
352    pub fn add_days(self, value: f64) -> DateTime {
353        self.try_add_days(value).expect(OUT_OF_RANGE)
354    }
355
356    /// The value `months` months later (earlier when negative). The day is
357    /// kept, or becomes the last day of the resulting month when that month
358    /// is shorter. `None` when `months` is not in -120000..=120000 or the
359    /// result is out of range.
360    pub fn try_add_months(self, months: i32) -> Option<DateTime> {
361        if !(-MAX_MONTHS..=MAX_MONTHS).contains(&months) {
362            return None;
363        }
364        let (year, month, day, _) = self.date_parts();
365        let total = year * 12 + (month - 1) + months;
366        let (year, month) = (total.div_euclid(12), total.rem_euclid(12) + 1);
367        if !(1..=9999).contains(&year) {
368            return None;
369        }
370        let day = day.min(Self::days_in_month(year, month));
371        let date = Self::try_new(year, month, day)?;
372        Some(DateTime { ticks: date.ticks + self.ticks % TICKS_PER_DAY, kind: self.kind })
373    }
374
375    /// The value `months` months later; see [`try_add_months`](Self::try_add_months).
376    /// Panics when `months` or the result is out of range.
377    #[track_caller]
378    pub fn add_months(self, months: i32) -> DateTime {
379        self.try_add_months(months).expect(OUT_OF_RANGE)
380    }
381
382    /// The value `years` years later (earlier when negative); February 29
383    /// becomes February 28 in a year that is not a leap year. `None` when
384    /// `years` is not in -10000..=10000 or the result is out of range.
385    pub fn try_add_years(self, years: i32) -> Option<DateTime> {
386        if !(-10_000..=10_000).contains(&years) {
387            return None;
388        }
389        self.try_add_months(years * 12)
390    }
391
392    /// The value `years` years later; see [`try_add_years`](Self::try_add_years).
393    /// Panics when `years` or the result is out of range.
394    #[track_caller]
395    pub fn add_years(self, years: i32) -> DateTime {
396        self.try_add_years(years).expect(OUT_OF_RANGE)
397    }
398
399    // --- comparison ---
400
401    /// Compares two values by their ticks: negative, zero or positive
402    /// (.NET `DateTime.Compare`, which returns -1, 0 or 1).
403    #[inline]
404    pub fn compare(t1: DateTime, t2: DateTime) -> i32 {
405        match t1.ticks.cmp(&t2.ticks) {
406            Ordering::Less => -1,
407            Ordering::Equal => 0,
408            Ordering::Greater => 1,
409        }
410    }
411
412    /// Compares this value with `other`: -1, 0 or 1.
413    #[inline]
414    pub fn compare_to(self, other: DateTime) -> i32 {
415        Self::compare(self, other)
416    }
417
418    // --- time zone conversion ---
419
420    /// The value converted to UTC. A value of unspecified kind is taken as
421    /// local time. A result outside the range is clamped to it.
422    pub fn to_universal_time(self) -> DateTime {
423        if self.kind == DateTimeKind::Utc {
424            return self;
425        }
426        let offset = TimeZoneInfo::get_utc_offset_of_local_time(self);
427        DateTime { ticks: (self.ticks - offset.ticks()).clamp(0, MAX_TICKS), kind: DateTimeKind::Utc }
428    }
429
430    /// The value converted to local time. A value of unspecified kind is
431    /// taken as UTC. A result outside the range is clamped to it.
432    pub fn to_local_time(self) -> DateTime {
433        if self.kind == DateTimeKind::Local {
434            return self;
435        }
436        let offset = TimeZoneInfo::get_local_utc_offset(self);
437        DateTime { ticks: (self.ticks + offset.ticks()).clamp(0, MAX_TICKS), kind: DateTimeKind::Local }
438    }
439
440    // --- formatting ---
441
442    /// Formats with a standard or custom .NET date and time format string
443    /// and the conventions of `provider` (a [`CultureInfo`] or a
444    /// [`DateTimeFormatInfo`](super::DateTimeFormatInfo)). An empty format is
445    /// the general format `"G"`. Panics when the format string is invalid
446    /// (.NET throws `FormatException`).
447    #[track_caller]
448    pub fn to_string_format(self, format: &str, provider: &(impl DateTimeFormatProvider + ?Sized)) -> String {
449        match self.try_to_string_format(format, provider) {
450            Ok(text) => text,
451            Err(error) => panic!("{error}"),
452        }
453    }
454
455    /// Like [`to_string_format`](Self::to_string_format), but gives the error
456    /// of an invalid format string.
457    pub fn try_to_string_format(
458        self,
459        format: &str,
460        provider: &(impl DateTimeFormatProvider + ?Sized),
461    ) -> Result<String, FormatError> {
462        date_time_format::format(self, None, format, &provider.get_date_time_format())
463    }
464
465    /// Formats with a format string and the conventions of the current
466    /// culture (.NET `ToString(string)`). Panics when the format is invalid.
467    #[track_caller]
468    pub fn to_string_with(self, format: &str) -> String {
469        self.to_string_format(format, &CultureInfo::current_culture())
470    }
471
472    /// Formats with the general format `"G"` and the conventions of
473    /// `provider` (.NET `ToString(IFormatProvider)`).
474    pub fn to_string_provider(self, provider: &(impl DateTimeFormatProvider + ?Sized)) -> String {
475        self.to_string_format("G", provider)
476    }
477
478    // --- parsing ---
479
480    fn from_parsed(parsed: Parsed, styles: DateTimeStyles) -> Result<DateTime, FormatError> {
481        let clock = parsed.date_time;
482        let out_of_range = || FormatError::new("The UTC representation of the date falls outside the supported range.");
483        match parsed.offset {
484            Some(offset) => {
485                let utc = DateTime::try_from_ticks(clock.ticks - offset.ticks(), DateTimeKind::Utc).ok_or_else(out_of_range)?;
486                if styles.contains(DateTimeStyles::ADJUST_TO_UNIVERSAL)
487                    || (styles.contains(DateTimeStyles::ROUNDTRIP_KIND) && parsed.utc_designator)
488                {
489                    Ok(utc)
490                } else {
491                    Ok(utc.to_local_time())
492                }
493            }
494            None if styles.contains(DateTimeStyles::ASSUME_UNIVERSAL) => {
495                let utc = clock.specify_kind(DateTimeKind::Utc);
496                Ok(if styles.contains(DateTimeStyles::ADJUST_TO_UNIVERSAL) { utc } else { utc.to_local_time() })
497            }
498            None if styles.contains(DateTimeStyles::ASSUME_LOCAL) => {
499                let local = clock.specify_kind(DateTimeKind::Local);
500                Ok(if styles.contains(DateTimeStyles::ADJUST_TO_UNIVERSAL) { local.to_universal_time() } else { local })
501            }
502            None => Ok(clock),
503        }
504    }
505
506    /// Parses a date and time written in the conventions of `provider`
507    /// (.NET `DateTime.Parse(string, IFormatProvider)`).
508    ///
509    /// This is not the heuristic parser of .NET. It is checked against
510    /// results recorded from .NET (`date_time_net_data.rs`, tables `PARSE`,
511    /// `PARSE_ROUND_TRIP` and `PARSE_STYLES`) for the invariant culture,
512    /// `en`, `en-US` and `en-GB`, which covers what the date picker feeds
513    /// it: the text it writes itself with the short and the long date
514    /// pattern of the culture, and typed dates of these forms:
515    ///
516    /// * numeric dates with `/`, `-`, `.` or spaces in the order of the
517    ///   short date pattern; a year of three or more digits written first
518    ///   gives year-month-day; two-digit years are 1950..=2049;
519    /// * month names and abbreviations (of the culture or English), with
520    ///   the day before or after and an optional, matching day name;
521    /// * a month and a year alone (the first of the month), a month and a
522    ///   day alone (the current year);
523    /// * an offset from UTC (`Z`, `GMT`, a signed `hh:mm`, `hhmm` or `h[h]`)
524    ///   after the time or after a date without a time; the result is then
525    ///   the local time of that instant;
526    /// * ISO 8601 `yyyy-MM-ddTHH:mm:ss[.fffffff]` with `Z` or an offset,
527    ///   the sortable, universal sortable and RFC 1123 formats;
528    /// * a time `H:mm[:ss[.fffffff]]` with an optional designator before or
529    ///   after the date, or alone (the current date).
530    ///
531    /// Everything else is rejected, and has not been compared with .NET
532    /// beyond the recorded rows: other time zone names than `GMT`
533    /// and `Z` (.NET rejects `UTC` too), era names, a two-number date that is only valid in the
534    /// order the culture does not use, and the inflected (genitive) month
535    /// names of cultures that have them, which .NET accepts.
536    pub fn parse(text: &str, provider: &(impl DateTimeFormatProvider + ?Sized)) -> Result<DateTime, FormatError> {
537        Self::parse_with_styles(text, provider, DateTimeStyles::NONE)
538    }
539
540    /// Parses a date and time written in the conventions of `provider`
541    /// (.NET `DateTime.Parse(string, IFormatProvider, DateTimeStyles)`).
542    pub fn parse_with_styles(
543        text: &str,
544        provider: &(impl DateTimeFormatProvider + ?Sized),
545        styles: DateTimeStyles,
546    ) -> Result<DateTime, FormatError> {
547        let parsed = date_time_format::parse(text, &provider.get_date_time_format(), styles)?;
548        Self::from_parsed(parsed, styles)
549    }
550
551    /// Parses like [`parse_with_styles`](Self::parse_with_styles); `None` on failure
552    /// (.NET `DateTime.TryParse`).
553    pub fn try_parse(
554        text: &str,
555        provider: &(impl DateTimeFormatProvider + ?Sized),
556        styles: DateTimeStyles,
557    ) -> Option<DateTime> {
558        Self::parse_with_styles(text, provider, styles).ok()
559    }
560
561    /// Parses a date and time that has to match `format` exactly
562    /// (.NET `DateTime.ParseExact(string, string, IFormatProvider)`).
563    pub fn parse_exact(
564        text: &str,
565        format: &str,
566        provider: &(impl DateTimeFormatProvider + ?Sized),
567    ) -> Result<DateTime, FormatError> {
568        Self::parse_exact_with_styles(text, format, provider, DateTimeStyles::NONE)
569    }
570
571    /// Parses a date and time that has to match `format` exactly
572    /// (.NET `DateTime.ParseExact(string, string, IFormatProvider, DateTimeStyles)`).
573    pub fn parse_exact_with_styles(
574        text: &str,
575        format: &str,
576        provider: &(impl DateTimeFormatProvider + ?Sized),
577        styles: DateTimeStyles,
578    ) -> Result<DateTime, FormatError> {
579        let parsed = date_time_format::parse_exact(text, format, &provider.get_date_time_format(), styles)?;
580        Self::from_parsed(parsed, styles)
581    }
582
583    /// Parses a date and time that has to match one of `formats` exactly; the
584    /// first format that matches wins (.NET `DateTime.ParseExact(string, string[], ..)`).
585    pub fn parse_exact_multiple(
586        text: &str,
587        formats: &[&str],
588        provider: &(impl DateTimeFormatProvider + ?Sized),
589        styles: DateTimeStyles,
590    ) -> Result<DateTime, FormatError> {
591        let info = provider.get_date_time_format();
592        for format in formats {
593            if let Ok(parsed) = date_time_format::parse_exact(text, format, &info, styles) {
594                if let Ok(value) = Self::from_parsed(parsed, styles) {
595                    return Ok(value);
596                }
597            }
598        }
599        Err(FormatError::from_string(format!("String '{text}' was not recognized as a valid DateTime.")))
600    }
601
602    /// Like [`parse_exact_with_styles`](Self::parse_exact_with_styles); `None` on failure
603    /// (.NET `DateTime.TryParseExact(string, string, ..)`).
604    pub fn try_parse_exact(
605        text: &str,
606        format: &str,
607        provider: &(impl DateTimeFormatProvider + ?Sized),
608        styles: DateTimeStyles,
609    ) -> Option<DateTime> {
610        Self::parse_exact_with_styles(text, format, provider, styles).ok()
611    }
612
613    /// Like [`parse_exact_multiple`](Self::parse_exact_multiple); `None` on failure
614    /// (.NET `DateTime.TryParseExact(string, string[], ..)`).
615    pub fn try_parse_exact_multiple(
616        text: &str,
617        formats: &[&str],
618        provider: &(impl DateTimeFormatProvider + ?Sized),
619        styles: DateTimeStyles,
620    ) -> Option<DateTime> {
621        Self::parse_exact_multiple(text, formats, provider, styles).ok()
622    }
623}
624
625/// The month of the `time` crate for a month number; `None` unless 1..=12.
626fn month_of(month: i32) -> Option<time::Month> {
627    time::Month::try_from(u8::try_from(month).ok()?).ok()
628}
629
630impl Default for DateTime {
631    /// [`DateTime::MIN_VALUE`], the C# `default(DateTime)`.
632    fn default() -> Self {
633        DateTime::MIN_VALUE
634    }
635}
636
637impl PartialEq for DateTime {
638    #[inline]
639    fn eq(&self, other: &Self) -> bool {
640        self.ticks == other.ticks
641    }
642}
643
644impl Eq for DateTime {}
645
646impl PartialOrd for DateTime {
647    #[inline]
648    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
649        Some(self.cmp(other))
650    }
651}
652
653impl Ord for DateTime {
654    #[inline]
655    fn cmp(&self, other: &Self) -> Ordering {
656        self.ticks.cmp(&other.ticks)
657    }
658}
659
660impl Hash for DateTime {
661    fn hash<H: Hasher>(&self, state: &mut H) {
662        self.ticks.hash(state);
663    }
664}
665
666impl Add<TimeSpan> for DateTime {
667    type Output = DateTime;
668    #[track_caller]
669    fn add(self, rhs: TimeSpan) -> DateTime {
670        DateTime::add(self, rhs)
671    }
672}
673
674impl Sub<TimeSpan> for DateTime {
675    type Output = DateTime;
676    #[track_caller]
677    fn sub(self, rhs: TimeSpan) -> DateTime {
678        self.subtract(rhs)
679    }
680}
681
682impl Sub<DateTime> for DateTime {
683    type Output = TimeSpan;
684    #[inline]
685    fn sub(self, rhs: DateTime) -> TimeSpan {
686        self.subtract_date_time(rhs)
687    }
688}
689
690impl AddAssign<TimeSpan> for DateTime {
691    #[track_caller]
692    fn add_assign(&mut self, rhs: TimeSpan) {
693        *self = *self + rhs;
694    }
695}
696
697impl SubAssign<TimeSpan> for DateTime {
698    #[track_caller]
699    fn sub_assign(&mut self, rhs: TimeSpan) {
700        *self = *self - rhs;
701    }
702}
703
704impl fmt::Display for DateTime {
705    /// The general format `"G"` of the current culture (.NET `ToString()`).
706    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
707        f.write_str(&self.to_string_format("G", &CultureInfo::current_culture()))
708    }
709}
710
711impl fmt::Debug for DateTime {
712    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
713        let (year, month, day, _) = self.date_parts();
714        write!(
715            f,
716            "DateTime({year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{:07}, {:?})",
717            self.hour(),
718            self.minute(),
719            self.second(),
720            self.ticks % TimeSpan::TICKS_PER_SECOND,
721            self.kind
722        )
723    }
724}
725
726impl FromStr for DateTime {
727    type Err = FormatError;
728    /// Parses with the conventions of the current culture (.NET `DateTime.Parse(string)`).
729    fn from_str(s: &str) -> Result<Self, Self::Err> {
730        DateTime::parse(s, &CultureInfo::current_culture())
731    }
732}