1use 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
28pub type UtcNowProvider = Rc<dyn Fn() -> DateTime>;
30
31crate::ferro_thread_local! {
32 static UTC_NOW_PROVIDER: RefCell<Option<UtcNowProvider>> = const { RefCell::new(None) };
34}
35
36const TICKS_PER_DAY: i64 = TimeSpan::TICKS_PER_DAY;
37const JULIAN_DAY_OF_YEAR_ONE: i32 = 1_721_426;
39const DAYS_TO_10000: i64 = 3_652_059;
41const 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#[derive(Clone, Copy)]
53pub struct DateTime {
54 ticks: i64,
55 kind: DateTimeKind,
56}
57
58impl DateTime {
59 pub const MIN_VALUE: DateTime = DateTime { ticks: 0, kind: DateTimeKind::Unspecified };
61 pub const MAX_VALUE: DateTime = DateTime { ticks: MAX_TICKS, kind: DateTimeKind::Unspecified };
63 pub const UNIX_EPOCH: DateTime = DateTime { ticks: DAYS_TO_1970 * TICKS_PER_DAY, kind: DateTimeKind::Utc };
65
66 #[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 #[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 #[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 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 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 #[track_caller]
125 pub const fn from_ticks(ticks: i64) -> DateTime {
126 Self::from_ticks_kind(ticks, DateTimeKind::Unspecified)
127 }
128
129 #[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 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 #[inline]
151 pub const fn specify_kind(self, kind: DateTimeKind) -> DateTime {
152 DateTime { ticks: self.ticks, kind }
153 }
154
155 pub fn set_utc_now_provider(provider: Option<UtcNowProvider>) -> Option<UtcNowProvider> {
161 UTC_NOW_PROVIDER.with(|current| current.replace(provider))
162 }
163
164 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 pub fn now() -> DateTime {
183 Self::utc_now().to_local_time()
184 }
185
186 pub fn today() -> DateTime {
188 Self::now().date()
189 }
190
191 #[inline]
195 pub const fn ticks(self) -> i64 {
196 self.ticks
197 }
198
199 #[inline]
201 pub const fn kind(self) -> DateTimeKind {
202 self.kind
203 }
204
205 #[inline]
207 pub const fn date(self) -> DateTime {
208 DateTime { ticks: self.ticks - self.ticks % TICKS_PER_DAY, kind: self.kind }
209 }
210
211 #[inline]
213 pub const fn time_of_day(self) -> TimeSpan {
214 TimeSpan::from_ticks(self.ticks % TICKS_PER_DAY)
215 }
216
217 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 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 pub fn year(self) -> i32 {
231 self.date_parts().0
232 }
233
234 pub fn month(self) -> i32 {
236 self.date_parts().1
237 }
238
239 pub fn day(self) -> i32 {
241 self.date_parts().2
242 }
243
244 pub fn day_of_year(self) -> i32 {
246 self.date_parts().3
247 }
248
249 pub fn day_of_week(self) -> DayOfWeek {
251 DayOfWeek::ALL[self.to_date().weekday().number_days_from_sunday() as usize]
252 }
253
254 #[inline]
256 pub const fn hour(self) -> i32 {
257 (self.ticks / TimeSpan::TICKS_PER_HOUR % 24) as i32
258 }
259
260 #[inline]
262 pub const fn minute(self) -> i32 {
263 (self.ticks / TimeSpan::TICKS_PER_MINUTE % 60) as i32
264 }
265
266 #[inline]
268 pub const fn second(self) -> i32 {
269 (self.ticks / TimeSpan::TICKS_PER_SECOND % 60) as i32
270 }
271
272 #[inline]
274 pub const fn millisecond(self) -> i32 {
275 (self.ticks / TimeSpan::TICKS_PER_MILLISECOND % 1000) as i32
276 }
277
278 #[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 #[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 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 #[track_caller]
308 pub fn add_ticks(self, ticks: i64) -> DateTime {
309 self.try_add_ticks(ticks).expect(OUT_OF_RANGE)
310 }
311
312 #[track_caller]
314 pub fn add(self, value: TimeSpan) -> DateTime {
315 self.add_ticks(value.ticks())
316 }
317
318 pub fn try_add(self, value: TimeSpan) -> Option<DateTime> {
320 self.try_add_ticks(value.ticks())
321 }
322
323 #[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 #[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 pub fn try_add_days(self, value: f64) -> Option<DateTime> {
347 self.try_add_units(value, TICKS_PER_DAY)
348 }
349
350 #[track_caller]
352 pub fn add_days(self, value: f64) -> DateTime {
353 self.try_add_days(value).expect(OUT_OF_RANGE)
354 }
355
356 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 #[track_caller]
378 pub fn add_months(self, months: i32) -> DateTime {
379 self.try_add_months(months).expect(OUT_OF_RANGE)
380 }
381
382 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 #[track_caller]
395 pub fn add_years(self, years: i32) -> DateTime {
396 self.try_add_years(years).expect(OUT_OF_RANGE)
397 }
398
399 #[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 #[inline]
414 pub fn compare_to(self, other: DateTime) -> i32 {
415 Self::compare(self, other)
416 }
417
418 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 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 #[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 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 #[track_caller]
468 pub fn to_string_with(self, format: &str) -> String {
469 self.to_string_format(format, &CultureInfo::current_culture())
470 }
471
472 pub fn to_string_provider(self, provider: &(impl DateTimeFormatProvider + ?Sized)) -> String {
475 self.to_string_format("G", provider)
476 }
477
478 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 pub fn parse(text: &str, provider: &(impl DateTimeFormatProvider + ?Sized)) -> Result<DateTime, FormatError> {
537 Self::parse_with_styles(text, provider, DateTimeStyles::NONE)
538 }
539
540 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 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 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 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 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 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 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
625fn 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 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 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 fn from_str(s: &str) -> Result<Self, Self::Err> {
730 DateTime::parse(s, &CultureInfo::current_culture())
731 }
732}