1use 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#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
23pub enum Mantissa {
24 Digits(u32),
29 Fraction(BigDecimal),
34}
35
36#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
42pub enum TSPrecision {
43 Year,
45 Month,
47 Day,
49 Minute,
51 Second,
53 Fractional(Mantissa),
55}
56
57#[derive(Debug, Copy, Clone, PartialEq, Eq)]
64pub enum TSOffsetKind {
65 KnownOffset,
66 UnknownOffset,
67}
68
69#[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 #[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 #[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 #[inline]
194 pub fn as_datetime(&self) -> &DateTime<FixedOffset> {
195 &(self.date_time)
196 }
197
198 #[inline]
200 pub fn precision(&self) -> &TSPrecision {
201 &(self.precision)
202 }
203
204 #[inline]
206 pub fn offset_kind(&self) -> TSOffsetKind {
207 self.offset_kind
208 }
209
210 #[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}