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
6pub(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
16pub(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
26pub(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 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 DateOp::Century => {
122 let (date, _) = extract_date_or_timestamp(&args[0])?;
123 let year = date.year();
124 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 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 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 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
191pub(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 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 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}