use crate::result::*;
use crate::*;
use self::Mantissa::*;
use self::TSOffsetKind::*;
use self::TSPrecision::*;
use bigdecimal::{BigDecimal, ToPrimitive};
use chrono::{DateTime, FixedOffset, Timelike};
pub(crate) const TS_MAX_MANTISSA_DIGITS: i64 = 9;
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
pub enum Mantissa {
Digits(u32),
Fraction(BigDecimal),
}
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
pub enum TSPrecision {
Year,
Month,
Day,
Minute,
Second,
Fractional(Mantissa),
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum TSOffsetKind {
KnownOffset,
UnknownOffset,
}
#[derive(Debug, Clone, PartialEq)]
pub struct IonDateTime {
date_time: DateTime<FixedOffset>,
precision: TSPrecision,
offset_kind: TSOffsetKind,
}
impl IonDateTime {
#[inline]
pub(crate) fn new(
date_time: DateTime<FixedOffset>,
precision: TSPrecision,
offset_kind: TSOffsetKind,
) -> Self {
Self {
date_time,
precision,
offset_kind,
}
}
#[inline]
pub fn try_new(
date_time: DateTime<FixedOffset>,
precision: TSPrecision,
offset_kind: TSOffsetKind,
) -> IonCResult<Self> {
match offset_kind {
KnownOffset => {
if precision <= Day {
return Err(IonCError::with_additional(
ion_error_code_IERR_INVALID_TIMESTAMP,
"Day precision or less must not have KnownOffset",
));
}
}
UnknownOffset => {
if date_time.offset().utc_minus_local() != 0 {
return Err(IonCError::with_additional(
ion_error_code_IERR_INVALID_TIMESTAMP,
"Mismatched offset with UnknownOffset",
));
}
}
};
match &precision {
Fractional(mantissa) => match mantissa {
Digits(digits) => {
if (*digits as i64) > TS_MAX_MANTISSA_DIGITS {
return Err(IonCError::with_additional(
ion_error_code_IERR_INVALID_TIMESTAMP,
"Invalid digits in precision",
));
}
}
Fraction(frac) => {
if frac < &BigDecimal::zero() || frac >= &BigDecimal::from(1) {
return Err(IonCError::with_additional(
ion_error_code_IERR_INVALID_TIMESTAMP,
"Mantissa outside of range",
));
}
let (_, scale) = frac.as_bigint_and_exponent();
if scale <= TS_MAX_MANTISSA_DIGITS {
return Err(IonCError::with_additional(
ion_error_code_IERR_INVALID_TIMESTAMP,
"Fractional mantissa not allowed for sub-nanosecond precision"
));
}
let ns = date_time.nanosecond();
let frac_ns = (frac * BigDecimal::from(NS_IN_SEC)).abs().to_u32().ok_or(
IonCError::with_additional(
ion_error_code_IERR_INVALID_TIMESTAMP,
"Invalid mantissa in precision",
),
)?;
if ns != frac_ns {
return Err(IonCError::with_additional(
ion_error_code_IERR_INVALID_TIMESTAMP,
"Fractional mantissa inconsistent in precision",
));
}
}
},
_ => {}
};
Ok(Self::new(date_time, precision, offset_kind))
}
#[inline]
pub fn as_datetime(&self) -> &DateTime<FixedOffset> {
&(self.date_time)
}
#[inline]
pub fn precision(&self) -> &TSPrecision {
&(self.precision)
}
#[inline]
pub fn offset_kind(&self) -> TSOffsetKind {
self.offset_kind
}
#[inline]
pub fn into_datetime(self) -> DateTime<FixedOffset> {
self.date_time
}
}
#[cfg(test)]
mod test_iondt {
use super::*;
use rstest::rstest;
fn frac(lit: &str) -> Mantissa {
Fraction(BigDecimal::parse_bytes(lit.as_bytes(), 10).unwrap())
}
#[rstest(
dt_lit,
precision,
offset_kind,
error,
case::year("2020-01-01T00:01:00.1234567Z", Year, UnknownOffset, None),
case::month("2020-01-01T00:01:00.1234567Z", Month, UnknownOffset, None),
case::day("2020-01-01T00:01:00.1234567Z", Day, UnknownOffset, None),
case::year_bad_known_offset(
"2020-01-01T00:01:00.1234567Z",
Year,
KnownOffset,
Some(ion_error_code_IERR_INVALID_TIMESTAMP),
),
case::month_bad_known_offset(
"2020-01-01T00:01:00.1234567Z",
Month,
KnownOffset,
Some(ion_error_code_IERR_INVALID_TIMESTAMP),
),
case::day_bad_known_offset(
"2020-01-01T00:01:00.1234567Z",
Day,
KnownOffset,
Some(ion_error_code_IERR_INVALID_TIMESTAMP),
),
case::minute("2020-01-01T00:01:00.1234567Z", Minute, KnownOffset, None),
case::second("2020-01-01T00:01:00.1234567Z", Second, KnownOffset, None),
case::second_unknown_offset("2020-01-01T00:01:00.1234567Z", Second, UnknownOffset, None),
case::second_bad_unknown_offset(
"2020-01-01T00:01:00.1234567-00:15",
Second,
UnknownOffset,
Some(ion_error_code_IERR_INVALID_TIMESTAMP),
),
case::fractional_digits(
"2020-01-01T00:01:00.1234567Z",
Fractional(Digits(3)),
KnownOffset,
None,
),
case::fractional_digits_too_big(
"2020-01-01T00:01:00.1234567Z",
Fractional(Digits(10)),
KnownOffset,
Some(ion_error_code_IERR_INVALID_TIMESTAMP),
),
case::fractional_mantissa_neg(
"2020-01-01T00:01:00.1234567Z",
Fractional(frac("-0.1234567")),
KnownOffset,
Some(ion_error_code_IERR_INVALID_TIMESTAMP),
),
case::fractional_mantissa_not_fractional(
"2020-01-01T00:01:00.1234567Z",
Fractional(frac("1.234567")),
KnownOffset,
Some(ion_error_code_IERR_INVALID_TIMESTAMP),
),
case::fractional_mantissa_too_small(
"2020-01-01T00:01:00.1234567Z",
Fractional(frac("0.1234567")),
KnownOffset,
Some(ion_error_code_IERR_INVALID_TIMESTAMP),
),
case::fractional_mantissa_more_precision(
"2020-01-01T00:01:00.1234567Z",
Fractional(frac("0.1234567001234567")),
KnownOffset,
None,
),
case::fractional_mantissa_mismatch_digits(
"2020-01-01T00:01:00.1234567Z",
Fractional(frac("0.123456789")),
KnownOffset,
Some(ion_error_code_IERR_INVALID_TIMESTAMP),
)
)]
fn try_new_precision(
dt_lit: &str,
precision: TSPrecision,
offset_kind: TSOffsetKind,
error: Option<i32>,
) -> IonCResult<()> {
let dt = DateTime::parse_from_rfc3339(dt_lit).unwrap();
let res = IonDateTime::try_new(dt, precision, offset_kind);
match res {
Ok(_) => {
assert_eq!(None, error);
}
Err(actual) => {
if let Some(expected_code) = error {
assert_eq!(expected_code, actual.code, "Testing expected error codes");
} else {
assert!(false, "Expected no error, but got: {:?}", actual);
}
}
}
Ok(())
}
}