use super::date_time_format;
use super::{CultureInfo, DateTime, DateTimeFormatProvider, DateTimeKind, DayOfWeek, FormatError, TimeZoneInfo};
use crate::animation::TimeSpan;
use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign, Sub, SubAssign};
const OUT_OF_RANGE: &str = "The UTC time represented when the offset is applied must be between year 0 and 10,000.";
#[derive(Clone, Copy)]
pub struct DateTimeOffset {
utc: DateTime,
offset_minutes: i16,
}
impl DateTimeOffset {
pub const MIN_VALUE: DateTimeOffset = DateTimeOffset { utc: DateTime::MIN_VALUE, offset_minutes: 0 };
pub const MAX_VALUE: DateTimeOffset = DateTimeOffset { utc: DateTime::MAX_VALUE, offset_minutes: 0 };
pub const UNIX_EPOCH: DateTimeOffset =
DateTimeOffset { utc: DateTime::UNIX_EPOCH.specify_kind(DateTimeKind::Unspecified), offset_minutes: 0 };
fn validate_offset(offset: TimeSpan) -> Result<i16, &'static str> {
let ticks = offset.ticks();
if ticks % TimeSpan::TICKS_PER_MINUTE != 0 {
return Err("Offset must be specified in whole minutes. (Parameter 'offset')");
}
if ticks.abs() > TimeZoneInfo::MAX_OFFSET.ticks() {
return Err("Offset must be within plus or minus 14 hours. (Parameter 'offset')");
}
Ok((ticks / TimeSpan::TICKS_PER_MINUTE) as i16)
}
fn from_clock(clock: DateTime, offset: TimeSpan) -> Result<DateTimeOffset, &'static str> {
let offset_minutes = Self::validate_offset(offset)?;
let utc = DateTime::try_from_ticks(clock.ticks() - offset.ticks(), DateTimeKind::Unspecified).ok_or(OUT_OF_RANGE)?;
Ok(DateTimeOffset { utc, offset_minutes })
}
#[track_caller]
pub fn new(year: i32, month: i32, day: i32, hour: i32, minute: i32, second: i32, offset: TimeSpan) -> DateTimeOffset {
let clock = DateTime::new_with_time(year, month, day, hour, minute, second);
Self::from_clock(clock, offset).unwrap_or_else(|message| panic!("{message}"))
}
pub fn try_new(
year: i32,
month: i32,
day: i32,
hour: i32,
minute: i32,
second: i32,
offset: TimeSpan,
) -> Option<DateTimeOffset> {
let clock = DateTime::try_new_with_time(year, month, day, hour, minute, second)?;
Self::from_clock(clock, offset).ok()
}
#[track_caller]
pub fn from_ticks(ticks: i64, offset: TimeSpan) -> DateTimeOffset {
Self::from_clock(DateTime::from_ticks(ticks), offset).unwrap_or_else(|message| panic!("{message}"))
}
#[track_caller]
pub fn from_date_time(date_time: DateTime) -> DateTimeOffset {
let offset = if date_time.kind() == DateTimeKind::Utc {
TimeSpan::ZERO
} else {
TimeZoneInfo::get_utc_offset_of_local_time(date_time)
};
Self::from_clock(date_time, offset).unwrap_or_else(|message| panic!("{message}"))
}
#[track_caller]
pub fn from_date_time_offset(date_time: DateTime, offset: TimeSpan) -> DateTimeOffset {
match date_time.kind() {
DateTimeKind::Utc => assert!(offset == TimeSpan::ZERO, "The UTC Offset for Utc DateTime instances must be 0. (Parameter 'offset')"),
DateTimeKind::Local => assert!(
offset == TimeZoneInfo::get_utc_offset_of_local_time(date_time),
"The UTC Offset of the local dateTime parameter does not match the offset argument. (Parameter 'offset')"
),
DateTimeKind::Unspecified => {}
}
Self::from_clock(date_time, offset).unwrap_or_else(|message| panic!("{message}"))
}
pub fn now() -> DateTimeOffset {
Self::utc_now().to_local_time()
}
pub fn utc_now() -> DateTimeOffset {
DateTimeOffset { utc: DateTime::utc_now().specify_kind(DateTimeKind::Unspecified), offset_minutes: 0 }
}
#[inline]
pub const fn offset(self) -> TimeSpan {
TimeSpan::from_ticks(self.offset_minutes as i64 * TimeSpan::TICKS_PER_MINUTE)
}
#[inline]
pub const fn date_time(self) -> DateTime {
DateTime::from_ticks(self.utc.ticks() + self.offset().ticks())
}
#[inline]
pub const fn utc_date_time(self) -> DateTime {
self.utc.specify_kind(DateTimeKind::Utc)
}
pub fn local_date_time(self) -> DateTime {
self.utc_date_time().to_local_time()
}
#[inline]
pub const fn date(self) -> DateTime {
self.date_time().date()
}
#[inline]
pub const fn time_of_day(self) -> TimeSpan {
self.date_time().time_of_day()
}
#[inline]
pub const fn ticks(self) -> i64 {
self.date_time().ticks()
}
#[inline]
pub const fn utc_ticks(self) -> i64 {
self.utc.ticks()
}
pub fn year(self) -> i32 {
self.date_time().year()
}
pub fn month(self) -> i32 {
self.date_time().month()
}
pub fn day(self) -> i32 {
self.date_time().day()
}
pub fn day_of_year(self) -> i32 {
self.date_time().day_of_year()
}
pub fn day_of_week(self) -> DayOfWeek {
self.date_time().day_of_week()
}
pub const fn hour(self) -> i32 {
self.date_time().hour()
}
pub const fn minute(self) -> i32 {
self.date_time().minute()
}
pub const fn second(self) -> i32 {
self.date_time().second()
}
pub const fn millisecond(self) -> i32 {
self.date_time().millisecond()
}
#[track_caller]
fn with_clock(self, clock: DateTime) -> DateTimeOffset {
Self::from_clock(clock, self.offset()).unwrap_or_else(|message| panic!("{message}"))
}
#[track_caller]
pub fn add(self, value: TimeSpan) -> DateTimeOffset {
self.with_clock(self.date_time().add(value))
}
#[track_caller]
pub fn subtract(self, value: TimeSpan) -> DateTimeOffset {
self.with_clock(self.date_time().subtract(value))
}
#[inline]
pub fn subtract_date_time_offset(self, other: DateTimeOffset) -> TimeSpan {
self.utc - other.utc
}
#[track_caller]
pub fn add_days(self, value: f64) -> DateTimeOffset {
self.with_clock(self.date_time().add_days(value))
}
#[track_caller]
pub fn add_months(self, months: i32) -> DateTimeOffset {
self.with_clock(self.date_time().add_months(months))
}
#[track_caller]
pub fn add_years(self, years: i32) -> DateTimeOffset {
self.with_clock(self.date_time().add_years(years))
}
#[track_caller]
pub fn to_offset(self, offset: TimeSpan) -> DateTimeOffset {
let offset_minutes = Self::validate_offset(offset).unwrap_or_else(|message| panic!("{message}"));
assert!(DateTime::try_from_ticks(self.utc.ticks() + offset.ticks(), DateTimeKind::Unspecified).is_some(), "{OUT_OF_RANGE}");
DateTimeOffset { utc: self.utc, offset_minutes }
}
pub fn to_local_time(self) -> DateTimeOffset {
let offset = TimeZoneInfo::get_local_utc_offset(self.utc_date_time());
match DateTime::try_from_ticks(self.utc.ticks() + offset.ticks(), DateTimeKind::Unspecified) {
Some(_) => DateTimeOffset { utc: self.utc, offset_minutes: (offset.ticks() / TimeSpan::TICKS_PER_MINUTE) as i16 },
None => DateTimeOffset { utc: self.utc, offset_minutes: 0 },
}
}
#[inline]
pub const fn to_universal_time(self) -> DateTimeOffset {
DateTimeOffset { utc: self.utc, offset_minutes: 0 }
}
pub fn compare(first: DateTimeOffset, second: DateTimeOffset) -> i32 {
DateTime::compare(first.utc, second.utc)
}
pub fn compare_to(self, other: DateTimeOffset) -> i32 {
Self::compare(self, other)
}
pub fn equals_exact(self, other: DateTimeOffset) -> bool {
self.utc == other.utc && self.offset_minutes == other.offset_minutes
}
#[track_caller]
pub fn to_string_format(self, format: &str, provider: &(impl DateTimeFormatProvider + ?Sized)) -> String {
match self.try_to_string_format(format, provider) {
Ok(text) => text,
Err(error) => panic!("{error}"),
}
}
pub fn try_to_string_format(
self,
format: &str,
provider: &(impl DateTimeFormatProvider + ?Sized),
) -> Result<String, FormatError> {
date_time_format::format(self.date_time(), Some(self.offset()), format, &provider.get_date_time_format())
}
#[track_caller]
pub fn to_string_with(self, format: &str) -> String {
self.to_string_format(format, &CultureInfo::current_culture())
}
pub fn to_string_provider(self, provider: &(impl DateTimeFormatProvider + ?Sized)) -> String {
self.to_string_format("", provider)
}
}
impl std::str::FromStr for DateTimeOffset {
type Err = FormatError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
DateTimeOffset::parse(text, &CultureInfo::invariant_culture())
}
}
impl DateTimeOffset {
pub fn parse(text: &str, provider: &(impl DateTimeFormatProvider + ?Sized)) -> Result<DateTimeOffset, FormatError> {
let parsed =
date_time_format::parse(text, &provider.get_date_time_format(), super::DateTimeStyles::NONE)?;
let clock = parsed.date_time.specify_kind(DateTimeKind::Unspecified);
let offset = match parsed.offset {
Some(offset) => offset,
None => TimeZoneInfo::get_utc_offset_of_local_time(clock),
};
if Self::validate_offset(offset).is_err() {
return Err(FormatError::from_string(format!(
"The time zone offset of string '{text}' must be within plus or minus 14 hours."
)));
}
Self::from_clock(clock, offset).map_err(|_| {
FormatError::from_string(format!(
"The UTC representation of the date '{text}' falls outside the year range 1-9999."
))
})
}
pub fn try_parse(text: &str, provider: &(impl DateTimeFormatProvider + ?Sized)) -> Option<DateTimeOffset> {
Self::parse(text, provider).ok()
}
}
impl Default for DateTimeOffset {
fn default() -> Self {
DateTimeOffset::MIN_VALUE
}
}
impl From<DateTime> for DateTimeOffset {
#[track_caller]
fn from(value: DateTime) -> Self {
DateTimeOffset::from_date_time(value)
}
}
impl PartialEq for DateTimeOffset {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.utc == other.utc
}
}
impl Eq for DateTimeOffset {}
impl PartialOrd for DateTimeOffset {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DateTimeOffset {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
self.utc.cmp(&other.utc)
}
}
impl Hash for DateTimeOffset {
fn hash<H: Hasher>(&self, state: &mut H) {
self.utc.hash(state);
}
}
impl Add<TimeSpan> for DateTimeOffset {
type Output = DateTimeOffset;
#[track_caller]
fn add(self, rhs: TimeSpan) -> DateTimeOffset {
DateTimeOffset::add(self, rhs)
}
}
impl Sub<TimeSpan> for DateTimeOffset {
type Output = DateTimeOffset;
#[track_caller]
fn sub(self, rhs: TimeSpan) -> DateTimeOffset {
self.subtract(rhs)
}
}
impl Sub<DateTimeOffset> for DateTimeOffset {
type Output = TimeSpan;
#[inline]
fn sub(self, rhs: DateTimeOffset) -> TimeSpan {
self.subtract_date_time_offset(rhs)
}
}
impl AddAssign<TimeSpan> for DateTimeOffset {
#[track_caller]
fn add_assign(&mut self, rhs: TimeSpan) {
*self = *self + rhs;
}
}
impl SubAssign<TimeSpan> for DateTimeOffset {
#[track_caller]
fn sub_assign(&mut self, rhs: TimeSpan) {
*self = *self - rhs;
}
}
impl fmt::Display for DateTimeOffset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_string_format("", &CultureInfo::current_culture()))
}
}
impl fmt::Debug for DateTimeOffset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let clock = self.date_time();
let (year, month, day, _) = clock.date_parts();
let minutes = self.offset_minutes.unsigned_abs();
write!(
f,
"DateTimeOffset({year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{:07}{}{:02}:{:02})",
clock.hour(),
clock.minute(),
clock.second(),
clock.ticks() % TimeSpan::TICKS_PER_SECOND,
if self.offset_minutes < 0 { '-' } else { '+' },
minutes / 60,
minutes % 60
)
}
}