re_log_types/index/
time_type.rs

1use std::sync::Arc;
2
3use arrow::datatypes::DataType as ArrowDataType;
4
5use crate::{AbsoluteTimeRange, TimestampFormat};
6
7use super::TimeInt;
8
9/// The type of a [`TimeInt`] or [`crate::Timeline`].
10#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, num_derive::FromPrimitive)]
11#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
12pub enum TimeType {
13    /// Used e.g. for frames in a film.
14    Sequence,
15
16    /// Duration measured in nanoseconds.
17    DurationNs,
18
19    /// Nanoseconds since unix epoch (1970-01-01 00:00:00 UTC).
20    TimestampNs,
21}
22
23impl std::fmt::Display for TimeType {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Self::Sequence => f.write_str("sequence"),
27            Self::DurationNs => f.write_str("duration"),
28            Self::TimestampNs => f.write_str("timestamp"),
29        }
30    }
31}
32
33impl TimeType {
34    #[inline]
35    pub(crate) fn hash(&self) -> u64 {
36        match self {
37            Self::Sequence => 0,
38            Self::DurationNs => 1,
39            Self::TimestampNs => 2,
40        }
41    }
42
43    pub fn format_sequence(time_int: TimeInt) -> String {
44        Self::Sequence.format(time_int, TimestampFormat::utc())
45    }
46
47    pub fn parse_sequence(s: &str) -> Option<TimeInt> {
48        match s {
49            "<static>" | "static" => Some(TimeInt::STATIC),
50            "−∞" | "-inf" | "-infinity" => Some(TimeInt::MIN),
51            "∞" | "+∞" | "inf" | "infinity" => Some(TimeInt::MAX),
52            _ => {
53                let s = s.strip_prefix('#').unwrap_or(s);
54                re_format::parse_i64(s).map(TimeInt::new_temporal)
55            }
56        }
57    }
58
59    /// Parses a human-readable time string into a [`TimeInt`].
60    pub fn parse_time(&self, s: &str, timestamp_format: TimestampFormat) -> Option<TimeInt> {
61        match s.to_lowercase().as_str() {
62            "<static>" | "static" => Some(TimeInt::STATIC),
63            "−∞" | "-inf" | "-infinity" => Some(TimeInt::MIN),
64            "∞" | "+∞" | "inf" | "infinity" => Some(TimeInt::MAX),
65            _ => {
66                match self {
67                    Self::Sequence => {
68                        if let Some(s) = s.strip_prefix('#') {
69                            TimeInt::try_from(re_format::parse_i64(s)?).ok()
70                        } else {
71                            TimeInt::try_from(re_format::parse_i64(s)?).ok()
72                        }
73                    }
74                    Self::DurationNs => {
75                        if let Some(nanos) = re_format::parse_i64(s) {
76                            // If it's just numbers, interpret it as a raw nanoseconds
77                            nanos.try_into().ok()
78                        } else {
79                            s.parse::<super::Duration>()
80                                .ok()
81                                .map(|duration| duration.into())
82                        }
83                    }
84                    Self::TimestampNs => {
85                        if let Some(nanos) = re_format::parse_i64(s) {
86                            // If it's just numbers, interpret it as a raw nanoseconds since epoch
87                            nanos.try_into().ok()
88                        } else {
89                            // Otherwise, try to make sense of the time string depending on the timezone setting:
90                            super::Timestamp::parse_with_format(s, timestamp_format)
91                                .map(|timestamp| timestamp.into())
92                        }
93                    }
94                }
95            }
96        }
97    }
98
99    pub fn format(
100        &self,
101        time_int: impl Into<TimeInt>,
102        timestamp_format: TimestampFormat,
103    ) -> String {
104        let time_int = time_int.into();
105        match time_int {
106            TimeInt::STATIC => "<static>".into(),
107            TimeInt::MIN => "−∞".into(),
108            TimeInt::MAX => "+∞".into(),
109            _ => match self {
110                Self::Sequence => format!("#{}", re_format::format_int(time_int.as_i64())),
111                Self::DurationNs => super::Duration::from(time_int).format_secs(),
112                Self::TimestampNs => super::Timestamp::from(time_int).format(timestamp_format),
113            },
114        }
115    }
116
117    #[inline]
118    pub fn format_utc(&self, time_int: TimeInt) -> String {
119        self.format(time_int, TimestampFormat::utc())
120    }
121
122    #[inline]
123    pub fn format_range(
124        &self,
125        time_range: AbsoluteTimeRange,
126        timestamp_format: TimestampFormat,
127    ) -> String {
128        format!(
129            "{}..={}",
130            self.format(time_range.min(), timestamp_format),
131            self.format(time_range.max(), timestamp_format)
132        )
133    }
134
135    #[inline]
136    pub fn format_range_utc(&self, time_range: AbsoluteTimeRange) -> String {
137        self.format_range(time_range, TimestampFormat::utc())
138    }
139
140    /// Returns the appropriate arrow datatype to represent this timeline.
141    #[inline]
142    pub fn datatype(self) -> ArrowDataType {
143        match self {
144            Self::Sequence => ArrowDataType::Int64,
145            Self::DurationNs => ArrowDataType::Duration(arrow::datatypes::TimeUnit::Nanosecond),
146            Self::TimestampNs => {
147                // TODO(zehiko) add back timezone support (#9310)
148                ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Nanosecond, None)
149            }
150        }
151    }
152
153    pub fn from_arrow_datatype(datatype: &ArrowDataType) -> Option<Self> {
154        match datatype {
155            ArrowDataType::Int64 => Some(Self::Sequence),
156            ArrowDataType::Duration(arrow::datatypes::TimeUnit::Nanosecond) => {
157                Some(Self::DurationNs)
158            }
159            ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Nanosecond, timezone) => {
160                // If the timezone is empty/None, it means we don't know the epoch.
161                // But we will assume it's UTC anyway.
162                if timezone.as_ref().is_none_or(|tz| tz.is_empty()) {
163                    // TODO(#9310): warn when timezone is missing
164                } else {
165                    // Regardless of the timezone, that actual values are in UTC (per arrow standard)
166                    // The timezone is mostly a hint on how to _display_ the time, and we currently ignore that.
167                }
168
169                Some(Self::TimestampNs)
170            }
171            _ => None,
172        }
173    }
174
175    /// Returns an array with the appropriate datatype.
176    pub fn make_arrow_array(
177        self,
178        times: impl Into<arrow::buffer::ScalarBuffer<i64>>,
179    ) -> arrow::array::ArrayRef {
180        let times = times.into();
181        match self {
182            Self::Sequence => Arc::new(arrow::array::Int64Array::new(times, None)),
183            Self::DurationNs => Arc::new(arrow::array::DurationNanosecondArray::new(times, None)),
184            // TODO(zehiko) add back timezone support (#9310)
185            Self::TimestampNs => Arc::new(arrow::array::TimestampNanosecondArray::new(times, None)),
186        }
187    }
188
189    /// Returns an array with the appropriate datatype, using `None` for [`TimeInt::STATIC`].
190    pub fn make_arrow_array_from_time_ints(
191        self,
192        times: impl Iterator<Item = TimeInt>,
193    ) -> arrow::array::ArrayRef {
194        match self {
195            Self::Sequence => Arc::new(
196                times
197                    .map(|time| {
198                        if time.is_static() {
199                            None
200                        } else {
201                            Some(time.as_i64())
202                        }
203                    })
204                    .collect::<arrow::array::Int64Array>(),
205            ),
206
207            Self::DurationNs => Arc::new(
208                times
209                    .map(|time| {
210                        if time.is_static() {
211                            None
212                        } else {
213                            Some(time.as_i64())
214                        }
215                    })
216                    .collect::<arrow::array::DurationNanosecondArray>(),
217            ),
218
219            Self::TimestampNs => Arc::new(
220                times
221                    .map(|time| {
222                        if time.is_static() {
223                            None
224                        } else {
225                            Some(time.as_i64())
226                        }
227                    })
228                    // TODO(zehiko) add back timezone support (#9310)
229                    .collect::<arrow::array::TimestampNanosecondArray>(),
230            ),
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use crate::{TimeInt, TimeType};
238
239    #[test]
240    fn test_format_parse() {
241        let cases = [
242            (TimeInt::STATIC, "<static>"),
243            (TimeInt::MIN, "−∞"),
244            (TimeInt::MAX, "+∞"),
245            (TimeInt::new_temporal(-42), "#−42"),
246            (TimeInt::new_temporal(12345), "#12 345"),
247        ];
248
249        for (int, s) in cases {
250            assert_eq!(TimeType::format_sequence(int), s);
251            assert_eq!(TimeType::parse_sequence(s), Some(int));
252        }
253    }
254}