#[cfg(feature = "system-clock")]
use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
#[cfg(not(feature = "system-clock"))]
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, Utc};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum TimeZoneSpec {
#[default]
Local,
Utc,
FixedOffsetSeconds(i32),
}
impl TimeZoneSpec {
pub fn fixed_offset(&self) -> Option<FixedOffset> {
match self {
TimeZoneSpec::Utc => FixedOffset::east_opt(0),
TimeZoneSpec::FixedOffsetSeconds(secs) => FixedOffset::east_opt(*secs),
TimeZoneSpec::Local => None,
}
}
pub fn validate_for_determinism(&self) -> Result<(), String> {
match self {
TimeZoneSpec::Local => Err(
"Deterministic mode forbids `Local` timezone (use UTC or a fixed offset)"
.to_string(),
),
TimeZoneSpec::Utc => Ok(()),
TimeZoneSpec::FixedOffsetSeconds(secs) => {
FixedOffset::east_opt(*secs).ok_or_else(|| {
format!("Invalid fixed offset: {secs} seconds (must be within +/-24h)")
})?;
Ok(())
}
}
}
}
pub trait ClockProvider: std::fmt::Debug + Send + Sync {
fn timezone(&self) -> &TimeZoneSpec;
fn now(&self) -> NaiveDateTime;
fn today(&self) -> NaiveDate {
self.now().date()
}
}
#[cfg(feature = "system-clock")]
#[derive(Clone, Debug)]
pub struct SystemClock {
timezone: TimeZoneSpec,
}
#[cfg(feature = "system-clock")]
impl SystemClock {
pub fn new(timezone: TimeZoneSpec) -> Self {
Self { timezone }
}
}
#[cfg(feature = "system-clock")]
impl ClockProvider for SystemClock {
fn timezone(&self) -> &TimeZoneSpec {
&self.timezone
}
fn now(&self) -> NaiveDateTime {
match &self.timezone {
TimeZoneSpec::Local => Local::now().naive_local(),
TimeZoneSpec::Utc => Utc::now().naive_utc(),
TimeZoneSpec::FixedOffsetSeconds(secs) => {
let off = FixedOffset::east_opt(*secs)
.unwrap_or_else(|| FixedOffset::east_opt(0).unwrap());
let utc_now: DateTime<Utc> = Utc::now();
utc_now.with_timezone(&off).naive_local()
}
}
}
}
#[derive(Clone, Debug)]
pub struct FixedClock {
timestamp_utc: DateTime<Utc>,
timezone: TimeZoneSpec,
}
impl FixedClock {
pub fn new(timestamp_utc: DateTime<Utc>, timezone: TimeZoneSpec) -> Self {
Self {
timestamp_utc,
timezone,
}
}
pub fn new_deterministic(
timestamp_utc: DateTime<Utc>,
timezone: TimeZoneSpec,
) -> Result<Self, String> {
timezone.validate_for_determinism()?;
Ok(Self::new(timestamp_utc, timezone))
}
fn now_in_timezone(&self) -> NaiveDateTime {
match &self.timezone {
TimeZoneSpec::Utc => self.timestamp_utc.naive_utc(),
TimeZoneSpec::FixedOffsetSeconds(secs) => {
let off = FixedOffset::east_opt(*secs).expect("validated fixed offset");
self.timestamp_utc.with_timezone(&off).naive_local()
}
TimeZoneSpec::Local => {
#[cfg(feature = "system-clock")]
{
self.timestamp_utc.with_timezone(&Local).naive_local()
}
#[cfg(not(feature = "system-clock"))]
{
self.timestamp_utc.naive_utc()
}
}
}
}
}
impl ClockProvider for FixedClock {
fn timezone(&self) -> &TimeZoneSpec {
&self.timezone
}
fn now(&self) -> NaiveDateTime {
self.now_in_timezone()
}
}
pub struct SnapshotClock {
inner: std::sync::Arc<dyn ClockProvider>,
timezone: TimeZoneSpec,
timezone_failed: bool,
sample: std::sync::RwLock<Result<NaiveDateTime, formualizer_common::ExcelError>>,
}
impl SnapshotClock {
pub fn new(inner: std::sync::Arc<dyn ClockProvider>) -> Self {
let timezone = crate::function::catch_function_panic("resolver:clock", || inner.timezone());
let timezone_failed = timezone.is_err();
let now = crate::function::catch_function_panic("resolver:clock", || inner.now());
let sample = timezone.as_ref().map_err(Clone::clone).and(now);
let timezone = timezone.cloned().unwrap_or(TimeZoneSpec::Utc);
Self {
inner,
timezone,
timezone_failed,
sample: std::sync::RwLock::new(sample),
}
}
pub fn refresh(&self) {
let now = crate::function::catch_function_panic("resolver:clock", || self.inner.now());
let now = if self.timezone_failed {
crate::function::catch_function_panic("resolver:clock", || self.inner.timezone())
.and(now)
} else {
now
};
*self.sample.write().expect("clock sample lock poisoned") = now;
}
}
impl std::fmt::Debug for SnapshotClock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SnapshotClock")
.field("timezone", &self.timezone)
.finish_non_exhaustive()
}
}
impl ClockProvider for SnapshotClock {
fn timezone(&self) -> &TimeZoneSpec {
if self.timezone_failed {
match crate::function::catch_function_panic("resolver:clock", || self.inner.timezone())
{
Ok(timezone) => timezone,
Err(error) => {
crate::external_callbacks::clock_failure(error);
&self.timezone
}
}
} else {
&self.timezone
}
}
fn now(&self) -> NaiveDateTime {
let sample = self
.sample
.read()
.expect("clock sample lock poisoned")
.clone();
match sample {
Ok(now) => now,
Err(error) => {
crate::external_callbacks::clock_failure(error);
chrono::DateTime::UNIX_EPOCH.naive_utc()
}
}
}
}