1use crate::error::{ErrorType, IntoResult, Res};
6use crate::TillerError;
7use anyhow::bail;
8use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime};
9use schemars::{json_schema, JsonSchema, Schema, SchemaGenerator};
10use sqlx::encode::IsNull;
11use sqlx::error::BoxDynError;
12use sqlx::sqlite::{SqliteArgumentValue, SqliteTypeInfo, SqliteValueRef};
13use sqlx::{Decode, Encode, Sqlite, Type};
14use std::borrow::Cow;
15use std::fmt::{Debug, Display, Formatter};
16use std::str::FromStr;
17
18#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
20pub enum Date {
21 Naive(NaiveDate),
22 NaiveTime(NaiveDateTime),
23 Timestamp(DateTime<FixedOffset>),
24}
25
26impl Type<Sqlite> for Date {
27 fn type_info() -> SqliteTypeInfo {
28 <String as Type<Sqlite>>::type_info()
29 }
30
31 fn compatible(ty: &SqliteTypeInfo) -> bool {
32 <String as Type<Sqlite>>::compatible(ty)
33 }
34}
35
36impl Encode<'_, Sqlite> for Date {
37 fn encode_by_ref(&self, buf: &mut Vec<SqliteArgumentValue<'_>>) -> Result<IsNull, BoxDynError> {
38 Encode::<Sqlite>::encode(self.to_string(), buf)
39 }
40}
41
42impl Decode<'_, Sqlite> for Date {
43 fn decode(value: SqliteValueRef<'_>) -> Result<Self, BoxDynError> {
44 let s = <String as Decode<Sqlite>>::decode(value)?;
45 Date::parse(&s).map_err(|e| e.into())
46 }
47}
48
49impl JsonSchema for Date {
50 fn schema_name() -> Cow<'static, str> {
51 "Date".into()
52 }
53
54 fn json_schema(_: &mut SchemaGenerator) -> Schema {
55 json_schema!({
56 "type": "string",
57 "format": "date",
58 "description": "A date in YYYY-MM-DD format (e.g., 2025-01-23), or ISO 8601 RFC 3339"
59 })
60 }
61}
62
63impl Default for Date {
64 fn default() -> Self {
65 Date::Naive(NaiveDate::from_ymd_opt(1999, 12, 31).unwrap_or_default())
66 }
67}
68
69impl Date {
70 pub fn parse(s: impl AsRef<str>) -> Res<Self> {
71 let s = s.as_ref();
72
73 if let Some(d) = NAIVE_DATE_FORMATS
74 .iter()
75 .find_map(|&fmt| NaiveDate::parse_from_str(s, fmt).ok())
76 {
77 return Ok(Date::Naive(d));
78 }
79
80 if let Some(d) = NAIVE_DATE_TIME_FORMATS
81 .iter()
82 .find_map(|&fmt| NaiveDateTime::parse_from_str(s, fmt).ok())
83 {
84 return Ok(Date::NaiveTime(d));
85 }
86
87 if let Some(d) = DATE_TIME_FORMATS
88 .iter()
89 .find_map(|&fmt| DateTime::parse_from_str(s, fmt).ok())
90 {
91 return Ok(Date::Timestamp(d));
92 }
93
94 if let Ok(d) = DateTime::parse_from_rfc3339(s) {
96 return Ok(Date::Timestamp(d));
97 }
98
99 bail!("Unable to parse {s} as a date")
100 }
101
102 pub(crate) fn to_sheet_string(&self, y: Y) -> String {
109 match y {
110 Y::Y2 => match self {
111 Date::Naive(d) => d.format("%-m/%-d/%y").to_string(),
112 Date::NaiveTime(d) => d.format("%-m/%-d/%y %-I:%M:%S %p").to_string(),
113 Date::Timestamp(d) => d.format("%-m/%-d/%y %-I:%M:%S %p").to_string(),
114 },
115 Y::Y4 => match self {
116 Date::Naive(d) => d.format("%m/%d/%Y").to_string(),
117 Date::NaiveTime(d) => d.format("%m/%d/%Y %I:%M:%S %p").to_string(),
118 Date::Timestamp(d) => d.format("%m/%d/%Y %I:%M:%S %p").to_string(),
119 },
120 }
121 }
122
123 fn from_opt(o: Option<String>) -> Res<Option<Self>> {
125 match o {
126 None => Ok(None),
127 Some(s) => Self::from_opt_s(s),
128 }
129 }
130
131 fn from_opt_s(s: impl AsRef<str>) -> Res<Option<Self>> {
133 let s = s.as_ref();
134 if s.is_empty() {
135 Ok(None)
136 } else {
137 Ok(Some(Self::parse(s)?))
138 }
139 }
140}
141
142#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
144pub(crate) enum Y {
145 Y2,
147 Y4,
149}
150
151impl TryFrom<String> for Date {
152 type Error = TillerError;
153
154 fn try_from(value: String) -> Result<Self, Self::Error> {
155 Self::parse(value).pub_result(ErrorType::Internal)
156 }
157}
158
159impl TryFrom<&str> for Date {
160 type Error = TillerError;
161
162 fn try_from(value: &str) -> Result<Self, Self::Error> {
163 Self::parse(value).pub_result(ErrorType::Internal)
164 }
165}
166
167impl Display for Date {
168 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
169 let s = match self {
170 Date::Naive(d) => d.format("%Y-%m-%d").to_string(),
171 Date::NaiveTime(d) => d.format("%Y-%m-%dT%H:%M:%S").to_string(),
172 Date::Timestamp(d) => d.to_rfc3339(),
173 };
174 Display::fmt(&s, f)
175 }
176}
177
178impl FromStr for Date {
179 type Err = TillerError;
180
181 fn from_str(s: &str) -> Result<Self, Self::Err> {
182 Self::parse(s).pub_result(ErrorType::Internal)
183 }
184}
185
186pub(crate) trait DateFromOpt: Sized {
188 fn date_from_opt(self) -> Res<Option<Date>>;
189}
190
191impl<S> DateFromOpt for Option<S>
192where
193 S: AsRef<str> + Sized,
194{
195 fn date_from_opt(self) -> Res<Option<Date>> {
196 let o = self.map(|s| s.as_ref().to_string());
197 Date::from_opt(o)
198 }
199}
200
201pub(crate) trait DateFromOptStr: Sized {
204 fn date_from_opt_s(self) -> Res<Option<Date>>;
205}
206
207impl<S> DateFromOptStr for S
208where
209 S: AsRef<str> + Sized,
210{
211 fn date_from_opt_s(self) -> Res<Option<Date>> {
212 Date::from_opt_s(self)
213 }
214}
215
216pub(crate) trait DateToSheetStr {
220 fn d_to_s(&self, y: Y) -> String;
221}
222
223impl DateToSheetStr for Date {
224 fn d_to_s(&self, y: Y) -> String {
225 self.to_sheet_string(y)
226 }
227}
228
229impl DateToSheetStr for &Date {
230 fn d_to_s(&self, y: Y) -> String {
231 self.to_sheet_string(y)
232 }
233}
234
235impl DateToSheetStr for Option<Date> {
236 fn d_to_s(&self, y: Y) -> String {
237 self.as_ref()
238 .map(|d| d.to_sheet_string(y))
239 .unwrap_or_default()
240 }
241}
242
243impl DateToSheetStr for Option<&Date> {
244 fn d_to_s(&self, y: Y) -> String {
245 self.map(|d| d.to_sheet_string(y)).unwrap_or_default()
246 }
247}
248
249impl DateToSheetStr for &Option<Date> {
250 fn d_to_s(&self, y: Y) -> String {
251 self.as_ref()
252 .map(|d| d.to_sheet_string(y))
253 .unwrap_or_default()
254 }
255}
256
257serde_plain::derive_deserialize_from_fromstr!(Date, "Valid date in M/D/YYYY or YYYY-MM-DD");
258serde_plain::derive_serialize_from_display!(Date);
259
260const NAIVE_DATE_FORMATS: [&str; 23] = [
261 "%Y-%m-%d", "%Y%m%d", "%m/%d/%y", "%m-%d-%y", "%m/%d/%Y", "%m-%d-%Y", "%m.%d.%Y", "%d/%m/%y", "%d-%m-%y", "%d.%m.%y", "%d/%m/%Y", "%d-%m-%Y", "%d.%m.%Y", "%d-%b-%y", "%B %d, %Y", "%b %d, %Y", "%d %B %Y", "%d %b %Y", "%B %d %Y", "%b %d %Y", "%d-%b-%Y", "%Y/%m/%d", "%Y.%m.%d", ];
290
291const NAIVE_DATE_TIME_FORMATS: [&str; 12] = [
292 "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S%.f", "%Y%m%dT%H%M%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%m/%d/%y %H:%M:%S", "%m/%d/%y %I:%M:%S %p", "%d/%m/%y %H:%M:%S", "%m/%d/%Y %H:%M:%S", "%m/%d/%Y %I:%M:%S %p", "%d/%m/%Y %H:%M:%S", "%b %d %H:%M:%S %Y", ];
308
309const DATE_TIME_FORMATS: [&str; 12] = [
310 "%Y-%m-%dT%H:%M:%S%Z", "%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S%.f%Z", "%Y-%m-%dT%H:%M:%S%.f%z", "%Y%m%dT%H%M%S%Z", "%Y%m%dT%H%M%S%z", "%Y-%m-%d %H:%M:%S%Z", "%Y-%m-%d %H:%M:%S%z", "%Y-%m-%d %H:%M:%S%.f%Z", "%Y-%m-%d %H:%M:%S%.f%z", "%Y%m%d %H%M%S%Z", "%Y%m%d %H%M%S%z", ];
324
325#[cfg(test)]
326mod test {
327 use super::*;
328
329 fn success_case(input: &str, expected: &str, sheet_y2: &str, sheet_y4: &str) {
330 let text = format!("Test failure parsing {input} and expecting {expected}");
331 let actual = Date::parse(&input).expect(&text);
332 assert_eq!(expected, actual.to_string());
333
334 let json_str = format!("[\"{input}\"]");
335 let arr: Vec<Date> = serde_json::from_str(&json_str).expect(&format!(
336 "{text}: the json '{json_str}' could not be deserialized"
337 ));
338 let serialized =
339 serde_json::to_string(&arr).expect(&format!("{text}, unable to serialize"));
340 let json_expected = format!("[\"{expected}\"]");
341 assert_eq!(
342 json_expected, serialized,
343 "{text}, did not get the expected serialization"
344 );
345
346 let actual_sheet_y2 = actual.d_to_s(Y::Y2);
347 assert_eq!(
348 sheet_y2, actual_sheet_y2,
349 "{text} Sheet Y2 formatting is incorrect"
350 );
351 let actual_sheet_y4 = actual.d_to_s(Y::Y4);
352 assert_eq!(
353 sheet_y4, actual_sheet_y4,
354 "{text} Sheet Y4 formatting is incorrect"
355 );
356 }
357
358 fn failure_case(input: &str) {
359 let res = Date::parse(&input);
360 assert!(
361 res.is_err(),
362 "Expected an error when parsing {input} but received Ok"
363 );
364 let msg = res.err().unwrap().to_string();
365 let contains_input = msg.contains(input);
366 assert!(
367 contains_input,
368 "Expected the error message when parsing {input} to contain the \
369 input string, but it did not"
370 );
371 }
372
373 #[test]
374 fn test_parse_good_1() {
375 success_case("9/30/2025", "2025-09-30", "9/30/25", "09/30/2025");
376 }
377
378 #[test]
379 fn test_parse_good_2() {
380 success_case("2025-09-30", "2025-09-30", "9/30/25", "09/30/2025");
381 }
382
383 #[test]
384 fn test_parse_good_3() {
385 success_case("1999-6-2", "1999-06-02", "6/2/99", "06/02/1999");
386 }
387
388 #[test]
389 fn test_parse_bad_leading_zeros() {
390 failure_case("12/000001/1932");
391 }
392
393 #[test]
394 fn test_parse_good_5() {
395 success_case("10/31/05", "2005-10-31", "10/31/05", "10/31/2005");
397 }
398
399 #[test]
400 fn test_parse_good_6() {
401 success_case("10/1/25", "2025-10-01", "10/1/25", "10/01/2025");
403 }
404
405 #[test]
406 fn test_parse_bad_1() {
407 failure_case("99/30/2025");
408 }
409
410 #[test]
411 fn test_parse_bad_2() {
412 failure_case("9/32/2025")
413 }
414
415 #[test]
416 fn test_parse_bad_3() {
417 failure_case("foo")
418 }
419
420 #[test]
423 fn test_parse_chrono_iso_format() {
424 success_case(
425 "2025-01-23T10:30:45",
426 "2025-01-23T10:30:45",
427 "1/23/25 10:30:45 AM",
428 "01/23/2025 10:30:45 AM",
429 );
430 }
431
432 #[test]
433 fn test_parse_chrono_iso_midnight() {
434 success_case(
435 "2025-12-31T00:00:00",
436 "2025-12-31T00:00:00",
437 "12/31/25 12:00:00 AM",
438 "12/31/2025 12:00:00 AM",
439 );
440 }
441
442 #[test]
443 fn test_parse_chrono_iso_end_of_day() {
444 success_case(
445 "2025-06-15T23:59:59",
446 "2025-06-15T23:59:59",
447 "6/15/25 11:59:59 PM",
448 "06/15/2025 11:59:59 PM",
449 );
450 }
451
452 #[test]
453 fn test_parse_chrono_us_format_am() {
454 success_case(
455 "01/23/2025 10:30:45 AM",
456 "2025-01-23T10:30:45",
457 "1/23/25 10:30:45 AM",
458 "01/23/2025 10:30:45 AM",
459 );
460 }
461
462 #[test]
463 fn test_parse_chrono_us_format_pm() {
464 success_case(
465 "01/23/2025 02:30:45 PM",
466 "2025-01-23T14:30:45",
467 "1/23/25 2:30:45 PM",
468 "01/23/2025 02:30:45 PM",
469 );
470 }
471
472 #[test]
473 fn test_parse_chrono_us_format_noon() {
474 success_case(
475 "07/04/2025 12:00:00 PM",
476 "2025-07-04T12:00:00",
477 "7/4/25 12:00:00 PM",
478 "07/04/2025 12:00:00 PM",
479 );
480 }
481
482 #[test]
483 fn test_parse_chrono_us_format_midnight() {
484 success_case(
485 "12/25/2025 12:00:00 AM",
486 "2025-12-25T00:00:00",
487 "12/25/25 12:00:00 AM",
488 "12/25/2025 12:00:00 AM",
489 );
490 }
491
492 #[test]
493 fn test_parse_chrono_bad_iso() {
494 failure_case("2025-13-01T10:30:45");
495 }
496
497 #[test]
498 fn test_parse_chrono_bad_us_format() {
499 failure_case("13/01/2025 10:30:45 AM");
500 }
501
502 #[test]
503 fn test_parse_chrono_bad_time() {
504 failure_case("2025-01-23T25:00:00");
505 }
506
507 #[test]
510 fn test_parse_chrono_with_negative_offset() {
511 success_case(
513 "2024-12-31T06:17:17-0800",
514 "2024-12-31T06:17:17-08:00",
515 "12/31/24 6:17:17 AM",
516 "12/31/2024 06:17:17 AM",
517 );
518 }
519
520 #[test]
521 fn test_parse_chrono_with_positive_offset() {
522 success_case(
523 "2025-01-23T15:30:00+0530",
524 "2025-01-23T15:30:00+05:30",
525 "1/23/25 3:30:00 PM",
526 "01/23/2025 03:30:00 PM",
527 );
528 }
529
530 #[test]
531 fn test_parse_chrono_with_rfc3339_offset() {
532 success_case(
534 "2025-01-23T10:00:00-05:00",
535 "2025-01-23T10:00:00-05:00",
536 "1/23/25 10:00:00 AM",
537 "01/23/2025 10:00:00 AM",
538 );
539 }
540
541 #[test]
542 fn test_parse_chrono_with_z_suffix() {
543 success_case(
544 "2025-01-23T10:00:00Z",
545 "2025-01-23T10:00:00+00:00",
546 "1/23/25 10:00:00 AM",
547 "01/23/2025 10:00:00 AM",
548 );
549 }
550
551 #[test]
552 fn test_parse_chrono_with_fractional_seconds_and_z() {
553 success_case(
555 "2025-01-23T10:00:00.123456Z",
556 "2025-01-23T10:00:00.123456+00:00",
557 "1/23/25 10:00:00 AM",
558 "01/23/2025 10:00:00 AM",
559 );
560 }
561
562 #[test]
563 fn test_parse_chrono_with_fractional_seconds_and_offset() {
564 success_case(
566 "2024-12-31T06:17:17.465339-08:00",
567 "2024-12-31T06:17:17.465339-08:00",
568 "12/31/24 6:17:17 AM",
569 "12/31/2024 06:17:17 AM",
570 );
571 }
572}