use crate::error::{Result as ThingsResult, ThingsError};
use crate::models::{TaskStatus, TaskType};
use chrono::NaiveDate;
pub(crate) fn safe_timestamp_convert(ts_f64: f64) -> i64 {
if ts_f64.is_finite() && ts_f64 >= 0.0 {
let max_timestamp = 4_102_444_800_f64; if ts_f64 <= max_timestamp {
let ts_str = format!("{:.0}", ts_f64.trunc());
ts_str.parse::<i64>().unwrap_or(0)
} else {
0 }
} else {
0 }
}
pub(crate) fn things_date_to_naive_date(seconds_since_2001: i64) -> Option<chrono::NaiveDate> {
use chrono::{TimeZone, Utc};
if seconds_since_2001 <= 0 {
return None;
}
let base_date = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).single().unwrap();
let date_time = base_date + chrono::Duration::seconds(seconds_since_2001);
Some(date_time.date_naive())
}
pub fn naive_date_to_things_timestamp(date: NaiveDate) -> i64 {
use chrono::{NaiveTime, TimeZone, Utc};
let base_date = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).single().unwrap();
let date_time = date
.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
.and_local_timezone(Utc)
.single()
.unwrap();
date_time.timestamp() - base_date.timestamp()
}
pub fn serialize_tags_to_blob(tags: &[String]) -> ThingsResult<Vec<u8>> {
serde_json::to_vec(tags)
.map_err(|e| ThingsError::unknown(format!("Failed to serialize tags: {e}")))
}
pub fn deserialize_tags_from_blob(blob: &[u8]) -> ThingsResult<Vec<String>> {
if blob.is_empty() {
return Ok(Vec::new());
}
serde_json::from_slice(blob)
.map_err(|e| ThingsError::unknown(format!("Failed to deserialize tags: {e}")))
}
impl TaskStatus {
pub(crate) fn from_i32(value: i32) -> Option<Self> {
match value {
0 => Some(TaskStatus::Incomplete),
2 => Some(TaskStatus::Canceled),
3 => Some(TaskStatus::Completed),
_ => None,
}
}
}
impl TaskType {
pub(crate) fn from_i32(value: i32) -> Option<Self> {
match value {
0 => Some(TaskType::Todo),
1 => Some(TaskType::Project),
2 => Some(TaskType::Heading),
3 => Some(TaskType::Area),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_status_from_i32() {
assert_eq!(TaskStatus::from_i32(0), Some(TaskStatus::Incomplete));
assert_eq!(TaskStatus::from_i32(1), None); assert_eq!(TaskStatus::from_i32(2), Some(TaskStatus::Canceled));
assert_eq!(TaskStatus::from_i32(3), Some(TaskStatus::Completed));
assert_eq!(TaskStatus::from_i32(4), None);
assert_eq!(TaskStatus::from_i32(-1), None);
}
#[test]
fn test_task_type_from_i32() {
assert_eq!(TaskType::from_i32(0), Some(TaskType::Todo));
assert_eq!(TaskType::from_i32(1), Some(TaskType::Project));
assert_eq!(TaskType::from_i32(2), Some(TaskType::Heading));
assert_eq!(TaskType::from_i32(3), Some(TaskType::Area));
assert_eq!(TaskType::from_i32(4), None);
assert_eq!(TaskType::from_i32(-1), None);
}
#[test]
fn test_safe_timestamp_convert_edge_cases() {
assert_eq!(safe_timestamp_convert(1_609_459_200.0), 1_609_459_200);
assert_eq!(safe_timestamp_convert(0.0), 0);
assert_eq!(safe_timestamp_convert(-1.0), 0);
assert_eq!(safe_timestamp_convert(f64::INFINITY), 0);
assert_eq!(safe_timestamp_convert(f64::NAN), 0);
assert_eq!(safe_timestamp_convert(5_000_000_000.0), 0);
let max_timestamp = 4_102_444_800_f64; assert_eq!(safe_timestamp_convert(max_timestamp), 4_102_444_800);
}
#[test]
fn test_task_status_from_i32_all_variants() {
assert_eq!(TaskStatus::from_i32(0), Some(TaskStatus::Incomplete));
assert_eq!(TaskStatus::from_i32(1), None); assert_eq!(TaskStatus::from_i32(2), Some(TaskStatus::Canceled));
assert_eq!(TaskStatus::from_i32(3), Some(TaskStatus::Completed));
assert_eq!(TaskStatus::from_i32(999), None);
assert_eq!(TaskStatus::from_i32(-1), None);
}
#[test]
fn test_task_type_from_i32_all_variants() {
assert_eq!(TaskType::from_i32(0), Some(TaskType::Todo));
assert_eq!(TaskType::from_i32(1), Some(TaskType::Project));
assert_eq!(TaskType::from_i32(2), Some(TaskType::Heading));
assert_eq!(TaskType::from_i32(3), Some(TaskType::Area));
assert_eq!(TaskType::from_i32(999), None);
assert_eq!(TaskType::from_i32(-1), None);
}
#[test]
fn test_things_date_negative_returns_none() {
assert_eq!(things_date_to_naive_date(-1), None);
assert_eq!(things_date_to_naive_date(-100), None);
assert_eq!(things_date_to_naive_date(i64::MIN), None);
}
#[test]
fn test_things_date_zero_returns_none() {
assert_eq!(things_date_to_naive_date(0), None);
}
#[test]
fn test_things_date_boundary_2001() {
use chrono::Datelike;
let result = things_date_to_naive_date(1);
assert!(result.is_some());
let date = result.unwrap();
assert_eq!(date.year(), 2001);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 1);
}
#[test]
fn test_things_date_one_day() {
use chrono::Datelike;
let seconds_per_day = 86400i64;
let result = things_date_to_naive_date(seconds_per_day);
assert!(result.is_some());
let date = result.unwrap();
assert_eq!(date.year(), 2001);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 2);
}
#[test]
fn test_things_date_one_year() {
use chrono::Datelike;
let seconds_per_year = 365 * 86400i64;
let result = things_date_to_naive_date(seconds_per_year);
assert!(result.is_some());
let date = result.unwrap();
assert_eq!(date.year(), 2002);
}
#[test]
fn test_things_date_current_era() {
use chrono::Datelike;
let days_to_2024 = 8401i64;
let seconds_to_2024 = days_to_2024 * 86400;
let result = things_date_to_naive_date(seconds_to_2024);
assert!(result.is_some());
let date = result.unwrap();
assert_eq!(date.year(), 2024);
}
#[test]
fn test_things_date_leap_year() {
use chrono::{Datelike, TimeZone, Utc};
let base_date = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).single().unwrap();
let target_date = Utc.with_ymd_and_hms(2004, 2, 29, 0, 0, 0).single().unwrap();
let seconds_diff = (target_date - base_date).num_seconds();
let result = things_date_to_naive_date(seconds_diff);
assert!(result.is_some());
let date = result.unwrap();
assert_eq!(date.year(), 2004);
assert_eq!(date.month(), 2);
assert_eq!(date.day(), 29);
}
#[test]
fn test_safe_timestamp_convert_normal_values() {
let ts = 1_700_000_000.0; let result = safe_timestamp_convert(ts);
assert_eq!(result, 1_700_000_000);
}
#[test]
fn test_safe_timestamp_convert_zero() {
assert_eq!(safe_timestamp_convert(0.0), 0);
}
#[test]
fn test_safe_timestamp_convert_negative() {
assert_eq!(safe_timestamp_convert(-1.0), 0);
assert_eq!(safe_timestamp_convert(-1000.0), 0);
}
#[test]
fn test_safe_timestamp_convert_infinity() {
assert_eq!(safe_timestamp_convert(f64::INFINITY), 0);
assert_eq!(safe_timestamp_convert(f64::NEG_INFINITY), 0);
}
#[test]
fn test_safe_timestamp_convert_nan() {
assert_eq!(safe_timestamp_convert(f64::NAN), 0);
}
#[test]
fn test_date_roundtrip_known_dates() {
use chrono::{Datelike, TimeZone, Utc};
let test_cases = vec![
(2001, 1, 2), (2010, 6, 15),
(2020, 12, 31),
(2024, 2, 29), (2025, 7, 4),
];
for (year, month, day) in test_cases {
let base_date = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).single().unwrap();
let target_date = Utc
.with_ymd_and_hms(year, month, day, 0, 0, 0)
.single()
.unwrap();
let seconds = (target_date - base_date).num_seconds();
let converted = things_date_to_naive_date(seconds);
assert!(
converted.is_some(),
"Failed to convert {}-{:02}-{:02}",
year,
month,
day
);
let result_date = converted.unwrap();
assert_eq!(
result_date.year(),
year,
"Year mismatch for {}-{:02}-{:02}",
year,
month,
day
);
assert_eq!(
result_date.month(),
month,
"Month mismatch for {}-{:02}-{:02}",
year,
month,
day
);
assert_eq!(
result_date.day(),
day,
"Day mismatch for {}-{:02}-{:02}",
year,
month,
day
);
}
}
}