Skip to main content

jlens_core/functions/
mod.rs

1//! The built-in function library. Ported from jsonata-js `src/functions.js` and
2//! `src/datetime.js` (MIT).
3//!
4//! Every built-in is described by a `Native` entry: its name, signature, declared parameter
5//! names (used when it is partially applied), category, a one-line summary and implementation.
6//! The table is the single source of truth for evaluation, editor completion and the generated
7//! function reference ([`catalog`]).
8
9mod collections;
10mod numeric;
11mod strings;
12
13use std::sync::OnceLock;
14
15use serde::Serialize;
16
17use crate::error::{Error, Result};
18use crate::eval::{Env, Evaluator};
19use crate::signature::Signature;
20use crate::value::{self, Function, Value};
21
22/// The context of a call to a built-in function.
23pub(crate) struct Call<'c, 'a> {
24    /// The context value of the call (the input of the function call expression).
25    pub input: &'c Value<'a>,
26    /// The environment of the call.
27    pub env: &'c Env<'a>,
28}
29
30pub(crate) type NativeImpl =
31    for<'a> fn(&mut Evaluator<'_, 'a>, Vec<Value<'a>>, &Call<'_, 'a>) -> Result<Value<'a>>;
32
33/// Groups of built-in functions, as in the JSONata documentation.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
35#[serde(rename_all = "kebab-case")]
36pub enum Category {
37    /// `$string`, `$substring`, `$replace`, ...
38    String,
39    /// `$number`, `$round`, `$formatNumber`, ...
40    Numeric,
41    /// `$sum`, `$count`, `$max`, `$min`, `$average`.
42    Aggregation,
43    /// `$boolean`, `$not`, `$exists`.
44    Boolean,
45    /// `$count`, `$append`, `$sort`, `$distinct`, ...
46    Array,
47    /// `$keys`, `$lookup`, `$merge`, `$each`, ...
48    Object,
49    /// `$map`, `$filter`, `$reduce`, `$single`, `$sift`.
50    HigherOrder,
51    /// `$now`, `$fromMillis`, `$toMillis`, ...
52    DateTime,
53    /// `$base64encode`, `$encodeUrl`, ...
54    Encoding,
55    /// `$type`, `$eval`, `$error`, `$assert`, `$clone`.
56    Misc,
57}
58
59/// A built-in function.
60pub(crate) struct Native {
61    pub name: &'static str,
62    pub signature: Option<Signature>,
63    /// Parameter names of the reference implementation, used for partial application.
64    pub params: &'static [&'static str],
65    /// Number of parameters before any default value (JavaScript `Function.length`).
66    pub arity: usize,
67    pub category: Category,
68    pub summary: &'static str,
69    pub imp: NativeImpl,
70}
71
72/// Public description of a built-in function.
73#[derive(Debug, Clone, Serialize)]
74pub struct FunctionInfo {
75    /// The name, without the leading `$`.
76    pub name: &'static str,
77    /// The signature, such as `<s-nn?:s>`.
78    pub signature: String,
79    /// Parameter names.
80    pub params: &'static [&'static str],
81    /// The group the function belongs to.
82    pub category: Category,
83    /// One-line description.
84    pub summary: &'static str,
85}
86
87pub(crate) fn arg<'a>(args: &[Value<'a>], i: usize) -> Value<'a> {
88    args.get(i).cloned().unwrap_or_default()
89}
90
91pub(crate) fn num<'a>(args: &[Value<'a>], i: usize) -> Option<f64> {
92    args.get(i).and_then(Value::as_number)
93}
94
95pub(crate) fn text<'a>(args: &[Value<'a>], i: usize) -> Option<std::rc::Rc<str>> {
96    match args.get(i) {
97        Some(Value::String(s)) => Some(s.clone()),
98        _ => None,
99    }
100}
101
102macro_rules! natives {
103    ($( $name:literal $sig:literal [$($param:literal),*] $arity:literal $cat:ident $summary:literal => $imp:expr ;)*) => {
104        fn build() -> Vec<Native> {
105            vec![$(Native {
106                name: $name,
107                signature: Some(Signature::parse($sig).expect("valid built-in signature")),
108                params: &[$($param),*],
109                arity: $arity,
110                category: Category::$cat,
111                summary: $summary,
112                imp: $imp,
113            }),*]
114        }
115    };
116}
117
118use collections as c;
119use numeric as n;
120use strings as s;
121
122natives! {
123    // Aggregation
124    "sum" "<a<n>:n>" ["array"] 1 Aggregation "Sum of an array of numbers" => |_, a, _| sum(&arg(&a, 0));
125    "count" "<a:n>" ["array"] 1 Aggregation "Number of items in an array" => |_, a, _| Ok(match arg(&a, 0) {
126        Value::Undefined => Value::Number(0.0),
127        Value::Array(arr) => Value::Number(arr.len() as f64),
128        _ => Value::Number(1.0),
129    });
130    "max" "<a<n>:n>" ["array"] 1 Aggregation "Largest number in an array" => |_, a, _| extreme(&arg(&a, 0), f64::max);
131    "min" "<a<n>:n>" ["array"] 1 Aggregation "Smallest number in an array" => |_, a, _| extreme(&arg(&a, 0), f64::min);
132    "average" "<a<n>:n>" ["array"] 1 Aggregation "Mean of an array of numbers" => |_, a, _| {
133        let items = arg(&a, 0);
134        let Some(arr) = items.as_array().filter(|a| !a.items().is_empty()) else {
135            return Ok(Value::Undefined);
136        };
137        let n = arr.len() as f64;
138        let total: f64 = arr.items().iter().filter_map(Value::as_number).sum();
139        if total.is_infinite() {
140            // The sum overflows, but the mean of finite numbers is finite.
141            let mean = arr.items().iter().filter_map(Value::as_number).map(|x| x / n).sum();
142            return value::finite(mean);
143        }
144        Ok(Value::Number(total / n))
145    };
146
147    // String
148    "string" "<x-b?:s>" ["arg", "prettify"] 1 String "Casts a value to a string; objects and arrays become JSON" => |ev, a, _| {
149        let pretty = matches!(arg(&a, 1), Value::Bool(true));
150        Ok(to_string(&arg(&a, 0), pretty, ev.max_string_length())?.map_or(Value::Undefined, Value::string))
151    };
152    "substring" "<s-nn?:s>" ["str", "start", "length"] 3 String "Characters from a start position, optionally limited to a length" => s::substring;
153    "substringBefore" "<s-s:s>" ["str", "chars"] 2 String "The part of a string before the first occurrence of a substring" => s::substring_before;
154    "substringAfter" "<s-s:s>" ["str", "chars"] 2 String "The part of a string after the first occurrence of a substring" => s::substring_after;
155    "lowercase" "<s-:s>" ["str"] 1 String "Converts a string to lower case" => s::lowercase;
156    "uppercase" "<s-:s>" ["str"] 1 String "Converts a string to upper case" => s::uppercase;
157    "length" "<s-:n>" ["str"] 1 String "Number of characters in a string" => s::length;
158    "trim" "<s-:s>" ["str"] 1 String "Normalizes and trims whitespace" => s::trim;
159    "pad" "<s-ns?:s>" ["str", "width", "char"] 3 String "Pads a string to a width; positive pads on the right, negative on the left" => s::pad;
160    "match" "<s-f<s:o>n?:a<o>>" ["str", "regex", "limit"] 3 String "Matches a regular expression, returning the matches with their positions and groups" => s::match_;
161    "contains" "<s-(sf):b>" ["str", "token"] 2 String "Whether a string contains a substring or matches a regular expression" => s::contains;
162    "replace" "<s-(sf)(sf)n?:s>" ["str", "pattern", "replacement", "limit"] 4 String "Replaces occurrences of a substring or regular expression" => s::replace;
163    "split" "<s-(sf)n?:a<s>>" ["str", "separator", "limit"] 3 String "Splits a string by a separator or regular expression" => s::split;
164    "join" "<a<s>s?:s>" ["strs", "separator"] 2 String "Joins an array of strings with an optional separator" => s::join;
165    "formatNumber" "<n-so?:s>" ["value", "picture", "options"] 3 Numeric "Formats a number with an XPath picture string" => n::format_number;
166    "formatBase" "<n-n?:s>" ["value", "radix"] 2 Numeric "Formats a number in another base (2 to 36)" => n::format_base;
167    "formatInteger" "<n-s:s>" ["value", "picture"] 2 Numeric "Formats an integer with an XPath picture string (words, roman, letters, digits)" => |ev, a, _| match num(&a, 0) {
168        None => Ok(Value::Undefined),
169        Some(v) => crate::datetime::format_integer(v, &text(&a, 1).unwrap_or_default(), ev.max_string_length()).map(Value::string),
170    };
171    "parseInteger" "<s-s:n>" ["value", "picture"] 2 Numeric "Parses an integer written according to an XPath picture string" => |_, a, _| match text(&a, 0) {
172        None => Ok(Value::Undefined),
173        Some(v) => crate::datetime::parse_integer(&v, &text(&a, 1).unwrap_or_default()).and_then(value::finite),
174    };
175
176    // Numeric
177    "number" "<(nsb)-:n>" ["arg"] 1 Numeric "Casts a value to a number" => n::number;
178    "floor" "<n-:n>" ["arg"] 1 Numeric "Rounds down" => |_, a, _| Ok(num(&a, 0).map_or(Value::Undefined, |v| Value::Number(v.floor())));
179    "ceil" "<n-:n>" ["arg"] 1 Numeric "Rounds up" => |_, a, _| Ok(num(&a, 0).map_or(Value::Undefined, |v| Value::Number(v.ceil())));
180    "round" "<n-n?:n>" ["arg", "precision"] 2 Numeric "Rounds half to even, optionally to a number of decimal places" => |_, a, _| {
181        num(&a, 0).map_or(Ok(Value::Undefined), |v| value::finite(n::round(v, num(&a, 1).unwrap_or(0.0))))
182    };
183    "abs" "<n-:n>" ["arg"] 1 Numeric "Absolute value" => |_, a, _| Ok(num(&a, 0).map_or(Value::Undefined, |v| Value::Number(v.abs())));
184    "sqrt" "<n-:n>" ["arg"] 1 Numeric "Square root" => n::sqrt;
185    "power" "<n-n:n>" ["arg", "exp"] 2 Numeric "Raises a number to a power" => n::power;
186    "random" "<:n>" [] 0 Numeric "A pseudo-random number between 0 (inclusive) and 1 (exclusive)" => |ev, _, _| Ok(Value::Number(ev.random()));
187
188    // Boolean
189    "boolean" "<x-:b>" ["arg"] 1 Boolean "The effective boolean value of a value" => |_, a, _| Ok(boolean(&arg(&a, 0))?.map_or(Value::Undefined, Value::Bool));
190    "not" "<x-:b>" ["arg"] 1 Boolean "Logical negation of the effective boolean value" => |_, a, _| Ok(boolean(&arg(&a, 0))?.map_or(Value::Undefined, |b| Value::Bool(!b)));
191    "exists" "<x:b>" ["arg"] 1 Boolean "Whether a value is defined" => |_, a, _| Ok(Value::Bool(!arg(&a, 0).is_undefined()));
192
193    // Higher-order
194    "map" "<af>" ["arr", "func"] 2 HigherOrder "Applies a function to each item of an array" => c::map;
195    "zip" "<a+>" [] 0 Array "Combines arrays item by item into an array of arrays" => c::zip;
196    "filter" "<af>" ["arr", "func"] 2 HigherOrder "The items of an array for which a predicate holds" => c::filter;
197    "single" "<af?>" ["arr", "func"] 2 HigherOrder "The only item of an array matching a predicate; an error otherwise" => c::single;
198    "reduce" "<afj?:j>" ["sequence", "func", "init"] 3 HigherOrder "Folds an array into a single value with a function" => c::reduce;
199    "sift" "<o-f?:o>" ["arg", "func"] 2 HigherOrder "The key/value pairs of an object for which a predicate holds" => c::sift;
200
201    // Object
202    "keys" "<x-:a<s>>" ["arg"] 1 Object "The keys of an object, or the union of keys of an array of objects" => c::keys;
203    "lookup" "<x-s:x>" ["input", "key"] 2 Object "The value of a key in an object, or in each object of an array" => |_, a, _| {
204        Ok(lookup(&arg(&a, 0), text(&a, 1).as_deref().unwrap_or_default()))
205    };
206    "spread" "<x-:a<o>>" ["arg"] 1 Object "Splits an object into an array of single-key objects" => c::spread;
207    "merge" "<a<o>:o>" ["arg"] 1 Object "Merges an array of objects into one; later keys win" => c::merge;
208    "each" "<o-f:a>" ["obj", "func"] 2 Object "Applies a function to each key/value pair of an object" => c::each;
209
210    // Array
211    "append" "<xx:a>" ["arg1", "arg2"] 2 Array "Concatenates two values into an array" => |_, a, _| Ok(append(arg(&a, 0), arg(&a, 1)));
212    "reverse" "<a:a>" ["arr"] 1 Array "Reverses an array" => c::reverse;
213    "sort" "<af?:a>" ["arr", "comparator"] 2 Array "Sorts an array, optionally with a comparator function" => c::sort;
214    "shuffle" "<a:a>" ["arr"] 1 Array "Shuffles an array into a random order" => c::shuffle;
215    "distinct" "<x:x>" ["arr"] 1 Array "Removes duplicate values from an array" => c::distinct;
216
217    // Misc
218    "error" "<s?:x>" ["message"] 1 Misc "Raises an error (D3137) with a message" => c::error;
219    "assert" "<bs?:x>" ["condition", "message"] 2 Misc "Raises an error (D3141) when a condition is false" => c::assert;
220    "type" "<x:s>" ["value"] 1 Misc "The type of a value: null, number, string, boolean, array, object or function" => c::type_;
221    "eval" "<sx?:x>" ["expr", "focus"] 2 Misc "Parses and evaluates a JSONata expression given as a string" => c::eval;
222    "clone" "<(oa)-:o>" ["arg"] 1 Misc "A deep copy of a value" => |ev, a, _| clone(&arg(&a, 0), ev.max_string_length());
223
224    // Encoding
225    "base64encode" "<s-:s>" ["str"] 1 Encoding "Encodes a string as Base64 (each character taken as a byte)" => s::base64encode;
226    "base64decode" "<s-:s>" ["str"] 1 Encoding "Decodes a Base64 string (each byte becomes a character)" => s::base64decode;
227    "encodeUrlComponent" "<s-:s>" ["str"] 1 Encoding "Percent-encodes a URL component" => s::encode_url_component;
228    "encodeUrl" "<s-:s>" ["str"] 1 Encoding "Percent-encodes a URL, leaving reserved characters" => s::encode_url;
229    "decodeUrlComponent" "<s-:s>" ["str"] 1 Encoding "Decodes a percent-encoded URL component" => s::decode_url_component;
230    "decodeUrl" "<s-:s>" ["str"] 1 Encoding "Decodes a percent-encoded URL, leaving reserved characters encoded" => s::decode_url;
231
232    // Date/time
233    "now" "<s?s?:s>" ["picture", "timezone"] 2 DateTime "The evaluation's timestamp as an ISO 8601 string, or formatted with a picture" => |ev, a, _| {
234        crate::datetime::from_millis(ev.timestamp(), text(&a, 0).as_deref(), text(&a, 1).as_deref(), ev.max_string_length()).map(Value::string)
235    };
236    "millis" "<:n>" [] 0 DateTime "The evaluation's timestamp in milliseconds since the epoch" => |ev, _, _| Ok(Value::Number(ev.timestamp()));
237    "fromMillis" "<n-s?s?:s>" ["millis", "picture", "timezone"] 3 DateTime "Formats milliseconds since the epoch as a timestamp string" => |ev, a, _| match num(&a, 0) {
238        None => Ok(Value::Undefined),
239        Some(ms) => crate::datetime::from_millis(ms, text(&a, 1).as_deref(), text(&a, 2).as_deref(), ev.max_string_length()).map(Value::string),
240    };
241    "toMillis" "<s-s?:n>" ["timestamp", "picture"] 2 DateTime "Parses a timestamp string into milliseconds since the epoch" => |ev, a, _| match text(&a, 0) {
242        None => Ok(Value::Undefined),
243        Some(ts) => crate::datetime::to_millis(&ts, text(&a, 1).as_deref(), ev.timestamp(), |re, s| ev.find_regex(re, s, 0))
244            .map(|v| v.map_or(Value::Undefined, Value::Number)),
245    };
246}
247
248fn table() -> &'static [Native] {
249    static TABLE: OnceLock<Vec<Native>> = OnceLock::new();
250    TABLE.get_or_init(build)
251}
252
253/// The built-in function with this name.
254pub(crate) fn native(name: &str) -> Option<&'static Native> {
255    table().iter().find(|n| n.name == name)
256}
257
258/// Descriptions of every built-in function, in documentation order.
259pub fn catalog() -> Vec<FunctionInfo> {
260    table()
261        .iter()
262        .map(|n| FunctionInfo {
263            name: n.name,
264            signature: n
265                .signature
266                .as_ref()
267                .map(|s| s.definition.clone())
268                .unwrap_or_default(),
269            params: n.params,
270            category: n.category,
271            summary: n.summary,
272        })
273        .collect()
274}
275
276/// A new value for a built-in function name, if there is one.
277pub(crate) fn native_value<'a>(name: &str) -> Option<Value<'a>> {
278    native(name).map(|native| Value::function(Function::Native(native)))
279}
280
281// ---------------------------------------------------------------------------------------------
282// Helpers shared with the evaluator.
283
284/// The effective boolean value (`$boolean`); `None` for undefined.
285pub(crate) fn boolean<'a>(v: &Value<'a>) -> Result<Option<bool>> {
286    // An array of one item has the value of its item.
287    let mut v = v;
288    while let Value::Array(a) = v
289        && a.items().len() == 1
290    {
291        v = &a.items()[0];
292    }
293    Ok(Some(match v {
294        Value::Undefined => return Ok(None),
295        // Whether any item is true, the items of nested arrays included, all of them checked.
296        Value::Array(a) => {
297            let mut any = false;
298            value::for_each_leaf(a.items(), |item| {
299                any |= boolean(item)? == Some(true);
300                Ok::<_, Error>(())
301            })?;
302            any
303        }
304        Value::String(s) => !s.is_empty(),
305        Value::Number(n) => v.is_numeric()? && *n != 0.0,
306        Value::Object(o) => !o.borrow().is_empty(),
307        Value::Bool(b) => *b,
308        Value::Null | Value::Function(_) => false,
309    }))
310}
311
312/// Looks up a field in an object, or in each object of an array (flattening one level).
313pub(crate) fn lookup<'a>(input: &Value<'a>, key: &str) -> Value<'a> {
314    match input {
315        Value::Array(a) => {
316            let mut out = Vec::new();
317            let Ok(()) = value::for_each_leaf(a.items(), |item| {
318                match lookup(item, key) {
319                    Value::Undefined => {}
320                    Value::Array(inner) => out.extend(inner.items().iter().cloned()),
321                    other => out.push(other),
322                }
323                Ok::<_, std::convert::Infallible>(())
324            });
325            Value::sequence(out)
326        }
327        Value::Object(o) => o.get(key),
328        _ => Value::Undefined,
329    }
330}
331
332/// `$append`: concatenates, treating non-arrays as single items. The result is a plain array.
333pub(crate) fn append<'a>(a: Value<'a>, b: Value<'a>) -> Value<'a> {
334    if a.is_undefined() {
335        return b;
336    }
337    if b.is_undefined() {
338        return a;
339    }
340    let mut items = a.items_or_self();
341    match &b {
342        Value::Array(arr) => items.extend(arr.items().iter().cloned()),
343        other => items.push(other.clone()),
344    }
345    Value::array(items, 0)
346}
347
348/// `$string`: `None` for undefined.
349pub(crate) fn to_string<'a>(v: &Value<'a>, pretty: bool, limit: usize) -> Result<Option<String>> {
350    Ok(Some(match v {
351        Value::Undefined => return Ok(None),
352        Value::String(s) => s.to_string(),
353        Value::Function(_) => String::new(),
354        Value::Number(n) if !n.is_finite() => {
355            return Err(Error::new("D3001").with("value", value::json_number(*n)));
356        }
357        other => value::stringify(other, pretty, limit)?,
358    }))
359}
360
361fn sum<'a>(items: &Value<'a>) -> Result<Value<'a>> {
362    let Value::Array(a) = items else {
363        return Ok(Value::Undefined);
364    };
365    value::finite(a.items().iter().filter_map(Value::as_number).sum())
366}
367
368fn extreme<'a>(items: &Value<'a>, pick: fn(f64, f64) -> f64) -> Result<Value<'a>> {
369    match items {
370        Value::Array(a) if !a.items().is_empty() => Ok(Value::Number(
371            a.items()
372                .iter()
373                .filter_map(Value::as_number)
374                .reduce(pick)
375                .unwrap_or(f64::NAN),
376        )),
377        _ => Ok(Value::Undefined),
378    }
379}
380
381/// `$clone`: a deep copy made the way jsonata-js does it, by serializing and parsing. The text
382/// is limited to `limit` bytes, like a string.
383pub(crate) fn clone<'a>(v: &Value<'a>, limit: usize) -> Result<Value<'a>> {
384    let Some(text) = to_string(v, false, limit)? else {
385        return Ok(Value::Undefined);
386    };
387    let json: serde_json::Value = serde_json::from_str(&text)
388        .map_err(|e| Error::new("D3141").with("message", e.to_string()))?;
389    Ok(Value::from_json(&json))
390}
391
392/// The stable merge sort of jsonata-js: `swap(a, b)` returns true when `b` must come first.
393pub(crate) fn merge_sort<T: Clone>(
394    items: Vec<T>,
395    swap: &mut dyn FnMut(&T, &T) -> Result<bool>,
396) -> Result<Vec<T>> {
397    if items.len() <= 1 {
398        return Ok(items);
399    }
400    let mut left = items;
401    let right = left.split_off(left.len() / 2);
402    let left = merge_sort(left, swap)?;
403    let right = merge_sort(right, swap)?;
404    let mut out = Vec::with_capacity(left.len() + right.len());
405    let (mut i, mut j) = (0, 0);
406    while i < left.len() && j < right.len() {
407        if swap(&left[i], &right[j])? {
408            out.push(right[j].clone());
409            j += 1;
410        } else {
411            out.push(left[i].clone());
412            i += 1;
413        }
414    }
415    out.extend_from_slice(&left[i..]);
416    out.extend_from_slice(&right[j..]);
417    Ok(out)
418}
419
420/// The arguments passed to the function argument of `$map`, `$filter`, `$each`, ...: the value,
421/// then the index (or key) and the whole collection if the function declares them.
422pub(crate) fn hof_args<'a>(
423    func: &Value<'a>,
424    a1: Value<'a>,
425    a2: Value<'a>,
426    a3: Value<'a>,
427) -> Vec<Value<'a>> {
428    let arity = match func {
429        Value::Function(f) => f.arity(),
430        _ => 1,
431    };
432    let mut args = vec![a1];
433    if arity >= 2 {
434        args.push(a2);
435    }
436    if arity >= 3 {
437        args.push(a3);
438    }
439    args
440}