sofie_values/time/
date.rs1use std::convert::Infallible;
2use std::fmt;
3
4use jiff::civil;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use sofie_core::value::Value;
7use sofie_core::{Error, Ordered, Result};
8
9#[cfg(feature = "web")]
10use crate::Conventions;
11use crate::parsed::deserialize_parsed;
12use crate::{Input, Instant, Present, TimeZone, Weekday};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
15pub struct Date {
16 day: civil::Date,
17}
18
19impl Date {
20 pub fn try_new(year: i16, month: i8, day: i8) -> Result<Self> {
21 civil::Date::new(year, month, day)
22 .map(Self::from)
23 .map_err(|_| invalid_date())
24 }
25
26 pub fn parse(text: &str) -> Result<Self> {
27 let day: civil::Date = text.parse().map_err(|_| invalid_date())?;
28 if day.to_string() != text {
29 return Err(invalid_date());
30 }
31 Ok(Self::from(day))
32 }
33
34 #[must_use]
36 pub fn utc_of(instant: Instant) -> Self {
37 Self::from(instant.timestamp().to_zoned(jiff::tz::TimeZone::UTC).date())
38 }
39
40 #[must_use]
42 pub fn of(instant: Instant, zone: &TimeZone) -> Self {
43 Self::from(zone.zoned(instant).date())
44 }
45
46 #[must_use]
47 pub fn civil(self) -> civil::Date {
48 self.day
49 }
50
51 #[must_use]
52 pub fn weekday(self) -> Weekday {
53 Weekday::from(self.day.weekday())
54 }
55}
56
57fn invalid_date() -> Error {
58 Error::invalid("date", "must be a calendar day such as 2026-10-11")
59}
60
61impl From<civil::Date> for Date {
62 fn from(day: civil::Date) -> Self {
63 Self { day }
64 }
65}
66
67impl fmt::Display for Date {
68 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69 fmt::Display::fmt(&self.day, formatter)
70 }
71}
72
73impl Value for Date {
74 type Inner = civil::Date;
75
76 fn inner(&self) -> &civil::Date {
77 &self.day
78 }
79
80 fn try_from_inner(day: civil::Date) -> Result<Self, impl fmt::Display> {
81 Ok::<_, Infallible>(Self::from(day))
82 }
83}
84
85impl Ordered for Date {}
86
87impl Serialize for Date {
88 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
89 serializer.collect_str(self)
90 }
91}
92
93impl<'de> Deserialize<'de> for Date {
94 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
95 deserialize_parsed(deserializer, "a calendar day", Self::parse)
96 }
97}
98
99impl Present for Date {
100 const INPUT: Input = Input::typed(&[("type", "date")]);
101 const SEARCH: crate::Search = crate::Search::Time;
102
103 fn csv(&self) -> String {
104 self.to_string()
105 }
106
107 #[cfg(feature = "web")]
108 fn write_localized(&self, out: &mut String, conventions: &dyn Conventions) {
109 conventions.write_date(out, &self.csv());
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use sofie_core::SqlValue;
116
117 use super::*;
118 use crate::testing::round_trip;
119
120 #[test]
121 fn an_instant_falls_on_its_utc_day_and_on_its_zone_s_local_day() {
122 let late_evening = Instant::parse("2026-10-11T22:30:00Z").unwrap();
123 let istanbul = TimeZone::parse("Europe/Istanbul").unwrap();
124
125 assert_eq!(
126 Date::utc_of(late_evening),
127 Date::parse("2026-10-11").unwrap()
128 );
129 assert_eq!(
130 Date::of(late_evening, &istanbul),
131 Date::parse("2026-10-12").unwrap()
132 );
133 }
134
135 #[test]
136 fn accepts_real_days_only() {
137 assert_eq!(
138 Date::try_new(2024, 2, 29).unwrap(),
139 Date::parse("2024-02-29").unwrap()
140 );
141 assert!(Date::try_new(2026, 2, 29).is_err());
142 assert!(Date::try_new(2026, 13, 1).is_err());
143 assert!(Date::try_new(2026, 1, 0).is_err());
144 }
145
146 #[test]
147 fn reads_exactly_year_month_day() {
148 for text in [
149 "2026-10-11T09:30",
150 "2026-1-1",
151 "20261011",
152 " 2026-10-11",
153 "2026-10-32",
154 "",
155 ] {
156 assert!(Date::parse(text).is_err(), "{text:?}");
157 }
158 }
159
160 #[test]
161 fn round_trips_through_a_date_column_and_json() {
162 let day = Date::try_new(2026, 10, 11).unwrap();
163 assert_eq!(round_trip(&day), vec![SqlValue::Date(day.civil())]);
164 let json = serde_json::to_value(day).unwrap();
165 assert_eq!(json, serde_json::json!("2026-10-11"));
166 assert_eq!(serde_json::from_value::<Date>(json).unwrap(), day);
167 assert!(day < Date::try_new(2026, 10, 12).unwrap());
168 }
169}