Skip to main content

jsonata_core/
functions.rs

1// Built-in function implementations
2// Mirrors functions.js from the reference implementation
3
4#![allow(clippy::explicit_counter_loop)]
5#![allow(clippy::approx_constant)]
6
7use crate::value::JValue;
8use indexmap::IndexMap;
9use thiserror::Error;
10
11/// Function errors
12#[derive(Error, Debug)]
13pub enum FunctionError {
14    #[error("Argument error: {0}")]
15    ArgumentError(String),
16
17    #[error("Type error: {0}")]
18    TypeError(String),
19
20    #[error("Runtime error: {0}")]
21    RuntimeError(String),
22
23    /// Python→JValue conversion failed while comparing/materializing a lazy
24    /// element (e.g. inside `$distinct`). Mirrors `EvaluatorError::PyConversionError`
25    /// -- `impl From<FunctionError> for EvaluatorError` maps this specific
26    /// variant to `EvaluatorError::PyConversionError` (surfacing as Python
27    /// `TypeError` at the boundary) instead of the generic `EvaluationError`
28    /// (`ValueError`) every other `FunctionError` variant collapses to.
29    #[cfg(feature = "python")]
30    #[error("Type error: {0}")]
31    PyConversionError(String),
32}
33
34/// Built-in string functions
35/// Mimic JS Array.prototype.slice(start, end) semantics.
36/// - Negative start/end count from the end of the array.
37/// - Out-of-bounds values are clamped.
38fn js_slice<T: Clone>(arr: &[T], start: i64, end: Option<i64>) -> Vec<T> {
39    let len = arr.len() as i64;
40    let s = if start < 0 {
41        (len + start).max(0) as usize
42    } else {
43        start.min(len) as usize
44    };
45    let e = match end {
46        Some(end) => {
47            if end < 0 {
48                (len + end).max(0) as usize
49            } else {
50                (end.min(len)) as usize
51            }
52        }
53        None => arr.len(),
54    };
55    if s >= e {
56        return Vec::new();
57    }
58    arr[s..e].to_vec()
59}
60
61pub mod string {
62    use super::*;
63    use regex::Regex;
64
65    /// Helper to detect and extract regex from a JValue
66    pub fn extract_regex(value: &JValue) -> Option<(String, String)> {
67        match value {
68            JValue::Regex { pattern, flags } => Some((pattern.to_string(), flags.to_string())),
69            _ => None,
70        }
71    }
72
73    /// Helper to build a Regex from pattern and flags
74    pub fn build_regex(pattern: &str, flags: &str) -> Result<Regex, FunctionError> {
75        // Convert JSONata flags to Rust regex flags
76        let mut regex_pattern = String::new();
77
78        // Add inline flags
79        if !flags.is_empty() {
80            regex_pattern.push_str("(?");
81            if flags.contains('i') {
82                regex_pattern.push('i'); // case-insensitive
83            }
84            if flags.contains('m') {
85                regex_pattern.push('m'); // multi-line
86            }
87            if flags.contains('s') {
88                regex_pattern.push('s'); // dot matches newline
89            }
90            regex_pattern.push(')');
91        }
92
93        regex_pattern.push_str(pattern);
94
95        Regex::new(&regex_pattern)
96            .map_err(|e| FunctionError::ArgumentError(format!("Invalid regex: {}", e)))
97    }
98
99    /// $string(value, prettify) - Convert value to string
100    ///
101    /// - undefined inputs return undefined (but this is handled at call site)
102    /// - strings returned unchanged
103    /// - functions/lambdas return empty string
104    /// - non-finite numbers (Infinity, NaN) throw error D3001
105    /// - other values use JSON.stringify with number precision
106    /// - prettify=true uses 2-space indentation
107    pub fn string(value: &JValue, prettify: Option<bool>) -> Result<JValue, FunctionError> {
108        // Check if this is undefined or a function first
109        if value.is_undefined() {
110            return Ok(JValue::Undefined);
111        }
112        if value.is_function() {
113            return Ok(JValue::string(""));
114        }
115
116        let result = match value {
117            JValue::String(s) => s.to_string(),
118            JValue::Number(n) => {
119                let f = *n;
120                // Check for non-finite numbers (Infinity, NaN)
121                if !f.is_finite() {
122                    return Err(FunctionError::RuntimeError(format!(
123                        "D3001: Attempting to invoke string function with non-finite number: {}",
124                        f
125                    )));
126                }
127
128                // Format numbers like JavaScript does
129                if f.fract() == 0.0 && f.abs() < (i64::MAX as f64) {
130                    (f as i64).to_string()
131                } else {
132                    // Non-integer - use precision formatting
133                    // JavaScript uses toPrecision(15) for non-integers in JSON.stringify
134                    format_number_with_precision(f)
135                }
136            }
137            JValue::Bool(b) => b.to_string(),
138            JValue::Null => {
139                // Explicit null goes through JSON.stringify to become "null"
140                // Undefined variables are handled at the evaluator level
141                "null".to_string()
142            }
143            JValue::Array(_) | JValue::Object(_) => {
144                // JSON.stringify with optional prettification
145                // Uses custom serialization to handle numbers and functions correctly
146                let indent = if prettify.unwrap_or(false) {
147                    Some(2)
148                } else {
149                    None
150                };
151                stringify_value_custom(value, indent)?
152            }
153            #[cfg(feature = "python")]
154            JValue::LazyPyDict(_) => {
155                let indent = if prettify.unwrap_or(false) {
156                    Some(2)
157                } else {
158                    None
159                };
160                stringify_value_custom(value, indent)?
161            }
162            _ => String::new(),
163        };
164        Ok(JValue::string(result))
165    }
166
167    /// Helper to format a number with precision like JavaScript's toPrecision(15)
168    ///
169    /// JavaScript uses `toPrecision(15)` which formats with 15 significant figures.
170    /// This matches that behavior by:
171    /// 1. Formatting with 15 significant figures
172    /// 2. Removing trailing zeros
173    /// 3. Converting back to number to normalize format
174    fn format_number_with_precision(f: f64) -> String {
175        // Format with 15 significant figures like JavaScript's toPrecision(15)
176        // The format uses scientific notation to ensure precision
177        let formatted = format!("{:.14e}", f);
178
179        // Parse back to f64 and format normally to get the canonical representation
180        // This mimics JavaScript's behavior of normalizing the result
181        if let Ok(parsed) = formatted.parse::<f64>() {
182            // Convert to string without exponential notation unless necessary
183            if parsed.abs() >= 1e-6 && parsed.abs() < 1e21 {
184                // Regular notation
185                let s = format!("{}", parsed);
186                // Ensure we don't have excessive precision
187                if s.contains('.') {
188                    let parts: Vec<&str> = s.split('.').collect();
189                    if parts.len() == 2 {
190                        let int_part = parts[0];
191                        let frac_part = parts[1];
192                        let total_digits = int_part.trim_start_matches('-').len() + frac_part.len();
193
194                        if total_digits > 15 {
195                            // Truncate to 15 significant figures
196                            let sig_figs = 15 - int_part.trim_start_matches('-').len();
197                            if sig_figs > 0 && sig_figs <= frac_part.len() {
198                                let truncated_frac = &frac_part[..sig_figs];
199                                // Remove trailing zeros
200                                let trimmed = truncated_frac.trim_end_matches('0');
201                                if trimmed.is_empty() {
202                                    return int_part.to_string();
203                                } else {
204                                    return format!("{}.{}", int_part, trimmed);
205                                }
206                            }
207                        }
208                    }
209                }
210                s
211            } else {
212                // Use exponential notation for very small or large numbers
213                // Format matches JavaScript: always include sign in exponent
214                let exp_str = format!("{:e}", parsed);
215                // Ensure exponent has + sign: "1e100" -> "1e+100"
216                if exp_str.contains('e') && !exp_str.contains("e-") && !exp_str.contains("e+") {
217                    exp_str.replace('e', "e+")
218                } else {
219                    exp_str
220                }
221            }
222        } else {
223            // Fallback
224            format!("{}", f)
225        }
226    }
227
228    /// Helper to stringify a value as JSON with custom replacer logic
229    ///
230    /// Mimics JavaScript's JSON.stringify with a replacer function that:
231    /// - Converts non-integer numbers to 15 significant figures
232    /// - Keeps integers without decimal point
233    /// - Converts functions to empty string
234    fn stringify_value_custom(
235        value: &JValue,
236        indent: Option<usize>,
237    ) -> Result<String, FunctionError> {
238        // Transform the value recursively before stringifying
239        let transformed = transform_for_stringify(value);
240
241        let result = if indent.is_some() {
242            serde_json::to_string_pretty(&transformed)
243                .map_err(|e| FunctionError::RuntimeError(format!("JSON stringify error: {}", e)))?
244        } else {
245            serde_json::to_string(&transformed)
246                .map_err(|e| FunctionError::RuntimeError(format!("JSON stringify error: {}", e)))?
247        };
248        Ok(result)
249    }
250
251    /// Transform a value for JSON.stringify, applying the replacer logic
252    fn transform_for_stringify(value: &JValue) -> JValue {
253        match value {
254            JValue::Number(n) => {
255                let f = *n;
256                // Check if it's an integer first
257                if f.fract() == 0.0 && f.is_finite() && f.abs() < (1i64 << 53) as f64 {
258                    // Keep as integer
259                    value.clone()
260                } else {
261                    // Non-integer: apply toPrecision(15) and keep as f64
262                    let formatted = format_number_with_precision(f);
263                    if let Ok(parsed) = formatted.parse::<f64>() {
264                        JValue::Number(parsed)
265                    } else {
266                        value.clone()
267                    }
268                }
269            }
270            JValue::Array(arr) => {
271                let transformed: Vec<JValue> = arr
272                    .iter()
273                    .map(|v| {
274                        if v.is_function() {
275                            return JValue::string("");
276                        }
277                        transform_for_stringify(v)
278                    })
279                    .collect();
280                JValue::array(transformed)
281            }
282            JValue::Object(obj) => {
283                if value.is_function() {
284                    return JValue::string("");
285                }
286
287                let transformed: IndexMap<String, JValue> = obj
288                    .iter()
289                    .map(|(k, v)| {
290                        if v.is_function() {
291                            return (k.clone(), JValue::string(""));
292                        }
293                        (k.clone(), transform_for_stringify(v))
294                    })
295                    .collect();
296                JValue::object(transformed)
297            }
298            #[cfg(feature = "python")]
299            JValue::LazyPyDict(lazy) => match lazy.to_object_ref() {
300                Some(obj) => {
301                    let transformed: IndexMap<String, JValue> = obj
302                        .iter()
303                        .map(|(k, v)| {
304                            if v.is_function() {
305                                return (k.clone(), JValue::string(""));
306                            }
307                            (k.clone(), transform_for_stringify(v))
308                        })
309                        .collect();
310                    JValue::object(transformed)
311                }
312                None => JValue::Null,
313            },
314            _ => value.clone(),
315        }
316    }
317
318    /// $length() - Get string length with proper Unicode support
319    /// Returns the number of Unicode characters (not bytes)
320    pub fn length(s: &str) -> Result<JValue, FunctionError> {
321        Ok(JValue::Number(s.chars().count() as f64))
322    }
323
324    /// $uppercase() - Convert to uppercase
325    pub fn uppercase(s: &str) -> Result<JValue, FunctionError> {
326        Ok(JValue::string(s.to_uppercase()))
327    }
328
329    /// $lowercase() - Convert to lowercase
330    pub fn lowercase(s: &str) -> Result<JValue, FunctionError> {
331        Ok(JValue::string(s.to_lowercase()))
332    }
333
334    /// $substring(str, start, length) - Extract substring
335    /// Extracts a substring from a string using Unicode character positions.
336    /// Follows the JSONata spec (which mirrors JS Array.prototype.slice):
337    /// - start: zero-based position; negative means count from end
338    /// - length: optional max number of characters to extract
339    pub fn substring(s: &str, start: i64, length: Option<i64>) -> Result<JValue, FunctionError> {
340        let chars: Vec<char> = s.chars().collect();
341        let str_len = chars.len() as i64;
342
343        // Clamp start if it goes past the beginning
344        // Matches JS: if (strLength + start < 0) { start = 0; }
345        let start = if str_len + start < 0 { 0 } else { start };
346
347        if let Some(len) = length {
348            // Negative or zero length → empty string (matches JS reference)
349            if len <= 0 {
350                return Ok(JValue::string(""));
351            }
352            // Compute end index: mirrors JS reference exactly
353            // JS: var end = start >= 0 ? start + length : strLength + start + length;
354            let end = if start >= 0 {
355                start + len
356            } else {
357                str_len + start + len
358            };
359            // JS: strArray.slice(start, end).join('')
360            // JS slice handles negative start natively (counts from end)
361            let slice = js_slice(&chars, start, Some(end));
362            Ok(JValue::string(slice.iter().collect::<String>()))
363        } else {
364            // No length: take from start to end of string
365            // JS: strArray.slice(start).join('')
366            let slice = js_slice(&chars, start, None);
367            Ok(JValue::string(slice.iter().collect::<String>()))
368        }
369    }
370
371    /// $substringBefore(str, separator) - Get substring before separator
372    pub fn substring_before(s: &str, separator: &str) -> Result<JValue, FunctionError> {
373        if separator.is_empty() {
374            return Ok(JValue::string(""));
375        }
376
377        let result = s.split(separator).next().unwrap_or(s).to_string();
378        Ok(JValue::string(result))
379    }
380
381    /// $substringAfter(str, separator) - Get substring after separator
382    pub fn substring_after(s: &str, separator: &str) -> Result<JValue, FunctionError> {
383        if separator.is_empty() {
384            return Ok(JValue::string(s));
385        }
386
387        if let Some(pos) = s.find(separator) {
388            let result = s[pos + separator.len()..].to_string();
389            Ok(JValue::string(result))
390        } else {
391            // If separator not found, return the original string
392            Ok(JValue::string(s))
393        }
394    }
395
396    /// $trim(str) - Normalize and trim whitespace
397    ///
398    /// Normalizes whitespace by replacing runs of whitespace characters (space, tab, newline, etc.)
399    /// with a single space, then strips leading and trailing spaces.
400    pub fn trim(s: &str) -> Result<JValue, FunctionError> {
401        use regex::Regex;
402        use std::sync::OnceLock;
403
404        static WS_REGEX: OnceLock<Regex> = OnceLock::new();
405        let ws_regex = WS_REGEX.get_or_init(|| Regex::new(r"[ \t\n\r]+").unwrap());
406
407        let normalized = ws_regex.replace_all(s, " ");
408        Ok(JValue::string(normalized.trim()))
409    }
410
411    /// $contains(str, pattern) - Check if string contains substring or matches regex
412    pub fn contains(s: &str, pattern: &JValue) -> Result<JValue, FunctionError> {
413        // Check if pattern is a regex
414        if let Some((pat, flags)) = extract_regex(pattern) {
415            let re = build_regex(&pat, &flags)?;
416            return Ok(JValue::Bool(re.is_match(s)));
417        }
418
419        // Handle string pattern
420        let pat = match pattern {
421            JValue::String(s) => &**s,
422            _ => {
423                return Err(FunctionError::TypeError(
424                    "contains() requires string arguments".to_string(),
425                ))
426            }
427        };
428
429        Ok(JValue::Bool(s.contains(pat)))
430    }
431
432    /// $split(str, separator, limit) - Split string into array
433    /// separator can be a string or a regex object
434    pub fn split(
435        s: &str,
436        separator: &JValue,
437        limit: Option<usize>,
438    ) -> Result<JValue, FunctionError> {
439        // Check if separator is a regex
440        if let Some((pattern, flags)) = extract_regex(separator) {
441            let re = build_regex(&pattern, &flags)?;
442
443            let parts: Vec<JValue> = re.split(s).map(JValue::string).collect();
444
445            // Truncate to limit if specified (limit is max number of results)
446            let result = if let Some(lim) = limit {
447                parts.into_iter().take(lim).collect()
448            } else {
449                parts
450            };
451
452            return Ok(JValue::array(result));
453        }
454
455        // Handle string separator
456        let sep = match separator {
457            JValue::String(s) => &**s,
458            _ => {
459                return Err(FunctionError::TypeError(
460                    "split() requires string arguments".to_string(),
461                ))
462            }
463        };
464
465        if sep.is_empty() {
466            // Split into individual characters
467            let chars: Vec<JValue> = s.chars().map(|c| JValue::string(c.to_string())).collect();
468            // Truncate to limit if specified
469            let result = if let Some(lim) = limit {
470                chars.into_iter().take(lim).collect()
471            } else {
472                chars
473            };
474            return Ok(JValue::array(result));
475        }
476
477        let parts: Vec<JValue> = s.split(sep).map(JValue::string).collect();
478
479        // Truncate to limit if specified (limit is max number of results)
480        let result = if let Some(lim) = limit {
481            parts.into_iter().take(lim).collect()
482        } else {
483            parts
484        };
485
486        Ok(JValue::array(result))
487    }
488
489    /// $join(array, separator) - Join array into string
490    pub fn join(arr: &[JValue], separator: Option<&str>) -> Result<JValue, FunctionError> {
491        let sep = separator.unwrap_or("");
492        let parts: Result<Vec<String>, FunctionError> = arr
493            .iter()
494            .map(|v| match v {
495                JValue::String(s) => Ok(s.to_string()),
496                JValue::Number(n) => Ok(format_join_number(*n)),
497                JValue::Bool(b) => Ok(b.to_string()),
498                JValue::Null => Ok(String::new()),
499                _ => Err(FunctionError::TypeError(
500                    "Cannot join array containing objects or nested arrays".to_string(),
501                )),
502            })
503            .collect();
504
505        let parts = parts?;
506        Ok(JValue::string(parts.join(sep)))
507    }
508
509    /// Helper to format a number for $join (matching serde_json Number's Display)
510    fn format_join_number(n: f64) -> String {
511        if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
512            (n as i64).to_string()
513        } else {
514            n.to_string()
515        }
516    }
517
518    /// Helper to perform capture group substitution in replacement string
519    /// Handles $0 (full match), $1, $2, etc. (capture groups), and $$ (literal $)
520    fn substitute_capture_groups(
521        replacement: &str,
522        full_match: &str,
523        groups: &[Option<regex::Match>],
524    ) -> String {
525        let mut result = String::new();
526        let mut position = 0;
527        let chars: Vec<char> = replacement.chars().collect();
528
529        while position < chars.len() {
530            if chars[position] == '$' {
531                position += 1;
532
533                if position >= chars.len() {
534                    // $ at end of string, treat as literal
535                    result.push('$');
536                    break;
537                }
538
539                let next_ch = chars[position];
540
541                if next_ch == '$' {
542                    // $$ → literal $
543                    result.push('$');
544                    position += 1;
545                } else if next_ch == '0' {
546                    // $0 → full match
547                    result.push_str(full_match);
548                    position += 1;
549                } else if next_ch.is_ascii_digit() {
550                    // Calculate maxDigits based on number of capture groups
551                    // This matches the JavaScript implementation's logic
552                    let max_digits = if groups.is_empty() {
553                        1
554                    } else {
555                        // floor(log10(groups.len())) + 1
556                        ((groups.len() as f64).log10().floor() as usize) + 1
557                    };
558
559                    // Collect up to max_digits consecutive digits
560                    let mut digits_end = position;
561                    let mut digit_count = 0;
562                    while digits_end < chars.len()
563                        && chars[digits_end].is_ascii_digit()
564                        && digit_count < max_digits
565                    {
566                        digits_end += 1;
567                        digit_count += 1;
568                    }
569
570                    if digit_count > 0 {
571                        // Try to parse as group number
572                        let num_str: String = chars[position..digits_end].iter().collect();
573                        let mut group_num = num_str.parse::<usize>().unwrap();
574
575                        // If the group number is out of range and we collected more than 1 digit,
576                        // try parsing with one fewer digit (fallback logic)
577                        let mut used_digits = digit_count;
578                        if max_digits > 1 && group_num > groups.len() && digit_count > 1 {
579                            let fallback_str: String =
580                                chars[position..digits_end - 1].iter().collect();
581                            if let Ok(fallback_num) = fallback_str.parse::<usize>() {
582                                group_num = fallback_num;
583                                used_digits = digit_count - 1;
584                            }
585                        }
586
587                        // Check if this is a valid group reference
588                        if groups.is_empty() {
589                            // No capture groups at all - $n is replaced with empty string
590                            // and position advances past the digits (per JS implementation)
591                            position += used_digits;
592                        } else if group_num > 0 && group_num <= groups.len() {
593                            // Valid group reference
594                            if let Some(m) = &groups[group_num - 1] {
595                                result.push_str(m.as_str());
596                            }
597                            // If group didn't match (None), add nothing (empty string)
598                            position += used_digits;
599                        } else {
600                            // Group number out of range - replace with empty string
601                            // and advance position (per JS implementation)
602                            position += used_digits;
603                        }
604                    } else {
605                        // No digits found (shouldn't happen since we checked next_ch.is_ascii_digit())
606                        result.push('$');
607                    }
608                } else {
609                    // $ followed by non-digit, treat as literal $
610                    result.push('$');
611                    // Don't consume the next character, let it be processed in next iteration
612                }
613            } else {
614                result.push(chars[position]);
615                position += 1;
616            }
617        }
618
619        result
620    }
621
622    /// $replace(str, pattern, replacement, limit) - Replace substring or regex matches
623    pub fn replace(
624        s: &str,
625        pattern: &JValue,
626        replacement: &str,
627        limit: Option<usize>,
628    ) -> Result<JValue, FunctionError> {
629        // Check if pattern is a regex
630        if let Some((pat, flags)) = extract_regex(pattern) {
631            let re = build_regex(&pat, &flags)?;
632
633            let mut count = 0;
634            let mut last_match = 0;
635            let mut output = String::new();
636
637            for cap in re.captures_iter(s) {
638                if limit.is_some_and(|lim| count >= lim) {
639                    break;
640                }
641
642                let m = cap.get(0).unwrap();
643
644                // D1004: Regular expression matches zero length string
645                if m.as_str().is_empty() {
646                    return Err(FunctionError::RuntimeError(
647                        "D1004: Regular expression matches zero length string".to_string(),
648                    ));
649                }
650
651                output.push_str(&s[last_match..m.start()]);
652
653                // Collect capture groups
654                let groups: Vec<Option<regex::Match>> =
655                    (1..cap.len()).map(|i| cap.get(i)).collect();
656
657                // Perform capture group substitution
658                let substituted = substitute_capture_groups(replacement, m.as_str(), &groups);
659                output.push_str(&substituted);
660
661                last_match = m.end();
662                count += 1;
663            }
664
665            output.push_str(&s[last_match..]);
666            return Ok(JValue::string(output));
667        }
668
669        // Handle string pattern
670        let pat = match pattern {
671            JValue::String(s) => &**s,
672            _ => {
673                return Err(FunctionError::TypeError(
674                    "replace() requires string arguments".to_string(),
675                ))
676            }
677        };
678
679        if pat.is_empty() {
680            return Err(FunctionError::RuntimeError(
681                "D3010: Pattern cannot be empty".to_string(),
682            ));
683        }
684
685        let result = if let Some(lim) = limit {
686            let mut remaining = s;
687            let mut output = String::new();
688            let mut count = 0;
689
690            while count < lim {
691                if let Some(pos) = remaining.find(pat) {
692                    output.push_str(&remaining[..pos]);
693                    output.push_str(replacement);
694                    remaining = &remaining[pos + pat.len()..];
695                    count += 1;
696                } else {
697                    output.push_str(remaining);
698                    break;
699                }
700            }
701            if count == lim {
702                output.push_str(remaining);
703            }
704            output
705        } else {
706            s.replace(pat, replacement)
707        };
708
709        Ok(JValue::string(result))
710    }
711}
712
713/// Built-in boolean functions
714pub mod boolean {
715    use super::*;
716
717    /// $boolean(value) - Convert value to boolean
718    ///
719    /// Conversion rules:
720    /// - boolean: unchanged
721    /// - string: zero-length -> false; otherwise -> true
722    /// - number: 0 -> false; otherwise -> true
723    /// - null -> false
724    /// - array: empty -> false; single element -> recursive; multi-element -> any truthy
725    /// - object: empty -> false; non-empty -> true
726    /// - function -> false
727    pub fn boolean(value: &JValue) -> Result<JValue, FunctionError> {
728        Ok(JValue::Bool(to_boolean(value)))
729    }
730
731    /// Helper function to recursively convert values to boolean.
732    fn to_boolean(value: &JValue) -> bool {
733        match value {
734            JValue::Null | JValue::Undefined => false,
735            JValue::Bool(b) => *b,
736            JValue::Number(n) => *n != 0.0,
737            JValue::String(s) => !s.is_empty(),
738            JValue::Array(arr) => {
739                if arr.len() == 1 {
740                    to_boolean(&arr[0])
741                } else {
742                    // Empty arrays are falsy; multi-element: true if any element is truthy
743                    arr.iter().any(to_boolean)
744                }
745            }
746            JValue::Object(obj) => !obj.is_empty(),
747            JValue::Lambda { .. } | JValue::Builtin { .. } => false,
748            JValue::Regex { .. } => true,
749            #[cfg(feature = "python")]
750            JValue::LazyPyDict(lazy) => !lazy.is_empty(),
751        }
752    }
753}
754
755/// Built-in numeric functions
756pub mod numeric {
757    use super::*;
758
759    /// $number(value) - Convert value to number
760    /// Supports decimal, hex (0x), octal (0o), and binary (0b) formats
761    pub fn number(value: &JValue) -> Result<JValue, FunctionError> {
762        match value {
763            JValue::Number(n) => {
764                let f = *n;
765                if !f.is_finite() {
766                    return Err(FunctionError::RuntimeError(
767                        "D3030: Cannot convert infinite number".to_string(),
768                    ));
769                }
770                Ok(JValue::Number(f))
771            }
772            JValue::String(s) => {
773                let trimmed = s.trim();
774
775                // Try hex, octal, or binary format first (0x, 0o, 0b)
776                if let Some(stripped) = trimmed
777                    .strip_prefix("0x")
778                    .or_else(|| trimmed.strip_prefix("0X"))
779                {
780                    // Hexadecimal
781                    return i64::from_str_radix(stripped, 16)
782                        .map(|n| JValue::Number(n as f64))
783                        .map_err(|_| {
784                            FunctionError::RuntimeError(format!(
785                                "D3030: Cannot convert '{}' to number",
786                                s
787                            ))
788                        });
789                } else if let Some(stripped) = trimmed
790                    .strip_prefix("0o")
791                    .or_else(|| trimmed.strip_prefix("0O"))
792                {
793                    // Octal
794                    return i64::from_str_radix(stripped, 8)
795                        .map(|n| JValue::Number(n as f64))
796                        .map_err(|_| {
797                            FunctionError::RuntimeError(format!(
798                                "D3030: Cannot convert '{}' to number",
799                                s
800                            ))
801                        });
802                } else if let Some(stripped) = trimmed
803                    .strip_prefix("0b")
804                    .or_else(|| trimmed.strip_prefix("0B"))
805                {
806                    // Binary
807                    return i64::from_str_radix(stripped, 2)
808                        .map(|n| JValue::Number(n as f64))
809                        .map_err(|_| {
810                            FunctionError::RuntimeError(format!(
811                                "D3030: Cannot convert '{}' to number",
812                                s
813                            ))
814                        });
815                }
816
817                // Try decimal format
818                match trimmed.parse::<f64>() {
819                    Ok(n) => {
820                        // Validate the number is finite
821                        if !n.is_finite() {
822                            return Err(FunctionError::RuntimeError(format!(
823                                "D3030: Cannot convert '{}' to number",
824                                s
825                            )));
826                        }
827                        Ok(JValue::Number(n))
828                    }
829                    Err(_) => Err(FunctionError::RuntimeError(format!(
830                        "D3030: Cannot convert '{}' to number",
831                        s
832                    ))),
833                }
834            }
835            JValue::Bool(true) => Ok(JValue::Number(1.0)),
836            JValue::Bool(false) => Ok(JValue::Number(0.0)),
837            JValue::Null => Err(FunctionError::RuntimeError(
838                "D3030: Cannot convert null to number".to_string(),
839            )),
840            _ => Err(FunctionError::RuntimeError(
841                "D3030: Cannot convert array or object to number".to_string(),
842            )),
843        }
844    }
845
846    /// $sum(array) - Sum array of numbers
847    pub fn sum(arr: &[JValue]) -> Result<JValue, FunctionError> {
848        if arr.is_empty() {
849            return Ok(JValue::Number(0.0));
850        }
851
852        let mut total = 0.0;
853        for value in arr {
854            match value {
855                JValue::Number(n) => {
856                    total += n;
857                }
858                _ => {
859                    return Err(FunctionError::TypeError(format!(
860                        "sum() requires all array elements to be numbers, got: {:?}",
861                        value
862                    )))
863                }
864            }
865        }
866        Ok(JValue::Number(total))
867    }
868
869    /// $max(array) - Maximum value
870    pub fn max(arr: &[JValue]) -> Result<JValue, FunctionError> {
871        if arr.is_empty() {
872            return Ok(JValue::Null);
873        }
874
875        let mut max_val = f64::NEG_INFINITY;
876        for value in arr {
877            match value {
878                JValue::Number(n) => {
879                    if *n > max_val {
880                        max_val = *n;
881                    }
882                }
883                _ => {
884                    return Err(FunctionError::TypeError(
885                        "max() requires all array elements to be numbers".to_string(),
886                    ))
887                }
888            }
889        }
890        Ok(JValue::Number(max_val))
891    }
892
893    /// $min(array) - Minimum value
894    pub fn min(arr: &[JValue]) -> Result<JValue, FunctionError> {
895        if arr.is_empty() {
896            return Ok(JValue::Null);
897        }
898
899        let mut min_val = f64::INFINITY;
900        for value in arr {
901            match value {
902                JValue::Number(n) => {
903                    if *n < min_val {
904                        min_val = *n;
905                    }
906                }
907                _ => {
908                    return Err(FunctionError::TypeError(
909                        "min() requires all array elements to be numbers".to_string(),
910                    ))
911                }
912            }
913        }
914        Ok(JValue::Number(min_val))
915    }
916
917    /// $average(array) - Average value
918    pub fn average(arr: &[JValue]) -> Result<JValue, FunctionError> {
919        if arr.is_empty() {
920            return Ok(JValue::Null);
921        }
922
923        let sum_result = sum(arr)?;
924        if let JValue::Number(n) = sum_result {
925            let avg = n / arr.len() as f64;
926            Ok(JValue::Number(avg))
927        } else {
928            Err(FunctionError::RuntimeError("Sum failed".to_string()))
929        }
930    }
931
932    /// $abs(number) - Absolute value
933    pub fn abs(n: f64) -> Result<JValue, FunctionError> {
934        Ok(JValue::Number(n.abs()))
935    }
936
937    /// $floor(number) - Floor
938    pub fn floor(n: f64) -> Result<JValue, FunctionError> {
939        Ok(JValue::Number(n.floor()))
940    }
941
942    /// $ceil(number) - Ceiling
943    pub fn ceil(n: f64) -> Result<JValue, FunctionError> {
944        Ok(JValue::Number(n.ceil()))
945    }
946
947    /// $round(number, precision) - Round to precision using "round half to even" (banker's rounding)
948    ///
949    /// This implements the same rounding behavior as JSONata's JavaScript implementation,
950    /// which rounds .5 values to the nearest even number.
951    ///
952    /// precision can be:
953    /// - positive: round to that many decimal places (e.g., 2 -> 0.01)
954    /// - zero or omitted: round to nearest integer
955    /// - negative: round to powers of 10 (e.g., -2 -> nearest 100)
956    pub fn round(n: f64, precision: Option<i32>) -> Result<JValue, FunctionError> {
957        let prec = precision.unwrap_or(0);
958
959        // Shift decimal place for precision (works for both positive and negative)
960        let multiplier = 10_f64.powi(prec);
961        let scaled = n * multiplier;
962
963        // Implement round-half-to-even (banker's rounding)
964        let floor_val = scaled.floor();
965        let frac = scaled - floor_val;
966
967        // Use a small epsilon for floating point comparison
968        let epsilon = 1e-10;
969        let result = if (frac - 0.5).abs() < epsilon {
970            // Exactly at .5 (within tolerance) - round to even
971            let floor_int = floor_val as i64;
972            if floor_int % 2 == 0 {
973                floor_val // floor is even, stay there
974            } else {
975                floor_val + 1.0 // floor is odd, round up to even
976            }
977        } else if frac > 0.5 {
978            floor_val + 1.0 // round up
979        } else {
980            floor_val // round down
981        };
982
983        // Shift back
984        let final_result = result / multiplier;
985
986        Ok(JValue::Number(final_result))
987    }
988
989    /// $sqrt(number) - Square root
990    pub fn sqrt(n: f64) -> Result<JValue, FunctionError> {
991        if n < 0.0 {
992            return Err(FunctionError::ArgumentError(
993                "Cannot take square root of negative number".to_string(),
994            ));
995        }
996        Ok(JValue::Number(n.sqrt()))
997    }
998
999    /// $power(base, exponent) - Power
1000    pub fn power(base: f64, exponent: f64) -> Result<JValue, FunctionError> {
1001        let result = base.powf(exponent);
1002        if result.is_nan() || result.is_infinite() {
1003            return Err(FunctionError::RuntimeError(
1004                "Power operation resulted in invalid number".to_string(),
1005            ));
1006        }
1007        Ok(JValue::Number(result))
1008    }
1009
1010    /// $formatNumber(value, picture, options) - Format number with picture string
1011    /// Implements XPath F&O number formatting specification
1012    pub fn format_number(
1013        value: f64,
1014        picture: &str,
1015        options: Option<&JValue>,
1016    ) -> Result<JValue, FunctionError> {
1017        // Default format properties (can be overridden by options)
1018        let mut decimal_separator = '.';
1019        let mut grouping_separator = ',';
1020        let mut zero_digit = '0';
1021        let mut percent_symbol = "%".to_string();
1022        let mut per_mille_symbol = "\u{2030}".to_string();
1023        let digit_char = '#';
1024        let pattern_separator = ';';
1025
1026        // Parse options if provided
1027        if let Some(JValue::Object(opts)) = options {
1028            if let Some(JValue::String(s)) = opts.get("decimal-separator") {
1029                decimal_separator = s.chars().next().unwrap_or('.');
1030            }
1031            if let Some(JValue::String(s)) = opts.get("grouping-separator") {
1032                grouping_separator = s.chars().next().unwrap_or(',');
1033            }
1034            if let Some(JValue::String(s)) = opts.get("zero-digit") {
1035                zero_digit = s.chars().next().unwrap_or('0');
1036            }
1037            if let Some(JValue::String(s)) = opts.get("percent") {
1038                percent_symbol = s.to_string();
1039            }
1040            if let Some(JValue::String(s)) = opts.get("per-mille") {
1041                per_mille_symbol = s.to_string();
1042            }
1043        }
1044
1045        // Split picture into sub-pictures (positive and negative patterns)
1046        let sub_pictures: Vec<&str> = picture.split(pattern_separator).collect();
1047        if sub_pictures.len() > 2 {
1048            return Err(FunctionError::ArgumentError(
1049                "D3080: Too many pattern separators in picture string".to_string(),
1050            ));
1051        }
1052
1053        // Parse and analyze the picture string
1054        let parts = parse_picture(
1055            sub_pictures[0],
1056            decimal_separator,
1057            grouping_separator,
1058            zero_digit,
1059            digit_char,
1060            &percent_symbol,
1061            &per_mille_symbol,
1062        )?;
1063
1064        // For negative numbers, use second pattern or add minus sign to first pattern
1065        let is_negative = value < 0.0;
1066        let mut abs_value = value.abs();
1067
1068        // Apply percent or per-mille scaling
1069        if parts.has_percent {
1070            abs_value *= 100.0;
1071        } else if parts.has_per_mille {
1072            abs_value *= 1000.0;
1073        }
1074
1075        // Apply the pattern
1076        let formatted = apply_number_picture(
1077            abs_value,
1078            &parts,
1079            decimal_separator,
1080            grouping_separator,
1081            zero_digit,
1082        )?;
1083
1084        // Add prefix/suffix and handle negative
1085        let result = if is_negative {
1086            if sub_pictures.len() == 2 {
1087                // Use second pattern for negatives
1088                let neg_parts = parse_picture(
1089                    sub_pictures[1],
1090                    decimal_separator,
1091                    grouping_separator,
1092                    zero_digit,
1093                    digit_char,
1094                    &percent_symbol,
1095                    &per_mille_symbol,
1096                )?;
1097                let neg_formatted = apply_number_picture(
1098                    abs_value,
1099                    &neg_parts,
1100                    decimal_separator,
1101                    grouping_separator,
1102                    zero_digit,
1103                )?;
1104                format!("{}{}{}", neg_parts.prefix, neg_formatted, neg_parts.suffix)
1105            } else {
1106                // Add minus sign to prefix
1107                format!("-{}{}{}", parts.prefix, formatted, parts.suffix)
1108            }
1109        } else {
1110            format!("{}{}{}", parts.prefix, formatted, parts.suffix)
1111        };
1112
1113        Ok(JValue::string(result))
1114    }
1115
1116    /// Helper to check if a character is in the digit family (0-9 or custom zero-digit family)
1117    fn is_digit_in_family(c: char, zero_digit: char) -> bool {
1118        if c.is_ascii_digit() {
1119            return true;
1120        }
1121        // Check if c is in custom digit family (zero_digit to zero_digit+9)
1122        let zero_code = zero_digit as u32;
1123        let c_code = c as u32;
1124        c_code >= zero_code && c_code < zero_code + 10
1125    }
1126
1127    /// Parse a picture string into its components
1128    fn parse_picture(
1129        picture: &str,
1130        decimal_sep: char,
1131        grouping_sep: char,
1132        zero_digit: char,
1133        digit_char: char,
1134        percent_symbol: &str,
1135        per_mille_symbol: &str,
1136    ) -> Result<PictureParts, FunctionError> {
1137        // Work with character vectors to avoid UTF-8 byte boundary issues
1138        let chars: Vec<char> = picture.chars().collect();
1139
1140        // Find prefix (chars before any active char)
1141        // Active chars for prefix/suffix: decimal sep, grouping sep, digit char, or digit family members
1142        // NOTE: 'e'/'E' are NOT included here to avoid treating them as exponent markers in prefix/suffix
1143        let prefix_end = chars
1144            .iter()
1145            .position(|&c| {
1146                c == decimal_sep
1147                    || c == grouping_sep
1148                    || c == digit_char
1149                    || is_digit_in_family(c, zero_digit)
1150            })
1151            .unwrap_or(chars.len());
1152        let prefix: String = chars[..prefix_end].iter().collect();
1153
1154        // Find suffix (chars after last active char)
1155        let suffix_start = chars
1156            .iter()
1157            .rposition(|&c| {
1158                c == decimal_sep
1159                    || c == grouping_sep
1160                    || c == digit_char
1161                    || is_digit_in_family(c, zero_digit)
1162            })
1163            .map(|pos| pos + 1)
1164            .unwrap_or(chars.len());
1165        let suffix: String = chars[suffix_start..].iter().collect();
1166
1167        // Active part (between prefix and suffix)
1168        let active: String = chars[prefix_end..suffix_start].iter().collect();
1169
1170        // Check for exponential notation (e.g., "00.000e0")
1171        let exponent_pos = active.find('e').or_else(|| active.find('E'));
1172        let (mantissa_part, exponent_part): (String, String) = if let Some(pos) = exponent_pos {
1173            (active[..pos].to_string(), active[pos + 1..].to_string())
1174        } else {
1175            (active.clone(), String::new())
1176        };
1177
1178        // Split mantissa into integer and fractional parts using character positions
1179        let mantissa_chars: Vec<char> = mantissa_part.chars().collect();
1180        let decimal_pos = mantissa_chars.iter().position(|&c| c == decimal_sep);
1181        let (integer_part, fractional_part): (String, String) = if let Some(pos) = decimal_pos {
1182            (
1183                mantissa_chars[..pos].iter().collect(),
1184                mantissa_chars[pos + 1..].iter().collect(),
1185            )
1186        } else {
1187            (mantissa_part.clone(), String::new())
1188        };
1189
1190        // Validate: only one decimal separator
1191        if active.matches(decimal_sep).count() > 1 {
1192            return Err(FunctionError::ArgumentError(
1193                "D3081: Multiple decimal separators in picture".to_string(),
1194            ));
1195        }
1196
1197        // Validate: no grouping separator adjacent to decimal
1198        if let Some(pos) = decimal_pos {
1199            if pos > 0 && active.chars().nth(pos - 1) == Some(grouping_sep) {
1200                return Err(FunctionError::ArgumentError(
1201                    "D3087: Grouping separator adjacent to decimal separator".to_string(),
1202                ));
1203            }
1204            if pos + 1 < active.len() && active.chars().nth(pos + 1) == Some(grouping_sep) {
1205                return Err(FunctionError::ArgumentError(
1206                    "D3087: Grouping separator adjacent to decimal separator".to_string(),
1207                ));
1208            }
1209        }
1210
1211        // Validate: no consecutive grouping separators
1212        let grouping_str = format!("{}{}", grouping_sep, grouping_sep);
1213        if picture.contains(&grouping_str) {
1214            return Err(FunctionError::ArgumentError(
1215                "D3089: Consecutive grouping separators in picture".to_string(),
1216            ));
1217        }
1218
1219        // Detect percent and per-mille symbols
1220        let has_percent = picture.contains(percent_symbol);
1221        let has_per_mille = picture.contains(per_mille_symbol);
1222
1223        // Validate: multiple percent signs
1224        if picture.matches(percent_symbol).count() > 1 {
1225            return Err(FunctionError::ArgumentError(
1226                "D3082: Multiple percent signs in picture".to_string(),
1227            ));
1228        }
1229
1230        // Validate: multiple per-mille signs
1231        if picture.matches(per_mille_symbol).count() > 1 {
1232            return Err(FunctionError::ArgumentError(
1233                "D3083: Multiple per-mille signs in picture".to_string(),
1234            ));
1235        }
1236
1237        // Validate: cannot have both percent and per-mille
1238        if has_percent && has_per_mille {
1239            return Err(FunctionError::ArgumentError(
1240                "D3084: Cannot have both percent and per-mille in picture".to_string(),
1241            ));
1242        }
1243
1244        // Validate: integer part cannot end with grouping separator
1245        if !integer_part.is_empty() && integer_part.ends_with(grouping_sep) {
1246            return Err(FunctionError::ArgumentError(
1247                "D3088: Integer part ends with grouping separator".to_string(),
1248            ));
1249        }
1250
1251        // Validate: at least one digit in mantissa (integer or fractional part)
1252        let has_digit_in_integer = integer_part
1253            .chars()
1254            .any(|c| is_digit_in_family(c, zero_digit) || c == digit_char);
1255        let has_digit_in_fractional = fractional_part
1256            .chars()
1257            .any(|c| is_digit_in_family(c, zero_digit) || c == digit_char);
1258        if !has_digit_in_integer && !has_digit_in_fractional {
1259            return Err(FunctionError::ArgumentError(
1260                "D3085: Picture must contain at least one digit".to_string(),
1261            ));
1262        }
1263
1264        // Count minimum integer digits (mandatory digits in digit family)
1265        let min_integer_digits = integer_part
1266            .chars()
1267            .filter(|&c| is_digit_in_family(c, zero_digit))
1268            .count();
1269
1270        // Count minimum and maximum fractional digits
1271        let min_fractional_digits = fractional_part
1272            .chars()
1273            .filter(|&c| is_digit_in_family(c, zero_digit))
1274            .count();
1275        let mut max_fractional_digits = fractional_part
1276            .chars()
1277            .filter(|&c| is_digit_in_family(c, zero_digit) || c == digit_char)
1278            .count();
1279
1280        // If there's a decimal point but no fractional digits specified, default to 1
1281        // This handles cases like "#.e0" where some fractional precision is expected
1282        if decimal_pos.is_some() && max_fractional_digits == 0 {
1283            max_fractional_digits = 1;
1284        }
1285
1286        // Find grouping positions in integer part
1287        let mut grouping_positions = Vec::new();
1288        let int_chars: Vec<char> = integer_part.chars().collect();
1289        for (i, &c) in int_chars.iter().enumerate() {
1290            if c == grouping_sep {
1291                // Count digits to the right of this separator
1292                let digits_to_right = int_chars[i + 1..]
1293                    .iter()
1294                    .filter(|&&ch| is_digit_in_family(ch, zero_digit) || ch == digit_char)
1295                    .count();
1296                grouping_positions.push(digits_to_right);
1297            }
1298        }
1299
1300        // Check if grouping is regular (same interval)
1301        let regular_grouping = if grouping_positions.is_empty() {
1302            0
1303        } else if grouping_positions.len() == 1 {
1304            grouping_positions[0]
1305        } else {
1306            // Check if all intervals are the same
1307            let first_interval = grouping_positions[0];
1308            if grouping_positions.iter().all(|&p| {
1309                grouping_positions.iter().filter(|&&x| x == p).count()
1310                    == grouping_positions.len() / first_interval
1311                    || (p % first_interval == 0 && grouping_positions.contains(&first_interval))
1312            }) {
1313                first_interval
1314            } else {
1315                0 // Irregular grouping
1316            }
1317        };
1318
1319        // Find grouping positions in fractional part
1320        let mut fractional_grouping_positions = Vec::new();
1321        let frac_chars: Vec<char> = fractional_part.chars().collect();
1322        for (i, &c) in frac_chars.iter().enumerate() {
1323            if c == grouping_sep {
1324                // For fractional part, count digits to the left of this separator
1325                let digits_to_left = frac_chars[..i]
1326                    .iter()
1327                    .filter(|&&ch| is_digit_in_family(ch, zero_digit) || ch == digit_char)
1328                    .count();
1329                fractional_grouping_positions.push(digits_to_left);
1330            }
1331        }
1332
1333        // Process exponent part if present (recognize both ASCII and custom digit families)
1334        let min_exponent_digits = if !exponent_part.is_empty() {
1335            exponent_part
1336                .chars()
1337                .filter(|&c| is_digit_in_family(c, zero_digit))
1338                .count()
1339        } else {
1340            0
1341        };
1342
1343        // Validate: exponent part must contain only digit characters (ASCII or custom digit family)
1344        if !exponent_part.is_empty()
1345            && exponent_part
1346                .chars()
1347                .any(|c| !is_digit_in_family(c, zero_digit))
1348        {
1349            return Err(FunctionError::ArgumentError(
1350                "D3093: Exponent must contain only digit characters".to_string(),
1351            ));
1352        }
1353
1354        // Validate: exponent cannot be empty if 'e' is present
1355        if exponent_pos.is_some() && min_exponent_digits == 0 {
1356            return Err(FunctionError::ArgumentError(
1357                "D3093: Exponent cannot be empty".to_string(),
1358            ));
1359        }
1360
1361        // Validate: percent/per-mille not allowed with exponential notation
1362        if min_exponent_digits > 0 && (has_percent || has_per_mille) {
1363            return Err(FunctionError::ArgumentError(
1364                "D3092: Percent/per-mille not allowed with exponential notation".to_string(),
1365            ));
1366        }
1367
1368        // Validate: # cannot appear after 0 in integer part
1369        // In integer part, # must come before 0 (e.g., "##00" valid, "00##" invalid)
1370        let mut seen_zero_in_integer = false;
1371        for c in integer_part.chars() {
1372            if is_digit_in_family(c, zero_digit) {
1373                seen_zero_in_integer = true;
1374            } else if c == digit_char && seen_zero_in_integer {
1375                return Err(FunctionError::ArgumentError(
1376                    "D3090: Optional digit (#) cannot appear after mandatory digit (0) in integer part".to_string()
1377                ));
1378            }
1379        }
1380
1381        // Validate: # cannot appear before 0 in fractional part
1382        // In fractional part, 0 must come before # (e.g., "00##" valid, "##00" invalid)
1383        let mut seen_hash_in_fractional = false;
1384        for c in fractional_part.chars() {
1385            if c == digit_char {
1386                seen_hash_in_fractional = true;
1387            } else if is_digit_in_family(c, zero_digit) && seen_hash_in_fractional {
1388                return Err(FunctionError::ArgumentError(
1389                    "D3091: Mandatory digit (0) cannot appear after optional digit (#) in fractional part".to_string()
1390                ));
1391            }
1392        }
1393
1394        // Validate: invalid characters in picture
1395        // All characters in the active part must be valid (digits, decimal, grouping, or 'e'/'E')
1396        let valid_chars: Vec<char> =
1397            vec![decimal_sep, grouping_sep, zero_digit, digit_char, 'e', 'E'];
1398        for c in mantissa_part.chars() {
1399            if !is_digit_in_family(c, zero_digit) && !valid_chars.contains(&c) {
1400                return Err(FunctionError::ArgumentError(format!(
1401                    "D3086: Invalid character in picture: '{}'",
1402                    c
1403                )));
1404            }
1405        }
1406
1407        // Scaling factor = minimum integer digits in mantissa
1408        let scaling_factor = min_integer_digits;
1409
1410        Ok(PictureParts {
1411            prefix,
1412            suffix,
1413            min_integer_digits,
1414            min_fractional_digits,
1415            max_fractional_digits,
1416            grouping_positions,
1417            fractional_grouping_positions,
1418            regular_grouping,
1419            has_decimal: decimal_pos.is_some(),
1420            has_integer_part: !integer_part.is_empty(),
1421            has_percent,
1422            has_per_mille,
1423            min_exponent_digits,
1424            scaling_factor,
1425        })
1426    }
1427
1428    /// Apply the picture pattern to format a number
1429    fn apply_number_picture(
1430        value: f64,
1431        parts: &PictureParts,
1432        decimal_sep: char,
1433        grouping_sep: char,
1434        zero_digit: char,
1435    ) -> Result<String, FunctionError> {
1436        // Handle exponential notation
1437        let (mantissa, exponent) = if parts.min_exponent_digits > 0 {
1438            // Calculate mantissa and exponent: mantissa * 10^exponent = value
1439            let max_mantissa = 10_f64.powi(parts.scaling_factor as i32);
1440            let min_mantissa = 10_f64.powi(parts.scaling_factor as i32 - 1);
1441
1442            let mut m = value;
1443            let mut e = 0_i32;
1444
1445            // Scale mantissa to be within [min_mantissa, max_mantissa)
1446            while m < min_mantissa && m != 0.0 {
1447                m *= 10.0;
1448                e -= 1;
1449            }
1450            while m >= max_mantissa {
1451                m /= 10.0;
1452                e += 1;
1453            }
1454
1455            (m, Some(e))
1456        } else {
1457            (value, None)
1458        };
1459
1460        // Round mantissa to max fractional digits
1461        let factor = 10_f64.powi(parts.max_fractional_digits as i32);
1462        let rounded = (mantissa * factor).round() / factor;
1463
1464        // Convert to string with fixed decimal places
1465        let mut num_str = format!("{:.prec$}", rounded, prec = parts.max_fractional_digits);
1466
1467        // Replace '.' with decimal separator
1468        if decimal_sep != '.' {
1469            num_str = num_str.replace('.', &decimal_sep.to_string());
1470        }
1471
1472        // Split into integer and fractional parts
1473        let decimal_pos = num_str.find(decimal_sep).unwrap_or(num_str.len());
1474        let mut integer_str = num_str[..decimal_pos].to_string();
1475        let mut fractional_str = if decimal_pos < num_str.len() {
1476            num_str[decimal_pos + 1..].to_string()
1477        } else {
1478            String::new()
1479        };
1480
1481        // Strip leading zeros from integer part
1482        while integer_str.len() > 1 && integer_str.starts_with(zero_digit) {
1483            integer_str.remove(0);
1484        }
1485        // If we stripped down to a single zero and picture has no integer part, remove it
1486        if integer_str == zero_digit.to_string() && !parts.has_integer_part {
1487            integer_str.clear();
1488        }
1489        // If integer part is empty and picture had integer part, add one zero
1490        if integer_str.is_empty() && parts.has_integer_part {
1491            integer_str.push(zero_digit);
1492        }
1493
1494        // Strip trailing zeros from fractional part
1495        while !fractional_str.is_empty() && fractional_str.ends_with(zero_digit) {
1496            fractional_str.pop();
1497        }
1498
1499        // Pad integer part to minimum size
1500        while integer_str.len() < parts.min_integer_digits {
1501            integer_str.insert(0, zero_digit);
1502        }
1503
1504        // Pad fractional part to minimum size
1505        while fractional_str.len() < parts.min_fractional_digits {
1506            fractional_str.push(zero_digit);
1507        }
1508
1509        // Trim trailing zeros beyond minimum (for optional # digits)
1510        while fractional_str.len() > parts.min_fractional_digits {
1511            if fractional_str.ends_with(zero_digit) {
1512                fractional_str.pop();
1513            } else {
1514                break;
1515            }
1516        }
1517
1518        // Add grouping separators to integer part
1519        if parts.regular_grouping > 0 {
1520            // Regular grouping (e.g., every 3 digits for "#,###")
1521            let mut grouped = String::new();
1522            let chars: Vec<char> = integer_str.chars().collect();
1523            for (i, &c) in chars.iter().enumerate() {
1524                grouped.push(c);
1525                let pos_from_right = chars.len() - i - 1;
1526                if pos_from_right > 0 && pos_from_right % parts.regular_grouping == 0 {
1527                    grouped.push(grouping_sep);
1528                }
1529            }
1530            integer_str = grouped;
1531        } else if !parts.grouping_positions.is_empty() {
1532            // Irregular grouping (e.g., "9,99,999")
1533            let mut grouped = String::new();
1534            let chars: Vec<char> = integer_str.chars().collect();
1535            for (i, &c) in chars.iter().enumerate() {
1536                grouped.push(c);
1537                let pos_from_right = chars.len() - i - 1;
1538                if parts.grouping_positions.contains(&pos_from_right) {
1539                    grouped.push(grouping_sep);
1540                }
1541            }
1542            integer_str = grouped;
1543        }
1544
1545        // Add grouping separators to fractional part
1546        if !parts.fractional_grouping_positions.is_empty() {
1547            let mut grouped = String::new();
1548            let chars: Vec<char> = fractional_str.chars().collect();
1549            for (i, &c) in chars.iter().enumerate() {
1550                grouped.push(c);
1551                // For fractional grouping, positions are counted from the left
1552                let pos_from_left = i + 1;
1553                if parts.fractional_grouping_positions.contains(&pos_from_left) {
1554                    grouped.push(grouping_sep);
1555                }
1556            }
1557            fractional_str = grouped;
1558        }
1559
1560        // Combine integer and fractional parts
1561        let mut result = if parts.has_decimal || !fractional_str.is_empty() {
1562            format!("{}{}{}", integer_str, decimal_sep, fractional_str)
1563        } else {
1564            integer_str
1565        };
1566
1567        // Convert digits to custom zero-digit base if needed (mantissa part)
1568        if zero_digit != '0' {
1569            let zero_code = zero_digit as u32;
1570            result = result
1571                .chars()
1572                .map(|c| {
1573                    if c.is_ascii_digit() {
1574                        let digit_value = c as u32 - '0' as u32;
1575                        char::from_u32(zero_code + digit_value).unwrap_or(c)
1576                    } else {
1577                        c
1578                    }
1579                })
1580                .collect();
1581        }
1582
1583        // Append exponent if present
1584        if let Some(exp) = exponent {
1585            // Format exponent with minimum digits
1586            let exp_str = format!("{:0width$}", exp.abs(), width = parts.min_exponent_digits);
1587
1588            // Convert exponent digits to custom zero-digit base if needed
1589            let exp_formatted = if zero_digit != '0' {
1590                let zero_code = zero_digit as u32;
1591                exp_str
1592                    .chars()
1593                    .map(|c| {
1594                        if c.is_ascii_digit() {
1595                            let digit_value = c as u32 - '0' as u32;
1596                            char::from_u32(zero_code + digit_value).unwrap_or(c)
1597                        } else {
1598                            c
1599                        }
1600                    })
1601                    .collect()
1602            } else {
1603                exp_str
1604            };
1605
1606            // Append 'e' and exponent (with sign if negative)
1607            result.push('e');
1608            if exp < 0 {
1609                result.push('-');
1610            }
1611            result.push_str(&exp_formatted);
1612        }
1613
1614        Ok(result)
1615    }
1616
1617    /// Holds parsed picture pattern components
1618    #[derive(Debug)]
1619    struct PictureParts {
1620        prefix: String,
1621        suffix: String,
1622        min_integer_digits: usize,
1623        min_fractional_digits: usize,
1624        max_fractional_digits: usize,
1625        grouping_positions: Vec<usize>,
1626        fractional_grouping_positions: Vec<usize>,
1627        regular_grouping: usize,
1628        has_decimal: bool,
1629        has_integer_part: bool,
1630        has_percent: bool,
1631        has_per_mille: bool,
1632        min_exponent_digits: usize,
1633        scaling_factor: usize,
1634    }
1635
1636    /// $formatBase(value, radix) - Convert number to string in specified base
1637    /// radix defaults to 10, must be between 2 and 36
1638    pub fn format_base(value: f64, radix: Option<i64>) -> Result<JValue, FunctionError> {
1639        // Round to integer
1640        let int_value = value.round() as i64;
1641
1642        // Default radix is 10
1643        let radix = radix.unwrap_or(10);
1644
1645        // Validate radix is between 2 and 36
1646        if !(2..=36).contains(&radix) {
1647            return Err(FunctionError::ArgumentError(format!(
1648                "D3100: Radix must be between 2 and 36, got {}",
1649                radix
1650            )));
1651        }
1652
1653        // Handle negative numbers
1654        let is_negative = int_value < 0;
1655        let abs_value = int_value.unsigned_abs();
1656
1657        // Convert to string in specified base
1658        let digits = "0123456789abcdefghijklmnopqrstuvwxyz";
1659        let mut result = String::new();
1660        let mut val = abs_value;
1661
1662        if val == 0 {
1663            result.push('0');
1664        } else {
1665            while val > 0 {
1666                let digit = (val % radix as u64) as usize;
1667                result.insert(0, digits.chars().nth(digit).unwrap());
1668                val /= radix as u64;
1669            }
1670        }
1671
1672        // Add negative sign if needed
1673        if is_negative {
1674            result.insert(0, '-');
1675        }
1676
1677        Ok(JValue::string(result))
1678    }
1679}
1680
1681/// Built-in array functions
1682pub mod array {
1683    use super::*;
1684
1685    /// $count(array) - Count array elements
1686    pub fn count(arr: &[JValue]) -> Result<JValue, FunctionError> {
1687        Ok(JValue::Number(arr.len() as f64))
1688    }
1689
1690    /// $append(array1, array2) - Append arrays/values
1691    pub fn append(arr1: &[JValue], val: &JValue) -> Result<JValue, FunctionError> {
1692        let mut result = arr1.to_vec();
1693        match val {
1694            JValue::Array(arr2) => result.extend(arr2.iter().cloned()),
1695            other => result.push(other.clone()),
1696        }
1697        Ok(JValue::array(result))
1698    }
1699
1700    /// $reverse(array) - Reverse array
1701    pub fn reverse(arr: &[JValue]) -> Result<JValue, FunctionError> {
1702        let mut result = arr.to_vec();
1703        result.reverse();
1704        Ok(JValue::array(result))
1705    }
1706
1707    /// $sort(array) - Sort array
1708    pub fn sort(arr: &[JValue]) -> Result<JValue, FunctionError> {
1709        let mut result = arr.to_vec();
1710
1711        // Check if all elements are of comparable types
1712        let all_numbers = result.iter().all(|v| matches!(v, JValue::Number(_)));
1713        let all_strings = result.iter().all(|v| matches!(v, JValue::String(_)));
1714
1715        if all_numbers {
1716            result.sort_by(|a, b| {
1717                let a_num = a.as_f64().unwrap();
1718                let b_num = b.as_f64().unwrap();
1719                a_num
1720                    .partial_cmp(&b_num)
1721                    .unwrap_or(std::cmp::Ordering::Equal)
1722            });
1723        } else if all_strings {
1724            result.sort_by(|a, b| {
1725                let a_str = a.as_str().unwrap();
1726                let b_str = b.as_str().unwrap();
1727                a_str.cmp(b_str)
1728            });
1729        } else {
1730            return Err(FunctionError::TypeError(
1731                "sort() requires all elements to be of the same comparable type".to_string(),
1732            ));
1733        }
1734
1735        Ok(JValue::array(result))
1736    }
1737
1738    /// $distinct(array) - Get unique elements
1739    pub fn distinct(arr: &[JValue]) -> Result<JValue, FunctionError> {
1740        let mut result = Vec::new();
1741        let mut seen: Vec<JValue> = Vec::new();
1742
1743        for value in arr {
1744            // Materialize a lazy element before comparing so an unconvertible
1745            // Python value raises `TypeError` here instead of being silently
1746            // treated as "always distinct from everything else" -- `values_equal`'s
1747            // lazy arms return `false` (not-equal) on conversion failure, by
1748            // design (see its doc comment), which would let two references to
1749            // the very same unconvertible value (e.g. two aliases of one
1750            // Python `set`-valued field) both survive dedup.
1751            #[cfg(feature = "python")]
1752            let compare_value = match value {
1753                JValue::LazyPyDict(lazy) => JValue::Object(
1754                    lazy.to_object()
1755                        .map_err(|e| FunctionError::PyConversionError(e.0))?,
1756                ),
1757                other => other.clone(),
1758            };
1759            #[cfg(not(feature = "python"))]
1760            let compare_value = value.clone();
1761
1762            let mut is_new = true;
1763            for seen_value in &seen {
1764                if values_equal(&compare_value, seen_value) {
1765                    is_new = false;
1766                    break;
1767                }
1768            }
1769            if is_new {
1770                seen.push(compare_value);
1771                result.push(value.clone());
1772            }
1773        }
1774
1775        Ok(JValue::array(result))
1776    }
1777
1778    /// $exists(value) - Check if value exists (not null/undefined)
1779    pub fn exists(value: &JValue) -> Result<JValue, FunctionError> {
1780        let is_missing = value.is_null() || value.is_undefined();
1781        Ok(JValue::Bool(!is_missing))
1782    }
1783
1784    /// Compare two JSON values for deep equality (JSONata semantics)
1785    ///
1786    /// Cannot return `Result`, so a lazy-conversion failure in the `LazyPyDict` arms
1787    /// below is swallowed and yields `false` (not-equal), by design -- callers that need
1788    /// the failure to surface as a Python `TypeError` (the `=`/`!=`/`in` operators) must
1789    /// call `evaluator::normalize_lazy` on their operands *before* calling this function.
1790    pub fn values_equal(a: &JValue, b: &JValue) -> bool {
1791        match (a, b) {
1792            (JValue::Null, JValue::Null) => true,
1793            (JValue::Bool(a), JValue::Bool(b)) => a == b,
1794            (JValue::Number(a), JValue::Number(b)) => a == b,
1795            (JValue::String(a), JValue::String(b)) => a == b,
1796            (JValue::Array(a), JValue::Array(b)) => {
1797                a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))
1798            }
1799            (JValue::Object(a), JValue::Object(b)) => {
1800                a.len() == b.len()
1801                    && a.iter()
1802                        .all(|(k, v)| b.get(k).is_some_and(|v2| values_equal(v, v2)))
1803            }
1804            #[cfg(feature = "python")]
1805            (JValue::LazyPyDict(x), JValue::Object(bm)) => x.to_object_ref().is_some_and(|am| {
1806                am.len() == bm.len()
1807                    && am
1808                        .iter()
1809                        .all(|(k, v)| bm.get(k).is_some_and(|v2| values_equal(v, v2)))
1810            }),
1811            #[cfg(feature = "python")]
1812            (JValue::Object(am), JValue::LazyPyDict(y)) => y.to_object_ref().is_some_and(|bm| {
1813                am.len() == bm.len()
1814                    && am
1815                        .iter()
1816                        .all(|(k, v)| bm.get(k).is_some_and(|v2| values_equal(v, v2)))
1817            }),
1818            #[cfg(feature = "python")]
1819            (JValue::LazyPyDict(x), JValue::LazyPyDict(y)) => {
1820                // Pointer-identity fast path (mirrors `PartialEq for JValue`): the same
1821                // wrapped Python dict is trivially equal to itself without touching any
1822                // (possibly unconvertible) field content.
1823                std::rc::Rc::ptr_eq(x, y)
1824                    || x.same_object(y)
1825                    || x.to_object_ref().is_some_and(|am| {
1826                        y.to_object_ref().is_some_and(|bm| {
1827                            am.len() == bm.len()
1828                                && am
1829                                    .iter()
1830                                    .all(|(k, v)| bm.get(k).is_some_and(|v2| values_equal(v, v2)))
1831                        })
1832                    })
1833            }
1834            _ => false,
1835        }
1836    }
1837
1838    /// $shuffle(array) - Randomly shuffle array elements
1839    /// Uses Fisher-Yates (inside-out variant) algorithm
1840    pub fn shuffle(arr: &[JValue]) -> Result<JValue, FunctionError> {
1841        if arr.len() <= 1 {
1842            return Ok(JValue::array(arr.to_vec()));
1843        }
1844
1845        use rand::seq::SliceRandom;
1846
1847        let mut result = arr.to_vec();
1848        let mut rng = rand::rng();
1849        result.shuffle(&mut rng);
1850
1851        Ok(JValue::array(result))
1852    }
1853}
1854
1855/// Built-in object functions
1856pub mod object {
1857    use super::*;
1858
1859    /// $keys(object) - Get object keys
1860    pub fn keys(obj: &IndexMap<String, JValue>) -> Result<JValue, FunctionError> {
1861        let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.as_str())).collect();
1862        Ok(JValue::array(keys))
1863    }
1864
1865    /// $lookup(object, key) - Lookup value by key
1866    pub fn lookup(obj: &IndexMap<String, JValue>, key: &str) -> Result<JValue, FunctionError> {
1867        Ok(obj.get(key).cloned().unwrap_or(JValue::Null))
1868    }
1869
1870    /// $spread(object) - Spread object into array of key-value pairs
1871    pub fn spread(obj: &IndexMap<String, JValue>) -> Result<JValue, FunctionError> {
1872        // Each key-value pair becomes a single-key object: {"key": value}
1873        let pairs: Vec<JValue> = obj
1874            .iter()
1875            .map(|(k, v)| {
1876                let mut pair = IndexMap::new();
1877                pair.insert(k.clone(), v.clone());
1878                JValue::object(pair)
1879            })
1880            .collect();
1881        Ok(JValue::array(pairs))
1882    }
1883
1884    /// $merge(objects) - Merge multiple objects
1885    pub fn merge(objects: &[JValue]) -> Result<JValue, FunctionError> {
1886        let mut result = IndexMap::new();
1887
1888        for obj in objects {
1889            #[cfg(feature = "python")]
1890            if let JValue::LazyPyDict(lazy) = obj {
1891                match lazy.to_object_ref() {
1892                    Some(map) => {
1893                        for (k, v) in map.iter() {
1894                            result.insert(k.clone(), v.clone());
1895                        }
1896                        continue;
1897                    }
1898                    None => {
1899                        return Err(FunctionError::TypeError(
1900                            "merge() argument could not be converted".to_string(),
1901                        ))
1902                    }
1903                }
1904            }
1905            match obj {
1906                JValue::Object(map) => {
1907                    for (k, v) in map.iter() {
1908                        result.insert(k.clone(), v.clone());
1909                    }
1910                }
1911                _ => {
1912                    return Err(FunctionError::TypeError(
1913                        "merge() requires all arguments to be objects".to_string(),
1914                    ))
1915                }
1916            }
1917        }
1918
1919        Ok(JValue::object(result))
1920    }
1921}
1922
1923/// Encoding/decoding functions
1924pub mod encoding {
1925    use super::*;
1926    use base64::{engine::general_purpose, Engine as _};
1927
1928    /// $base64encode(string) - Encode string to base64
1929    pub fn base64encode(s: &str) -> Result<JValue, FunctionError> {
1930        let encoded = general_purpose::STANDARD.encode(s.as_bytes());
1931        Ok(JValue::string(encoded))
1932    }
1933
1934    /// $base64decode(string) - Decode base64 string
1935    pub fn base64decode(s: &str) -> Result<JValue, FunctionError> {
1936        match general_purpose::STANDARD.decode(s.as_bytes()) {
1937            Ok(bytes) => match String::from_utf8(bytes) {
1938                Ok(decoded) => Ok(JValue::string(decoded)),
1939                Err(_) => Err(FunctionError::RuntimeError(
1940                    "Invalid UTF-8 in decoded base64".to_string(),
1941                )),
1942            },
1943            Err(_) => Err(FunctionError::RuntimeError(
1944                "Invalid base64 string".to_string(),
1945            )),
1946        }
1947    }
1948
1949    /// $encodeUrlComponent(string) - Encode URL component
1950    pub fn encode_url_component(s: &str) -> Result<JValue, FunctionError> {
1951        let encoded = percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC)
1952            .to_string();
1953        Ok(JValue::string(encoded))
1954    }
1955
1956    /// $decodeUrlComponent(string) - Decode URL component
1957    pub fn decode_url_component(s: &str) -> Result<JValue, FunctionError> {
1958        match percent_encoding::percent_decode_str(s).decode_utf8() {
1959            Ok(decoded) => Ok(JValue::string(decoded.to_string())),
1960            Err(_) => Err(FunctionError::RuntimeError(
1961                "Invalid percent-encoded string".to_string(),
1962            )),
1963        }
1964    }
1965
1966    /// $encodeUrl(string) - Encode full URL
1967    /// More permissive than encodeUrlComponent - allows URL structure characters
1968    pub fn encode_url(s: &str) -> Result<JValue, FunctionError> {
1969        // Use CONTROLS to preserve URL structure (://?#[]@!$&'()*+,;=)
1970        let encoded =
1971            percent_encoding::utf8_percent_encode(s, percent_encoding::CONTROLS).to_string();
1972        Ok(JValue::string(encoded))
1973    }
1974
1975    /// $decodeUrl(string) - Decode full URL
1976    pub fn decode_url(s: &str) -> Result<JValue, FunctionError> {
1977        match percent_encoding::percent_decode_str(s).decode_utf8() {
1978            Ok(decoded) => Ok(JValue::string(decoded.to_string())),
1979            Err(_) => Err(FunctionError::RuntimeError(
1980                "Invalid percent-encoded URL".to_string(),
1981            )),
1982        }
1983    }
1984}
1985
1986#[cfg(test)]
1987mod tests {
1988    use super::*;
1989
1990    // ===== String Functions Tests =====
1991
1992    #[test]
1993    fn test_string_conversion() {
1994        // String to string
1995        assert_eq!(
1996            string::string(&JValue::string("hello"), None).unwrap(),
1997            JValue::string("hello")
1998        );
1999
2000        // Number to string
2001        assert_eq!(
2002            string::string(&JValue::Number(42.0), None).unwrap(),
2003            JValue::string("42")
2004        );
2005
2006        // Float to string
2007        assert_eq!(
2008            string::string(&JValue::Number(3.14), None).unwrap(),
2009            JValue::string("3.14")
2010        );
2011
2012        // Boolean to string
2013        assert_eq!(
2014            string::string(&JValue::Bool(true), None).unwrap(),
2015            JValue::string("true")
2016        );
2017
2018        // Null becomes "null" via JSON.stringify
2019        assert_eq!(
2020            string::string(&JValue::Null, None).unwrap(),
2021            JValue::string("null")
2022        );
2023
2024        // Array gets JSON.stringify'd
2025        assert_eq!(
2026            string::string(
2027                &JValue::array(vec![
2028                    JValue::from(1i64),
2029                    JValue::from(2i64),
2030                    JValue::from(3i64)
2031                ]),
2032                None
2033            )
2034            .unwrap(),
2035            JValue::string("[1,2,3]")
2036        );
2037    }
2038
2039    #[test]
2040    fn test_length() {
2041        assert_eq!(string::length("hello").unwrap(), JValue::Number(5.0));
2042        assert_eq!(string::length("").unwrap(), JValue::Number(0.0));
2043        // Unicode support
2044        assert_eq!(
2045            string::length("Hello \u{4e16}\u{754c}").unwrap(),
2046            JValue::Number(8.0)
2047        );
2048        assert_eq!(
2049            string::length("\u{1f389}\u{1f38a}").unwrap(),
2050            JValue::Number(2.0)
2051        );
2052    }
2053
2054    #[test]
2055    fn test_uppercase_lowercase() {
2056        assert_eq!(string::uppercase("hello").unwrap(), JValue::string("HELLO"));
2057        assert_eq!(string::lowercase("HELLO").unwrap(), JValue::string("hello"));
2058        assert_eq!(
2059            string::uppercase("Hello World").unwrap(),
2060            JValue::string("HELLO WORLD")
2061        );
2062    }
2063
2064    #[test]
2065    fn test_substring() {
2066        // Basic substring
2067        assert_eq!(
2068            string::substring("hello world", 0, Some(5)).unwrap(),
2069            JValue::string("hello")
2070        );
2071
2072        // From position to end
2073        assert_eq!(
2074            string::substring("hello world", 6, None).unwrap(),
2075            JValue::string("world")
2076        );
2077
2078        // Negative start position
2079        assert_eq!(
2080            string::substring("hello world", -5, Some(5)).unwrap(),
2081            JValue::string("world")
2082        );
2083
2084        // Unicode support
2085        assert_eq!(
2086            string::substring("Hello \u{4e16}\u{754c}", 6, Some(2)).unwrap(),
2087            JValue::string("\u{4e16}\u{754c}")
2088        );
2089
2090        // Negative length returns empty string
2091        assert_eq!(
2092            string::substring("hello", 0, Some(-1)).unwrap(),
2093            JValue::string("")
2094        );
2095    }
2096
2097    #[test]
2098    fn test_substring_before_after() {
2099        // substringBefore
2100        assert_eq!(
2101            string::substring_before("hello world", " ").unwrap(),
2102            JValue::string("hello")
2103        );
2104        assert_eq!(
2105            string::substring_before("hello world", "x").unwrap(),
2106            JValue::string("hello world")
2107        );
2108        assert_eq!(
2109            string::substring_before("hello world", "").unwrap(),
2110            JValue::string("")
2111        );
2112
2113        // substringAfter
2114        assert_eq!(
2115            string::substring_after("hello world", " ").unwrap(),
2116            JValue::string("world")
2117        );
2118        // When separator is not found, return the original string
2119        assert_eq!(
2120            string::substring_after("hello world", "x").unwrap(),
2121            JValue::string("hello world")
2122        );
2123        assert_eq!(
2124            string::substring_after("hello world", "").unwrap(),
2125            JValue::string("hello world")
2126        );
2127    }
2128
2129    #[test]
2130    fn test_trim() {
2131        assert_eq!(string::trim("  hello  ").unwrap(), JValue::string("hello"));
2132        assert_eq!(string::trim("hello").unwrap(), JValue::string("hello"));
2133        assert_eq!(
2134            string::trim("\t\nhello\r\n").unwrap(),
2135            JValue::string("hello")
2136        );
2137    }
2138
2139    #[test]
2140    fn test_contains() {
2141        assert_eq!(
2142            string::contains("hello world", &JValue::string("world")).unwrap(),
2143            JValue::Bool(true)
2144        );
2145        assert_eq!(
2146            string::contains("hello world", &JValue::string("xyz")).unwrap(),
2147            JValue::Bool(false)
2148        );
2149        assert_eq!(
2150            string::contains("hello world", &JValue::string("")).unwrap(),
2151            JValue::Bool(true)
2152        );
2153    }
2154
2155    #[test]
2156    fn test_split() {
2157        // Split with separator
2158        assert_eq!(
2159            string::split("a,b,c", &JValue::string(","), None).unwrap(),
2160            JValue::array(vec![
2161                JValue::string("a"),
2162                JValue::string("b"),
2163                JValue::string("c")
2164            ])
2165        );
2166
2167        // Split with limit - truncates to limit number of results
2168        assert_eq!(
2169            string::split("a,b,c,d", &JValue::string(","), Some(2)).unwrap(),
2170            JValue::array(vec![JValue::string("a"), JValue::string("b")])
2171        );
2172
2173        // Split with empty separator (split into chars)
2174        assert_eq!(
2175            string::split("abc", &JValue::string(""), None).unwrap(),
2176            JValue::array(vec![
2177                JValue::string("a"),
2178                JValue::string("b"),
2179                JValue::string("c")
2180            ])
2181        );
2182    }
2183
2184    #[test]
2185    fn test_join() {
2186        // Join with separator
2187        let arr = vec![
2188            JValue::string("a"),
2189            JValue::string("b"),
2190            JValue::string("c"),
2191        ];
2192        assert_eq!(
2193            string::join(&arr, Some(",")).unwrap(),
2194            JValue::string("a,b,c")
2195        );
2196
2197        // Join without separator
2198        assert_eq!(string::join(&arr, None).unwrap(), JValue::string("abc"));
2199
2200        // Join with numbers
2201        let arr = vec![JValue::from(1i64), JValue::from(2i64), JValue::from(3i64)];
2202        assert_eq!(
2203            string::join(&arr, Some("-")).unwrap(),
2204            JValue::string("1-2-3")
2205        );
2206    }
2207
2208    #[test]
2209    fn test_replace() {
2210        // Replace all occurrences
2211        assert_eq!(
2212            string::replace("hello hello", &JValue::string("hello"), "hi", None).unwrap(),
2213            JValue::string("hi hi")
2214        );
2215
2216        // Replace with limit
2217        assert_eq!(
2218            string::replace("hello hello hello", &JValue::string("hello"), "hi", Some(2)).unwrap(),
2219            JValue::string("hi hi hello")
2220        );
2221
2222        // Replace empty pattern returns error D3010
2223        assert!(string::replace("hello", &JValue::string(""), "x", None).is_err());
2224    }
2225
2226    // ===== Numeric Functions Tests =====
2227
2228    #[test]
2229    fn test_number_conversion() {
2230        // Number to number
2231        assert_eq!(
2232            numeric::number(&JValue::Number(42.0)).unwrap(),
2233            JValue::Number(42.0)
2234        );
2235
2236        // String to number
2237        assert_eq!(
2238            numeric::number(&JValue::string("42")).unwrap(),
2239            JValue::Number(42.0)
2240        );
2241        assert_eq!(
2242            numeric::number(&JValue::string("3.14")).unwrap(),
2243            JValue::Number(3.14)
2244        );
2245        assert_eq!(
2246            numeric::number(&JValue::string("  123  ")).unwrap(),
2247            JValue::Number(123.0)
2248        );
2249
2250        // Boolean to number
2251        assert_eq!(
2252            numeric::number(&JValue::Bool(true)).unwrap(),
2253            JValue::Number(1.0)
2254        );
2255        assert_eq!(
2256            numeric::number(&JValue::Bool(false)).unwrap(),
2257            JValue::Number(0.0)
2258        );
2259
2260        // Invalid conversions
2261        assert!(numeric::number(&JValue::Null).is_err());
2262        assert!(numeric::number(&JValue::string("not a number")).is_err());
2263    }
2264
2265    #[test]
2266    fn test_sum() {
2267        // Sum of numbers
2268        let arr = vec![JValue::from(1i64), JValue::from(2i64), JValue::from(3i64)];
2269        assert_eq!(numeric::sum(&arr).unwrap(), JValue::Number(6.0));
2270
2271        // Empty array
2272        assert_eq!(numeric::sum(&[]).unwrap(), JValue::Number(0.0));
2273
2274        // Array with non-numbers should error
2275        let arr = vec![JValue::from(1i64), JValue::string("2")];
2276        assert!(numeric::sum(&arr).is_err());
2277    }
2278
2279    #[test]
2280    fn test_max_min() {
2281        let arr = vec![
2282            JValue::from(3i64),
2283            JValue::from(1i64),
2284            JValue::from(4i64),
2285            JValue::from(2i64),
2286        ];
2287
2288        assert_eq!(numeric::max(&arr).unwrap(), JValue::Number(4.0));
2289        assert_eq!(numeric::min(&arr).unwrap(), JValue::Number(1.0));
2290
2291        // Empty array
2292        assert_eq!(numeric::max(&[]).unwrap(), JValue::Null);
2293        assert_eq!(numeric::min(&[]).unwrap(), JValue::Null);
2294    }
2295
2296    #[test]
2297    fn test_average() {
2298        let arr = vec![
2299            JValue::from(1i64),
2300            JValue::from(2i64),
2301            JValue::from(3i64),
2302            JValue::from(4i64),
2303        ];
2304        assert_eq!(numeric::average(&arr).unwrap(), JValue::Number(2.5));
2305
2306        // Empty array
2307        assert_eq!(numeric::average(&[]).unwrap(), JValue::Null);
2308    }
2309
2310    #[test]
2311    fn test_math_functions() {
2312        // abs
2313        assert_eq!(numeric::abs(-5.5).unwrap(), JValue::Number(5.5));
2314        assert_eq!(numeric::abs(5.5).unwrap(), JValue::Number(5.5));
2315
2316        // floor
2317        assert_eq!(numeric::floor(3.7).unwrap(), JValue::Number(3.0));
2318        assert_eq!(numeric::floor(-3.7).unwrap(), JValue::Number(-4.0));
2319
2320        // ceil
2321        assert_eq!(numeric::ceil(3.2).unwrap(), JValue::Number(4.0));
2322        assert_eq!(numeric::ceil(-3.2).unwrap(), JValue::Number(-3.0));
2323
2324        // round - whole number results are returned as numbers
2325        assert_eq!(
2326            numeric::round(3.14159, Some(2)).unwrap(),
2327            JValue::Number(3.14)
2328        );
2329        assert_eq!(numeric::round(3.14159, None).unwrap(), JValue::Number(3.0));
2330        // Negative precision is supported (rounds to powers of 10)
2331        assert_eq!(numeric::round(3.14, Some(-1)).unwrap(), JValue::Number(0.0));
2332
2333        // sqrt
2334        assert_eq!(numeric::sqrt(16.0).unwrap(), JValue::Number(4.0));
2335        assert!(numeric::sqrt(-1.0).is_err());
2336
2337        // power
2338        assert_eq!(numeric::power(2.0, 3.0).unwrap(), JValue::Number(8.0));
2339        assert_eq!(numeric::power(9.0, 0.5).unwrap(), JValue::Number(3.0));
2340    }
2341
2342    // ===== Array Functions Tests =====
2343
2344    #[test]
2345    fn test_count() {
2346        let arr = vec![JValue::from(1i64), JValue::from(2i64), JValue::from(3i64)];
2347        assert_eq!(array::count(&arr).unwrap(), JValue::Number(3.0));
2348        assert_eq!(array::count(&[]).unwrap(), JValue::Number(0.0));
2349    }
2350
2351    #[test]
2352    fn test_append() {
2353        let arr1 = vec![JValue::from(1i64), JValue::from(2i64)];
2354
2355        // Append a single value
2356        let result = array::append(&arr1, &JValue::from(3i64)).unwrap();
2357        assert_eq!(
2358            result,
2359            JValue::array(vec![
2360                JValue::from(1i64),
2361                JValue::from(2i64),
2362                JValue::from(3i64)
2363            ])
2364        );
2365
2366        // Append an array
2367        let arr2 = JValue::array(vec![JValue::from(3i64), JValue::from(4i64)]);
2368        let result = array::append(&arr1, &arr2).unwrap();
2369        assert_eq!(
2370            result,
2371            JValue::array(vec![
2372                JValue::from(1i64),
2373                JValue::from(2i64),
2374                JValue::from(3i64),
2375                JValue::from(4i64)
2376            ])
2377        );
2378    }
2379
2380    #[test]
2381    fn test_reverse() {
2382        let arr = vec![JValue::from(1i64), JValue::from(2i64), JValue::from(3i64)];
2383        assert_eq!(
2384            array::reverse(&arr).unwrap(),
2385            JValue::array(vec![
2386                JValue::from(3i64),
2387                JValue::from(2i64),
2388                JValue::from(1i64)
2389            ])
2390        );
2391    }
2392
2393    #[test]
2394    fn test_sort() {
2395        // Sort numbers
2396        let arr = vec![
2397            JValue::from(3i64),
2398            JValue::from(1i64),
2399            JValue::from(4i64),
2400            JValue::from(2i64),
2401        ];
2402        assert_eq!(
2403            array::sort(&arr).unwrap(),
2404            JValue::array(vec![
2405                JValue::from(1i64),
2406                JValue::from(2i64),
2407                JValue::from(3i64),
2408                JValue::from(4i64)
2409            ])
2410        );
2411
2412        // Sort strings
2413        let arr = vec![
2414            JValue::string("charlie"),
2415            JValue::string("alice"),
2416            JValue::string("bob"),
2417        ];
2418        assert_eq!(
2419            array::sort(&arr).unwrap(),
2420            JValue::array(vec![
2421                JValue::string("alice"),
2422                JValue::string("bob"),
2423                JValue::string("charlie")
2424            ])
2425        );
2426
2427        // Mixed types should error
2428        let arr = vec![JValue::from(1i64), JValue::string("a")];
2429        assert!(array::sort(&arr).is_err());
2430    }
2431
2432    #[test]
2433    fn test_distinct() {
2434        let arr = vec![
2435            JValue::from(1i64),
2436            JValue::from(2i64),
2437            JValue::from(1i64),
2438            JValue::from(3i64),
2439            JValue::from(2i64),
2440        ];
2441        assert_eq!(
2442            array::distinct(&arr).unwrap(),
2443            JValue::array(vec![
2444                JValue::from(1i64),
2445                JValue::from(2i64),
2446                JValue::from(3i64)
2447            ])
2448        );
2449
2450        // With strings
2451        let arr = vec![
2452            JValue::string("a"),
2453            JValue::string("b"),
2454            JValue::string("a"),
2455        ];
2456        assert_eq!(
2457            array::distinct(&arr).unwrap(),
2458            JValue::array(vec![JValue::string("a"), JValue::string("b")])
2459        );
2460    }
2461
2462    #[test]
2463    fn test_exists() {
2464        assert_eq!(
2465            array::exists(&JValue::Number(42.0)).unwrap(),
2466            JValue::Bool(true)
2467        );
2468        assert_eq!(
2469            array::exists(&JValue::string("hello")).unwrap(),
2470            JValue::Bool(true)
2471        );
2472        assert_eq!(array::exists(&JValue::Null).unwrap(), JValue::Bool(false));
2473    }
2474
2475    // ===== Object Functions Tests =====
2476
2477    #[test]
2478    fn test_keys() {
2479        let mut obj = IndexMap::new();
2480        obj.insert("name".to_string(), JValue::string("Alice"));
2481        obj.insert("age".to_string(), JValue::Number(30.0));
2482
2483        let result = object::keys(&obj).unwrap();
2484        if let JValue::Array(keys) = result {
2485            assert_eq!(keys.len(), 2);
2486            assert!(keys.contains(&JValue::string("name")));
2487            assert!(keys.contains(&JValue::string("age")));
2488        } else {
2489            panic!("Expected array of keys");
2490        }
2491    }
2492
2493    #[test]
2494    fn test_lookup() {
2495        let mut obj = IndexMap::new();
2496        obj.insert("name".to_string(), JValue::string("Alice"));
2497        obj.insert("age".to_string(), JValue::Number(30.0));
2498
2499        assert_eq!(
2500            object::lookup(&obj, "name").unwrap(),
2501            JValue::string("Alice")
2502        );
2503        assert_eq!(object::lookup(&obj, "age").unwrap(), JValue::Number(30.0));
2504        assert_eq!(object::lookup(&obj, "missing").unwrap(), JValue::Null);
2505    }
2506
2507    #[test]
2508    fn test_spread() {
2509        let mut obj = IndexMap::new();
2510        obj.insert("a".to_string(), JValue::from(1i64));
2511        obj.insert("b".to_string(), JValue::from(2i64));
2512
2513        let result = object::spread(&obj).unwrap();
2514        if let JValue::Array(pairs) = result {
2515            assert_eq!(pairs.len(), 2);
2516            // Each key-value pair becomes a single-key object: {"key": value}
2517            for pair in pairs.iter() {
2518                if let JValue::Object(p) = pair {
2519                    assert_eq!(
2520                        p.len(),
2521                        1,
2522                        "Each spread element should be a single-key object"
2523                    );
2524                } else {
2525                    panic!("Expected Object in spread result");
2526                }
2527            }
2528            // Verify the actual spread results contain expected keys
2529            let all_keys: Vec<String> = pairs
2530                .iter()
2531                .filter_map(|p| {
2532                    if let JValue::Object(m) = p {
2533                        m.keys().next().cloned()
2534                    } else {
2535                        None
2536                    }
2537                })
2538                .collect();
2539            assert!(all_keys.contains(&"a".to_string()));
2540            assert!(all_keys.contains(&"b".to_string()));
2541        } else {
2542            panic!("Expected array of key-value pairs");
2543        }
2544    }
2545
2546    #[test]
2547    fn test_merge() {
2548        let mut obj1 = IndexMap::new();
2549        obj1.insert("a".to_string(), JValue::from(1i64));
2550        obj1.insert("b".to_string(), JValue::from(2i64));
2551
2552        let mut obj2 = IndexMap::new();
2553        obj2.insert("b".to_string(), JValue::from(3i64));
2554        obj2.insert("c".to_string(), JValue::from(4i64));
2555
2556        let arr = vec![JValue::object(obj1), JValue::object(obj2)];
2557        let result = object::merge(&arr).unwrap();
2558
2559        if let JValue::Object(merged) = result {
2560            assert_eq!(merged.get("a"), Some(&JValue::from(1i64)));
2561            assert_eq!(merged.get("b"), Some(&JValue::from(3i64))); // Later value wins
2562            assert_eq!(merged.get("c"), Some(&JValue::from(4i64)));
2563        } else {
2564            panic!("Expected merged object");
2565        }
2566    }
2567}