1use std::convert::Infallible;
2use std::fmt;
3use std::time::Duration;
4
5use jiff::Timestamp;
6use jiff::civil::DateTime;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use crate::value::Value;
10use crate::{DisplayZone, Error, FieldError, Ordered, Result};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct Instant {
15 timestamp: Timestamp,
16}
17
18impl Instant {
19 pub fn parse(text: &str) -> Result<Self> {
21 Self::parse_in(text, DisplayZone::current())
22 }
23
24 pub fn parse_in(text: &str, zone: &DisplayZone) -> Result<Self> {
25 let text = text.trim();
26 if Self::is_wall_clock(text)
27 && let Ok(wall_clock) = text.parse::<DateTime>()
28 {
29 return zone.instant_at(wall_clock);
30 }
31 text.parse::<Timestamp>().map(Self::from).map_err(|_| {
32 Error::Invalid(vec![FieldError::new(
33 "instant",
34 "validation.instant.invalid",
35 "must be a time with an offset, such as 2026-10-11T09:30:00Z",
36 )])
37 })
38 }
39
40 #[must_use]
42 pub fn is_wall_clock(text: &str) -> bool {
43 let (clock, fraction) = text.split_once('.').unwrap_or((text, ""));
44 let fraction_fits = clock.len() == text.len()
45 || (clock.len() == 19
46 && !fraction.is_empty()
47 && fraction.bytes().all(|byte| byte.is_ascii_digit()));
48 fraction_fits
49 && matches!(
50 (clock.len(), clock.as_bytes().get(10)),
51 (16 | 19, Some(b'T'))
52 )
53 && clock
54 .bytes()
55 .skip(11)
56 .all(|byte| byte.is_ascii_digit() || byte == b':')
57 }
58
59 #[must_use]
60 pub fn timestamp(self) -> Timestamp {
61 self.timestamp
62 }
63
64 pub fn checked_add(self, duration: Duration) -> Result<Self> {
65 self.timestamp
66 .checked_add(duration)
67 .map(Self::from)
68 .map_err(|error| Error::internal("adding to an instant", error))
69 }
70
71 pub fn checked_sub(self, duration: Duration) -> Result<Self> {
72 self.timestamp
73 .checked_sub(duration)
74 .map(Self::from)
75 .map_err(|error| Error::internal("subtracting from an instant", error))
76 }
77}
78
79impl From<Timestamp> for Instant {
80 fn from(timestamp: Timestamp) -> Self {
81 let timestamp = Timestamp::from_microsecond(timestamp.as_microsecond()).expect(
82 "a timestamp truncated to the microseconds both databases store stays in range",
83 );
84 Self { timestamp }
85 }
86}
87
88impl fmt::Display for Instant {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 fmt::Display::fmt(&self.timestamp, formatter)
91 }
92}
93
94impl Value for Instant {
95 type Inner = Timestamp;
96
97 fn inner(&self) -> &Timestamp {
98 &self.timestamp
99 }
100
101 fn try_from_inner(timestamp: Timestamp) -> Result<Self, impl fmt::Display> {
102 Ok::<_, Infallible>(Self::from(timestamp))
103 }
104}
105
106impl Ordered for Instant {}
107
108impl Serialize for Instant {
109 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
110 serializer.collect_str(self)
111 }
112}
113
114impl<'de> Deserialize<'de> for Instant {
115 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
116 crate::deserialize_parsed(deserializer, "an RFC 3339 instant", Self::parse)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::{Cast, RowReader, SqlValue};
124
125 fn instant(text: &str) -> Instant {
126 Instant::parse(text).unwrap()
127 }
128
129 #[test]
130 fn reads_any_offset_and_writes_utc() {
131 assert_eq!(
132 instant("2026-10-11T12:30:00+03:00").to_string(),
133 "2026-10-11T09:30:00Z"
134 );
135 assert_eq!(
136 instant("2026-10-11T09:30:00Z"),
137 instant("2026-10-11T12:30:00+03:00")
138 );
139 assert!(instant("2026-10-11T09:30:00Z") < instant("2026-10-11T09:30:01Z"));
140 }
141
142 #[test]
143 fn reads_a_date_time_control_s_minute_or_second_in_the_display_zone() {
144 let istanbul = DisplayZone::named("Europe/Istanbul").unwrap();
145 let read = |text| Instant::parse_in(text, &istanbul).unwrap();
146
147 assert_eq!(read("2026-10-11T12:30"), instant("2026-10-11T09:30:00Z"));
148 assert_eq!(read("2026-10-11T12:30:15"), instant("2026-10-11T09:30:15Z"));
149 assert_eq!(
150 read("2026-10-11T12:30:15.25"),
151 instant("2026-10-11T09:30:15.25Z")
152 );
153 assert_eq!(
154 read("2026-10-11T12:30:00Z"),
155 instant("2026-10-11T12:30:00Z")
156 );
157 assert_eq!(instant("2026-10-11T09:30"), instant("2026-10-11T09:30:00Z"));
158 }
159
160 #[test]
161 fn reads_a_wall_clock_skipped_by_a_forward_shift_where_the_shift_ends() {
162 let berlin = DisplayZone::named("Europe/Berlin").unwrap();
163
164 let read = Instant::parse_in("2026-03-29T02:30", &berlin).unwrap();
165
166 assert_eq!(read, instant("2026-03-29T01:00:00Z"));
167 }
168
169 #[test]
170 fn refuses_a_repeated_wall_clock_naming_both_offsets() {
171 let berlin = DisplayZone::named("Europe/Berlin").unwrap();
172
173 let error = Instant::parse_in("2026-10-25T02:30", &berlin).unwrap_err();
174
175 let Error::Invalid(errors) = &error else {
176 panic!("{error}");
177 };
178 let message = &errors[0].message;
179 assert!(
180 message.contains("+02:00") && message.contains("+01:00"),
181 "{message}"
182 );
183 assert!(message.contains("2026-10-25T02:30:00+02:00"), "{message}");
184 assert_eq!(
185 Instant::parse_in("2026-10-25T02:30:00+01:00", &berlin).unwrap(),
186 instant("2026-10-25T01:30:00Z")
187 );
188 }
189
190 #[test]
191 fn never_reads_a_short_offset_as_a_wall_clock() {
192 assert_ne!(
193 Instant::parse("2026-10-11T09:30+03").ok(),
194 Some(instant("2026-10-11T09:30:00Z"))
195 );
196 }
197
198 #[test]
199 fn refuses_what_is_neither_an_offset_time_nor_a_date_time_control_s_value() {
200 for text in ["2026-10-11 09:30:00", "2026-10-11", "yesterday", ""] {
201 assert!(Instant::parse(text).is_err(), "{text:?}");
202 }
203 }
204
205 #[test]
206 fn round_trips_through_a_timestamp_column_and_json() {
207 let moment = instant("2026-10-11T09:30:00.5Z");
208 let mut values = Vec::new();
209 moment.encode(&mut values);
210
211 assert_eq!(values, vec![SqlValue::Timestamp(moment.timestamp())]);
212 assert_eq!(
213 Instant::decode(&mut RowReader::new(&values)).unwrap(),
214 moment
215 );
216 let json = serde_json::to_value(moment).unwrap();
217 assert_eq!(json, serde_json::json!("2026-10-11T09:30:00.5Z"));
218 assert_eq!(serde_json::from_value::<Instant>(json).unwrap(), moment);
219 assert!(serde_json::from_value::<Instant>(serde_json::json!(0)).is_err());
220 }
221
222 #[test]
223 fn keeps_the_microseconds_a_database_stores() {
224 assert_eq!(
225 instant("2026-10-11T09:30:00.123456789Z"),
226 instant("2026-10-11T09:30:00.123456Z")
227 );
228 let before_1970 = Timestamp::new(-5, -123_456_789).unwrap();
229 assert_eq!(
230 Instant::from(before_1970).timestamp(),
231 Timestamp::new(-5, -123_456_000).unwrap()
232 );
233 }
234
235 #[test]
236 fn moves_by_a_duration() {
237 let moment = instant("2026-10-11T09:30:00Z");
238
239 assert_eq!(
240 moment.checked_add(Duration::from_secs(60)).unwrap(),
241 instant("2026-10-11T09:31:00Z")
242 );
243 assert_eq!(
244 moment.checked_sub(Duration::from_secs(60)).unwrap(),
245 instant("2026-10-11T09:29:00Z")
246 );
247 }
248}