use chrono::{Datelike, NaiveDate, NaiveTime, TimeZone, Utc};
use thiserror::Error;
const THINGS_EPOCH_YEAR: i32 = 2001;
const MAX_YEAR: i32 = 2100;
const MIN_REASONABLE_TIMESTAMP: i64 = -31536000;
const MAX_REASONABLE_TIMESTAMP: i64 = 3_124_224_000;
#[derive(Debug, Error, Clone, PartialEq)]
pub enum DateConversionError {
#[error("Date is before Things 3 epoch (2001-01-01): {0}")]
BeforeEpoch(NaiveDate),
#[error("Date timestamp {0} is invalid or would cause overflow")]
InvalidTimestamp(i64),
#[error("Date is too far in the future (after year {MAX_YEAR}): {0}")]
TooFarFuture(NaiveDate),
#[error("Date conversion overflow during calculation")]
Overflow,
#[error("Failed to parse date string '{string}': {reason}")]
ParseError { string: String, reason: String },
}
#[derive(Debug, Error, Clone, PartialEq)]
pub enum DateValidationError {
#[error("Deadline {deadline} cannot be before start date {start_date}")]
DeadlineBeforeStartDate {
start_date: NaiveDate,
deadline: NaiveDate,
},
#[error("Date conversion failed: {0}")]
ConversionFailed(#[from] DateConversionError),
}
pub fn is_valid_things_timestamp(seconds: i64) -> bool {
(MIN_REASONABLE_TIMESTAMP..=MAX_REASONABLE_TIMESTAMP).contains(&seconds)
}
pub fn safe_things_date_to_naive_date(
seconds_since_2001: i64,
) -> Result<NaiveDate, DateConversionError> {
if !is_valid_things_timestamp(seconds_since_2001) {
return Err(DateConversionError::InvalidTimestamp(seconds_since_2001));
}
let base_date = Utc
.with_ymd_and_hms(THINGS_EPOCH_YEAR, 1, 1, 0, 0, 0)
.single()
.ok_or(DateConversionError::Overflow)?;
let date_time = base_date
.checked_add_signed(chrono::Duration::seconds(seconds_since_2001))
.ok_or(DateConversionError::Overflow)?;
let naive_date = date_time.date_naive();
if naive_date.year() > MAX_YEAR {
return Err(DateConversionError::TooFarFuture(naive_date));
}
Ok(naive_date)
}
pub fn safe_naive_date_to_things_timestamp(date: NaiveDate) -> Result<i64, DateConversionError> {
let epoch_date =
NaiveDate::from_ymd_opt(THINGS_EPOCH_YEAR, 1, 1).ok_or(DateConversionError::Overflow)?;
if date < epoch_date {
return Err(DateConversionError::BeforeEpoch(date));
}
if date.year() > MAX_YEAR {
return Err(DateConversionError::TooFarFuture(date));
}
let base_date = Utc
.with_ymd_and_hms(THINGS_EPOCH_YEAR, 1, 1, 0, 0, 0)
.single()
.ok_or(DateConversionError::Overflow)?;
let date_time = date
.and_time(NaiveTime::from_hms_opt(0, 0, 0).ok_or(DateConversionError::Overflow)?)
.and_local_timezone(Utc)
.single()
.ok_or(DateConversionError::Overflow)?;
let seconds = date_time.signed_duration_since(base_date).num_seconds();
Ok(seconds)
}
pub fn validate_date_range(
start_date: Option<NaiveDate>,
deadline: Option<NaiveDate>,
) -> Result<(), DateValidationError> {
if let (Some(start), Some(end)) = (start_date, deadline) {
if end < start {
return Err(DateValidationError::DeadlineBeforeStartDate {
start_date: start,
deadline: end,
});
}
}
Ok(())
}
pub fn format_date_for_display(date: Option<NaiveDate>) -> String {
match date {
Some(d) => d.format("%Y-%m-%d").to_string(),
None => "None".to_string(),
}
}
pub fn parse_date_from_string(s: &str) -> Result<NaiveDate, DateConversionError> {
NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| DateConversionError::ParseError {
string: s.to_string(),
reason: e.to_string(),
})
}
pub fn is_date_in_past(date: NaiveDate) -> bool {
date < Utc::now().date_naive()
}
pub fn is_date_in_future(date: NaiveDate) -> bool {
date > Utc::now().date_naive()
}
pub fn add_days(date: NaiveDate, days: i64) -> Result<NaiveDate, DateConversionError> {
date.checked_add_signed(chrono::Duration::days(days))
.ok_or(DateConversionError::Overflow)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_things_timestamp() {
assert!(is_valid_things_timestamp(0)); assert!(is_valid_things_timestamp(86400)); assert!(is_valid_things_timestamp(31536000));
assert!(!is_valid_things_timestamp(-100000000));
assert!(!is_valid_things_timestamp(4000000000));
}
#[test]
fn test_safe_things_date_conversion_epoch() {
let date = safe_things_date_to_naive_date(0).unwrap();
assert_eq!(date, NaiveDate::from_ymd_opt(2001, 1, 1).unwrap());
}
#[test]
fn test_safe_things_date_conversion_normal() {
let date = safe_things_date_to_naive_date(86400).unwrap();
assert_eq!(date, NaiveDate::from_ymd_opt(2001, 1, 2).unwrap());
}
#[test]
fn test_safe_things_date_conversion_invalid() {
assert!(safe_things_date_to_naive_date(10000000000).is_err());
assert!(safe_things_date_to_naive_date(-100000000).is_err());
}
#[test]
fn test_safe_naive_date_to_things_timestamp_epoch() {
let date = NaiveDate::from_ymd_opt(2001, 1, 1).unwrap();
let timestamp = safe_naive_date_to_things_timestamp(date).unwrap();
assert_eq!(timestamp, 0);
}
#[test]
fn test_safe_naive_date_to_things_timestamp_normal() {
let date = NaiveDate::from_ymd_opt(2001, 1, 2).unwrap();
let timestamp = safe_naive_date_to_things_timestamp(date).unwrap();
assert_eq!(timestamp, 86400);
}
#[test]
fn test_safe_naive_date_to_things_timestamp_before_epoch() {
let date = NaiveDate::from_ymd_opt(2000, 12, 31).unwrap();
let result = safe_naive_date_to_things_timestamp(date);
assert!(matches!(result, Err(DateConversionError::BeforeEpoch(_))));
}
#[test]
fn test_safe_naive_date_to_things_timestamp_too_far_future() {
let date = NaiveDate::from_ymd_opt(2150, 1, 1).unwrap();
let result = safe_naive_date_to_things_timestamp(date);
assert!(matches!(result, Err(DateConversionError::TooFarFuture(_))));
}
#[test]
fn test_round_trip_conversion() {
let original_date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
let timestamp = safe_naive_date_to_things_timestamp(original_date).unwrap();
let converted_date = safe_things_date_to_naive_date(timestamp).unwrap();
assert_eq!(original_date, converted_date);
}
#[test]
fn test_validate_date_range_valid() {
let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
assert!(validate_date_range(Some(start), Some(end)).is_ok());
}
#[test]
fn test_validate_date_range_invalid() {
let start = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let result = validate_date_range(Some(start), Some(end));
assert!(matches!(
result,
Err(DateValidationError::DeadlineBeforeStartDate { .. })
));
}
#[test]
fn test_validate_date_range_same_date() {
let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
assert!(validate_date_range(Some(date), Some(date)).is_ok());
}
#[test]
fn test_validate_date_range_only_start() {
let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
assert!(validate_date_range(Some(start), None).is_ok());
}
#[test]
fn test_validate_date_range_only_end() {
let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
assert!(validate_date_range(None, Some(end)).is_ok());
}
#[test]
fn test_validate_date_range_both_none() {
assert!(validate_date_range(None, None).is_ok());
}
#[test]
fn test_format_date_for_display() {
let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
assert_eq!(format_date_for_display(Some(date)), "2024-06-15");
assert_eq!(format_date_for_display(None), "None");
}
#[test]
fn test_parse_date_from_string_valid() {
let date = parse_date_from_string("2024-06-15").unwrap();
assert_eq!(date, NaiveDate::from_ymd_opt(2024, 6, 15).unwrap());
}
#[test]
fn test_parse_date_from_string_invalid() {
assert!(parse_date_from_string("invalid").is_err());
assert!(parse_date_from_string("2024-13-01").is_err());
assert!(parse_date_from_string("2024-06-32").is_err());
}
#[test]
fn test_add_days_positive() {
let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let new_date = add_days(date, 10).unwrap();
assert_eq!(new_date, NaiveDate::from_ymd_opt(2024, 1, 11).unwrap());
}
#[test]
fn test_add_days_negative() {
let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
let new_date = add_days(date, -10).unwrap();
assert_eq!(new_date, NaiveDate::from_ymd_opt(2024, 1, 5).unwrap());
}
}