ion_c_sys/
timestamp.rs

1// Copyright Amazon.com, Inc. or its affiliates.
2
3//! Provides integration between `ION_TIMESTAMP` and `chrono::DateTime`.
4//!
5//! Specifically, models the Ion notion of a Timestamp, with [`IonDateTime`](./struct.IonDateTime.html)
6//! Which combines a `DateTime` with the concept of [*precision*](./enum.TSPrecision.html) and
7//! [**known** versus **unknown** *offsets*](./enum.TSOffsetKind.html).
8
9use crate::result::*;
10use crate::*;
11
12use self::Mantissa::*;
13use self::TSOffsetKind::*;
14use self::TSPrecision::*;
15
16use bigdecimal::{BigDecimal, ToPrimitive};
17use chrono::{DateTime, FixedOffset, Timelike};
18
19pub(crate) const TS_MAX_MANTISSA_DIGITS: i64 = 9;
20
21/// The fractional part of a `Fractional` [`TSPrecision`](./enum.TSPrecision.html).
22#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
23pub enum Mantissa {
24    /// A kind of precision that uses digits from the nanoseconds field of the associated
25    /// `DateTime` to represent the amount of mantissa.
26    ///
27    /// This is required for precision of nanoseconds or lower.
28    Digits(u32),
29    /// Specifies the mantissa precisely as a `BigDecimal` in the range `>= 0` and `< 1`.
30    /// This should correspond to the nanoseconds field insofar as it is not truncated.
31    ///
32    /// This is only used for precision of greater than nanoseconds.
33    Fraction(BigDecimal),
34}
35
36/// Precision of an [`IonDateTime`](./struct.IonDateTime.html).
37///
38/// All Ion timestamps are complete points in time, but they have explicit precision
39/// that is either at the date components, the minute, or second (including sub-second)
40/// granularity.
41#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
42pub enum TSPrecision {
43    /// Year-level precision (e.g. `2020T`)
44    Year,
45    /// Month-level precision (e.g. `2020-08T`)
46    Month,
47    /// Day-level precision (e.g. `2020-08-01T`)
48    Day,
49    /// Minute-level precision (e.g. `2020-08-01T12:34Z`)
50    Minute,
51    /// Second-level precision. (e.g. `2020-08-01T12:34:56Z`)
52    Second,
53    /// Sub-second precision (e.g. `2020-08-01T12:34:56.123456789Z`)
54    Fractional(Mantissa),
55}
56
57/// The kind of offset associated with a [`IonDateTime`](./struct.IonDateTime.html).
58///
59/// This is generally some specific `FixedOffset` associated with the `DateTime`,
60/// but in the case of a timestamp with an *unknown UTC offset*, this will be `Unknown`,
61/// and the effective `FixedOffset` will be UTC+00:00--this allows an application to
62/// preserve the difference between UTC+00:00 (zulu time) and UTC-00:00 which is the unknown offset.
63#[derive(Debug, Copy, Clone, PartialEq, Eq)]
64pub enum TSOffsetKind {
65    KnownOffset,
66    UnknownOffset,
67}
68
69/// Higher-level wrapper over `DateTime` preserving `ION_TIMESTAMP` properties
70/// that `DateTime` does not preserve on its own.
71///
72/// Specifically, this adds the [*precision*](./enum.TSPrecision.html) of the timestamp and
73/// its associated [*kind of offset*](./enum.TSOffsetKind.html).
74///
75/// ## Usage
76/// Generally, users will create their own `DateTime<FixedOffset>`
77/// and construct an `IonDateTime` indicating the precision as follows:
78/// ```
79/// # use ion_c_sys::timestamp::*;
80/// # use ion_c_sys::timestamp::TSPrecision::*;
81/// # use ion_c_sys::timestamp::TSOffsetKind::*;
82/// # use ion_c_sys::timestamp::Mantissa::*;
83/// # use ion_c_sys::result::*;
84/// # use chrono::*;
85/// # fn main() -> IonCResult<()> {
86/// // construct a DateTime with milliseconds of fractional seconds
87/// use ion_c_sys::timestamp::Mantissa::Digits;
88/// let dt = DateTime::parse_from_rfc3339("2020-02-27T04:15:00.123Z").unwrap();
89/// // move that into an IonDateTime with the explicit milliseconds of precision
90/// let ion_dt = IonDateTime::try_new(dt, Fractional(Digits(3)), KnownOffset)?;
91/// # Ok(())
92/// # }
93/// ```
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct IonDateTime {
96    date_time: DateTime<FixedOffset>,
97    precision: TSPrecision,
98    offset_kind: TSOffsetKind,
99}
100
101impl IonDateTime {
102    /// Constructs a new `IonDateTime` directly without validating the `Fractional` precision.
103    #[inline]
104    pub(crate) fn new(
105        date_time: DateTime<FixedOffset>,
106        precision: TSPrecision,
107        offset_kind: TSOffsetKind,
108    ) -> Self {
109        Self {
110            date_time,
111            precision,
112            offset_kind,
113        }
114    }
115
116    /// Constructs a new `IonDateTime` from its constituent components.
117    ///
118    /// Note that the `Fractional` precision must match the nanoseconds field of this
119    /// will fail.  Also note that the `TSOffsetKind` must be `Unknown` for precision less than
120    /// a `Minute` and must correspond to UTC+00:00 in the `DateTime`.
121    #[inline]
122    pub fn try_new(
123        date_time: DateTime<FixedOffset>,
124        precision: TSPrecision,
125        offset_kind: TSOffsetKind,
126    ) -> IonCResult<Self> {
127        match offset_kind {
128            KnownOffset => {
129                if precision <= Day {
130                    return Err(IonCError::with_additional(
131                        ion_error_code_IERR_INVALID_TIMESTAMP,
132                        "Day precision or less must not have KnownOffset",
133                    ));
134                }
135            }
136            UnknownOffset => {
137                if date_time.offset().utc_minus_local() != 0 {
138                    return Err(IonCError::with_additional(
139                        ion_error_code_IERR_INVALID_TIMESTAMP,
140                        "Mismatched offset with UnknownOffset",
141                    ));
142                }
143            }
144        };
145        if let Fractional(mantissa) = &precision {
146            match mantissa {
147                Digits(digits) => {
148                    if (*digits as i64) > TS_MAX_MANTISSA_DIGITS {
149                        return Err(IonCError::with_additional(
150                            ion_error_code_IERR_INVALID_TIMESTAMP,
151                            "Invalid digits in precision",
152                        ));
153                    }
154                }
155                Fraction(frac) => {
156                    if frac < &BigDecimal::zero() || frac >= &BigDecimal::from(1) {
157                        return Err(IonCError::with_additional(
158                            ion_error_code_IERR_INVALID_TIMESTAMP,
159                            "Mantissa outside of range",
160                        ));
161                    }
162                    let (_, scale) = frac.as_bigint_and_exponent();
163                    if scale <= TS_MAX_MANTISSA_DIGITS {
164                        return Err(IonCError::with_additional(
165                            ion_error_code_IERR_INVALID_TIMESTAMP,
166                            "Fractional mantissa not allowed for sub-nanosecond precision",
167                        ));
168                    }
169                    let ns = date_time.nanosecond();
170                    let frac_ns = (frac * BigDecimal::from(NS_IN_SEC))
171                        .abs()
172                        .to_u32()
173                        .ok_or_else(|| {
174                            IonCError::with_additional(
175                                ion_error_code_IERR_INVALID_TIMESTAMP,
176                                "Invalid mantissa in precision",
177                            )
178                        })?;
179                    if ns != frac_ns {
180                        return Err(IonCError::with_additional(
181                            ion_error_code_IERR_INVALID_TIMESTAMP,
182                            "Fractional mantissa inconsistent in precision",
183                        ));
184                    }
185                }
186            }
187        };
188
189        Ok(Self::new(date_time, precision, offset_kind))
190    }
191
192    /// Returns a reference to the underlying `DateTime`.
193    #[inline]
194    pub fn as_datetime(&self) -> &DateTime<FixedOffset> {
195        &(self.date_time)
196    }
197
198    /// Returns the precision of this `IonDateTime`.
199    #[inline]
200    pub fn precision(&self) -> &TSPrecision {
201        &(self.precision)
202    }
203
204    /// Returns the offset of this `IonDateTime`.
205    #[inline]
206    pub fn offset_kind(&self) -> TSOffsetKind {
207        self.offset_kind
208    }
209
210    /// Consumes the underlying components of this `IonDateTime` into a `DateTime`.
211    #[inline]
212    pub fn into_datetime(self) -> DateTime<FixedOffset> {
213        self.date_time
214    }
215}
216
217#[cfg(test)]
218mod test_iondt {
219    use super::*;
220
221    use rstest::rstest;
222
223    fn frac(lit: &str) -> Mantissa {
224        Fraction(BigDecimal::parse_bytes(lit.as_bytes(), 10).unwrap())
225    }
226
227    #[rstest(
228        dt_lit,
229        precision,
230        offset_kind,
231        error,
232        case::year("2020-01-01T00:01:00.1234567Z", Year, UnknownOffset, None),
233        case::month("2020-01-01T00:01:00.1234567Z", Month, UnknownOffset, None),
234        case::day("2020-01-01T00:01:00.1234567Z", Day, UnknownOffset, None),
235        case::year_bad_known_offset(
236            "2020-01-01T00:01:00.1234567Z",
237            Year,
238            KnownOffset,
239            Some(ion_error_code_IERR_INVALID_TIMESTAMP),
240        ),
241        case::month_bad_known_offset(
242            "2020-01-01T00:01:00.1234567Z",
243            Month,
244            KnownOffset,
245            Some(ion_error_code_IERR_INVALID_TIMESTAMP),
246        ),
247        case::day_bad_known_offset(
248            "2020-01-01T00:01:00.1234567Z",
249            Day,
250            KnownOffset,
251            Some(ion_error_code_IERR_INVALID_TIMESTAMP),
252        ),
253        case::minute("2020-01-01T00:01:00.1234567Z", Minute, KnownOffset, None),
254        case::second("2020-01-01T00:01:00.1234567Z", Second, KnownOffset, None),
255        case::second_unknown_offset("2020-01-01T00:01:00.1234567Z", Second, UnknownOffset, None),
256        case::second_bad_unknown_offset(
257            "2020-01-01T00:01:00.1234567-00:15",
258            Second,
259            UnknownOffset,
260            Some(ion_error_code_IERR_INVALID_TIMESTAMP),
261        ),
262        case::fractional_digits(
263            "2020-01-01T00:01:00.1234567Z",
264            Fractional(Digits(3)),
265            KnownOffset,
266            None,
267        ),
268        case::fractional_digits_too_big(
269            "2020-01-01T00:01:00.1234567Z",
270            Fractional(Digits(10)),
271            KnownOffset,
272            Some(ion_error_code_IERR_INVALID_TIMESTAMP),
273        ),
274        case::fractional_mantissa_neg(
275            "2020-01-01T00:01:00.1234567Z",
276            Fractional(frac("-0.1234567")),
277            KnownOffset,
278            Some(ion_error_code_IERR_INVALID_TIMESTAMP),
279        ),
280        case::fractional_mantissa_not_fractional(
281            "2020-01-01T00:01:00.1234567Z",
282            Fractional(frac("1.234567")),
283            KnownOffset,
284            Some(ion_error_code_IERR_INVALID_TIMESTAMP),
285        ),
286        case::fractional_mantissa_too_small(
287            "2020-01-01T00:01:00.1234567Z",
288            Fractional(frac("0.1234567")),
289            KnownOffset,
290            Some(ion_error_code_IERR_INVALID_TIMESTAMP),
291        ),
292        case::fractional_mantissa_more_precision(
293            "2020-01-01T00:01:00.1234567Z",
294            Fractional(frac("0.1234567001234567")),
295            KnownOffset,
296            None,
297        ),
298        case::fractional_mantissa_mismatch_digits(
299            "2020-01-01T00:01:00.1234567Z",
300            Fractional(frac("0.123456789")),
301            KnownOffset,
302            Some(ion_error_code_IERR_INVALID_TIMESTAMP),
303        )
304    )]
305    fn try_new_precision(
306        dt_lit: &str,
307        precision: TSPrecision,
308        offset_kind: TSOffsetKind,
309        error: Option<i32>,
310    ) -> IonCResult<()> {
311        let dt = DateTime::parse_from_rfc3339(dt_lit).unwrap();
312        let res = IonDateTime::try_new(dt, precision, offset_kind);
313        match res {
314            Ok(_) => {
315                assert_eq!(None, error);
316            }
317            Err(actual) => {
318                if let Some(expected_code) = error {
319                    assert_eq!(expected_code, actual.code, "Testing expected error codes");
320                } else {
321                    assert!(false, "Expected no error, but got: {:?}", actual);
322                }
323            }
324        }
325        Ok(())
326    }
327}