json_eval_rs/rlogic/evaluator/
date_ops.rs

1use super::Evaluator;
2use serde_json::Value;
3use super::super::compiled::CompiledLogic;
4use super::helpers;
5use chrono::Datelike;
6
7impl Evaluator {
8    /// Unwrap single-element arrays (common pattern: [{"TODAY": []}])
9    #[inline]
10    fn unwrap_array(val: Value) -> Value {
11        if let Value::Array(arr) = &val {
12            if arr.len() == 1 {
13                return arr[0].clone();
14            }
15        }
16        val
17    }
18
19    /// Parse date string with fallback formats
20    /// Supports multiple common date formats including ISO 8601, US, European, and slash/dot separators
21    #[inline]
22    pub(super) fn parse_date(&self, date_str: &str) -> Option<chrono::NaiveDate> {
23        use chrono::NaiveDate;
24        
25        // Try formats in order of specificity (most specific first)
26        let formats = [
27            // ISO 8601 formats (most common)
28            "%Y-%m-%dT%H:%M:%S%.fZ",      // 2024-01-15T10:30:45.123Z
29            "%Y-%m-%dT%H:%M:%SZ",         // 2024-01-15T10:30:45Z
30            "%Y-%m-%dT%H:%M:%S%.f",       // 2024-01-15T10:30:45.123
31            "%Y-%m-%dT%H:%M:%S",          // 2024-01-15T10:30:45
32            "%Y-%m-%dT%H:%M:%S%z",        // 2024-01-15T10:30:45+00:00
33            "%Y-%m-%dT%H:%M:%S%#z",       // 2024-01-15T10:30:45+0000
34            // Date with time (space separator)
35            "%Y-%m-%d %H:%M:%S",          // 2024-01-15 10:30:45
36            "%Y-%m-%d %H:%M",             // 2024-01-15 10:30
37            // Simple date formats
38            "%Y-%m-%d",                   // 2024-01-15
39            "%Y/%m/%d",                   // 2024/01/15
40            "%Y.%m.%d",                   // 2024.01.15
41            // US format (MM/DD/YYYY)
42            "%m/%d/%Y",                   // 01/15/2024
43            "%m-%d-%Y",                   // 01-15-2024
44            // European format (DD/MM/YYYY)
45            "%d/%m/%Y",                   // 15/01/2024
46            "%d-%m-%Y",                   // 15-01-2024
47            "%d.%m.%Y",                   // 15.01.2024
48        ];
49        
50        for format in &formats {
51            if let Ok(date) = NaiveDate::parse_from_str(date_str, format) {
52                return Some(date);
53            }
54        }
55        
56        None
57    }
58
59    /// Extract date component (year/month/day) - ZERO-COPY
60    pub(super) fn extract_date_component(&self, expr: &CompiledLogic, component: &str, user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
61        let val = self.evaluate_with_context(expr, user_data, internal_context, depth + 1)?;
62        let val = Self::unwrap_array(val);
63        
64        if let Value::String(date_str) = &val {
65            if let Some(d) = self.parse_date(date_str) {
66                let value = match component {
67                    "year" => d.year() as f64,
68                    "month" => d.month() as f64,
69                    "day" => d.day() as f64,
70                    _ => return Ok(Value::Null),
71                };
72                return Ok(self.f64_to_json(value));
73            }
74        }
75        Ok(Value::Null)
76    }
77
78    /// Evaluate Today operation
79    pub(super) fn eval_today(&self) -> Result<Value, String> {
80        let now = chrono::Utc::now();
81        Ok(Value::String(helpers::build_iso_date_string(now.date_naive())))
82    }
83
84    /// Evaluate Now operation
85    pub(super) fn eval_now(&self) -> Result<Value, String> {
86        let now = chrono::Utc::now();
87        Ok(Value::String(now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)))
88    }
89
90    /// Evaluate Days operation - ZERO-COPY
91    pub(super) fn eval_days(&self, end_expr: &CompiledLogic, start_expr: &CompiledLogic, user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
92        let end_val = self.evaluate_with_context(end_expr, user_data, internal_context, depth + 1)?;
93        let start_val = self.evaluate_with_context(start_expr, user_data, internal_context, depth + 1)?;
94        
95        let end_val = Self::unwrap_array(end_val);
96        let start_val = Self::unwrap_array(start_val);
97
98        if let (Value::String(end), Value::String(start)) = (&end_val, &start_val) {
99            if let (Some(e), Some(s)) = (self.parse_date(end), self.parse_date(start)) {
100                return Ok(self.f64_to_json((e - s).num_days() as f64));
101            }
102        }
103        Ok(Value::Null)
104    }
105
106    /// Evaluate Date operation with JavaScript-compatible normalization
107    /// Handles overflow/underflow of day values (e.g., day=-16 subtracts from month)
108    pub(super) fn eval_date(&self, year_expr: &CompiledLogic, month_expr: &CompiledLogic, day_expr: &CompiledLogic, user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
109        let year_val = self.evaluate_with_context(year_expr, user_data, internal_context, depth + 1)?;
110        let month_val = self.evaluate_with_context(month_expr, user_data, internal_context, depth + 1)?;
111        let day_val = self.evaluate_with_context(day_expr, user_data, internal_context, depth + 1)?;
112
113        let year = helpers::to_number(&year_val) as i32;
114        let month = helpers::to_number(&month_val) as i32;
115        let day = helpers::to_number(&day_val) as i32;
116
117        use chrono::{NaiveDate, Duration};
118        
119        // JavaScript-compatible date normalization:
120        // Start with year/month, then add days offset
121        // This allows negative days to roll back months/years
122        
123        // First normalize month (can also be out of range)
124        let mut normalized_year = year;
125        let mut normalized_month = month;
126        
127        // Handle month overflow/underflow (JS allows month=-1, month=13, etc.)
128        if normalized_month < 1 {
129            let months_back = (1 - normalized_month) / 12 + 1;
130            normalized_year -= months_back;
131            normalized_month += months_back * 12;
132        } else if normalized_month > 12 {
133            let months_forward = (normalized_month - 1) / 12;
134            normalized_year += months_forward;
135            normalized_month = ((normalized_month - 1) % 12) + 1;
136        }
137        
138        // Create base date at day 1 of the normalized month
139        if let Some(base_date) = NaiveDate::from_ymd_opt(normalized_year, normalized_month as u32, 1) {
140            // Add (day - 1) days to get final date
141            // This handles negative days and days > month length automatically
142            if let Some(final_date) = base_date.checked_add_signed(Duration::days((day - 1) as i64)) {
143                Ok(Value::String(helpers::build_iso_date_string(final_date)))
144            } else {
145                // Date overflow (e.g., year > 9999 or < -9999)
146                Ok(Value::Null)
147            }
148        } else {
149            // Invalid base date
150            Ok(Value::Null)
151        }
152    }
153
154    /// Evaluate YearFrac operation - ZERO-COPY
155    pub(super) fn eval_year_frac(&self, start_expr: &CompiledLogic, end_expr: &CompiledLogic, basis_expr: &Option<Box<CompiledLogic>>, user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
156        let start_val = self.evaluate_with_context(start_expr, user_data, internal_context, depth + 1)?;
157        let end_val = self.evaluate_with_context(end_expr, user_data, internal_context, depth + 1)?;
158        
159        let start_val = Self::unwrap_array(start_val);
160        let end_val = Self::unwrap_array(end_val);
161        
162        let basis = if let Some(b_expr) = basis_expr {
163            let b_val = self.evaluate_with_context(b_expr, user_data, internal_context, depth + 1)?;
164            helpers::to_number(&b_val) as i32
165        } else { 0 };
166
167        if let (Value::String(start_str), Value::String(end_str)) = (&start_val, &end_val) {
168            if let (Some(start), Some(end)) = (self.parse_date(start_str), self.parse_date(end_str)) {
169                let days = (end - start).num_days() as f64;
170                let result = match basis {
171                    0 => days / 360.0,
172                    1 => days / 365.25,
173                    2 => days / 360.0,
174                    3 => days / 365.0,
175                    4 => days / 360.0,
176                    _ => days / 365.0,
177                };
178                return Ok(self.f64_to_json(result));
179            }
180        }
181        Ok(Value::Null)
182    }
183
184    /// Evaluate DateDif operation - ZERO-COPY
185    pub(super) fn eval_date_dif(&self, start_expr: &CompiledLogic, end_expr: &CompiledLogic, unit_expr: &CompiledLogic, user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
186        let start_val = self.evaluate_with_context(start_expr, user_data, internal_context, depth + 1)?;
187        let end_val = self.evaluate_with_context(end_expr, user_data, internal_context, depth + 1)?;
188        let unit_val = self.evaluate_with_context(unit_expr, user_data, internal_context, depth + 1)?;
189        
190        let start_val = Self::unwrap_array(start_val);
191        let end_val = Self::unwrap_array(end_val);
192        let unit_val = Self::unwrap_array(unit_val);
193
194        if let (Value::String(start_str), Value::String(end_str), Value::String(unit)) =
195            (&start_val, &end_val, &unit_val) {
196            if let (Some(start), Some(end)) = (self.parse_date(start_str), self.parse_date(end_str)) {
197                let result = match unit.to_uppercase().as_str() {
198                    "D" => (end - start).num_days() as f64,
199                    "M" => {
200                        let years = end.year() - start.year();
201                        let months = end.month() as i32 - start.month() as i32;
202                        let mut total_months = years * 12 + months;
203                        if end.day() < start.day() {
204                            total_months -= 1;
205                        }
206                        total_months as f64
207                    }
208                    "Y" => {
209                        let mut years = end.year() - start.year();
210                        if end.month() < start.month() ||
211                           (end.month() == start.month() && end.day() < start.day()) {
212                            years -= 1;
213                        }
214                        years as f64
215                    }
216                    "MD" => {
217                        if start.day() <= end.day() {
218                            (end.day() - start.day()) as f64
219                        } else {
220                            let days_in_month = 30u32; // Simplified
221                            (days_in_month as i32 - (start.day() as i32 - end.day() as i32)) as f64
222                        }
223                    }
224                    "YM" => {
225                        let months = end.month() as i32 - start.month() as i32;
226                        let mut result = if months < 0 { months + 12 } else { months };
227                        if end.day() < start.day() {
228                            result -= 1;
229                            if result < 0 {
230                                result += 12;
231                            }
232                        }
233                        result as f64
234                    }
235                    "YD" => {
236                        let mut temp_start = start.with_year(end.year()).unwrap_or(start);
237                        if temp_start > end {
238                            temp_start = start.with_year(end.year() - 1).unwrap_or(start);
239                        }
240                        (end - temp_start).num_days() as f64
241                    }
242                    _ => return Ok(Value::Null),
243                };
244
245                Ok(self.f64_to_json(result))
246            } else {
247                Ok(Value::Null)
248            }
249        } else {
250            Ok(Value::Null)
251        }
252    }
253
254    /// Format date with specified format (Excel-like TEXT function for dates)
255    /// Supports prebuilt formats: "short", "long", "iso", "us", "eu", or custom strftime format
256    pub(super) fn eval_date_format(
257        &self,
258        date_expr: &CompiledLogic,
259        format_expr: &Option<Box<CompiledLogic>>,
260        user_data: &Value,
261        internal_context: &Value,
262        depth: usize,
263    ) -> Result<Value, String> {
264        let date_val = self.evaluate_with_context(date_expr, user_data, internal_context, depth + 1)?;
265        let date_val = Self::unwrap_array(date_val);
266        
267        // Parse the date
268        let date = if let Value::String(date_str) = &date_val {
269            self.parse_date(date_str)
270        } else {
271            None
272        };
273        
274        if date.is_none() {
275            return Ok(Value::Null);
276        }
277        let date = date.unwrap();
278        
279        // Get format string (default "iso")
280        let format_str = if let Some(fmt_expr) = format_expr {
281            let fmt_val = self.evaluate_with_context(fmt_expr, user_data, internal_context, depth + 1)?;
282            super::helpers::to_string(&fmt_val)
283        } else {
284            "iso".to_string()
285        };
286        
287        // Apply prebuilt or custom format
288        let formatted = match format_str.to_lowercase().as_str() {
289            "short" => date.format("%m/%d/%Y").to_string(),           // 01/15/2024
290            "long" => date.format("%B %d, %Y").to_string(),           // January 15, 2024
291            "iso" => date.format("%Y-%m-%d").to_string(),             // 2024-01-15
292            "us" => date.format("%m/%d/%Y").to_string(),              // 01/15/2024
293            "eu" => date.format("%d/%m/%Y").to_string(),              // 15/01/2024
294            "full" => date.format("%A, %B %d, %Y").to_string(),       // Monday, January 15, 2024
295            "monthday" => date.format("%B %d").to_string(),           // January 15
296            "yearmonth" => date.format("%Y-%m").to_string(),          // 2024-01
297            "ddmmyyyy" => date.format("%d/%m/%Y").to_string(),        // 15/01/2024
298            "mmddyyyy" => date.format("%m/%d/%Y").to_string(),        // 01/15/2024
299            "yyyymmdd" => date.format("%Y-%m-%d").to_string(),        // 2024-01-15
300            "dd-mm-yyyy" => date.format("%d-%m-%Y").to_string(),      // 15-01-2024
301            "mm-dd-yyyy" => date.format("%m-%d-%Y").to_string(),      // 01-15-2024
302            "yyyy-mm-dd" => date.format("%Y-%m-%d").to_string(),      // 2024-01-15
303            "dd.mm.yyyy" => date.format("%d.%m.%Y").to_string(),      // 15.01.2024
304            _ => {
305                // Custom format using strftime
306                date.format(&format_str).to_string()
307            }
308        };
309        
310        Ok(Value::String(formatted))
311    }
312}