use std::fmt;
use crate::{
ffi::{self, CXmpString},
XmpError, XmpResult,
};
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct XmpDateTime {
pub date: Option<XmpDate>,
pub time: Option<XmpTime>,
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct XmpDate {
pub year: i32,
pub month: i32,
pub day: i32,
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct XmpTime {
pub hour: i32,
pub minute: i32,
pub second: i32,
pub nanosecond: i32,
pub time_zone: Option<XmpTimeZone>,
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct XmpTimeZone {
pub hour: i32,
pub minute: i32,
}
impl XmpDateTime {
pub fn current() -> XmpResult<Self> {
let mut dt = ffi::CXmpDateTime::default();
let mut err = ffi::CXmpError::default();
unsafe { ffi::CXmpDateTimeCurrent(&mut dt, &mut err) };
XmpError::raise_from_c(&err)?;
Ok(Self::from_ffi(&dt))
}
pub fn set_local_time_zone(&mut self) -> XmpResult<()> {
let mut dt = self.as_ffi();
let mut err = ffi::CXmpError::default();
unsafe {
ffi::CXmpDateTimeSetTimeZone(&mut dt, &mut err);
}
XmpError::raise_from_c(&err)?;
self.update_from_ffi(&dt);
Ok(())
}
pub fn convert_to_local_time(&mut self) -> XmpResult<()> {
let mut dt = self.as_ffi();
let mut err = ffi::CXmpError::default();
unsafe {
ffi::CXmpDateTimeConvertToLocalTime(&mut dt, &mut err);
}
XmpError::raise_from_c(&err)?;
self.update_from_ffi(&dt);
Ok(())
}
pub fn convert_to_utc(&mut self) -> XmpResult<()> {
let mut dt = self.as_ffi();
let mut err = ffi::CXmpError::default();
unsafe {
ffi::CXmpDateTimeConvertToUTCTime(&mut dt, &mut err);
}
XmpError::raise_from_c(&err)?;
self.update_from_ffi(&dt);
Ok(())
}
pub(crate) fn from_ffi(dt: &ffi::CXmpDateTime) -> Self {
let mut result = Self::default();
result.update_from_ffi(dt);
result
}
pub(crate) fn update_from_ffi(&mut self, dt: &ffi::CXmpDateTime) {
self.date = if dt.has_date {
Some(XmpDate {
year: dt.year,
month: dt.month,
day: dt.day,
})
} else {
None
};
self.time = if dt.has_time {
Some(XmpTime {
hour: dt.hour,
minute: dt.minute,
second: dt.second,
nanosecond: dt.nanosecond,
time_zone: if dt.has_time_zone {
Some(XmpTimeZone {
hour: if dt.tz_sign < 0 {
-dt.tz_hour
} else {
dt.tz_hour
},
minute: dt.tz_minute,
})
} else {
None
},
})
} else {
None
};
}
pub(crate) fn as_ffi(&self) -> ffi::CXmpDateTime {
let mut result = ffi::CXmpDateTime::default();
if let Some(date) = &self.date {
result.has_date = true;
result.year = date.year;
result.month = date.month;
result.day = date.day;
}
if let Some(time) = &self.time {
result.has_time = true;
result.hour = time.hour;
result.minute = time.minute;
result.second = time.second;
result.nanosecond = time.nanosecond;
if let Some(tz) = &time.time_zone {
result.has_time_zone = true;
match tz.hour {
h if h < 0 => {
result.tz_sign = -1;
result.tz_hour = -h;
}
0 if tz.minute == 0 => {
result.tz_sign = 0;
result.tz_hour = 0;
}
h => {
result.tz_sign = 1;
result.tz_hour = h;
}
};
result.tz_minute = tz.minute;
}
}
result
}
}
impl fmt::Display for XmpDateTime {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut err = ffi::CXmpError::default();
unsafe {
match CXmpString::from_ptr(ffi::CXmpDateTimeToString(&self.as_ffi(), &mut err))
.map(|s| s)
{
Some(s) => {
write!(f, "{}", s)
}
None => {
let err = XmpError::raise_from_c(&err);
write!(f, "(unable to format date: {:#?})", err)
}
}
}
}
}
#[cfg(feature = "chrono")]
use chrono::{DateTime, Datelike, FixedOffset, LocalResult, NaiveDate, Timelike};
#[cfg(feature = "chrono")]
use thiserror::Error;
#[cfg(feature = "chrono")]
impl TryFrom<XmpDateTime> for DateTime<FixedOffset> {
type Error = DateTimeConvertError;
fn try_from(dt: XmpDateTime) -> Result<Self, Self::Error> {
Self::try_from(&dt)
}
}
#[cfg(feature = "chrono")]
impl TryFrom<&XmpDateTime> for DateTime<FixedOffset> {
type Error = DateTimeConvertError;
fn try_from(dt: &XmpDateTime) -> Result<Self, Self::Error> {
let date = dt.date.as_ref().ok_or(DateTimeConvertError::NoDate)?;
let time = dt.time.as_ref().ok_or(DateTimeConvertError::NoTime)?;
let tz = time
.time_zone
.as_ref()
.ok_or(DateTimeConvertError::NoTimeZone)?;
let offset = FixedOffset::east_opt(tz.hour * 3600 + tz.minute * 60)
.ok_or(DateTimeConvertError::InvalidTimeZone)?;
match NaiveDate::from_ymd_opt(date.year, date.month as u32, date.day as u32)
.ok_or(DateTimeConvertError::InvalidDate)?
.and_hms_nano_opt(
time.hour as u32,
time.minute as u32,
time.second as u32,
time.nanosecond as u32,
)
.ok_or(DateTimeConvertError::InvalidTime)?
.and_local_timezone(offset)
{
LocalResult::Single(t) => Ok(t),
_ => Err(DateTimeConvertError::InvalidTimeZone),
}
}
}
#[cfg(feature = "chrono")]
#[derive(Debug, Eq, Error, PartialEq)]
pub enum DateTimeConvertError {
#[error("the date value is None")]
NoDate,
#[error("the time value is None")]
NoTime,
#[error("the time.time_zone value is None")]
NoTimeZone,
#[error("the date value is out of bounds")]
InvalidDate,
#[error("the time value is out of bounds")]
InvalidTime,
#[error("the time.time_zone value is out of bounds")]
InvalidTimeZone,
}
#[cfg(feature = "chrono")]
impl From<DateTime<FixedOffset>> for XmpDateTime {
fn from(dt: DateTime<FixedOffset>) -> Self {
Self::from(&dt)
}
}
#[cfg(feature = "chrono")]
impl From<&DateTime<FixedOffset>> for XmpDateTime {
fn from(dt: &DateTime<FixedOffset>) -> Self {
let nd = dt.date_naive();
let date = XmpDate {
year: nd.year(),
month: nd.month() as i32,
day: nd.day() as i32,
};
let tz = dt.timezone().local_minus_utc();
let tz = XmpTimeZone {
hour: tz / 3600,
minute: (tz.abs() % 3600) / 60,
};
let nt = dt.time();
let time = XmpTime {
hour: nt.hour() as i32,
minute: nt.minute() as i32,
second: nt.second() as i32,
nanosecond: nt.nanosecond() as i32,
time_zone: Some(tz),
};
Self {
date: Some(date),
time: Some(time),
}
}
}