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
158pub struct SnapshotClock {
172 inner: std::sync::Arc<dyn ClockProvider>,
173 timezone: TimeZoneSpec,
176 timezone_failed: bool,
177 sample: std::sync::RwLock<Result<NaiveDateTime, formualizer_common::ExcelError>>,
180}
181
182impl SnapshotClock {
183 pub fn new(inner: std::sync::Arc<dyn ClockProvider>) -> Self {
185 let timezone = crate::function::catch_function_panic("resolver:clock", || inner.timezone());
186 let timezone_failed = timezone.is_err();
187 let now = crate::function::catch_function_panic("resolver:clock", || inner.now());
188 let sample = timezone.as_ref().map_err(Clone::clone).and(now);
189 let timezone = timezone.cloned().unwrap_or(TimeZoneSpec::Utc);
190 Self {
191 inner,
192 timezone,
193 timezone_failed,
194 sample: std::sync::RwLock::new(sample),
195 }
196 }
197
198 pub fn refresh(&self) {
201 let now = crate::function::catch_function_panic("resolver:clock", || self.inner.now());
203 let now = if self.timezone_failed {
204 crate::function::catch_function_panic("resolver:clock", || self.inner.timezone())
205 .and(now)
206 } else {
207 now
208 };
209 *self.sample.write().expect("clock sample lock poisoned") = now;
210 }
211}
212
213impl std::fmt::Debug for SnapshotClock {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 f.debug_struct("SnapshotClock")
216 .field("timezone", &self.timezone)
217 .finish_non_exhaustive()
218 }
219}
220impl ClockProvider for SnapshotClock {
221 fn timezone(&self) -> &TimeZoneSpec {
222 if self.timezone_failed {
223 match crate::function::catch_function_panic("resolver:clock", || self.inner.timezone())
224 {
225 Ok(timezone) => timezone,
226 Err(error) => {
227 crate::external_callbacks::clock_failure(error);
228 &self.timezone
229 }
230 }
231 } else {
232 &self.timezone
233 }
234 }
235
236 fn now(&self) -> NaiveDateTime {
237 let sample = self
238 .sample
239 .read()
240 .expect("clock sample lock poisoned")
241 .clone();
242 match sample {
243 Ok(now) => now,
244 Err(error) => {
245 crate::external_callbacks::clock_failure(error);
246 chrono::DateTime::UNIX_EPOCH.naive_utc()
247 }
248 }
249 }
250}