use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta};
use super::conversion::{
tc_millis_to_unix_millis, td_days_to_unix_days, th_halves_to_year_half,
tm_months_to_year_month, tq_quarters_to_year_quarter, tw_weeks_to_year_week,
};
use super::kind::TemporalKind;
use crate::stata::dta::value::Value;
#[must_use]
pub fn naive_date_from_td_days(stata_days: i32) -> Option<NaiveDate> {
let unix_days = td_days_to_unix_days(stata_days)?;
let unix_epoch = NaiveDate::from_ymd_opt(1970, 1, 1)?;
unix_epoch.checked_add_signed(TimeDelta::days(i64::from(unix_days)))
}
#[must_use]
pub fn naive_date_time_from_tc_millis(stata_millis: f64) -> Option<NaiveDateTime> {
let unix_millis = tc_millis_to_unix_millis(stata_millis)?;
DateTime::from_timestamp_millis(unix_millis).map(|dt| dt.naive_utc())
}
#[must_use]
pub fn naive_date_from_value(value: &Value<'_>) -> Option<NaiveDate> {
naive_date_from_td_days(extract_stata_int32(value)?)
}
fn extract_stata_int32(value: &Value<'_>) -> Option<i32> {
match value {
Value::Byte(byte_value) => byte_value.present().map(i32::from),
Value::Int(int_value) => int_value.present().map(i32::from),
Value::Long(long_value) => long_value.present(),
Value::Float(_) | Value::Double(_) | Value::String(_) | Value::LongStringRef(_) => None,
}
}
#[must_use]
pub fn naive_date_time_from_value(value: &Value<'_>) -> Option<NaiveDateTime> {
naive_date_time_from_tc_millis(extract_stata_float64(value)?)
}
fn extract_stata_float64(value: &Value<'_>) -> Option<f64> {
match value {
Value::Double(double_value) => double_value.present(),
Value::Float(float_value) => float_value.present().map(f64::from),
Value::Byte(_)
| Value::Int(_)
| Value::Long(_)
| Value::String(_)
| Value::LongStringRef(_) => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum StataTemporal {
Date(NaiveDate),
DateTime(NaiveDateTime),
Year(i32),
YearMonth {
year: i32,
month: u8,
},
YearQuarter {
year: i32,
quarter: u8,
},
YearHalf {
year: i32,
half: u8,
},
YearWeek {
year: i32,
week: u8,
},
}
#[must_use]
pub fn temporal_from_value(value: &Value<'_>, format: &str) -> Option<StataTemporal> {
match TemporalKind::from_format(format)? {
TemporalKind::Date => naive_date_from_value(value).map(StataTemporal::Date),
TemporalKind::DateTime => naive_date_time_from_value(value).map(StataTemporal::DateTime),
TemporalKind::DateTimeLeap => None,
TemporalKind::Year => extract_stata_int32(value).map(StataTemporal::Year),
TemporalKind::Month => {
let (year, month) = tm_months_to_year_month(extract_stata_int32(value)?);
Some(StataTemporal::YearMonth { year, month })
}
TemporalKind::Quarter => {
let (year, quarter) = tq_quarters_to_year_quarter(extract_stata_int32(value)?);
Some(StataTemporal::YearQuarter { year, quarter })
}
TemporalKind::HalfYear => {
let (year, half) = th_halves_to_year_half(extract_stata_int32(value)?);
Some(StataTemporal::YearHalf { year, half })
}
TemporalKind::Week => {
let (year, week) = tw_weeks_to_year_week(extract_stata_int32(value)?);
Some(StataTemporal::YearWeek { year, week })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn td_zero_is_stata_epoch() {
assert_eq!(
naive_date_from_td_days(0),
Some(NaiveDate::from_ymd_opt(1960, 1, 1).unwrap()),
);
}
#[test]
fn td_3653_is_unix_epoch() {
assert_eq!(
naive_date_from_td_days(3653),
Some(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()),
);
}
#[test]
fn td_one_is_january_2_1960() {
assert_eq!(
naive_date_from_td_days(1),
Some(NaiveDate::from_ymd_opt(1960, 1, 2).unwrap()),
);
}
#[test]
fn td_handles_1960_leap_day() {
assert_eq!(
naive_date_from_td_days(59),
Some(NaiveDate::from_ymd_opt(1960, 2, 29).unwrap()),
);
}
#[test]
fn td_negative_is_pre_epoch() {
assert_eq!(
naive_date_from_td_days(-1),
Some(NaiveDate::from_ymd_opt(1959, 12, 31).unwrap()),
);
}
#[test]
fn td_modern_date() {
let expected = NaiveDate::from_ymd_opt(2026, 4, 24).unwrap();
let stata_epoch = NaiveDate::from_ymd_opt(1960, 1, 1).unwrap();
let stata_days =
i32::try_from(expected.signed_duration_since(stata_epoch).num_days()).unwrap();
assert_eq!(naive_date_from_td_days(stata_days), Some(expected));
}
#[test]
fn td_overflow_at_layer_one_propagates() {
assert_eq!(naive_date_from_td_days(i32::MIN), None);
}
#[test]
fn td_extreme_positive_falls_outside_chrono_range() {
assert_eq!(naive_date_from_td_days(i32::MAX), None);
}
#[test]
fn tc_zero_is_stata_epoch_midnight() {
let expected = NaiveDate::from_ymd_opt(1960, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
assert_eq!(naive_date_time_from_tc_millis(0.0), Some(expected));
}
#[test]
fn tc_one_second_after_epoch() {
let expected = NaiveDate::from_ymd_opt(1960, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 1)
.unwrap();
assert_eq!(naive_date_time_from_tc_millis(1000.0), Some(expected));
}
#[test]
fn tc_with_subsecond_milliseconds() {
let expected = NaiveDate::from_ymd_opt(1960, 1, 1)
.unwrap()
.and_hms_milli_opt(0, 0, 0, 123)
.unwrap();
assert_eq!(naive_date_time_from_tc_millis(123.0), Some(expected));
}
#[test]
fn tc_unix_epoch() {
let unix_epoch = NaiveDate::from_ymd_opt(1970, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
let stata_millis_at_unix_epoch = 3653.0_f64 * 86_400_000.0;
assert_eq!(
naive_date_time_from_tc_millis(stata_millis_at_unix_epoch),
Some(unix_epoch),
);
}
#[test]
fn tc_pre_epoch() {
let expected = NaiveDate::from_ymd_opt(1959, 12, 31)
.unwrap()
.and_hms_opt(23, 59, 59)
.unwrap();
assert_eq!(naive_date_time_from_tc_millis(-1000.0), Some(expected),);
}
#[test]
fn tc_nan_is_none() {
assert_eq!(naive_date_time_from_tc_millis(f64::NAN), None);
}
#[test]
fn tc_positive_infinity_is_none() {
assert_eq!(naive_date_time_from_tc_millis(f64::INFINITY), None);
}
#[test]
fn tc_negative_infinity_is_none() {
assert_eq!(naive_date_time_from_tc_millis(f64::NEG_INFINITY), None);
}
#[test]
fn tc_above_i64_range_is_none() {
assert_eq!(naive_date_time_from_tc_millis(9.3e18), None,);
}
#[test]
fn tc_above_chrono_range_but_within_i64_is_none() {
let beyond_chrono = 1.0e16_f64; assert_eq!(naive_date_time_from_tc_millis(beyond_chrono), None);
}
mod date_from_value {
use super::super::*;
use crate::stata::dta::long_string_ref::LongStringRef;
use crate::stata::missing_value::MissingValue;
use crate::stata::stata_byte::StataByte;
use crate::stata::stata_double::StataDouble;
use crate::stata::stata_float::StataFloat;
use crate::stata::stata_int::StataInt;
use crate::stata::stata_long::StataLong;
fn epoch() -> NaiveDate {
NaiveDate::from_ymd_opt(1960, 1, 1).unwrap()
}
#[test]
fn long_present_zero_is_epoch() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(naive_date_from_value(&value), Some(epoch()));
}
#[test]
fn long_present_nonzero() {
let value = Value::Long(StataLong::Present(366));
assert_eq!(
naive_date_from_value(&value),
Some(NaiveDate::from_ymd_opt(1961, 1, 1).unwrap()),
);
}
#[test]
fn long_present_negative_pre_epoch() {
let value = Value::Long(StataLong::Present(-1));
assert_eq!(
naive_date_from_value(&value),
Some(NaiveDate::from_ymd_opt(1959, 12, 31).unwrap()),
);
}
#[test]
fn int_present_widens_to_i32() {
let value = Value::Int(StataInt::Present(366));
assert_eq!(
naive_date_from_value(&value),
Some(NaiveDate::from_ymd_opt(1961, 1, 1).unwrap()),
);
}
#[test]
fn byte_present_widens_to_i32() {
let value = Value::Byte(StataByte::Present(31));
assert_eq!(
naive_date_from_value(&value),
Some(NaiveDate::from_ymd_opt(1960, 2, 1).unwrap()),
);
}
#[test]
fn long_missing_system_is_none() {
let value = Value::Long(StataLong::Missing(MissingValue::System));
assert_eq!(naive_date_from_value(&value), None);
}
#[test]
fn long_missing_tagged_is_none() {
let value = Value::Long(StataLong::Missing(MissingValue::A));
assert_eq!(naive_date_from_value(&value), None);
}
#[test]
fn int_missing_is_none() {
let value = Value::Int(StataInt::Missing(MissingValue::System));
assert_eq!(naive_date_from_value(&value), None);
}
#[test]
fn byte_missing_is_none() {
let value = Value::Byte(StataByte::Missing(MissingValue::System));
assert_eq!(naive_date_from_value(&value), None);
}
#[test]
fn float_present_is_none() {
let value = Value::Float(StataFloat::Present(0.0));
assert_eq!(naive_date_from_value(&value), None);
}
#[test]
fn double_present_is_none() {
let value = Value::Double(StataDouble::Present(0.0));
assert_eq!(naive_date_from_value(&value), None);
}
#[test]
fn string_is_none() {
let value = Value::string("not a date");
assert_eq!(naive_date_from_value(&value), None);
}
#[test]
fn long_string_ref_is_none() {
let value = Value::LongStringRef(LongStringRef::new(1, 1));
assert_eq!(naive_date_from_value(&value), None);
}
#[test]
fn long_present_extreme_returns_none_via_chrono_range() {
let value = Value::Long(StataLong::Present(i32::MAX));
assert_eq!(naive_date_from_value(&value), None);
}
}
mod date_time_from_value {
use super::super::*;
use crate::stata::dta::long_string_ref::LongStringRef;
use crate::stata::missing_value::MissingValue;
use crate::stata::stata_byte::StataByte;
use crate::stata::stata_double::StataDouble;
use crate::stata::stata_float::StataFloat;
use crate::stata::stata_int::StataInt;
use crate::stata::stata_long::StataLong;
fn epoch_midnight() -> NaiveDateTime {
NaiveDate::from_ymd_opt(1960, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
}
#[test]
fn double_present_zero_is_epoch_midnight() {
let value = Value::Double(StataDouble::Present(0.0));
assert_eq!(naive_date_time_from_value(&value), Some(epoch_midnight()));
}
#[test]
fn double_present_one_second() {
let value = Value::Double(StataDouble::Present(1000.0));
let expected = NaiveDate::from_ymd_opt(1960, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 1)
.unwrap();
assert_eq!(naive_date_time_from_value(&value), Some(expected));
}
#[test]
fn float_present_widens_losslessly() {
let value = Value::Float(StataFloat::Present(1000.0));
let expected = NaiveDate::from_ymd_opt(1960, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 1)
.unwrap();
assert_eq!(naive_date_time_from_value(&value), Some(expected));
}
#[test]
fn double_missing_system_is_none() {
let value = Value::Double(StataDouble::Missing(MissingValue::System));
assert_eq!(naive_date_time_from_value(&value), None);
}
#[test]
fn double_missing_tagged_is_none() {
let value = Value::Double(StataDouble::Missing(MissingValue::Z));
assert_eq!(naive_date_time_from_value(&value), None);
}
#[test]
fn float_missing_is_none() {
let value = Value::Float(StataFloat::Missing(MissingValue::System));
assert_eq!(naive_date_time_from_value(&value), None);
}
#[test]
fn double_nan_present_is_none() {
let value = Value::Double(StataDouble::Present(f64::NAN));
assert_eq!(naive_date_time_from_value(&value), None);
}
#[test]
fn long_present_is_none() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(naive_date_time_from_value(&value), None);
}
#[test]
fn int_present_is_none() {
let value = Value::Int(StataInt::Present(0));
assert_eq!(naive_date_time_from_value(&value), None);
}
#[test]
fn byte_present_is_none() {
let value = Value::Byte(StataByte::Present(0));
assert_eq!(naive_date_time_from_value(&value), None);
}
#[test]
fn string_is_none() {
let value = Value::string("not a datetime");
assert_eq!(naive_date_time_from_value(&value), None);
}
#[test]
fn long_string_ref_is_none() {
let value = Value::LongStringRef(LongStringRef::new(1, 1));
assert_eq!(naive_date_time_from_value(&value), None);
}
}
mod dispatcher {
use super::super::*;
use crate::stata::missing_value::MissingValue;
use crate::stata::stata_double::StataDouble;
use crate::stata::stata_int::StataInt;
use crate::stata::stata_long::StataLong;
#[test]
fn td_dispatches_to_date() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(
temporal_from_value(&value, "%td"),
Some(StataTemporal::Date(
NaiveDate::from_ymd_opt(1960, 1, 1).unwrap()
)),
);
}
#[test]
fn td_with_display_suffix_still_dispatches() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(
temporal_from_value(&value, "%tdCCYY-NN-DD"),
Some(StataTemporal::Date(
NaiveDate::from_ymd_opt(1960, 1, 1).unwrap()
)),
);
}
#[test]
fn legacy_d_dispatches_to_date() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(
temporal_from_value(&value, "%d"),
Some(StataTemporal::Date(
NaiveDate::from_ymd_opt(1960, 1, 1).unwrap()
)),
);
}
#[test]
fn tc_dispatches_to_datetime() {
let value = Value::Double(StataDouble::Present(0.0));
let expected = NaiveDate::from_ymd_opt(1960, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
assert_eq!(
temporal_from_value(&value, "%tc"),
Some(StataTemporal::DateTime(expected)),
);
}
#[test]
fn tc_leap_returns_none() {
let value = Value::Double(StataDouble::Present(0.0));
assert_eq!(temporal_from_value(&value, "%tC"), None);
}
#[test]
fn ty_dispatches_to_year() {
let value = Value::Int(StataInt::Present(2026));
assert_eq!(
temporal_from_value(&value, "%ty"),
Some(StataTemporal::Year(2026)),
);
}
#[test]
fn tm_dispatches_to_year_month() {
let value = Value::Int(StataInt::Present(0));
assert_eq!(
temporal_from_value(&value, "%tm"),
Some(StataTemporal::YearMonth {
year: 1960,
month: 1
}),
);
}
#[test]
fn tm_negative_offset_decomposes_correctly() {
let value = Value::Int(StataInt::Present(-1));
assert_eq!(
temporal_from_value(&value, "%tm"),
Some(StataTemporal::YearMonth {
year: 1959,
month: 12
}),
);
}
#[test]
fn tq_dispatches_to_year_quarter() {
let value = Value::Int(StataInt::Present(5));
assert_eq!(
temporal_from_value(&value, "%tq"),
Some(StataTemporal::YearQuarter {
year: 1961,
quarter: 2
}),
);
}
#[test]
fn th_dispatches_to_year_half() {
let value = Value::Int(StataInt::Present(3));
assert_eq!(
temporal_from_value(&value, "%th"),
Some(StataTemporal::YearHalf {
year: 1961,
half: 2
}),
);
}
#[test]
fn tw_dispatches_to_year_week() {
let value = Value::Int(StataInt::Present(52));
assert_eq!(
temporal_from_value(&value, "%tw"),
Some(StataTemporal::YearWeek {
year: 1961,
week: 1
}),
);
}
#[test]
fn numeric_format_returns_none() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(temporal_from_value(&value, "%9.0g"), None);
}
#[test]
fn string_format_returns_none() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(temporal_from_value(&value, "%-12s"), None);
}
#[test]
fn empty_format_returns_none() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(temporal_from_value(&value, ""), None);
}
#[test]
fn malformed_format_returns_none() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(temporal_from_value(&value, "%t"), None);
assert_eq!(temporal_from_value(&value, "%tx"), None);
}
#[test]
fn missing_long_with_td_returns_none() {
let value = Value::Long(StataLong::Missing(MissingValue::System));
assert_eq!(temporal_from_value(&value, "%td"), None);
}
#[test]
fn missing_double_with_tc_returns_none() {
let value = Value::Double(StataDouble::Missing(MissingValue::A));
assert_eq!(temporal_from_value(&value, "%tc"), None);
}
#[test]
fn missing_int_with_tm_returns_none() {
let value = Value::Int(StataInt::Missing(MissingValue::System));
assert_eq!(temporal_from_value(&value, "%tm"), None);
}
#[test]
fn double_with_td_returns_none() {
let value = Value::Double(StataDouble::Present(0.0));
assert_eq!(temporal_from_value(&value, "%td"), None);
}
#[test]
fn long_with_tc_returns_none() {
let value = Value::Long(StataLong::Present(0));
assert_eq!(temporal_from_value(&value, "%tc"), None);
}
#[test]
fn double_with_tm_returns_none() {
let value = Value::Double(StataDouble::Present(0.0));
assert_eq!(temporal_from_value(&value, "%tm"), None);
}
}
}