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 let text = String::deserialize(deserializer)?;
117 Self::parse(&text).map_err(Error::into_deserialize_error)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::{Cast, RowReader, SqlValue};
125
126 fn instant(text: &str) -> Instant {
127 Instant::parse(text).unwrap()
128 }
129
130 #[test]
131 fn reads_any_offset_and_writes_utc() {
132 assert_eq!(
133 instant("2026-10-11T12:30:00+03:00").to_string(),
134 "2026-10-11T09:30:00Z"
135 );
136 assert_eq!(
137 instant("2026-10-11T09:30:00Z"),
138 instant("2026-10-11T12:30:00+03:00")
139 );
140 assert!(instant("2026-10-11T09:30:00Z") < instant("2026-10-11T09:30:01Z"));
141 }
142
143 #[test]
144 fn reads_a_date_time_control_s_minute_or_second_in_the_display_zone() {
145 let istanbul = DisplayZone::named("Europe/Istanbul").unwrap();
146 let read = |text| Instant::parse_in(text, &istanbul).unwrap();
147
148 assert_eq!(read("2026-10-11T12:30"), instant("2026-10-11T09:30:00Z"));
149 assert_eq!(read("2026-10-11T12:30:15"), instant("2026-10-11T09:30:15Z"));
150 assert_eq!(
151 read("2026-10-11T12:30:15.25"),
152 instant("2026-10-11T09:30:15.25Z")
153 );
154 assert_eq!(
155 read("2026-10-11T12:30:00Z"),
156 instant("2026-10-11T12:30:00Z")
157 );
158 assert_eq!(instant("2026-10-11T09:30"), instant("2026-10-11T09:30:00Z"));
159 }
160
161 #[test]
162 fn reads_a_wall_clock_skipped_by_a_forward_shift_where_the_shift_ends() {
163 let berlin = DisplayZone::named("Europe/Berlin").unwrap();
164
165 let read = Instant::parse_in("2026-03-29T02:30", &berlin).unwrap();
166
167 assert_eq!(read, instant("2026-03-29T01:00:00Z"));
168 }
169
170 #[test]
171 fn refuses_a_repeated_wall_clock_naming_both_offsets() {
172 let berlin = DisplayZone::named("Europe/Berlin").unwrap();
173
174 let error = Instant::parse_in("2026-10-25T02:30", &berlin).unwrap_err();
175
176 let Error::Invalid(errors) = &error else {
177 panic!("{error}");
178 };
179 let message = &errors[0].message;
180 assert!(
181 message.contains("+02:00") && message.contains("+01:00"),
182 "{message}"
183 );
184 assert!(message.contains("2026-10-25T02:30:00+02:00"), "{message}");
185 assert_eq!(
186 Instant::parse_in("2026-10-25T02:30:00+01:00", &berlin).unwrap(),
187 instant("2026-10-25T01:30:00Z")
188 );
189 }
190
191 #[test]
192 fn never_reads_a_short_offset_as_a_wall_clock() {
193 assert_ne!(
194 Instant::parse("2026-10-11T09:30+03").ok(),
195 Some(instant("2026-10-11T09:30:00Z"))
196 );
197 }
198
199 #[test]
200 fn refuses_what_is_neither_an_offset_time_nor_a_date_time_control_s_value() {
201 for text in ["2026-10-11 09:30:00", "2026-10-11", "yesterday", ""] {
202 assert!(Instant::parse(text).is_err(), "{text:?}");
203 }
204 }
205
206 #[test]
207 fn round_trips_through_a_timestamp_column_and_json() {
208 let moment = instant("2026-10-11T09:30:00.5Z");
209 let mut values = Vec::new();
210 moment.encode(&mut values);
211
212 assert_eq!(values, vec![SqlValue::Timestamp(moment.timestamp())]);
213 assert_eq!(
214 Instant::decode(&mut RowReader::new(&values)).unwrap(),
215 moment
216 );
217 let json = serde_json::to_value(moment).unwrap();
218 assert_eq!(json, serde_json::json!("2026-10-11T09:30:00.5Z"));
219 assert_eq!(serde_json::from_value::<Instant>(json).unwrap(), moment);
220 assert!(serde_json::from_value::<Instant>(serde_json::json!(0)).is_err());
221 }
222
223 #[test]
224 fn keeps_the_microseconds_a_database_stores() {
225 assert_eq!(
226 instant("2026-10-11T09:30:00.123456789Z"),
227 instant("2026-10-11T09:30:00.123456Z")
228 );
229 let before_1970 = Timestamp::new(-5, -123_456_789).unwrap();
230 assert_eq!(
231 Instant::from(before_1970).timestamp(),
232 Timestamp::new(-5, -123_456_000).unwrap()
233 );
234 }
235
236 #[test]
237 fn moves_by_a_duration() {
238 let moment = instant("2026-10-11T09:30:00Z");
239
240 assert_eq!(
241 moment.checked_add(Duration::from_secs(60)).unwrap(),
242 instant("2026-10-11T09:31:00Z")
243 );
244 assert_eq!(
245 moment.checked_sub(Duration::from_secs(60)).unwrap(),
246 instant("2026-10-11T09:29:00Z")
247 );
248 }
249}