Skip to main content

reifydb_routine/function/date/
age.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use reifydb_core::value::column::data::ColumnData;
5use reifydb_type::{
6	error::TypeError,
7	value::{container::temporal::TemporalContainer, date::Date, duration::Duration, r#type::Type},
8};
9
10use crate::function::{
11	ScalarFunction, ScalarFunctionContext,
12	error::{ScalarFunctionError, ScalarFunctionResult},
13	propagate_options,
14};
15
16pub struct DateAge;
17
18impl DateAge {
19	pub fn new() -> Self {
20		Self
21	}
22}
23
24/// Compute calendar-aware age between two dates.
25/// Returns Duration with months + days components.
26pub fn date_age(d1: &Date, d2: &Date) -> Result<Duration, TypeError> {
27	let y1 = d1.year();
28	let m1 = d1.month() as i32;
29	let day1 = d1.day() as i32;
30
31	let y2 = d2.year();
32	let m2 = d2.month() as i32;
33	let day2 = d2.day() as i32;
34
35	let mut years = y1 - y2;
36	let mut months = m1 - m2;
37	let mut days = day1 - day2;
38
39	if days < 0 {
40		months -= 1;
41		// Borrow days from previous month (relative to d2's perspective)
42		let borrow_month = if m1 - 1 < 1 {
43			12
44		} else {
45			m1 - 1
46		};
47		let borrow_year = if m1 - 1 < 1 {
48			y1 - 1
49		} else {
50			y1
51		};
52		days += Date::days_in_month(borrow_year, borrow_month as u32) as i32;
53	}
54
55	if months < 0 {
56		years -= 1;
57		months += 12;
58	}
59
60	let total_months = years * 12 + months;
61	Duration::new(total_months, days, 0)
62}
63
64impl ScalarFunction for DateAge {
65	fn scalar(&self, ctx: ScalarFunctionContext) -> ScalarFunctionResult<ColumnData> {
66		if let Some(result) = propagate_options(self, &ctx) {
67			return result;
68		}
69
70		let columns = ctx.columns;
71		let row_count = ctx.row_count;
72
73		if columns.len() != 2 {
74			return Err(ScalarFunctionError::ArityMismatch {
75				function: ctx.fragment.clone(),
76				expected: 2,
77				actual: columns.len(),
78			});
79		}
80
81		let col1 = columns.get(0).unwrap();
82		let col2 = columns.get(1).unwrap();
83
84		match (col1.data(), col2.data()) {
85			(ColumnData::Date(container1), ColumnData::Date(container2)) => {
86				let mut container = TemporalContainer::with_capacity(row_count);
87
88				for i in 0..row_count {
89					match (container1.get(i), container2.get(i)) {
90						(Some(d1), Some(d2)) => {
91							container.push(date_age(&d1, &d2)?);
92						}
93						_ => container.push_default(),
94					}
95				}
96
97				Ok(ColumnData::Duration(container))
98			}
99			(ColumnData::Date(_), other) => Err(ScalarFunctionError::InvalidArgumentType {
100				function: ctx.fragment.clone(),
101				argument_index: 1,
102				expected: vec![Type::Date],
103				actual: other.get_type(),
104			}),
105			(other, _) => Err(ScalarFunctionError::InvalidArgumentType {
106				function: ctx.fragment.clone(),
107				argument_index: 0,
108				expected: vec![Type::Date],
109				actual: other.get_type(),
110			}),
111		}
112	}
113
114	fn return_type(&self, _input_types: &[Type]) -> Type {
115		Type::Duration
116	}
117}