pub const STATA_EPOCH_UNIX_DAYS: i32 = -3653;
pub const STATA_EPOCH_UNIX_MILLIS: i64 = (STATA_EPOCH_UNIX_DAYS as i64) * 86_400_000;
#[must_use]
#[inline]
pub fn td_days_to_unix_days(stata_days: i32) -> Option<i32> {
stata_days.checked_add(STATA_EPOCH_UNIX_DAYS)
}
#[must_use]
pub fn tc_millis_to_unix_millis(stata_millis: f64) -> Option<i64> {
if !stata_millis.is_finite() {
return None;
}
let epoch_offset = f64::from(STATA_EPOCH_UNIX_DAYS) * 86_400_000.0;
let unix = stata_millis + epoch_offset;
let rounded = unix.round();
let i64_max_plus_one = 9_223_372_036_854_775_808.0_f64;
if rounded < -i64_max_plus_one || rounded >= i64_max_plus_one {
return None;
}
#[allow(clippy::cast_possible_truncation)]
Some(rounded as i64)
}
#[must_use]
pub fn tm_months_to_year_month(months_since_1960: i32) -> (i32, u8) {
decompose_period(months_since_1960, 12)
}
#[must_use]
pub fn tq_quarters_to_year_quarter(quarters_since_1960: i32) -> (i32, u8) {
decompose_period(quarters_since_1960, 4)
}
#[must_use]
pub fn th_halves_to_year_half(halves_since_1960: i32) -> (i32, u8) {
decompose_period(halves_since_1960, 2)
}
#[must_use]
pub fn tw_weeks_to_year_week(weeks_since_1960: i32) -> (i32, u8) {
decompose_period(weeks_since_1960, 52)
}
fn decompose_period(periods_since_1960: i32, periods_per_year: i32) -> (i32, u8) {
debug_assert!(
(1..=255).contains(&periods_per_year),
"periods_per_year must fit in u8 to support 1-indexed sub-period",
);
let year_offset = periods_since_1960.div_euclid(periods_per_year);
let sub_period_zero_indexed = periods_since_1960.rem_euclid(periods_per_year);
let year = year_offset.saturating_add(1960);
let sub_period = u8::try_from(sub_period_zero_indexed)
.expect("rem_euclid result fits in u8 for periods_per_year <= 255");
(year, sub_period + 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn epoch_days_constant() {
assert_eq!(STATA_EPOCH_UNIX_DAYS, -3653);
}
#[test]
fn epoch_millis_matches_days() {
assert_eq!(
STATA_EPOCH_UNIX_MILLIS,
i64::from(STATA_EPOCH_UNIX_DAYS) * 86_400_000,
);
}
#[test]
fn td_zero_is_stata_epoch() {
assert_eq!(td_days_to_unix_days(0), Some(STATA_EPOCH_UNIX_DAYS));
}
#[test]
fn td_3653_is_unix_epoch() {
assert_eq!(td_days_to_unix_days(3653), Some(0));
}
#[test]
fn td_negative_pre_epoch() {
assert_eq!(td_days_to_unix_days(-18262), Some(-21915));
}
#[test]
fn td_overflow_at_extreme_negative() {
assert_eq!(td_days_to_unix_days(i32::MIN), None);
assert_eq!(td_days_to_unix_days(i32::MIN + 3652), None);
assert_eq!(td_days_to_unix_days(i32::MIN + 3653), Some(i32::MIN));
}
#[test]
fn td_no_overflow_at_extreme_positive() {
assert_eq!(
td_days_to_unix_days(i32::MAX),
Some(i32::MAX + STATA_EPOCH_UNIX_DAYS),
);
}
#[test]
fn tc_zero_is_stata_epoch_in_ms() {
assert_eq!(tc_millis_to_unix_millis(0.0), Some(STATA_EPOCH_UNIX_MILLIS));
}
#[test]
fn tc_one_day_after_epoch() {
let one_day_ms = 86_400_000.0_f64;
assert_eq!(
tc_millis_to_unix_millis(one_day_ms),
Some(STATA_EPOCH_UNIX_MILLIS + 86_400_000),
);
}
#[test]
fn tc_3653_days_is_unix_epoch() {
let unix_epoch_in_stata_ms = 3653.0_f64 * 86_400_000.0;
assert_eq!(tc_millis_to_unix_millis(unix_epoch_in_stata_ms), Some(0));
}
#[test]
fn tc_rounds_fractional_to_nearest_even() {
assert_eq!(tc_millis_to_unix_millis(0.4), Some(STATA_EPOCH_UNIX_MILLIS));
assert_eq!(
tc_millis_to_unix_millis(0.6),
Some(STATA_EPOCH_UNIX_MILLIS + 1),
);
}
#[test]
fn tc_nan_is_none() {
assert_eq!(tc_millis_to_unix_millis(f64::NAN), None);
}
#[test]
fn tc_positive_infinity_is_none() {
assert_eq!(tc_millis_to_unix_millis(f64::INFINITY), None);
}
#[test]
fn tc_negative_infinity_is_none() {
assert_eq!(tc_millis_to_unix_millis(f64::NEG_INFINITY), None);
}
#[test]
fn tc_above_i64_range_is_none() {
let too_big = 9_300_000_000_000_000_000.0_f64;
assert_eq!(tc_millis_to_unix_millis(too_big), None);
}
#[test]
fn tc_below_i64_range_is_none() {
let too_small = -9_300_000_000_000_000_000.0_f64;
assert_eq!(tc_millis_to_unix_millis(too_small), None);
}
#[test]
fn tc_negative_pre_epoch() {
let before_epoch = -86_400_000.0_f64;
assert_eq!(
tc_millis_to_unix_millis(before_epoch),
Some(STATA_EPOCH_UNIX_MILLIS - 86_400_000),
);
}
#[test]
fn tm_zero_is_january_1960() {
assert_eq!(tm_months_to_year_month(0), (1960, 1));
}
#[test]
fn tm_eleven_is_december_1960() {
assert_eq!(tm_months_to_year_month(11), (1960, 12));
}
#[test]
fn tm_twelve_is_january_1961() {
assert_eq!(tm_months_to_year_month(12), (1961, 1));
}
#[test]
fn tm_negative_one_is_december_1959() {
assert_eq!(tm_months_to_year_month(-1), (1959, 12));
}
#[test]
fn tm_negative_twelve_is_january_1959() {
assert_eq!(tm_months_to_year_month(-12), (1959, 1));
}
#[test]
fn tm_negative_thirteen_is_december_1958() {
assert_eq!(tm_months_to_year_month(-13), (1958, 12));
}
#[test]
fn tm_extreme_positive_no_overflow() {
let (year, month) = tm_months_to_year_month(i32::MAX);
assert!((1..=12).contains(&month));
assert!(year > 1960);
}
#[test]
fn tm_extreme_negative_no_overflow() {
let (year, month) = tm_months_to_year_month(i32::MIN);
assert!((1..=12).contains(&month));
assert!(year < 1960);
}
#[test]
fn tq_zero_is_q1_1960() {
assert_eq!(tq_quarters_to_year_quarter(0), (1960, 1));
}
#[test]
fn tq_three_is_q4_1960() {
assert_eq!(tq_quarters_to_year_quarter(3), (1960, 4));
}
#[test]
fn tq_four_is_q1_1961() {
assert_eq!(tq_quarters_to_year_quarter(4), (1961, 1));
}
#[test]
fn tq_negative_one_is_q4_1959() {
assert_eq!(tq_quarters_to_year_quarter(-1), (1959, 4));
}
#[test]
fn tq_negative_four_is_q1_1959() {
assert_eq!(tq_quarters_to_year_quarter(-4), (1959, 1));
}
#[test]
fn th_zero_is_h1_1960() {
assert_eq!(th_halves_to_year_half(0), (1960, 1));
}
#[test]
fn th_one_is_h2_1960() {
assert_eq!(th_halves_to_year_half(1), (1960, 2));
}
#[test]
fn th_two_is_h1_1961() {
assert_eq!(th_halves_to_year_half(2), (1961, 1));
}
#[test]
fn th_negative_one_is_h2_1959() {
assert_eq!(th_halves_to_year_half(-1), (1959, 2));
}
#[test]
fn tw_zero_is_week1_1960() {
assert_eq!(tw_weeks_to_year_week(0), (1960, 1));
}
#[test]
fn tw_fifty_one_is_week52_1960() {
assert_eq!(tw_weeks_to_year_week(51), (1960, 52));
}
#[test]
fn tw_fifty_two_is_week1_1961() {
assert_eq!(tw_weeks_to_year_week(52), (1961, 1));
}
#[test]
fn tw_negative_one_is_week52_1959() {
assert_eq!(tw_weeks_to_year_week(-1), (1959, 52));
}
#[test]
fn tw_negative_fifty_two_is_week1_1959() {
assert_eq!(tw_weeks_to_year_week(-52), (1959, 1));
}
}