ion_rs/binary/
timestamp.rs

1// Copyright Amazon.com, Inc. or its affiliates.
2
3use std::io::Write;
4use std::ops::Neg;
5
6use arrayvec::ArrayVec;
7use chrono::{Datelike, Timelike};
8
9use crate::binary::decimal::DecimalBinaryEncoder;
10use crate::binary::raw_binary_writer::MAX_INLINE_LENGTH;
11use crate::binary::var_int::VarInt;
12use crate::binary::var_uint::VarUInt;
13use crate::result::IonResult;
14use crate::types::{Decimal, Mantissa, Precision, Timestamp};
15
16const MAX_TIMESTAMP_LENGTH: usize = 32;
17
18/// Provides support to write [`Timestamp`] into [Ion binary].
19///
20/// [Ion binary]: https://amazon-ion.github.io/ion-docs/docs/binary.html#6-timestamp
21pub trait TimestampBinaryEncoder {
22    /// Encodes the content of a [`Timestamp`] as per the Ion binary encoding.
23    /// Returns the length of the encoded bytes.
24    ///
25    /// This does not encode the type descriptor nor the associated length.
26    /// Prefer [`TimestampBinaryEncoder::encode_timestamp_value`] for that.
27    fn encode_timestamp(&mut self, timestamp: &Timestamp) -> IonResult<usize>;
28
29    /// Encodes a [`Timestamp`] as an Ion value with the type descriptor and length.
30    /// Returns the length of the encoded bytes.
31    fn encode_timestamp_value(&mut self, timestamp: &Timestamp) -> IonResult<usize>;
32}
33
34impl<W> TimestampBinaryEncoder for W
35where
36    W: Write,
37{
38    fn encode_timestamp(&mut self, timestamp: &Timestamp) -> IonResult<usize> {
39        const SECONDS_PER_MINUTE: f32 = 60f32;
40        let mut bytes_written: usize = 0;
41
42        // Each unit of the binary-encoded timestamp (hour, minute, etc) is written in UTC.
43        // The reader is expected to apply the encoded offset (in minutes) to derive the local time.
44
45        // [Timestamp]s are modeled as a UTC NaiveDateTime and an optional FixedOffset.
46        // We can use the UTC NaiveDateTime to query for the individual time fields (year, month,
47        // etc) that we need to write out.
48        let utc = timestamp.date_time;
49
50        // Write out the offset (minutes difference from UTC). If the offset is
51        // unknown, negative zero is used.
52        if let Some(offset) = timestamp.offset {
53            // Ion encodes offsets in minutes while chrono's DateTime stores it in seconds.
54            let offset_seconds = offset.local_minus_utc();
55            let offset_minutes = (offset_seconds as f32 / SECONDS_PER_MINUTE).round() as i64;
56            bytes_written += VarInt::write_i64(self, offset_minutes)?;
57        } else {
58            // The offset is unknown. Write negative zero.
59            bytes_written += VarInt::write_negative_zero(self)?;
60        }
61
62        bytes_written += VarUInt::write_u64(self, utc.year() as u64)?;
63
64        // So far, we've written required fields. The rest are optional!
65        if timestamp.precision > Precision::Year {
66            bytes_written += VarUInt::write_u64(self, utc.month() as u64)?;
67            if timestamp.precision > Precision::Month {
68                bytes_written += VarUInt::write_u64(self, utc.day() as u64)?;
69                if timestamp.precision > Precision::Day {
70                    bytes_written += VarUInt::write_u64(self, utc.hour() as u64)?;
71                    bytes_written += VarUInt::write_u64(self, utc.minute() as u64)?;
72                    if timestamp.precision > Precision::HourAndMinute {
73                        bytes_written += VarUInt::write_u64(self, utc.second() as u64)?;
74                        if let Some(ref mantissa) = timestamp.fractional_seconds {
75                            // TODO: Both branches encode directly due to one
76                            // branch owning vs borrowing the decimal
77                            // representation. #286 should provide a fix.
78                            match mantissa {
79                                Mantissa::Digits(precision) => {
80                                    // Consider the following case: `2000-01-01T00:00:00.123Z`.
81                                    // That's 123 millis, or 123,000,000 nanos.
82                                    // Our mantissa is 0.123, or 123d-3.
83                                    let scaled = utc.nanosecond() / 10u32.pow(9 - *precision); // 123,000,000 -> 123
84                                    let exponent = (*precision as i64).neg(); // -3
85                                    let fractional = Decimal::new(scaled, exponent); // 123d-3
86                                    bytes_written += self.encode_decimal(&fractional)?;
87                                }
88                                Mantissa::Arbitrary(decimal) => {
89                                    bytes_written += self.encode_decimal(decimal)?;
90                                }
91                            };
92                        }
93                    }
94                }
95            }
96        }
97
98        Ok(bytes_written)
99    }
100
101    fn encode_timestamp_value(&mut self, timestamp: &Timestamp) -> IonResult<usize> {
102        let mut bytes_written: usize = 0;
103
104        // First encode the timestamp. We need to know the encoded length before
105        // we can compute and write out the type descriptor.
106        let mut encoded: ArrayVec<u8, MAX_TIMESTAMP_LENGTH> = ArrayVec::new();
107        encoded.encode_timestamp(timestamp)?;
108
109        // Write the type descriptor and length.
110        let type_descriptor: u8;
111        if encoded.len() <= MAX_INLINE_LENGTH {
112            type_descriptor = 0x60 | encoded.len() as u8;
113            self.write_all(&[type_descriptor])?;
114            bytes_written += 1;
115        } else {
116            type_descriptor = 0x6E;
117            self.write_all(&[type_descriptor])?;
118            bytes_written += 1;
119            bytes_written += VarUInt::write_u64(self, encoded.len() as u64)?;
120        }
121
122        // Now we can write out the encoded timestamp!
123        self.write_all(&encoded[..])?;
124        bytes_written += &encoded[..].len();
125
126        Ok(bytes_written)
127    }
128}
129
130#[cfg(test)]
131mod binary_timestamp_tests {
132    use super::*;
133    use crate::{reader, IonReader, IonType, ReaderBuilder};
134    use rstest::*;
135
136    // These tests show how varying levels of precision affects number of bytes
137    // written (for binary encoding of timestamps).
138    #[rstest]
139    #[case::y2k_utc("2000-01-01T00:00:00+00:00", 9)]
140    #[case::seconds_utc("2021-01-08T14:12:36+00:00", 9)]
141    #[case::seconds_tz("2021-01-08T14:12:36-05:00", 10)]
142    #[case::millis_tz("2021-01-08T14:12:36.888-05:00", 13)]
143    #[case::micros_tz("2021-01-08T14:12:36.888888-05:00", 14)]
144    #[case::nanos_tz("2021-01-08T14:12:36.888888888-05:00", 16)]
145    fn timestamp_encoding_bytes_written(
146        #[case] input: &str,
147        #[case] expected: usize,
148    ) -> IonResult<()> {
149        let mut reader = ReaderBuilder::new().build(input).unwrap();
150        match reader.next().unwrap() {
151            reader::StreamItem::Value(IonType::Timestamp) => {
152                let timestamp = reader.read_timestamp().unwrap();
153                let mut buf = vec![];
154                let written = buf.encode_timestamp_value(&timestamp)?;
155                assert_eq!(buf.len(), expected);
156                assert_eq!(written, expected);
157            }
158            _ => panic!(
159                "reader.next() should only return reader::StreamItem::Value(IonType::Timestamp)"
160            ),
161        }
162        Ok(())
163    }
164}