Skip to main content

truecalc_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            // Google Sheets implementation: end.day - start.day, and if the result
50            // is negative, add the number of days in the month before end.
51            let diff = end.day() as i32 - start.day() as i32;
52            let result = if diff < 0 {
53                let prev = prev_month(end.year(), end.month());
54                diff + days_in_month(prev.0, prev.1) as i32
55            } else {
56                diff
57            };
58            Value::Number(result as f64)
59        }
60        "YM" => {
61            let total_months = (end.year() - start.year()) * 12
62                + (end.month() as i32 - start.month() as i32);
63            let had_day_pass = end.day() >= start.day();
64            let complete_months = if had_day_pass { total_months } else { total_months - 1 };
65            Value::Number((complete_months % 12) as f64)
66        }
67        "YD" => {
68            // Days from start to end as if they were in the same year (start's year).
69            let same_year_end = NaiveDate::from_ymd_opt(start.year(), end.month(), end.day())
70                .or_else(|| NaiveDate::from_ymd_opt(start.year(), end.month() + 1, 1))
71                .unwrap();
72            let days = if same_year_end >= start {
73                (same_year_end - start).num_days()
74            } else {
75                let next_year_end = NaiveDate::from_ymd_opt(start.year() + 1, end.month(), end.day())
76                    .or_else(|| NaiveDate::from_ymd_opt(start.year() + 1, end.month() + 1, 1))
77                    .unwrap();
78                (next_year_end - start).num_days()
79            };
80            Value::Number(days as f64)
81        }
82        _ => Value::Error(ErrorKind::Num),
83    }
84}
85
86/// Returns (year, month) for the month preceding the given month.
87fn prev_month(year: i32, month: u32) -> (i32, u32) {
88    if month == 1 {
89        (year - 1, 12)
90    } else {
91        (year, month - 1)
92    }
93}
94
95/// Number of days in the given (year, month).
96fn days_in_month(year: i32, month: u32) -> u32 {
97    let next_month = if month == 12 { 1 } else { month + 1 };
98    let next_year  = if month == 12 { year + 1 } else { year };
99    let first_of_next = NaiveDate::from_ymd_opt(next_year, next_month, 1).unwrap();
100    let first_of_curr = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
101    (first_of_next - first_of_curr).num_days() as u32
102}
103
104#[cfg(test)]
105mod tests;