Skip to main content

akar_function/scalar/
date.rs

1use super::get_string;
2use crate::registry::*;
3use akar_common::types::{Date, Timestamp, Value};
4use time::{Date as TimeDate, Month, OffsetDateTime, Time as TimeTime};
5
6// ==================== Date ====================
7
8/// Helper: convert Date (days since epoch) to time::Date.
9pub(crate) fn epoch_days_to_date(days: i32) -> Result<TimeDate, String> {
10    TimeDate::from_calendar_date(1970, Month::January, 1)
11        .map_err(|e| format!("Date error: {e}"))?
12        .checked_add(time::Duration::days(days as i64))
13        .ok_or_else(|| "Date overflow".into())
14}
15
16/// Helper: convert Timestamp (micros since epoch) to OffsetDateTime.
17pub(crate) fn epoch_micros_to_datetime(micros: i64) -> Result<OffsetDateTime, String> {
18    let secs = micros.div_euclid(1_000_000);
19    let nanos = (micros.rem_euclid(1_000_000) * 1000) as u32;
20    OffsetDateTime::from_unix_timestamp(secs)
21        .map_err(|e| format!("Timestamp error: {e}"))?
22        .replace_nanosecond(nanos)
23        .map_err(|e| format!("Timestamp nanos error: {e}"))
24}
25
26/// Helper: get a numeric value from args (i64 or f64) for date math.
27pub(crate) fn extract_numeric_value(v: &Value) -> Result<i64, String> {
28    match v {
29        Value::Int64(x) => Ok(*x),
30        Value::Int32(x) => Ok(*x as i64),
31        _ => Err("Expected numeric value for date operation".into()),
32    }
33}
34
35pub(crate) fn evaluate_date(op: DateOp, args: &[Value]) -> Result<Value, String> {
36    match op {
37        DateOp::CurrentDate => {
38            let now = OffsetDateTime::now_utc();
39            let epoch_start =
40                TimeDate::from_calendar_date(1970, Month::January, 1).map_err(|e| format!("Date error: {e}"))?;
41            let days = (now.date() - epoch_start).whole_days() as i32;
42            Ok(Value::Date(Date(days)))
43        }
44        DateOp::CurrentTimestamp => {
45            let now = OffsetDateTime::now_utc();
46            let micros = now.unix_timestamp() * 1_000_000 + now.nanosecond() as i64 / 1000;
47            Ok(Value::Timestamp(Timestamp(micros)))
48        }
49        DateOp::Year => {
50            let (date, _) = extract_date_or_timestamp(&args[0])?;
51            Ok(Value::Int64(date.year() as i64))
52        }
53        DateOp::Month => {
54            let (date, _) = extract_date_or_timestamp(&args[0])?;
55            Ok(Value::Int64(date.month() as u8 as i64))
56        }
57        DateOp::Day => {
58            let (date, _) = extract_date_or_timestamp(&args[0])?;
59            Ok(Value::Int64(date.day() as i64))
60        }
61        DateOp::Hour => {
62            let (_, time) = extract_date_or_timestamp(&args[0])?;
63            Ok(Value::Int64(time.hour() as i64))
64        }
65        DateOp::Minute => {
66            let (_, time) = extract_date_or_timestamp(&args[0])?;
67            Ok(Value::Int64(time.minute() as i64))
68        }
69        DateOp::Second => {
70            let (_, time) = extract_date_or_timestamp(&args[0])?;
71            Ok(Value::Int64(time.second() as i64))
72        }
73        DateOp::DayName => {
74            let (date, _) = extract_date_or_timestamp(&args[0])?;
75            let weekday = date.weekday();
76            Ok(Value::String(weekday.to_string()))
77        }
78        DateOp::MonthName => {
79            let (date, _) = extract_date_or_timestamp(&args[0])?;
80            Ok(Value::String(date.month().to_string()))
81        }
82        DateOp::LastDay => {
83            let (date, _) = extract_date_or_timestamp(&args[0])?;
84            // Go to first day of next month, then subtract one day
85            let next_month_first = TimeDate::from_calendar_date(
86                date.year() + if date.month() == time::Month::December { 1 } else { 0 },
87                date.month().next(),
88                1,
89            )
90            .map_err(|e| format!("Date error: {e}"))?;
91            let last_day = next_month_first
92                .previous_day()
93                .ok_or("Could not compute previous day")?;
94            let epoch = TimeDate::from_calendar_date(1970, Month::January, 1).unwrap();
95            let days = (last_day - epoch).whole_days() as i32;
96            Ok(Value::Date(Date(days)))
97        }
98        DateOp::MakeDate => {
99            if args.len() < 3 {
100                return Err("make_date requires 3 arguments (year, month, day)".into());
101            }
102            let year = match &args[0] {
103                Value::Int64(x) => *x as i32,
104                _ => return Err("make_date year must be integer".into()),
105            };
106            let month_val = match &args[1] {
107                Value::Int64(x) => *x as u8,
108                _ => return Err("make_date month must be integer".into()),
109            };
110            let day = match &args[2] {
111                Value::Int64(x) => *x as u8,
112                _ => return Err("make_date day must be integer".into()),
113            };
114            let month_enum = Month::try_from(month_val).map_err(|_| format!("Invalid month: {month_val}"))?;
115            let d = TimeDate::from_calendar_date(year, month_enum, day).map_err(|e| format!("Invalid date: {e}"))?;
116            let epoch = TimeDate::from_calendar_date(1970, Month::January, 1).unwrap();
117            let days = (d - epoch).whole_days() as i32;
118            Ok(Value::Date(Date(days)))
119        }
120        // --- Timestamp functions (C++ port) ---
121        DateOp::Century => {
122            let (date, _) = extract_date_or_timestamp(&args[0])?;
123            let year = date.year();
124            // PostgreSQL semantics: year 1→century 1, year 2000→20, year 2001→21
125            let century = if year > 0 { (year - 1) / 100 + 1 } else { year / 100 - 1 };
126            Ok(Value::Int64(century as i64))
127        }
128        DateOp::EpochMs => {
129            // EPOCH_MS(ms): convert milliseconds since epoch → Timestamp
130            let ms = match &args[0] {
131                Value::Int64(x) => *x,
132                _ => return Err("epoch_ms requires integer milliseconds".into()),
133            };
134            Ok(Value::Timestamp(Timestamp(ms * 1000)))
135        }
136        DateOp::ToTimestamp => {
137            // TO_TIMESTAMP(sec): convert seconds since epoch (double) → Timestamp
138            let secs = match &args[0] {
139                Value::Double(x) => *x,
140                Value::Int64(x) => *x as f64,
141                _ => return Err("to_timestamp requires numeric seconds".into()),
142            };
143            let micros = (secs * 1_000_000.0) as i64;
144            Ok(Value::Timestamp(Timestamp(micros)))
145        }
146        DateOp::ToEpochMs => {
147            // TO_EPOCH_MS(timestamp): convert Timestamp → milliseconds since epoch
148            let micros = match &args[0] {
149                Value::Timestamp(t) | Value::TimestampMs(t) | Value::TimestampNs(t) | Value::TimestampSec(t) => t.0,
150                _ => return Err("to_epoch_ms requires a timestamp argument".into()),
151            };
152            Ok(Value::Int64(micros / 1000))
153        }
154        DateOp::DatePart => {
155            if args.len() < 2 {
156                return Err("date_part requires 2 arguments".into());
157            }
158            let part = get_string(&args[0])?.to_lowercase();
159            let (date, time) = extract_date_or_timestamp(&args[1])?;
160            date_part_value(&part, &date, &time)
161        }
162        DateOp::DateTrunc => {
163            if args.len() < 2 {
164                return Err("date_trunc requires 2 arguments".into());
165            }
166            let part = get_string(&args[0])?.to_lowercase();
167            let (date, _) = extract_date_or_timestamp(&args[1])?;
168            date_trunc_value(&part, &date)
169        }
170        DateOp::DateDiff => {
171            if args.len() < 3 {
172                return Err("date_diff requires 3 arguments".into());
173            }
174            let part = get_string(&args[0])?.to_lowercase();
175            let (d1, _) = extract_date_or_timestamp(&args[1])?;
176            let (d2, _) = extract_date_or_timestamp(&args[2])?;
177            date_diff_value(&part, &d1, &d2)
178        }
179        DateOp::DateAdd => {
180            if args.len() < 3 {
181                return Err("date_add requires 3 arguments".into());
182            }
183            let part = get_string(&args[0])?.to_lowercase();
184            let count = extract_numeric_value(&args[1])?;
185            let (date, _) = extract_date_or_timestamp(&args[2])?;
186            date_add_value(&part, count, &date)
187        }
188    }
189}
190
191/// Extract date (and optionally time) from a Value that is Date or Timestamp.
192pub(crate) fn extract_date_or_timestamp(v: &Value) -> Result<(TimeDate, TimeTime), String> {
193    match v {
194        Value::Date(d) => {
195            let date = epoch_days_to_date(d.0)?;
196            Ok((
197                date,
198                TimeTime::from_hms(0, 0, 0).map_err(|e| format!("Time error: {e}"))?,
199            ))
200        }
201        Value::Timestamp(t) | Value::TimestampMs(t) | Value::TimestampNs(t) | Value::TimestampSec(t) => {
202            let dt = epoch_micros_to_datetime(t.0)?;
203            Ok((dt.date(), dt.time()))
204        }
205        Value::TimestampTz(t) => {
206            let dt = epoch_micros_to_datetime(t.0)?;
207            Ok((dt.date(), dt.time()))
208        }
209        _ => Err(format!("Expected date/timestamp, got {:?}", v.logical_type())),
210    }
211}
212
213pub(crate) fn date_part_value(part: &str, date: &TimeDate, time: &TimeTime) -> Result<Value, String> {
214    match part {
215        "year" => Ok(Value::Int64(date.year() as i64)),
216        "month" => Ok(Value::Int64(date.month() as u8 as i64)),
217        "day" => Ok(Value::Int64(date.day() as i64)),
218        "hour" => Ok(Value::Int64(time.hour() as i64)),
219        "minute" => Ok(Value::Int64(time.minute() as i64)),
220        "second" => Ok(Value::Int64(time.second() as i64)),
221        "millisecond" => Ok(Value::Int64(time.millisecond() as i64)),
222        "microsecond" => Ok(Value::Int64(time.microsecond() as i64)),
223        "quarter" => Ok(Value::Int64((date.month() as u8 as i64 - 1) / 3 + 1)),
224        "dayofyear" => Ok(Value::Int64(date.ordinal() as i64)),
225        "week" | "weekofyear" => {
226            let (_, iso_week, _) = date.to_iso_week_date();
227            Ok(Value::Int64(iso_week as i64))
228        }
229        "dayofweek" | "dow" => {
230            let w = date.weekday().number_from_monday();
231            Ok(Value::Int64(w as i64))
232        }
233        "isodow" => {
234            let w = date.weekday().number_from_monday();
235            Ok(Value::Int64(w as i64))
236        }
237        "epoch" => Ok(Value::Int64(date.midnight().nanosecond() as i64)),
238        _ => Err(format!("Unknown date_part: {}", part)),
239    }
240}
241
242pub(crate) fn date_trunc_value(part: &str, date: &TimeDate) -> Result<Value, String> {
243    let truncated = match part {
244        "year" => {
245            TimeDate::from_calendar_date(date.year(), Month::January, 1).map_err(|e| format!("Date error: {e}"))?
246        }
247        "month" => {
248            TimeDate::from_calendar_date(date.year(), date.month(), 1).map_err(|e| format!("Date error: {e}"))?
249        }
250        "day" => *date,
251        "week" => {
252            // Truncate to Monday of the week
253            let wd = date.weekday().number_from_monday() - 1;
254            (*date) - time::Duration::days(wd as i64)
255        }
256        "quarter" => {
257            let q = (date.month() as u8 as i64 - 1) / 3;
258            let month = (q * 3 + 1) as u8;
259            let m = Month::try_from(month).map_err(|_| "Invalid month".to_string())?;
260            TimeDate::from_calendar_date(date.year(), m, 1).map_err(|e| format!("Date error: {e}"))?
261        }
262        _ => return Err(format!("date_trunc not supported for: {part}")),
263    };
264    let epoch_start = TimeDate::from_calendar_date(1970, Month::January, 1).map_err(|e| format!("Date error: {e}"))?;
265    let days = (truncated - epoch_start).whole_days() as i32;
266    Ok(Value::Date(Date(days)))
267}
268
269pub(crate) fn date_diff_value(part: &str, d1: &TimeDate, d2: &TimeDate) -> Result<Value, String> {
270    let diff = match part {
271        "year" => (d2.year() - d1.year()) as i64,
272        "month" => ((d2.year() - d1.year()) * 12 + (d2.month() as i32 - d1.month() as i32)) as i64,
273        "day" => (*d2 - *d1).whole_days(),
274        "week" => (*d2 - *d1).whole_days() / 7,
275        "hour" => (*d2 - *d1).whole_hours(),
276        "minute" => (*d2 - *d1).whole_minutes(),
277        "second" => (*d2 - *d1).whole_seconds(),
278        "millisecond" => (*d2 - *d1).whole_milliseconds() as i64,
279        "microsecond" => (*d2 - *d1).whole_microseconds() as i64,
280        _ => return Err(format!("date_diff not supported for: {part}")),
281    };
282    Ok(Value::Int64(diff))
283}
284
285pub(crate) fn date_add_value(part: &str, count: i64, date: &TimeDate) -> Result<Value, String> {
286    let result = match part {
287        "year" => {
288            let new_year = date.year() + count as i32;
289            // Keep same month/day, clamp if needed
290            let m = date.month();
291            let d = date.day().min(days_in_month(new_year, m));
292            TimeDate::from_calendar_date(new_year, m, d).map_err(|e| format!("Date error: {e}"))?
293        }
294        "month" => {
295            let total_months = (date.year() * 12 + date.month() as i32 - 1) + count as i32;
296            let new_year = (total_months.div_euclid(12)) as i32;
297            let new_month = (total_months.rem_euclid(12) + 1) as u8;
298            let m = Month::try_from(new_month).map_err(|_| "Invalid month".to_string())?;
299            let d = date.day().min(days_in_month(new_year, m));
300            TimeDate::from_calendar_date(new_year, m, d).map_err(|e| format!("Date error: {e}"))?
301        }
302        "day" => *date + time::Duration::days(count),
303        "week" => *date + time::Duration::weeks(count),
304        _ => return Err(format!("date_add not supported for: {part}")),
305    };
306    let epoch_start = TimeDate::from_calendar_date(1970, Month::January, 1).map_err(|e| format!("Date error: {e}"))?;
307    let days = (result - epoch_start).whole_days() as i32;
308    Ok(Value::Date(Date(days)))
309}
310
311pub(crate) fn days_in_month(year: i32, month: Month) -> u8 {
312    match month {
313        Month::January | Month::March | Month::May | Month::July | Month::August | Month::October | Month::December => {
314            31
315        }
316        Month::April | Month::June | Month::September | Month::November => 30,
317        Month::February => {
318            if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
319                29
320            } else {
321                28
322            }
323        }
324    }
325}