formualizer_eval/
timezone.rs1#[cfg(feature = "system-clock")]
6use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
7#[cfg(not(feature = "system-clock"))]
8use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, Utc};
9
10#[derive(Clone, Debug, Default, Eq, PartialEq)]
14pub enum TimeZoneSpec {
15 #[default]
17 Local,
18 Utc,
20 FixedOffsetSeconds(i32),
24 }
26
27impl TimeZoneSpec {
30 pub fn fixed_offset(&self) -> Option<FixedOffset> {
31 match self {
32 TimeZoneSpec::Utc => FixedOffset::east_opt(0),
33 TimeZoneSpec::FixedOffsetSeconds(secs) => FixedOffset::east_opt(*secs),
34 TimeZoneSpec::Local => None,
35 }
36 }
37
38 pub fn validate_for_determinism(&self) -> Result<(), String> {
39 match self {
40 TimeZoneSpec::Local => Err(
41 "Deterministic mode forbids `Local` timezone (use UTC or a fixed offset)"
42 .to_string(),
43 ),
44 TimeZoneSpec::Utc => Ok(()),
45 TimeZoneSpec::FixedOffsetSeconds(secs) => {
46 FixedOffset::east_opt(*secs).ok_or_else(|| {
47 format!("Invalid fixed offset: {secs} seconds (must be within +/-24h)")
48 })?;
49 Ok(())
50 }
51 }
52 }
53}
54
55pub trait ClockProvider: std::fmt::Debug + Send + Sync {
57 fn timezone(&self) -> &TimeZoneSpec;
58 fn now(&self) -> NaiveDateTime;
59 fn today(&self) -> NaiveDate {
60 self.now().date()
61 }
62}
63
64#[cfg(feature = "system-clock")]
69#[derive(Clone, Debug)]
70pub struct SystemClock {
71 timezone: TimeZoneSpec,
72}
73
74#[cfg(feature = "system-clock")]
75impl SystemClock {
76 pub fn new(timezone: TimeZoneSpec) -> Self {
77 Self { timezone }
78 }
79}
80
81#[cfg(feature = "system-clock")]
82impl ClockProvider for SystemClock {
83 fn timezone(&self) -> &TimeZoneSpec {
84 &self.timezone
85 }
86
87 fn now(&self) -> NaiveDateTime {
88 match &self.timezone {
89 TimeZoneSpec::Local => Local::now().naive_local(),
90 TimeZoneSpec::Utc => Utc::now().naive_utc(),
91 TimeZoneSpec::FixedOffsetSeconds(secs) => {
92 let off = FixedOffset::east_opt(*secs)
93 .unwrap_or_else(|| FixedOffset::east_opt(0).unwrap());
94 let utc_now: DateTime<Utc> = Utc::now();
95 utc_now.with_timezone(&off).naive_local()
96 }
97 }
98 }
99}
100
101#[derive(Clone, Debug)]
103pub struct FixedClock {
104 timestamp_utc: DateTime<Utc>,
105 timezone: TimeZoneSpec,
106}
107
108impl FixedClock {
109 pub fn new(timestamp_utc: DateTime<Utc>, timezone: TimeZoneSpec) -> Self {
110 Self {
111 timestamp_utc,
112 timezone,
113 }
114 }
115
116 pub fn new_deterministic(
117 timestamp_utc: DateTime<Utc>,
118 timezone: TimeZoneSpec,
119 ) -> Result<Self, String> {
120 timezone.validate_for_determinism()?;
121 Ok(Self::new(timestamp_utc, timezone))
122 }
123
124 fn now_in_timezone(&self) -> NaiveDateTime {
125 match &self.timezone {
126 TimeZoneSpec::Utc => self.timestamp_utc.naive_utc(),
127 TimeZoneSpec::FixedOffsetSeconds(secs) => {
128 let off = FixedOffset::east_opt(*secs).expect("validated fixed offset");
129 self.timestamp_utc.with_timezone(&off).naive_local()
130 }
131 TimeZoneSpec::Local => {
132 #[cfg(feature = "system-clock")]
136 {
137 self.timestamp_utc.with_timezone(&Local).naive_local()
138 }
139 #[cfg(not(feature = "system-clock"))]
140 {
141 self.timestamp_utc.naive_utc()
142 }
143 }
144 }
145 }
146}
147
148impl ClockProvider for FixedClock {
149 fn timezone(&self) -> &TimeZoneSpec {
150 &self.timezone
151 }
152
153 fn now(&self) -> NaiveDateTime {
154 self.now_in_timezone()
155 }
156}
157
158#[derive(Debug)]
172pub struct SnapshotClock {
173 inner: std::sync::Arc<dyn ClockProvider>,
174 timezone: TimeZoneSpec,
177 sample: std::sync::RwLock<NaiveDateTime>,
180}
181
182impl SnapshotClock {
183 pub fn new(inner: std::sync::Arc<dyn ClockProvider>) -> Self {
185 let timezone = inner.timezone().clone();
186 let sample = inner.now();
187 Self {
188 inner,
189 timezone,
190 sample: std::sync::RwLock::new(sample),
191 }
192 }
193
194 pub fn refresh(&self) {
196 let now = self.inner.now();
197 *self.sample.write().expect("clock sample lock poisoned") = now;
198 }
199}
200
201impl ClockProvider for SnapshotClock {
202 fn timezone(&self) -> &TimeZoneSpec {
203 &self.timezone
204 }
205
206 fn now(&self) -> NaiveDateTime {
207 *self.sample.read().expect("clock sample lock poisoned")
208 }
209}