use crate::datetime_impl;
use chrono::{Datelike, NaiveDate, NaiveTime, Timelike};
use helios_fhirpath_support::{EvaluationError, EvaluationResult};
fn date_precision(date_str: &str) -> u8 {
match date_str.len() {
4 => 1, 7 => 2, _ => 3, }
}
fn required_date_precision(precision: &str) -> Option<u8> {
match precision {
"year" | "years" => Some(1),
"month" | "months" => Some(2),
"week" | "weeks" | "day" | "days" => Some(3),
_ => None,
}
}
fn is_time_precision(precision: &str) -> bool {
matches!(
precision,
"hour"
| "hours"
| "minute"
| "minutes"
| "second"
| "seconds"
| "millisecond"
| "milliseconds"
)
}
fn is_date_precision(precision: &str) -> bool {
matches!(
precision,
"year" | "years" | "month" | "months" | "week" | "weeks" | "day" | "days"
)
}
pub fn duration_function(
invocation_base: &EvaluationResult,
target: &EvaluationResult,
precision: &str,
) -> Result<EvaluationResult, EvaluationError> {
compute_interval(invocation_base, target, precision, false)
}
pub fn difference_function(
invocation_base: &EvaluationResult,
target: &EvaluationResult,
precision: &str,
) -> Result<EvaluationResult, EvaluationError> {
compute_interval(invocation_base, target, precision, true)
}
fn compute_interval(
from: &EvaluationResult,
to: &EvaluationResult,
precision: &str,
is_difference: bool,
) -> Result<EvaluationResult, EvaluationError> {
match (from, to) {
(EvaluationResult::Date(from_str, _, _), EvaluationResult::Date(to_str, _, _)) => {
if !is_date_precision(precision) {
return Err(EvaluationError::InvalidArgument(format!(
"Precision '{}' is not valid for Date values",
precision
)));
}
let req = required_date_precision(precision).unwrap();
if date_precision(from_str) < req || date_precision(to_str) < req {
return Ok(EvaluationResult::Empty);
}
let from_date = datetime_impl::parse_date(from_str).ok_or_else(|| {
EvaluationError::InvalidArgument(format!("Cannot parse date: {}", from_str))
})?;
let to_date = datetime_impl::parse_date(to_str).ok_or_else(|| {
EvaluationError::InvalidArgument(format!("Cannot parse date: {}", to_str))
})?;
let result = if is_difference {
date_difference(from_date, to_date, precision)
} else {
date_duration(from_date, to_date, precision)
};
Ok(EvaluationResult::integer(result))
}
(EvaluationResult::DateTime(from_str, _, _), EvaluationResult::DateTime(to_str, _, _)) => {
if !is_date_precision(precision) && !is_time_precision(precision) {
return Err(EvaluationError::InvalidArgument(format!(
"Precision '{}' is not valid for DateTime values",
precision
)));
}
if is_date_precision(precision) {
let from_date_str = from_str.split('T').next().unwrap_or(from_str);
let to_date_str = to_str.split('T').next().unwrap_or(to_str);
let req = required_date_precision(precision).unwrap();
if date_precision(from_date_str) < req || date_precision(to_date_str) < req {
return Ok(EvaluationResult::Empty);
}
let from_date = datetime_impl::parse_date(from_date_str).ok_or_else(|| {
EvaluationError::InvalidArgument(format!("Cannot parse date: {}", from_str))
})?;
let to_date = datetime_impl::parse_date(to_date_str).ok_or_else(|| {
EvaluationError::InvalidArgument(format!("Cannot parse date: {}", to_str))
})?;
let result = if is_difference {
date_difference(from_date, to_date, precision)
} else {
date_duration(from_date, to_date, precision)
};
Ok(EvaluationResult::integer(result))
} else {
let from_dt = datetime_impl::parse_datetime(from_str).ok_or_else(|| {
EvaluationError::InvalidArgument(format!("Cannot parse datetime: {}", from_str))
})?;
let to_dt = datetime_impl::parse_datetime(to_str).ok_or_else(|| {
EvaluationError::InvalidArgument(format!("Cannot parse datetime: {}", to_str))
})?;
let diff_ms = (to_dt - from_dt).num_milliseconds();
let result = time_interval_from_ms(diff_ms, precision, is_difference);
Ok(EvaluationResult::integer(result))
}
}
(EvaluationResult::Time(from_str, _, _), EvaluationResult::Time(to_str, _, _)) => {
if !is_time_precision(precision) {
return Err(EvaluationError::InvalidArgument(format!(
"Precision '{}' is not valid for Time values",
precision
)));
}
let from_time = datetime_impl::parse_time(from_str).ok_or_else(|| {
EvaluationError::InvalidArgument(format!("Cannot parse time: {}", from_str))
})?;
let to_time = datetime_impl::parse_time(to_str).ok_or_else(|| {
EvaluationError::InvalidArgument(format!("Cannot parse time: {}", to_str))
})?;
let diff_ms = time_diff_ms(from_time, to_time);
let result = time_interval_from_ms(diff_ms, precision, is_difference);
Ok(EvaluationResult::integer(result))
}
(EvaluationResult::Empty, _) | (_, EvaluationResult::Empty) => Ok(EvaluationResult::Empty),
_ => Err(EvaluationError::TypeError(format!(
"duration/difference requires matching date/time types, found {} and {}",
from.type_name(),
to.type_name()
))),
}
}
fn date_duration(from: NaiveDate, to: NaiveDate, precision: &str) -> i64 {
let sign = if to >= from { 1i64 } else { -1i64 };
let (earlier, later) = if to >= from { (from, to) } else { (to, from) };
match precision {
"year" | "years" => {
let mut years = later.year() as i64 - earlier.year() as i64;
let anniversary = add_years(earlier, years as i32);
if let Some(ann) = anniversary {
if ann > later {
years -= 1;
}
}
sign * years
}
"month" | "months" => {
let mut months = (later.year() as i64 - earlier.year() as i64) * 12
+ (later.month() as i64 - earlier.month() as i64);
let anniversary = add_months(earlier, months as i32);
if let Some(ann) = anniversary {
if ann > later {
months -= 1;
}
}
sign * months
}
"week" | "weeks" => {
let days = (later - earlier).num_days();
sign * (days / 7)
}
"day" | "days" => {
let days = (later - earlier).num_days();
sign * days
}
_ => 0,
}
}
fn date_difference(from: NaiveDate, to: NaiveDate, precision: &str) -> i64 {
let sign = if to >= from { 1i64 } else { -1i64 };
let (earlier, later) = if to >= from { (from, to) } else { (to, from) };
match precision {
"year" | "years" => {
sign * (later.year() as i64 - earlier.year() as i64)
}
"month" | "months" => {
sign * ((later.year() as i64 - earlier.year() as i64) * 12
+ (later.month() as i64 - earlier.month() as i64))
}
"week" | "weeks" => {
let earlier_week = iso_week_start(earlier);
let later_week = iso_week_start(later);
let weeks = (later_week - earlier_week).num_weeks();
sign * weeks
}
"day" | "days" => {
let days = (later - earlier).num_days();
sign * days
}
_ => 0,
}
}
fn iso_week_start(date: NaiveDate) -> NaiveDate {
let days_from_sunday = date.weekday().num_days_from_sunday();
date - chrono::Duration::days(days_from_sunday as i64)
}
fn time_interval_from_ms(diff_ms: i64, precision: &str, _is_difference: bool) -> i64 {
match precision {
"hour" | "hours" => diff_ms / 3_600_000,
"minute" | "minutes" => diff_ms / 60_000,
"second" | "seconds" => diff_ms / 1_000,
"millisecond" | "milliseconds" => diff_ms,
_ => 0,
}
}
fn time_diff_ms(from: NaiveTime, to: NaiveTime) -> i64 {
let from_ms =
from.num_seconds_from_midnight() as i64 * 1000 + from.nanosecond() as i64 / 1_000_000;
let to_ms = to.num_seconds_from_midnight() as i64 * 1000 + to.nanosecond() as i64 / 1_000_000;
to_ms - from_ms
}
fn add_years(date: NaiveDate, years: i32) -> Option<NaiveDate> {
let target_year = date.year() + years;
NaiveDate::from_ymd_opt(target_year, date.month(), date.day()).or_else(|| {
NaiveDate::from_ymd_opt(target_year, date.month(), 28)
})
}
fn add_months(date: NaiveDate, months: i32) -> Option<NaiveDate> {
let total_months = date.year() * 12 + date.month() as i32 - 1 + months;
let target_year = total_months.div_euclid(12);
let target_month = (total_months.rem_euclid(12) + 1) as u32;
NaiveDate::from_ymd_opt(target_year, target_month, date.day()).or_else(|| {
let last_day = last_day_of_month(target_year, target_month);
NaiveDate::from_ymd_opt(target_year, target_month, last_day)
})
}
fn last_day_of_month(year: i32, month: u32) -> u32 {
NaiveDate::from_ymd_opt(year, month + 1, 1)
.unwrap_or_else(|| NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap())
.pred_opt()
.unwrap()
.day()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_duration_days() {
let from = EvaluationResult::date("2025-01-02".to_string());
let to = EvaluationResult::date("2025-01-07".to_string());
let result = duration_function(&from, &to, "day").unwrap();
assert_eq!(result, EvaluationResult::integer(5));
}
#[test]
fn test_duration_weeks_partial() {
let from = EvaluationResult::date("2025-01-02".to_string());
let to = EvaluationResult::date("2025-01-07".to_string());
let result = duration_function(&from, &to, "week").unwrap();
assert_eq!(result, EvaluationResult::integer(0));
}
#[test]
fn test_duration_weeks_full() {
let from = EvaluationResult::date("2025-01-01".to_string());
let to = EvaluationResult::date("2025-01-15".to_string());
let result = duration_function(&from, &to, "week").unwrap();
assert_eq!(result, EvaluationResult::integer(2));
}
#[test]
fn test_duration_year_partial() {
let from = EvaluationResult::date("2025-01-01".to_string());
let to = EvaluationResult::date("2025-09-01".to_string());
let result = duration_function(&from, &to, "year").unwrap();
assert_eq!(result, EvaluationResult::integer(0));
}
#[test]
fn test_duration_year_dec_to_sep() {
let from = EvaluationResult::date("2024-12-01".to_string());
let to = EvaluationResult::date("2025-09-01".to_string());
let result = duration_function(&from, &to, "year").unwrap();
assert_eq!(result, EvaluationResult::integer(0));
}
#[test]
fn test_difference_week_boundary() {
let from = EvaluationResult::date("2025-01-02".to_string());
let to = EvaluationResult::date("2025-01-07".to_string());
let result = difference_function(&from, &to, "week").unwrap();
assert_eq!(result, EvaluationResult::integer(1));
}
#[test]
fn test_difference_year_same_year() {
let from = EvaluationResult::date("2025-01-01".to_string());
let to = EvaluationResult::date("2025-09-01".to_string());
let result = difference_function(&from, &to, "year").unwrap();
assert_eq!(result, EvaluationResult::integer(0));
}
#[test]
fn test_difference_year_cross() {
let from = EvaluationResult::date("2024-12-01".to_string());
let to = EvaluationResult::date("2025-09-01".to_string());
let result = difference_function(&from, &to, "year").unwrap();
assert_eq!(result, EvaluationResult::integer(1));
}
#[test]
fn test_duration_negative() {
let from = EvaluationResult::date("2025-01-10".to_string());
let to = EvaluationResult::date("2025-01-05".to_string());
let result = duration_function(&from, &to, "day").unwrap();
assert_eq!(result, EvaluationResult::integer(-5));
}
#[test]
fn test_duration_empty_input() {
let from = EvaluationResult::Empty;
let to = EvaluationResult::date("2025-01-05".to_string());
let result = duration_function(&from, &to, "day").unwrap();
assert_eq!(result, EvaluationResult::Empty);
}
#[test]
fn test_duration_insufficient_precision() {
let from = EvaluationResult::date("2025".to_string());
let to = EvaluationResult::date("2025-06-01".to_string());
let result = duration_function(&from, &to, "day").unwrap();
assert_eq!(result, EvaluationResult::Empty);
}
#[test]
fn test_duration_time_hours() {
let from = EvaluationResult::time("10:00:00".to_string());
let to = EvaluationResult::time("13:30:00".to_string());
let result = duration_function(&from, &to, "hour").unwrap();
assert_eq!(result, EvaluationResult::integer(3));
}
#[test]
fn test_duration_time_minutes() {
let from = EvaluationResult::time("10:00:00".to_string());
let to = EvaluationResult::time("10:45:30".to_string());
let result = duration_function(&from, &to, "minute").unwrap();
assert_eq!(result, EvaluationResult::integer(45));
}
#[test]
fn test_duration_months() {
let from = EvaluationResult::date("2025-01-15".to_string());
let to = EvaluationResult::date("2025-04-10".to_string());
let result = duration_function(&from, &to, "month").unwrap();
assert_eq!(result, EvaluationResult::integer(2));
}
#[test]
fn test_difference_months() {
let from = EvaluationResult::date("2025-01-15".to_string());
let to = EvaluationResult::date("2025-04-10".to_string());
let result = difference_function(&from, &to, "month").unwrap();
assert_eq!(result, EvaluationResult::integer(3));
}
#[test]
fn test_invalid_precision_for_date() {
let from = EvaluationResult::date("2025-01-01".to_string());
let to = EvaluationResult::date("2025-06-01".to_string());
assert!(duration_function(&from, &to, "hour").is_err());
}
#[test]
fn test_invalid_precision_for_time() {
let from = EvaluationResult::time("10:00:00".to_string());
let to = EvaluationResult::time("12:00:00".to_string());
assert!(duration_function(&from, &to, "year").is_err());
}
}