1use super::value::{Duration, Timestamp};
7use chrono::{DateTime, Datelike, FixedOffset, Offset, TimeZone, Timelike};
8use chrono_tz::Tz;
9
10pub fn parse_timestamp(s: &str) -> Result<Timestamp, String> {
17 let dt =
19 DateTime::parse_from_rfc3339(s).map_err(|e| format!("invalid timestamp format: {}", e))?;
20
21 let ts = Timestamp {
22 seconds: dt.timestamp(),
23 nanos: dt.timestamp_subsec_nanos() as i32,
24 };
25
26 if !ts.is_valid() {
27 return Err("timestamp out of range: must be between year 0001 and 9999".to_string());
28 }
29
30 Ok(ts)
31}
32
33pub fn parse_duration(s: &str) -> Result<Duration, String> {
46 if s.is_empty() {
47 return Err("empty duration string".to_string());
48 }
49
50 let (negative, s) = if let Some(rest) = s.strip_prefix('-') {
51 (true, rest)
52 } else {
53 (false, s)
54 };
55
56 if s.is_empty() {
57 return Err("invalid duration: no value".to_string());
58 }
59
60 let mut total_nanos: i128 = 0;
61 let mut remaining = s;
62
63 while !remaining.is_empty() {
64 let num_end = remaining
66 .find(|c: char| !c.is_ascii_digit() && c != '.')
67 .unwrap_or(remaining.len());
68
69 if num_end == 0 {
70 return Err(format!(
71 "invalid duration format: expected number at '{}'",
72 remaining
73 ));
74 }
75
76 let num_str = &remaining[..num_end];
77 remaining = &remaining[num_end..];
78
79 let unit_end = remaining
81 .find(|c: char| c.is_ascii_digit() || c == '.')
82 .unwrap_or(remaining.len());
83
84 if unit_end == 0 {
85 return Err(format!(
86 "invalid duration: missing unit after '{}'",
87 num_str
88 ));
89 }
90
91 let unit = &remaining[..unit_end];
92 remaining = &remaining[unit_end..];
93
94 let multiplier: i128 = match unit {
96 "h" => 3_600_000_000_000, "m" => 60_000_000_000, "s" => 1_000_000_000, "ms" => 1_000_000, "us" | "\u{00b5}s" => 1_000, "ns" => 1, _ => return Err(format!("invalid duration unit: '{}'", unit)),
103 };
104
105 if num_str.contains('.') {
107 let num: f64 = num_str
108 .parse()
109 .map_err(|_| format!("invalid number in duration: '{}'", num_str))?;
110 total_nanos += (num * multiplier as f64) as i128;
111 } else {
112 let num: i128 = num_str
113 .parse()
114 .map_err(|_| format!("invalid number in duration: '{}'", num_str))?;
115 total_nanos += num * multiplier;
116 }
117 }
118
119 if negative {
120 total_nanos = -total_nanos;
121 }
122
123 let seconds = (total_nanos / 1_000_000_000) as i64;
125 let nanos = (total_nanos % 1_000_000_000) as i32;
126
127 let duration = Duration::new(seconds, nanos);
128
129 if !duration.is_valid() {
130 return Err("duration out of range: must be within approximately 10000 years".to_string());
131 }
132
133 Ok(duration)
134}
135
136pub fn format_timestamp(ts: &Timestamp) -> String {
142 if let Some(dt) = ts.to_datetime_utc() {
143 if ts.nanos == 0 {
144 dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()
145 } else {
146 let nanos_str = format!("{:09}", ts.nanos);
148 let trimmed = nanos_str.trim_end_matches('0');
149 if trimmed.is_empty() {
150 dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()
151 } else {
152 format!("{}.{}Z", dt.format("%Y-%m-%dT%H:%M:%S"), trimmed)
153 }
154 }
155 } else {
156 format!("{}s", ts.seconds)
158 }
159}
160
161pub fn format_duration(d: &Duration) -> String {
165 if d.nanos == 0 {
166 format!("{}s", d.seconds)
167 } else {
168 let total_nanos = d.seconds as i128 * 1_000_000_000 + d.nanos as i128;
170 let sign = if total_nanos < 0 { "-" } else { "" };
171 let abs_nanos = total_nanos.abs();
172 let secs = abs_nanos / 1_000_000_000;
173 let frac = abs_nanos % 1_000_000_000;
174
175 if frac == 0 {
176 format!("{}{}s", sign, secs)
177 } else {
178 let frac_str = format!("{:09}", frac);
180 let trimmed = frac_str.trim_end_matches('0');
181 format!("{}{}.{}s", sign, secs, trimmed)
182 }
183 }
184}
185
186pub fn parse_timezone(tz: &str) -> Result<TimezoneInfo, String> {
194 if let Ok(tz_parsed) = tz.parse::<Tz>() {
196 return Ok(TimezoneInfo::Iana(tz_parsed));
197 }
198
199 parse_fixed_offset(tz).map(TimezoneInfo::Fixed)
201}
202
203fn parse_fixed_offset(s: &str) -> Result<FixedOffset, String> {
205 let s = s.trim();
206
207 if s.is_empty() {
208 return Err("empty timezone string".to_string());
209 }
210
211 let (negative, rest) = if let Some(r) = s.strip_prefix('-') {
213 (true, r)
214 } else if let Some(r) = s.strip_prefix('+') {
215 (false, r)
216 } else {
217 (false, s)
219 };
220
221 let parts: Vec<&str> = rest.split(':').collect();
223 if parts.len() != 2 {
224 return Err(format!("invalid timezone offset format: '{}'", s));
225 }
226
227 let hours: i32 = parts[0]
228 .parse()
229 .map_err(|_| format!("invalid hours in timezone: '{}'", parts[0]))?;
230 let minutes: i32 = parts[1]
231 .parse()
232 .map_err(|_| format!("invalid minutes in timezone: '{}'", parts[1]))?;
233
234 let total_seconds = (hours * 3600 + minutes * 60) * if negative { -1 } else { 1 };
235
236 FixedOffset::east_opt(total_seconds)
237 .ok_or_else(|| format!("timezone offset out of range: '{}'", s))
238}
239
240pub enum TimezoneInfo {
242 Iana(Tz),
243 Fixed(FixedOffset),
244}
245
246impl TimezoneInfo {
247 pub fn datetime_from_timestamp(&self, ts: &Timestamp) -> Option<DateTime<FixedOffset>> {
249 let utc_dt = ts.to_datetime_utc()?;
250
251 match self {
252 TimezoneInfo::Iana(tz) => {
253 let local = utc_dt.with_timezone(tz);
254 let offset = local.offset().fix();
256 Some(local.with_timezone(&offset))
257 }
258 TimezoneInfo::Fixed(offset) => Some(utc_dt.with_timezone(offset)),
259 }
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub enum TimestampComponent {
266 FullYear,
268 Month,
270 Date,
272 DayOfMonth,
274 DayOfWeek,
276 DayOfYear,
278 Hours,
280 Minutes,
282 Seconds,
284 Milliseconds,
286}
287
288impl TimestampComponent {
289 pub fn extract<Tz: TimeZone>(&self, dt: &DateTime<Tz>) -> i64 {
291 match self {
292 TimestampComponent::FullYear => dt.year() as i64,
293 TimestampComponent::Month => (dt.month0()) as i64, TimestampComponent::Date => dt.day() as i64, TimestampComponent::DayOfMonth => (dt.day() - 1) as i64, TimestampComponent::DayOfWeek => {
297 let weekday = dt.weekday().num_days_from_sunday();
299 weekday as i64
300 }
301 TimestampComponent::DayOfYear => (dt.ordinal0()) as i64, TimestampComponent::Hours => dt.hour() as i64,
303 TimestampComponent::Minutes => dt.minute() as i64,
304 TimestampComponent::Seconds => dt.second() as i64,
305 TimestampComponent::Milliseconds => (dt.nanosecond() / 1_000_000) as i64,
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_parse_timestamp_basic() {
316 let ts = parse_timestamp("2009-02-13T23:31:30Z").unwrap();
317 assert_eq!(ts.seconds, 1234567890);
318 assert_eq!(ts.nanos, 0);
319 }
320
321 #[test]
322 fn test_parse_timestamp_with_nanos() {
323 let ts = parse_timestamp("2009-02-13T23:31:30.123456789Z").unwrap();
324 assert_eq!(ts.seconds, 1234567890);
325 assert_eq!(ts.nanos, 123456789);
326 }
327
328 #[test]
329 fn test_parse_timestamp_with_offset() {
330 let ts = parse_timestamp("2009-02-13T18:31:30-05:00").unwrap();
331 assert_eq!(ts.seconds, 1234567890);
332 }
333
334 #[test]
335 fn test_parse_duration_seconds() {
336 let d = parse_duration("100s").unwrap();
337 assert_eq!(d.seconds, 100);
338 assert_eq!(d.nanos, 0);
339 }
340
341 #[test]
342 fn test_parse_duration_hours() {
343 let d = parse_duration("2h").unwrap();
344 assert_eq!(d.seconds, 7200);
345 }
346
347 #[test]
348 fn test_parse_duration_compound() {
349 let d = parse_duration("1h30m").unwrap();
350 assert_eq!(d.seconds, 5400);
351 }
352
353 #[test]
354 fn test_parse_duration_negative() {
355 let d = parse_duration("-30s").unwrap();
356 assert_eq!(d.seconds, -30);
357 }
358
359 #[test]
360 fn test_parse_duration_milliseconds() {
361 let d = parse_duration("500ms").unwrap();
362 assert_eq!(d.seconds, 0);
363 assert_eq!(d.nanos, 500_000_000);
364 }
365
366 #[test]
367 fn test_parse_duration_fractional() {
368 let d = parse_duration("1.5h").unwrap();
369 assert_eq!(d.seconds, 5400);
370 }
371
372 #[test]
373 fn test_format_timestamp() {
374 let ts = Timestamp::new(1234567890, 0);
375 assert_eq!(format_timestamp(&ts), "2009-02-13T23:31:30Z");
376 }
377
378 #[test]
379 fn test_format_timestamp_with_nanos() {
380 let ts = Timestamp::new(1234567890, 123000000);
381 assert_eq!(format_timestamp(&ts), "2009-02-13T23:31:30.123Z");
382 }
383
384 #[test]
385 fn test_format_duration() {
386 let d = Duration::new(100, 0);
387 assert_eq!(format_duration(&d), "100s");
388 }
389
390 #[test]
391 fn test_format_duration_with_nanos() {
392 let d = Duration::new(1, 500000000);
393 assert_eq!(format_duration(&d), "1.5s");
394 }
395
396 #[test]
397 fn test_parse_timezone_iana() {
398 let tz = parse_timezone("America/New_York").unwrap();
399 assert!(matches!(tz, TimezoneInfo::Iana(_)));
400 }
401
402 #[test]
403 fn test_parse_timezone_offset() {
404 let tz = parse_timezone("+05:30").unwrap();
405 assert!(matches!(tz, TimezoneInfo::Fixed(_)));
406 }
407
408 #[test]
409 fn test_parse_timezone_offset_no_sign() {
410 let tz = parse_timezone("05:30").unwrap();
411 assert!(matches!(tz, TimezoneInfo::Fixed(_)));
412 }
413
414 #[test]
415 fn test_timestamp_component_extract() {
416 let ts = Timestamp::new(1234567890, 0);
417 let dt = ts.to_datetime_utc().unwrap();
418
419 assert_eq!(TimestampComponent::FullYear.extract(&dt), 2009);
420 assert_eq!(TimestampComponent::Month.extract(&dt), 1); assert_eq!(TimestampComponent::Date.extract(&dt), 13);
422 assert_eq!(TimestampComponent::DayOfMonth.extract(&dt), 12); assert_eq!(TimestampComponent::Hours.extract(&dt), 23);
424 assert_eq!(TimestampComponent::Minutes.extract(&dt), 31);
425 assert_eq!(TimestampComponent::Seconds.extract(&dt), 30);
426 }
427
428 #[test]
429 fn test_day_of_week() {
430 let ts = Timestamp::new(1234567890, 0);
432 let dt = ts.to_datetime_utc().unwrap();
433 assert_eq!(TimestampComponent::DayOfWeek.extract(&dt), 5); }
435}