formualizer_eval/
timezone.rs1use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
6
7#[derive(Clone, Debug, Default, Eq, PartialEq)]
11pub enum TimeZoneSpec {
12 #[default]
14 Local,
15 Utc,
17 FixedOffsetSeconds(i32),
21 }
23
24impl TimeZoneSpec {
27 pub fn fixed_offset(&self) -> Option<FixedOffset> {
28 match self {
29 TimeZoneSpec::Utc => FixedOffset::east_opt(0),
30 TimeZoneSpec::FixedOffsetSeconds(secs) => FixedOffset::east_opt(*secs),
31 TimeZoneSpec::Local => None,
32 }
33 }
34
35 pub fn validate_for_determinism(&self) -> Result<(), String> {
36 match self {
37 TimeZoneSpec::Local => Err(
38 "Deterministic mode forbids `Local` timezone (use UTC or a fixed offset)"
39 .to_string(),
40 ),
41 TimeZoneSpec::Utc => Ok(()),
42 TimeZoneSpec::FixedOffsetSeconds(secs) => {
43 FixedOffset::east_opt(*secs).ok_or_else(|| {
44 format!("Invalid fixed offset: {secs} seconds (must be within +/-24h)")
45 })?;
46 Ok(())
47 }
48 }
49 }
50}
51
52pub trait ClockProvider: std::fmt::Debug + Send + Sync {
54 fn timezone(&self) -> &TimeZoneSpec;
55 fn now(&self) -> NaiveDateTime;
56 fn today(&self) -> NaiveDate {
57 self.now().date()
58 }
59}
60
61#[derive(Clone, Debug)]
63pub struct SystemClock {
64 timezone: TimeZoneSpec,
65}
66
67impl SystemClock {
68 pub fn new(timezone: TimeZoneSpec) -> Self {
69 Self { timezone }
70 }
71}
72
73impl ClockProvider for SystemClock {
74 fn timezone(&self) -> &TimeZoneSpec {
75 &self.timezone
76 }
77
78 fn now(&self) -> NaiveDateTime {
79 match &self.timezone {
80 TimeZoneSpec::Local => Local::now().naive_local(),
81 TimeZoneSpec::Utc => Utc::now().naive_utc(),
82 TimeZoneSpec::FixedOffsetSeconds(secs) => {
83 let off = FixedOffset::east_opt(*secs)
84 .unwrap_or_else(|| FixedOffset::east_opt(0).unwrap());
85 let utc_now: DateTime<Utc> = Utc::now();
86 utc_now.with_timezone(&off).naive_local()
87 }
88 }
89 }
90}
91
92#[derive(Clone, Debug)]
94pub struct FixedClock {
95 timestamp_utc: DateTime<Utc>,
96 timezone: TimeZoneSpec,
97}
98
99impl FixedClock {
100 pub fn new(timestamp_utc: DateTime<Utc>, timezone: TimeZoneSpec) -> Self {
101 Self {
102 timestamp_utc,
103 timezone,
104 }
105 }
106
107 pub fn new_deterministic(
108 timestamp_utc: DateTime<Utc>,
109 timezone: TimeZoneSpec,
110 ) -> Result<Self, String> {
111 timezone.validate_for_determinism()?;
112 Ok(Self::new(timestamp_utc, timezone))
113 }
114
115 fn now_in_timezone(&self) -> NaiveDateTime {
116 match &self.timezone {
117 TimeZoneSpec::Utc => self.timestamp_utc.naive_utc(),
118 TimeZoneSpec::FixedOffsetSeconds(secs) => {
119 let off = FixedOffset::east_opt(*secs).expect("validated fixed offset");
120 self.timestamp_utc.with_timezone(&off).naive_local()
121 }
122 TimeZoneSpec::Local => {
123 self.timestamp_utc.with_timezone(&Local).naive_local()
125 }
126 }
127 }
128}
129
130impl ClockProvider for FixedClock {
131 fn timezone(&self) -> &TimeZoneSpec {
132 &self.timezone
133 }
134
135 fn now(&self) -> NaiveDateTime {
136 self.now_in_timezone()
137 }
138}