Skip to main content

ganit_core/eval/functions/date/datedif/
mod.rs

1use chrono::{Datelike, NaiveDate};
2use crate::eval::coercion::to_number;
3use crate::eval::functions::check_arity;
4use crate::eval::functions::date::serial::serial_to_date;
5use crate::types::{ErrorKind, Value};
6
7/// `DATEDIF(start_date, end_date, unit)` — difference between two dates.
8pub fn datedif_fn(args: &[Value]) -> Value {
9    if let Some(e) = check_arity(args, 3, 3) {
10        return e;
11    }
12    let start_serial = match to_number(args[0].clone()) { Ok(n) => n, Err(e) => return e };
13    let end_serial   = match to_number(args[1].clone()) { Ok(n) => n, Err(e) => return e };
14
15    let unit = match &args[2] {
16        Value::Text(s) => s.to_uppercase(),
17        _ => return Value::Error(ErrorKind::Num),
18    };
19
20    let start = match serial_to_date(start_serial) {
21        Some(d) => d,
22        None => return Value::Error(ErrorKind::Num),
23    };
24    let end = match serial_to_date(end_serial) {
25        Some(d) => d,
26        None => return Value::Error(ErrorKind::Num),
27    };
28
29    if start > end {
30        return Value::Error(ErrorKind::Num);
31    }
32
33    match unit.as_str() {
34        "Y" => {
35            let years = end.year() - start.year();
36            let had_anniversary = (end.month(), end.day()) >= (start.month(), start.day());
37            Value::Number(if had_anniversary { years } else { years - 1 } as f64)
38        }
39        "M" => {
40            let months = (end.year() - start.year()) * 12
41                + (end.month() as i32 - start.month() as i32);
42            let had_day_pass = end.day() >= start.day();
43            Value::Number(if had_day_pass { months } else { months - 1 } as f64)
44        }
45        "D" => {
46            Value::Number((end - start).num_days() as f64)
47        }
48        "MD" => {
49            // Complete months elapsed from start to end, then measure remaining days.
50            let total_months = (end.year() - start.year()) * 12
51                + (end.month() as i32 - start.month() as i32);
52            let had_day_pass = end.day() >= start.day();
53            let complete_months = if had_day_pass { total_months } else { total_months - 1 };
54            let adjusted_start = add_months(start, complete_months);
55            Value::Number((end - adjusted_start).num_days() as f64)
56        }
57        "YM" => {
58            let total_months = (end.year() - start.year()) * 12
59                + (end.month() as i32 - start.month() as i32);
60            let had_day_pass = end.day() >= start.day();
61            let complete_months = if had_day_pass { total_months } else { total_months - 1 };
62            Value::Number((complete_months % 12) as f64)
63        }
64        "YD" => {
65            // Days from start to end as if they were in the same year (start's year).
66            let same_year_end = NaiveDate::from_ymd_opt(start.year(), end.month(), end.day())
67                .or_else(|| NaiveDate::from_ymd_opt(start.year(), end.month() + 1, 1))
68                .unwrap();
69            let days = if same_year_end >= start {
70                (same_year_end - start).num_days()
71            } else {
72                let next_year_end = NaiveDate::from_ymd_opt(start.year() + 1, end.month(), end.day())
73                    .or_else(|| NaiveDate::from_ymd_opt(start.year() + 1, end.month() + 1, 1))
74                    .unwrap();
75                (next_year_end - start).num_days()
76            };
77            Value::Number(days as f64)
78        }
79        _ => Value::Error(ErrorKind::Num),
80    }
81}
82
83/// Add `months` months to a date, clamping to end-of-month on overflow.
84fn add_months(date: NaiveDate, months: i32) -> NaiveDate {
85    let total_months = date.year() * 12 + date.month() as i32 - 1 + months;
86    let year = total_months / 12;
87    let month = (total_months % 12 + 1) as u32;
88    NaiveDate::from_ymd_opt(year, month, date.day())
89        .or_else(|| NaiveDate::from_ymd_opt(year, month + 1, 1))
90        .unwrap()
91}
92
93#[cfg(test)]
94mod tests;