diff --git a/docs/syntax.md b/docs/syntax.md index 191c774..a4bd022 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -132,13 +132,13 @@ Besides the "operator" functions, there are several pre-defined functions. You c | Function | Description | |:------------- |:----------- | | count(a) | Returns the number of items in an array. | -| map(f, a) | Array map: Pass each element of `a` the function `f`, and return an array of the results. | -| fold(f, y, a) | Array fold: Fold/reduce array `a` into a single value, `y` by setting `y = f(y, x, index)` for each element `x` of the array. | -| reduce(f, y, a) | Alias for `fold`. Reduces array `a` into a single value using function `f` starting with accumulator `y`. | -| filter(f, a) | Array filter: Return an array containing only the values from `a` where `f(x, index)` is `true`. | -| find(f, a) | Returns the first element in array `a` where `f(x, index)` is `true`, or `undefined` if not found. | -| some(f, a) | Returns `true` if at least one element in array `a` satisfies `f(x, index)`, `false` otherwise. | -| every(f, a) | Returns `true` if all elements in array `a` satisfy `f(x, index)`. Returns `true` for empty arrays. | +| map(a, f) | Array map: Pass each element of `a` to the function `f`, and return an array of the results. | +| fold(a, y, f) | Array fold: Fold/reduce array `a` into a single value, `y` by setting `y = f(y, x, index)` for each element `x` of the array. | +| reduce(a, y, f) | Alias for `fold`. Reduces array `a` into a single value using function `f` starting with accumulator `y`. | +| filter(a, f) | Array filter: Return an array containing only the values from `a` where `f(x, index)` is `true`. | +| find(a, f) | Returns the first element in array `a` where `f(x, index)` is `true`, or `undefined` if not found. | +| some(a, f) | Returns `true` if at least one element in array `a` satisfies `f(x, index)`, `false` otherwise. | +| every(a, f) | Returns `true` if all elements in array `a` satisfy `f(x, index)`. Returns `true` for empty arrays. | | unique(a) | Returns a new array with duplicate values removed from array `a`. | | distinct(a) | Alias for `unique`. Returns a new array with duplicate values removed. | | indexOf(x, a) | Return the first index of string or array `a` matching the value `x`, or `-1` if not found. | @@ -359,14 +359,14 @@ factorial(x) = x < 2 ? 1 : x * factorial(x - 1) These functions can than be used in other functions that require a function argument, such as `map`, `filter` or `fold`: ```js -name(u) = u.name; map(name, users) -add(a, b) = a+b; fold(add, 0, [1, 2, 3]) +name(u) = u.name; map(users, name) +add(a, b) = a+b; fold([1, 2, 3], 0, add) ``` You can also define the functions inline: ```js -filter(isEven(x) = x % 2 == 0, [1, 2, 3, 4, 5]) +filter([1, 2, 3, 4, 5], isEven(x) = x % 2 == 0) ``` ### Arrow Functions @@ -376,18 +376,18 @@ Arrow functions provide a concise syntax for inline functions, similar to JavaSc **Single parameter (no parentheses required):** ```js -map(x => x * 2, [1, 2, 3]) // [2, 4, 6] -filter(x => x > 2, [1, 2, 3, 4]) // [3, 4] -map(x => x.name, users) // Extract property from objects +map([1, 2, 3], x => x * 2) // [2, 4, 6] +filter([1, 2, 3, 4], x => x > 2) // [3, 4] +map(users, x => x.name) // Extract property from objects ``` **Multiple parameters (parentheses required):** ```js -fold((acc, x) => acc + x, 0, [1, 2, 3, 4, 5]) // 15 (sum) -fold((acc, x) => acc * x, 1, [1, 2, 3, 4, 5]) // 120 (product) -map((val, idx) => val + idx, [10, 20, 30]) // [10, 21, 32] -filter((x, i) => i >= 1, [10, 20, 30]) // [20, 30] +fold([1, 2, 3, 4, 5], 0, (acc, x) => acc + x) // 15 (sum) +fold([1, 2, 3, 4, 5], 1, (acc, x) => acc * x) // 120 (product) +map([10, 20, 30], (val, idx) => val + idx) // [10, 21, 32] +filter([10, 20, 30], (x, i) => i >= 1) // [20, 30] ``` **Zero parameters:** @@ -401,23 +401,23 @@ filter((x, i) => i >= 1, [10, 20, 30]) // [20, 30] Arrow functions can be assigned to variables for reuse: ```js -fn = x => x * 2; map(fn, [1, 2, 3]) // [2, 4, 6] -double = x => x * 2; triple = x => x * 3; map(double, map(triple, [1, 2])) // [6, 12] +fn = x => x * 2; map([1, 2, 3], fn) // [2, 4, 6] +double = x => x * 2; triple = x => x * 3; map(map([1, 2], triple), double) // [6, 12] ``` **Nested arrow functions:** ```js -map(row => map(x => x * 2, row), [[1, 2], [3, 4]]) // [[2, 4], [6, 8]] +map([[1, 2], [3, 4]], row => map(row, x => x * 2)) // [[2, 4], [6, 8]] ``` **With member access and complex expressions:** ```js -filter(x => x.age > 25, users) // Filter objects by property -map(x => x.value * 2 + 1, items) // Complex transformations -filter(x => x > 0 and x < 10, numbers) // Using logical operators -map(x => x > 5 ? "high" : "low", [3, 7, 2, 9]) // Using ternary operator +filter(users, x => x.age > 25) // Filter objects by property +map(items, x => x.value * 2 + 1) // Complex transformations +filter(numbers, x => x > 0 and x < 10) // Using logical operators +map([3, 7, 2, 9], x => x > 5 ? "high" : "low") // Using ternary operator ``` > **Note:** Arrow functions share the same `fndef` operator flag as traditional function definitions. If function definitions are disabled via parser options, arrow functions will also be disabled. @@ -429,25 +429,25 @@ The new array utility functions provide additional ways to work with arrays: **Using reduce (alias for fold):** ```js -reduce((acc, x) => acc + x, 0, [1, 2, 3, 4]) // 10 (sum using reduce) -reduce((acc, x) => acc * x, 1, [2, 3, 4]) // 24 (product) +reduce([1, 2, 3, 4], 0, (acc, x) => acc + x) // 10 (sum using reduce) +reduce([2, 3, 4], 1, (acc, x) => acc * x) // 24 (product) ``` **Using find:** ```js -find(x => x > 5, [1, 3, 7, 2, 9]) // 7 (first element > 5) -find(x => x < 0, [1, 2, 3]) // undefined (not found) -find(x => x.age > 18, users) // First user over 18 +find([1, 3, 7, 2, 9], x => x > 5) // 7 (first element > 5) +find([1, 2, 3], x => x < 0) // undefined (not found) +find(users, x => x.age > 18) // First user over 18 ``` **Using some and every:** ```js -some(x => x > 10, [1, 5, 15, 3]) // true (at least one > 10) -every(x => x > 0, [1, 2, 3, 4]) // true (all positive) -every(x => x % 2 == 0, [2, 4, 5, 6]) // false (not all even) -some(x => x < 0, [1, 2, 3]) // false (none negative) +some([1, 5, 15, 3], x => x > 10) // true (at least one > 10) +every([1, 2, 3, 4], x => x > 0) // true (all positive) +every([2, 4, 5, 6], x => x % 2 == 0) // false (not all even) +some([1, 2, 3], x => x < 0) // false (none negative) ``` **Using unique/distinct:** @@ -462,11 +462,11 @@ unique([]) // [] ```js // Filter positive numbers, remove duplicates, then double each -unique(filter(x => x > 0, [1, -2, 3, 3, -4, 5, 1])) // [1, 3, 5] -map(x => x * 2, unique([1, 2, 2, 3])) // [2, 4, 6] +unique(filter([1, -2, 3, 3, -4, 5, 1], x => x > 0)) // [1, 3, 5] +map(unique([1, 2, 2, 3]), x => x * 2) // [2, 4, 6] // Find first even number greater than 5 -find(x => x % 2 == 0, filter(x => x > 5, [3, 7, 8, 9, 10])) // 8 +find(filter([3, 7, 8, 9, 10], x => x > 5), x => x % 2 == 0) // 8 ``` ### Examples of Type Checking Functions @@ -497,29 +497,29 @@ if(isString(x), toUpper(x), x) // Uppercase if string **Using with filter:** ```js -filter(isNumber, [1, "a", 2, "b", 3]) // [1, 2, 3] -filter(isString, [1, "a", 2, "b", 3]) // ["a", "b"] +filter([1, "a", 2, "b", 3], isNumber) // [1, 2, 3] +filter([1, "a", 2, "b", 3], isString) // ["a", "b"] ``` **Using with some/every:** ```js -some(isString, [1, 2, "hello", 3]) // true (has at least one string) -every(isNumber, [1, 2, 3, 4]) // true (all are numbers) -every(isNumber, [1, "a", 3]) // false (not all numbers) +some([1, 2, "hello", 3], isString) // true (has at least one string) +every([1, 2, 3, 4], isNumber) // true (all are numbers) +every([1, "a", 3], isNumber) // false (not all numbers) ``` **Practical examples:** ```js // Count how many strings are in an array -count(filter(isString, [1, "a", 2, "b", 3])) // 2 +count(filter([1, "a", 2, "b", 3], isString)) // 2 // Get the first number in a mixed array -find(isNumber, ["a", "b", 3, "c", 5]) // 3 +find(["a", "b", 3, "c", 5], isNumber) // 3 // Check if any value is null or undefined -some(x => isNull(x) or isUndefined(x), data) // true/false +some(data, x => isNull(x) or isUndefined(x)) // true/false ``` ## Custom JavaScript Functions diff --git a/src/functions/array/operations.ts b/src/functions/array/operations.ts index a90c852..33b6820 100644 --- a/src/functions/array/operations.ts +++ b/src/functions/array/operations.ts @@ -12,42 +12,71 @@ function getTypeName(value: unknown): string { return typeof value; } -export function filter(f: Function, a: any[] | undefined): any[] | undefined { - if (a === undefined) { +export function filter(arg1: Function | any[] | undefined, arg2: Function | any[] | undefined): any[] | undefined { + // Support both filter(array, fn) and filter(fn, array) for backwards compatibility + // Early return for undefined first argument + if (arg1 === undefined) { return undefined; } - if (typeof f !== 'function') { + + let f: Function; + let a: any[] | undefined; + + if (Array.isArray(arg1) && typeof arg2 === 'function') { + // array-first: filter(array, fn) + a = arg1; + f = arg2; + } else if (typeof arg1 === 'function' && (Array.isArray(arg2) || arg2 === undefined)) { + // function-first: filter(fn, array) + f = arg1; + a = arg2 as any[] | undefined; + } else { throw new Error( - `filter(predicate, array) expects a function as first argument, got ${getTypeName(f)}.\n` + - 'Example: filter(x => x > 0, [1, -2, 3])' + `filter(array, predicate) expects an array and a function.\n` + + 'Example: filter([1, -2, 3], x => x > 0)' ); } - if (!Array.isArray(a)) { - throw new Error( - `filter(predicate, array) expects an array as second argument, got ${getTypeName(a)}.\n` + - 'Example: filter(x => x > 0, [1, -2, 3])' - ); + + if (a === undefined) { + return undefined; } return a.filter(function (x: any, i: number): any { return f(x, i); }); } -export function fold(f: Function, init: any, a: any[] | undefined): any { - if (a === undefined) { +export function fold(arg1: Function | any[] | undefined, arg2: any, arg3: Function | any[] | undefined): any { + // Support both fold(array, initial, fn) and fold(fn, initial, array) for backwards compatibility + // Early return for undefined arguments + if (arg1 === undefined) { return undefined; } - if (typeof f !== 'function') { + + let f: Function; + let init: any; + let a: any[] | undefined; + + if (Array.isArray(arg1) && typeof arg3 === 'function') { + // array-first: fold(array, initial, fn) + a = arg1; + init = arg2; + f = arg3; + } else if (typeof arg1 === 'function' && (Array.isArray(arg3) || arg3 === undefined)) { + // function-first: fold(fn, initial, array) + f = arg1; + init = arg2; + a = arg3 as any[] | undefined; + } else if (arg3 === undefined) { + return undefined; + } else { throw new Error( - `fold(reducer, initial, array) expects a function as first argument, got ${getTypeName(f)}.\n` + - 'Example: fold((acc, x) => acc + x, 0, [1, 2, 3])' + `fold(array, initial, reducer) expects an array, initial value, and a function.\n` + + 'Example: fold([1, 2, 3], 0, (acc, x) => acc + x)' ); } - if (!Array.isArray(a)) { - throw new Error( - `fold(reducer, initial, array) expects an array as third argument, got ${getTypeName(a)}.\n` + - 'Example: fold((acc, x) => acc + x, 0, [1, 2, 3])' - ); + + if (a === undefined) { + return undefined; } return a.reduce(function (acc: any, x: any, i: number): any { return f(acc, x, i); @@ -82,21 +111,33 @@ export function join(sep: string | undefined, a: any[] | undefined): string | un return a.join(sep); } -export function map(f: Function, a: any[] | undefined): any[] | undefined { - if (a === undefined) { +export function map(arg1: Function | any[] | undefined, arg2: Function | any[] | undefined): any[] | undefined { + // Support both map(array, fn) and map(fn, array) for backwards compatibility + // Early return for undefined first argument + if (arg1 === undefined) { return undefined; } - if (typeof f !== 'function') { + + let f: Function; + let a: any[] | undefined; + + if (Array.isArray(arg1) && typeof arg2 === 'function') { + // array-first: map(array, fn) + a = arg1; + f = arg2; + } else if (typeof arg1 === 'function' && (Array.isArray(arg2) || arg2 === undefined)) { + // function-first: map(fn, array) + f = arg1; + a = arg2 as any[] | undefined; + } else { throw new Error( - `map(mapper, array) expects a function as first argument, got ${getTypeName(f)}.\n` + - 'Example: map(x => x * 2, [1, 2, 3])' + `map(array, mapper) expects an array and a function.\n` + + 'Example: map([1, 2, 3], x => x * 2)' ); } - if (!Array.isArray(a)) { - throw new Error( - `map(mapper, array) expects an array as second argument, got ${getTypeName(a)}.\n` + - 'Example: map(x => x * 2, [1, 2, 3])' - ); + + if (a === undefined) { + return undefined; } return a.map(function (x: any, i: number): any { return f(x, i); @@ -135,68 +176,104 @@ export function count(array: any[] | undefined): number | undefined { return array.length; } -export function reduce(f: Function, init: any, a: any[] | undefined): any { - // reduce is an alias for fold - return fold(f, init, a); +export function reduce(arg1: Function | any[] | undefined, arg2: any, arg3: Function | any[] | undefined): any { + // reduce is an alias for fold - supports both argument orders + return fold(arg1, arg2, arg3); } -export function find(f: Function, a: any[] | undefined): any { - if (a === undefined) { +export function find(arg1: Function | any[] | undefined, arg2: Function | any[] | undefined): any { + // Support both find(array, fn) and find(fn, array) for backwards compatibility + // Early return for undefined first argument + if (arg1 === undefined) { return undefined; } - if (typeof f !== 'function') { + + let f: Function; + let a: any[] | undefined; + + if (Array.isArray(arg1) && typeof arg2 === 'function') { + // array-first: find(array, fn) + a = arg1; + f = arg2; + } else if (typeof arg1 === 'function' && (Array.isArray(arg2) || arg2 === undefined)) { + // function-first: find(fn, array) + f = arg1; + a = arg2 as any[] | undefined; + } else { throw new Error( - `find(predicate, array) expects a function as first argument, got ${getTypeName(f)}.\n` + - 'Example: find(x => x > 2, [1, 2, 3, 4])' + `find(array, predicate) expects an array and a function.\n` + + 'Example: find([1, 2, 3, 4], x => x > 2)' ); } - if (!Array.isArray(a)) { - throw new Error( - `find(predicate, array) expects an array as second argument, got ${getTypeName(a)}.\n` + - 'Example: find(x => x > 2, [1, 2, 3, 4])' - ); + + if (a === undefined) { + return undefined; } return a.find(function (x: any, i: number): any { return f(x, i); }); } -export function some(f: Function, a: any[] | undefined): boolean | undefined { - if (a === undefined) { +export function some(arg1: Function | any[] | undefined, arg2: Function | any[] | undefined): boolean | undefined { + // Support both some(array, fn) and some(fn, array) for backwards compatibility + // Early return for undefined first argument + if (arg1 === undefined) { return undefined; } - if (typeof f !== 'function') { + + let f: Function; + let a: any[] | undefined; + + if (Array.isArray(arg1) && typeof arg2 === 'function') { + // array-first: some(array, fn) + a = arg1; + f = arg2; + } else if (typeof arg1 === 'function' && (Array.isArray(arg2) || arg2 === undefined)) { + // function-first: some(fn, array) + f = arg1; + a = arg2 as any[] | undefined; + } else { throw new Error( - `some(predicate, array) expects a function as first argument, got ${getTypeName(f)}.\n` + - 'Example: some(x => x > 2, [1, 2, 3, 4])' + `some(array, predicate) expects an array and a function.\n` + + 'Example: some([1, 2, 3, 4], x => x > 2)' ); } - if (!Array.isArray(a)) { - throw new Error( - `some(predicate, array) expects an array as second argument, got ${getTypeName(a)}.\n` + - 'Example: some(x => x > 2, [1, 2, 3, 4])' - ); + + if (a === undefined) { + return undefined; } return a.some(function (x: any, i: number): any { return f(x, i); }); } -export function every(f: Function, a: any[] | undefined): boolean | undefined { - if (a === undefined) { +export function every(arg1: Function | any[] | undefined, arg2: Function | any[] | undefined): boolean | undefined { + // Support both every(array, fn) and every(fn, array) for backwards compatibility + // Early return for undefined first argument + if (arg1 === undefined) { return undefined; } - if (typeof f !== 'function') { + + let f: Function; + let a: any[] | undefined; + + if (Array.isArray(arg1) && typeof arg2 === 'function') { + // array-first: every(array, fn) + a = arg1; + f = arg2; + } else if (typeof arg1 === 'function' && (Array.isArray(arg2) || arg2 === undefined)) { + // function-first: every(fn, array) + f = arg1; + a = arg2 as any[] | undefined; + } else { throw new Error( - `every(predicate, array) expects a function as first argument, got ${getTypeName(f)}.\n` + - 'Example: every(x => x > 0, [1, 2, 3, 4])' + `every(array, predicate) expects an array and a function.\n` + + 'Example: every([1, 2, 3, 4], x => x > 0)' ); } - if (!Array.isArray(a)) { - throw new Error( - `every(predicate, array) expects an array as second argument, got ${getTypeName(a)}.\n` + - 'Example: every(x => x > 0, [1, 2, 3, 4])' - ); + + if (a === undefined) { + return undefined; } return a.every(function (x: any, i: number): any { return f(x, i); diff --git a/src/language-service/language-service.documentation.ts b/src/language-service/language-service.documentation.ts index dd50430..c7baa48 100644 --- a/src/language-service/language-service.documentation.ts +++ b/src/language-service/language-service.documentation.ts @@ -86,25 +86,25 @@ export const BUILTIN_FUNCTION_DOCS: Record = { name: 'map', description: 'Apply function f to each element of array a.', params: [ - { name: 'f', description: 'Mapping function (value, index).' }, - { name: 'a', description: 'Input array.' } + { name: 'a', description: 'Input array.' }, + { name: 'f', description: 'Mapping function (value, index).' } ] }, fold: { name: 'fold', description: 'Reduce array a using function f, starting with accumulator y.', params: [ - { name: 'f', description: 'Reducer function. Eg: `f(acc, x, i) = acc + x`.' }, + { name: 'a', description: 'Input array.' }, { name: 'y', description: 'Initial accumulator value.' }, - { name: 'a', description: 'Input array.' } + { name: 'f', description: 'Reducer function. Eg: `f(acc, x, i) = acc + x`.' } ] }, filter: { name: 'filter', description: 'Filter array a using predicate f.', params: [ - { name: 'f', description: 'Filter function. Eg:`f(x) = x % 2 == 0`' }, - { name: 'a', description: 'Input array.' } + { name: 'a', description: 'Input array.' }, + { name: 'f', description: 'Filter function. Eg:`f(x) = x % 2 == 0`' } ] }, indexOf: { @@ -157,33 +157,33 @@ export const BUILTIN_FUNCTION_DOCS: Record = { name: 'reduce', description: 'Alias for fold. Reduce array a using function f, starting with accumulator y.', params: [ - { name: 'f', description: 'Reducer function. Eg: `f(acc, x, i) = acc + x`.' }, + { name: 'a', description: 'Input array.' }, { name: 'y', description: 'Initial accumulator value.' }, - { name: 'a', description: 'Input array.' } + { name: 'f', description: 'Reducer function. Eg: `f(acc, x, i) = acc + x`.' } ] }, find: { name: 'find', description: 'Returns the first element in array a that satisfies predicate f, or undefined if not found.', params: [ - { name: 'f', description: 'Predicate function. Eg: `f(x) = x > 5`' }, - { name: 'a', description: 'Input array.' } + { name: 'a', description: 'Input array.' }, + { name: 'f', description: 'Predicate function. Eg: `f(x) = x > 5`' } ] }, some: { name: 'some', description: 'Returns true if at least one element in array a satisfies predicate f.', params: [ - { name: 'f', description: 'Predicate function. Eg: `f(x) = x > 5`' }, - { name: 'a', description: 'Input array.' } + { name: 'a', description: 'Input array.' }, + { name: 'f', description: 'Predicate function. Eg: `f(x) = x > 5`' } ] }, every: { name: 'every', description: 'Returns true if all elements in array a satisfy predicate f. Returns true for empty arrays.', params: [ - { name: 'f', description: 'Predicate function. Eg: `f(x) = x > 0`' }, - { name: 'a', description: 'Input array.' } + { name: 'a', description: 'Input array.' }, + { name: 'f', description: 'Predicate function. Eg: `f(x) = x > 0`' } ] }, unique: { diff --git a/test/functions/functions-array-errors.ts b/test/functions/functions-array-errors.ts index e520548..5acf369 100644 --- a/test/functions/functions-array-errors.ts +++ b/test/functions/functions-array-errors.ts @@ -5,111 +5,111 @@ import { expect } from 'vitest'; describe('Array Function Error Messages', function () { describe('filter()', function () { - it('should provide user-friendly error when first argument is not a function', function () { + it('should provide user-friendly error when arguments are invalid', function () { const parser = new Parser(); expect(() => parser.evaluate('filter(42, [1, 2, 3])')).toThrow( - /filter\(predicate, array\) expects a function as first argument, got number/ + /filter\(array, predicate\) expects an array and a function/ ); expect(() => parser.evaluate('filter(42, [1, 2, 3])')).toThrow(/Example:/); }); - it('should provide user-friendly error when second argument is not an array', function () { + it('should provide user-friendly error when second argument is not a function', function () { const parser = new Parser(); - expect(() => parser.evaluate('f(x) = x > 0; filter(f, "not an array")')).toThrow( - /filter\(predicate, array\) expects an array as second argument, got string/ + expect(() => parser.evaluate('filter([1, 2, 3], "not a function")')).toThrow( + /filter\(array, predicate\) expects an array and a function/ ); - expect(() => parser.evaluate('f(x) = x > 0; filter(f, "not an array")')).toThrow(/Example:/); + expect(() => parser.evaluate('filter([1, 2, 3], "not a function")')).toThrow(/Example:/); }); }); describe('map()', function () { - it('should provide user-friendly error when first argument is not a function', function () { + it('should provide user-friendly error when arguments are invalid', function () { const parser = new Parser(); expect(() => parser.evaluate('map("not a function", [1, 2, 3])')).toThrow( - /map\(mapper, array\) expects a function as first argument, got string/ + /map\(array, mapper\) expects an array and a function/ ); expect(() => parser.evaluate('map("not a function", [1, 2, 3])')).toThrow(/Example:/); }); - it('should provide user-friendly error when second argument is not an array', function () { + it('should provide user-friendly error when second argument is not a function', function () { const parser = new Parser(); - expect(() => parser.evaluate('f(x) = x * 2; map(f, 123)')).toThrow( - /map\(mapper, array\) expects an array as second argument, got number/ + expect(() => parser.evaluate('map([1, 2, 3], 123)')).toThrow( + /map\(array, mapper\) expects an array and a function/ ); }); }); describe('fold()', function () { - it('should provide user-friendly error when first argument is not a function', function () { + it('should provide user-friendly error when arguments are invalid', function () { const parser = new Parser(); expect(() => parser.evaluate('fold(null, 0, [1, 2, 3])', { null: null })).toThrow( - /fold\(reducer, initial, array\) expects a function as first argument, got null/ + /fold\(array, initial, reducer\) expects an array, initial value, and a function/ ); expect(() => parser.evaluate('fold(null, 0, [1, 2, 3])', { null: null })).toThrow(/Example:/); }); - it('should provide user-friendly error when third argument is not an array', function () { + it('should provide user-friendly error when third argument is not a function', function () { const parser = new Parser(); - expect(() => parser.evaluate('f(a, b) = a + b; fold(f, 0, {a: 1})')).toThrow( - /fold\(reducer, initial, array\) expects an array as third argument, got object/ + expect(() => parser.evaluate('fold([1, 2, 3], 0, {a: 1})')).toThrow( + /fold\(array, initial, reducer\) expects an array, initial value, and a function/ ); }); }); describe('reduce()', function () { - it('should provide user-friendly error from fold when first argument is not a function', function () { + it('should provide user-friendly error from fold when arguments are invalid', function () { const parser = new Parser(); expect(() => parser.evaluate('reduce(true, 0, [1, 2, 3])')).toThrow( - /fold\(reducer, initial, array\) expects a function as first argument, got boolean/ + /fold\(array, initial, reducer\) expects an array, initial value, and a function/ ); }); }); describe('find()', function () { - it('should provide user-friendly error when first argument is not a function', function () { + it('should provide user-friendly error when arguments are invalid', function () { const parser = new Parser(); expect(() => parser.evaluate('find([1, 2], [1, 2, 3])')).toThrow( - /find\(predicate, array\) expects a function as first argument, got array/ + /find\(array, predicate\) expects an array and a function/ ); expect(() => parser.evaluate('find([1, 2], [1, 2, 3])')).toThrow(/Example:/); }); - it('should provide user-friendly error when second argument is not an array', function () { + it('should provide user-friendly error when second argument is not a function', function () { const parser = new Parser(); - expect(() => parser.evaluate('f(x) = x > 0; find(f, 99)')).toThrow( - /find\(predicate, array\) expects an array as second argument, got number/ + expect(() => parser.evaluate('find([1, 2, 3], 99)')).toThrow( + /find\(array, predicate\) expects an array and a function/ ); }); }); describe('some()', function () { - it('should provide user-friendly error when first argument is not a function', function () { + it('should provide user-friendly error when arguments are invalid', function () { const parser = new Parser(); expect(() => parser.evaluate('some(5, [1, 2, 3])')).toThrow( - /some\(predicate, array\) expects a function as first argument, got number/ + /some\(array, predicate\) expects an array and a function/ ); }); - it('should provide user-friendly error when second argument is not an array', function () { + it('should provide user-friendly error when second argument is not a function', function () { const parser = new Parser(); - expect(() => parser.evaluate('f(x) = x > 0; some(f, "string")')).toThrow( - /some\(predicate, array\) expects an array as second argument, got string/ + expect(() => parser.evaluate('some([1, 2, 3], "string")')).toThrow( + /some\(array, predicate\) expects an array and a function/ ); }); }); describe('every()', function () { - it('should provide user-friendly error when first argument is not a function', function () { + it('should provide user-friendly error when arguments are invalid', function () { const parser = new Parser(); expect(() => parser.evaluate('every({a: 1}, [1, 2, 3])')).toThrow( - /every\(predicate, array\) expects a function as first argument, got object/ + /every\(array, predicate\) expects an array and a function/ ); }); - it('should provide user-friendly error when second argument is not an array', function () { + it('should provide user-friendly error when second argument is not a function', function () { const parser = new Parser(); - expect(() => parser.evaluate('f(x) = x > 0; every(f, false)')).toThrow( - /every\(predicate, array\) expects an array as second argument, got boolean/ + expect(() => parser.evaluate('every([1, 2, 3], false)')).toThrow( + /every\(array, predicate\) expects an array and a function/ ); }); }); diff --git a/test/functions/functions-array-first-order.ts b/test/functions/functions-array-first-order.ts new file mode 100644 index 0000000..c2713f5 --- /dev/null +++ b/test/functions/functions-array-first-order.ts @@ -0,0 +1,233 @@ +/* global describe, it */ + +import assert from 'assert'; +import { Parser } from '../../index'; + +describe('Array-First Argument Order', function () { + describe('map(array, f)', function () { + it('should work with array-first order', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map([1, 2, 3], x => x * 2)'), [2, 4, 6]); + }); + it('should still work with function-first order (backwards compatible)', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map(x => x * 2, [1, 2, 3])'), [2, 4, 6]); + }); + it('should work with array-first and built-in functions', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map([-1, -2, 3], abs)'), [1, 2, 3]); + }); + it('should work with array-first and self-defined functions', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('f(x) = x * x; map([1, 2, 3], f)'), [1, 4, 9]); + }); + it('should work with array-first and index parameter', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('map([10, 20, 30], (val, idx) => val + idx)'), [10, 21, 32]); + }); + it('should return undefined for array-first with undefined array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('map(undefined, x => x * 2)'), undefined); + }); + }); + + describe('filter(array, f)', function () { + it('should work with array-first order', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('filter([1, 2, 3, 4, 5], x => x > 2)'), [3, 4, 5]); + }); + it('should still work with function-first order (backwards compatible)', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('filter(x => x > 2, [1, 2, 3, 4, 5])'), [3, 4, 5]); + }); + it('should work with array-first and built-in functions', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('filter([1, 0, false, true, 2, ""], not)'), [0, false, '']); + }); + it('should work with array-first and self-defined functions', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('f(x) = x > 2; filter([1, 2, 0, 3, -1, 4], f)'), [3, 4]); + }); + it('should work with array-first and index parameter', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('filter([1, 0, 5, 3, 2], (a, i) => a <= i)'), [0, 3, 2]); + }); + it('should return undefined for array-first with undefined array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('filter(undefined, x => x > 0)'), undefined); + }); + }); + + describe('fold(array, init, f)', function () { + it('should work with array-first order', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('fold([1, 2, 3, 4, 5], 0, (acc, x) => acc + x)'), 15); + }); + it('should still work with function-first order (backwards compatible)', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('fold((acc, x) => acc + x, 0, [1, 2, 3, 4, 5])'), 15); + }); + it('should work with array-first and built-in functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('fold([1, 3, 5, 4, 2, 0], -1, max)'), 5); + }); + it('should work with array-first and self-defined functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(a, b) = a * b; fold([1, 2, 3, 4, 5], 1, f)'), 120); + }); + it('should work with array-first and index parameter', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('fold([1, 3, 5, 7, 9], 100, (a, b, i) => a * i + b)'), 193); + }); + it('should return the initial value on an empty array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('fold([], 15, atan2)'), 15); + }); + it('should return undefined for array-first with undefined array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('fold(undefined, 0, (a, b) => a + b)'), undefined); + }); + }); + + describe('reduce(array, init, f)', function () { + it('should work with array-first order', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('reduce([1, 2, 3, 4], 0, (acc, x) => acc + x)'), 10); + }); + it('should still work with function-first order (backwards compatible)', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('reduce((acc, x) => acc + x, 0, [1, 2, 3, 4])'), 10); + }); + }); + + describe('find(array, f)', function () { + it('should work with array-first order', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('find([1, 2, 3, 4], x => x > 2)'), 3); + }); + it('should still work with function-first order (backwards compatible)', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('find(x => x > 2, [1, 2, 3, 4])'), 3); + }); + it('should work with array-first and built-in functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('find([1, 2, 0, 3], not)'), 0); + }); + it('should work with array-first and index parameter', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('find([10, 20, 30, 40, 50], (a, i) => i > 2)'), 40); + }); + it('should return undefined if not found', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('find([1, 2, 3, 4], x => x > 10)'), undefined); + }); + it('should return undefined for array-first with undefined array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('find(undefined, x => x > 0)'), undefined); + }); + }); + + describe('some(array, f)', function () { + it('should work with array-first order', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('some([1, 2, 3, 4], x => x > 2)'), true); + }); + it('should still work with function-first order (backwards compatible)', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('some(x => x > 2, [1, 2, 3, 4])'), true); + }); + it('should work with array-first and built-in functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('some([1, 2, 0, 3], not)'), true); + }); + it('should work with array-first and index parameter', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('some([10, 20, 30], (a, i) => i > 1)'), true); + }); + it('should return false if none match', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('some([1, 2, 3, 4], x => x > 10)'), false); + }); + it('should return undefined for array-first with undefined array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('some(undefined, x => x > 0)'), undefined); + }); + }); + + describe('every(array, f)', function () { + it('should work with array-first order', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('every([1, 2, 3, 4], x => x > 0)'), true); + }); + it('should still work with function-first order (backwards compatible)', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('every(x => x > 0, [1, 2, 3, 4])'), true); + }); + it('should work with array-first and built-in functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('every([0, false, ""], not)'), true); + }); + it('should work with array-first and index parameter', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('every([10, 20, 30], (a, i) => i < 5)'), true); + }); + it('should return false if any do not match', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('every([1, 2, 3, 4], x => x > 2)'), false); + }); + it('should return undefined for array-first with undefined array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('every(undefined, x => x > 0)'), undefined); + }); + it('should return true on empty arrays (vacuous truth)', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('every([], x => x > 0)'), true); + }); + }); + + describe('Complex expressions with array-first order', function () { + it('should work with nested map calls', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map([[1, 2], [3, 4]], row => map(row, x => x * 2))'), + [[2, 4], [6, 8]] + ); + }); + it('should work with chained operations', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map(filter([1, 2, 3, 4, 5], x => x > 2), x => x * 2)'), + [6, 8, 10] + ); + }); + it('should work with fold and filter combination', function () { + const parser = new Parser(); + assert.strictEqual( + parser.evaluate('fold(filter([1, 2, 3, 4, 5], x => x % 2 == 0), 0, (acc, x) => acc + x)'), + 6 + ); + }); + it('should work with variable arrays', function () { + const parser = new Parser(); + assert.deepStrictEqual( + parser.evaluate('map(items, x => x * 2)', { items: [1, 2, 3] }), + [2, 4, 6] + ); + }); + it('should work with object property access', function () { + const parser = new Parser(); + const users = [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }]; + assert.deepStrictEqual( + parser.evaluate('map(users, x => x.name)', { users }), + ['Alice', 'Bob'] + ); + }); + it('should work with find and object filtering', function () { + const parser = new Parser(); + const users = [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }]; + const result = parser.evaluate('find(users, x => x.age > 26)', { users }); + assert.ok(result !== undefined); + assert.strictEqual((result as { name: string }).name, 'Bob'); + }); + }); +});