1use super::date_time_format;
5use super::{CultureInfo, DateTime, DateTimeFormatProvider, DateTimeKind, DayOfWeek, FormatError, TimeZoneInfo};
6use crate::animation::TimeSpan;
7use std::cmp::Ordering;
8use std::fmt;
9use std::hash::{Hash, Hasher};
10use std::ops::{Add, AddAssign, Sub, SubAssign};
11
12const OUT_OF_RANGE: &str = "The UTC time represented when the offset is applied must be between year 0 and 10,000.";
13
14#[derive(Clone, Copy)]
20pub struct DateTimeOffset {
21 utc: DateTime,
23 offset_minutes: i16,
24}
25
26impl DateTimeOffset {
27 pub const MIN_VALUE: DateTimeOffset = DateTimeOffset { utc: DateTime::MIN_VALUE, offset_minutes: 0 };
29 pub const MAX_VALUE: DateTimeOffset = DateTimeOffset { utc: DateTime::MAX_VALUE, offset_minutes: 0 };
31 pub const UNIX_EPOCH: DateTimeOffset =
33 DateTimeOffset { utc: DateTime::UNIX_EPOCH.specify_kind(DateTimeKind::Unspecified), offset_minutes: 0 };
34
35 fn validate_offset(offset: TimeSpan) -> Result<i16, &'static str> {
36 let ticks = offset.ticks();
37 if ticks % TimeSpan::TICKS_PER_MINUTE != 0 {
38 return Err("Offset must be specified in whole minutes. (Parameter 'offset')");
39 }
40 if ticks.abs() > TimeZoneInfo::MAX_OFFSET.ticks() {
41 return Err("Offset must be within plus or minus 14 hours. (Parameter 'offset')");
42 }
43 Ok((ticks / TimeSpan::TICKS_PER_MINUTE) as i16)
44 }
45
46 fn from_clock(clock: DateTime, offset: TimeSpan) -> Result<DateTimeOffset, &'static str> {
47 let offset_minutes = Self::validate_offset(offset)?;
48 let utc = DateTime::try_from_ticks(clock.ticks() - offset.ticks(), DateTimeKind::Unspecified).ok_or(OUT_OF_RANGE)?;
49 Ok(DateTimeOffset { utc, offset_minutes })
50 }
51
52 #[track_caller]
56 pub fn new(year: i32, month: i32, day: i32, hour: i32, minute: i32, second: i32, offset: TimeSpan) -> DateTimeOffset {
57 let clock = DateTime::new_with_time(year, month, day, hour, minute, second);
58 Self::from_clock(clock, offset).unwrap_or_else(|message| panic!("{message}"))
59 }
60
61 pub fn try_new(
63 year: i32,
64 month: i32,
65 day: i32,
66 hour: i32,
67 minute: i32,
68 second: i32,
69 offset: TimeSpan,
70 ) -> Option<DateTimeOffset> {
71 let clock = DateTime::try_new_with_time(year, month, day, hour, minute, second)?;
72 Self::from_clock(clock, offset).ok()
73 }
74
75 #[track_caller]
78 pub fn from_ticks(ticks: i64, offset: TimeSpan) -> DateTimeOffset {
79 Self::from_clock(DateTime::from_ticks(ticks), offset).unwrap_or_else(|message| panic!("{message}"))
80 }
81
82 #[track_caller]
87 pub fn from_date_time(date_time: DateTime) -> DateTimeOffset {
88 let offset = if date_time.kind() == DateTimeKind::Utc {
89 TimeSpan::ZERO
90 } else {
91 TimeZoneInfo::get_utc_offset_of_local_time(date_time)
92 };
93 Self::from_clock(date_time, offset).unwrap_or_else(|message| panic!("{message}"))
94 }
95
96 #[track_caller]
102 pub fn from_date_time_offset(date_time: DateTime, offset: TimeSpan) -> DateTimeOffset {
103 match date_time.kind() {
104 DateTimeKind::Utc => assert!(offset == TimeSpan::ZERO, "The UTC Offset for Utc DateTime instances must be 0. (Parameter 'offset')"),
105 DateTimeKind::Local => assert!(
106 offset == TimeZoneInfo::get_utc_offset_of_local_time(date_time),
107 "The UTC Offset of the local dateTime parameter does not match the offset argument. (Parameter 'offset')"
108 ),
109 DateTimeKind::Unspecified => {}
110 }
111 Self::from_clock(date_time, offset).unwrap_or_else(|message| panic!("{message}"))
112 }
113
114 pub fn now() -> DateTimeOffset {
116 Self::utc_now().to_local_time()
117 }
118
119 pub fn utc_now() -> DateTimeOffset {
121 DateTimeOffset { utc: DateTime::utc_now().specify_kind(DateTimeKind::Unspecified), offset_minutes: 0 }
122 }
123
124 #[inline]
128 pub const fn offset(self) -> TimeSpan {
129 TimeSpan::from_ticks(self.offset_minutes as i64 * TimeSpan::TICKS_PER_MINUTE)
130 }
131
132 #[inline]
134 pub const fn date_time(self) -> DateTime {
135 DateTime::from_ticks(self.utc.ticks() + self.offset().ticks())
137 }
138
139 #[inline]
141 pub const fn utc_date_time(self) -> DateTime {
142 self.utc.specify_kind(DateTimeKind::Utc)
143 }
144
145 pub fn local_date_time(self) -> DateTime {
147 self.utc_date_time().to_local_time()
148 }
149
150 #[inline]
152 pub const fn date(self) -> DateTime {
153 self.date_time().date()
154 }
155
156 #[inline]
158 pub const fn time_of_day(self) -> TimeSpan {
159 self.date_time().time_of_day()
160 }
161
162 #[inline]
164 pub const fn ticks(self) -> i64 {
165 self.date_time().ticks()
166 }
167
168 #[inline]
170 pub const fn utc_ticks(self) -> i64 {
171 self.utc.ticks()
172 }
173
174 pub fn year(self) -> i32 {
176 self.date_time().year()
177 }
178
179 pub fn month(self) -> i32 {
181 self.date_time().month()
182 }
183
184 pub fn day(self) -> i32 {
186 self.date_time().day()
187 }
188
189 pub fn day_of_year(self) -> i32 {
191 self.date_time().day_of_year()
192 }
193
194 pub fn day_of_week(self) -> DayOfWeek {
196 self.date_time().day_of_week()
197 }
198
199 pub const fn hour(self) -> i32 {
201 self.date_time().hour()
202 }
203
204 pub const fn minute(self) -> i32 {
206 self.date_time().minute()
207 }
208
209 pub const fn second(self) -> i32 {
211 self.date_time().second()
212 }
213
214 pub const fn millisecond(self) -> i32 {
216 self.date_time().millisecond()
217 }
218
219 #[track_caller]
222 fn with_clock(self, clock: DateTime) -> DateTimeOffset {
223 Self::from_clock(clock, self.offset()).unwrap_or_else(|message| panic!("{message}"))
224 }
225
226 #[track_caller]
228 pub fn add(self, value: TimeSpan) -> DateTimeOffset {
229 self.with_clock(self.date_time().add(value))
230 }
231
232 #[track_caller]
234 pub fn subtract(self, value: TimeSpan) -> DateTimeOffset {
235 self.with_clock(self.date_time().subtract(value))
236 }
237
238 #[inline]
240 pub fn subtract_date_time_offset(self, other: DateTimeOffset) -> TimeSpan {
241 self.utc - other.utc
242 }
243
244 #[track_caller]
246 pub fn add_days(self, value: f64) -> DateTimeOffset {
247 self.with_clock(self.date_time().add_days(value))
248 }
249
250 #[track_caller]
252 pub fn add_months(self, months: i32) -> DateTimeOffset {
253 self.with_clock(self.date_time().add_months(months))
254 }
255
256 #[track_caller]
258 pub fn add_years(self, years: i32) -> DateTimeOffset {
259 self.with_clock(self.date_time().add_years(years))
260 }
261
262 #[track_caller]
267 pub fn to_offset(self, offset: TimeSpan) -> DateTimeOffset {
268 let offset_minutes = Self::validate_offset(offset).unwrap_or_else(|message| panic!("{message}"));
269 assert!(DateTime::try_from_ticks(self.utc.ticks() + offset.ticks(), DateTimeKind::Unspecified).is_some(), "{OUT_OF_RANGE}");
270 DateTimeOffset { utc: self.utc, offset_minutes }
271 }
272
273 pub fn to_local_time(self) -> DateTimeOffset {
276 let offset = TimeZoneInfo::get_local_utc_offset(self.utc_date_time());
277 match DateTime::try_from_ticks(self.utc.ticks() + offset.ticks(), DateTimeKind::Unspecified) {
278 Some(_) => DateTimeOffset { utc: self.utc, offset_minutes: (offset.ticks() / TimeSpan::TICKS_PER_MINUTE) as i16 },
279 None => DateTimeOffset { utc: self.utc, offset_minutes: 0 },
280 }
281 }
282
283 #[inline]
285 pub const fn to_universal_time(self) -> DateTimeOffset {
286 DateTimeOffset { utc: self.utc, offset_minutes: 0 }
287 }
288
289 pub fn compare(first: DateTimeOffset, second: DateTimeOffset) -> i32 {
293 DateTime::compare(first.utc, second.utc)
294 }
295
296 pub fn compare_to(self, other: DateTimeOffset) -> i32 {
298 Self::compare(self, other)
299 }
300
301 pub fn equals_exact(self, other: DateTimeOffset) -> bool {
303 self.utc == other.utc && self.offset_minutes == other.offset_minutes
304 }
305
306 #[track_caller]
313 pub fn to_string_format(self, format: &str, provider: &(impl DateTimeFormatProvider + ?Sized)) -> String {
314 match self.try_to_string_format(format, provider) {
315 Ok(text) => text,
316 Err(error) => panic!("{error}"),
317 }
318 }
319
320 pub fn try_to_string_format(
323 self,
324 format: &str,
325 provider: &(impl DateTimeFormatProvider + ?Sized),
326 ) -> Result<String, FormatError> {
327 date_time_format::format(self.date_time(), Some(self.offset()), format, &provider.get_date_time_format())
328 }
329
330 #[track_caller]
333 pub fn to_string_with(self, format: &str) -> String {
334 self.to_string_format(format, &CultureInfo::current_culture())
335 }
336
337 pub fn to_string_provider(self, provider: &(impl DateTimeFormatProvider + ?Sized)) -> String {
340 self.to_string_format("", provider)
341 }
342}
343
344impl std::str::FromStr for DateTimeOffset {
345 type Err = FormatError;
346
347 fn from_str(text: &str) -> Result<Self, Self::Err> {
349 DateTimeOffset::parse(text, &CultureInfo::invariant_culture())
350 }
351}
352
353impl DateTimeOffset {
354 pub fn parse(text: &str, provider: &(impl DateTimeFormatProvider + ?Sized)) -> Result<DateTimeOffset, FormatError> {
373 let parsed =
374 date_time_format::parse(text, &provider.get_date_time_format(), super::DateTimeStyles::NONE)?;
375 let clock = parsed.date_time.specify_kind(DateTimeKind::Unspecified);
376 let offset = match parsed.offset {
377 Some(offset) => offset,
378 None => TimeZoneInfo::get_utc_offset_of_local_time(clock),
379 };
380 if Self::validate_offset(offset).is_err() {
381 return Err(FormatError::from_string(format!(
382 "The time zone offset of string '{text}' must be within plus or minus 14 hours."
383 )));
384 }
385 Self::from_clock(clock, offset).map_err(|_| {
386 FormatError::from_string(format!(
387 "The UTC representation of the date '{text}' falls outside the year range 1-9999."
388 ))
389 })
390 }
391
392 pub fn try_parse(text: &str, provider: &(impl DateTimeFormatProvider + ?Sized)) -> Option<DateTimeOffset> {
395 Self::parse(text, provider).ok()
396 }
397}
398
399impl Default for DateTimeOffset {
400 fn default() -> Self {
402 DateTimeOffset::MIN_VALUE
403 }
404}
405
406impl From<DateTime> for DateTimeOffset {
407 #[track_caller]
409 fn from(value: DateTime) -> Self {
410 DateTimeOffset::from_date_time(value)
411 }
412}
413
414impl PartialEq for DateTimeOffset {
415 #[inline]
416 fn eq(&self, other: &Self) -> bool {
417 self.utc == other.utc
418 }
419}
420
421impl Eq for DateTimeOffset {}
422
423impl PartialOrd for DateTimeOffset {
424 #[inline]
425 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
426 Some(self.cmp(other))
427 }
428}
429
430impl Ord for DateTimeOffset {
431 #[inline]
432 fn cmp(&self, other: &Self) -> Ordering {
433 self.utc.cmp(&other.utc)
434 }
435}
436
437impl Hash for DateTimeOffset {
438 fn hash<H: Hasher>(&self, state: &mut H) {
439 self.utc.hash(state);
440 }
441}
442
443impl Add<TimeSpan> for DateTimeOffset {
444 type Output = DateTimeOffset;
445 #[track_caller]
446 fn add(self, rhs: TimeSpan) -> DateTimeOffset {
447 DateTimeOffset::add(self, rhs)
448 }
449}
450
451impl Sub<TimeSpan> for DateTimeOffset {
452 type Output = DateTimeOffset;
453 #[track_caller]
454 fn sub(self, rhs: TimeSpan) -> DateTimeOffset {
455 self.subtract(rhs)
456 }
457}
458
459impl Sub<DateTimeOffset> for DateTimeOffset {
460 type Output = TimeSpan;
461 #[inline]
462 fn sub(self, rhs: DateTimeOffset) -> TimeSpan {
463 self.subtract_date_time_offset(rhs)
464 }
465}
466
467impl AddAssign<TimeSpan> for DateTimeOffset {
468 #[track_caller]
469 fn add_assign(&mut self, rhs: TimeSpan) {
470 *self = *self + rhs;
471 }
472}
473
474impl SubAssign<TimeSpan> for DateTimeOffset {
475 #[track_caller]
476 fn sub_assign(&mut self, rhs: TimeSpan) {
477 *self = *self - rhs;
478 }
479}
480
481impl fmt::Display for DateTimeOffset {
482 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 f.write_str(&self.to_string_format("", &CultureInfo::current_culture()))
485 }
486}
487
488impl fmt::Debug for DateTimeOffset {
489 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490 let clock = self.date_time();
491 let (year, month, day, _) = clock.date_parts();
492 let minutes = self.offset_minutes.unsigned_abs();
493 write!(
494 f,
495 "DateTimeOffset({year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{:07}{}{:02}:{:02})",
496 clock.hour(),
497 clock.minute(),
498 clock.second(),
499 clock.ticks() % TimeSpan::TICKS_PER_SECOND,
500 if self.offset_minutes < 0 { '-' } else { '+' },
501 minutes / 60,
502 minutes % 60
503 )
504 }
505}