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