diff --git a/docs/syntax.md b/docs/syntax.md index e4ec97eb..191c7745 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -134,7 +134,13 @@ Besides the "operator" functions, there are several pre-defined functions. You c | 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. | +| 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. | | join(sep, a) | Concatenate the elements of `a`, separated by `sep`. | | naturalSort(arr) | Sorts an array of strings using natural sort order (alphanumeric-aware). For example, `["file10", "file2", "file1"]` becomes `["file1", "file2", "file10"]`. | @@ -146,6 +152,19 @@ Besides the "operator" functions, there are several pre-defined functions. You c | if(c, a, b) | Function form of c ? a : b. Note: This always evaluates both `a` and `b`, regardless of whether `c` is `true` or not. Use `c ? a : b` instead if there are side effects, or if evaluating the branches could be expensive. | | coalesce(a, b, ...) | Returns the first non-null and non-empty string value from the arguments. Numbers and booleans (including 0 and false) are considered valid values. | +### Type Checking Functions + +| Function | Description | +|:------------- |:----------- | +| isArray(v) | Returns `true` if `v` is an array, `false` otherwise. | +| isObject(v) | Returns `true` if `v` is an object (excluding null and arrays), `false` otherwise. | +| isNumber(v) | Returns `true` if `v` is a number, `false` otherwise. | +| isString(v) | Returns `true` if `v` is a string, `false` otherwise. | +| isBoolean(v) | Returns `true` if `v` is a boolean, `false` otherwise. | +| isNull(v) | Returns `true` if `v` is null, `false` otherwise. | +| isUndefined(v)| Returns `true` if `v` is undefined, `false` otherwise. | +| isFunction(v) | Returns `true` if `v` is a function, `false` otherwise. | + ## String Functions The parser includes comprehensive string manipulation capabilities. @@ -403,6 +422,106 @@ map(x => x > 5 ? "high" : "low", [3, 7, 2, 9]) // 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. +### Examples of New Array Functions + +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) +``` + +**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 +``` + +**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) +``` + +**Using unique/distinct:** + +```js +unique([1, 2, 2, 3, 3, 3, 4]) // [1, 2, 3, 4] +distinct(["a", "b", "a", "c", "b"]) // ["a", "b", "c"] +unique([]) // [] +``` + +**Combining array functions:** + +```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] + +// Find first even number greater than 5 +find(x => x % 2 == 0, filter(x => x > 5, [3, 7, 8, 9, 10])) // 8 +``` + +### Examples of Type Checking Functions + +Type checking functions are useful for validating data types and conditional logic: + +**Basic type checking:** + +```js +isArray([1, 2, 3]) // true +isNumber(42) // true +isString("hello") // true +isBoolean(true) // true +isNull(null) // true +isUndefined(undefined) // true +isObject({a: 1}) // true +isFunction(abs) // true +``` + +**Using with conditionals:** + +```js +if(isArray(x), count(x), 0) // Get array length or 0 +if(isNumber(x), x * 2, x) // Double if number +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"] +``` + +**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) +``` + +**Practical examples:** + +```js +// Count how many strings are in an array +count(filter(isString, [1, "a", 2, "b", 3])) // 2 + +// Get the first number in a mixed array +find(isNumber, ["a", "b", 3, "c", 5]) // 3 + +// Check if any value is null or undefined +some(x => isNull(x) or isUndefined(x), data) // true/false +``` + ## Custom JavaScript Functions If you need additional functions that aren't supported out of the box, you can easily add them in your own code. Instances of the `Parser` class have a property called `functions` that's simply an object with all the functions that are in scope. You can add, replace, or delete any of the properties to customize what's available in the expressions. For example: diff --git a/src/functions/array/operations.ts b/src/functions/array/operations.ts index 901ff409..57e32793 100644 --- a/src/functions/array/operations.ts +++ b/src/functions/array/operations.ts @@ -95,3 +95,69 @@ 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 find(f: Function, a: any[] | undefined): any { + if (a === undefined) { + return undefined; + } + if (typeof f !== 'function') { + throw new Error('First argument to find is not a function'); + } + if (!Array.isArray(a)) { + throw new Error('Second argument to find is not an array'); + } + 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) { + return undefined; + } + if (typeof f !== 'function') { + throw new Error('First argument to some is not a function'); + } + if (!Array.isArray(a)) { + throw new Error('Second argument to some is not an array'); + } + 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) { + return undefined; + } + if (typeof f !== 'function') { + throw new Error('First argument to every is not a function'); + } + if (!Array.isArray(a)) { + throw new Error('Second argument to every is not an array'); + } + return a.every(function (x: any, i: number): any { + return f(x, i); + }); +} + +export function unique(a: any[] | undefined): any[] | undefined { + if (a === undefined) { + return undefined; + } + if (!Array.isArray(a)) { + throw new Error('Argument to unique is not an array'); + } + // Use Set to remove duplicates, then convert back to array + return Array.from(new Set(a)); +} + +export function distinct(a: any[] | undefined): any[] | undefined { + // distinct is an alias for unique + return unique(a); +} diff --git a/src/functions/utility/index.ts b/src/functions/utility/index.ts index 7e8ef1f1..9219a6db 100644 --- a/src/functions/utility/index.ts +++ b/src/functions/utility/index.ts @@ -9,3 +9,4 @@ export * from './conditional.js'; export * from './string-object.js'; +export * from './type-checking.js'; diff --git a/src/functions/utility/type-checking.ts b/src/functions/utility/type-checking.ts new file mode 100644 index 00000000..fe289de1 --- /dev/null +++ b/src/functions/utility/type-checking.ts @@ -0,0 +1,77 @@ +/** + * Type checking utility functions + * Provides functions to check the type of values + */ + +/** + * Checks if a value is an array + * @param value - The value to check + * @returns True if the value is an array, false otherwise + */ +export function isArray(value: any): boolean { + return Array.isArray(value); +} + +/** + * Checks if a value is an object (and not null or array) + * @param value - The value to check + * @returns True if the value is an object (excluding null and arrays), false otherwise + */ +export function isObject(value: any): boolean { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +/** + * Checks if a value is a number + * @param value - The value to check + * @returns True if the value is a number, false otherwise + */ +export function isNumber(value: any): boolean { + return typeof value === 'number'; +} + +/** + * Checks if a value is a string + * @param value - The value to check + * @returns True if the value is a string, false otherwise + */ +export function isString(value: any): boolean { + return typeof value === 'string'; +} + +/** + * Checks if a value is a boolean + * @param value - The value to check + * @returns True if the value is a boolean, false otherwise + */ +export function isBoolean(value: any): boolean { + return typeof value === 'boolean'; +} + +/** + * Checks if a value is null + * @param value - The value to check + * @returns True if the value is null, false otherwise + */ +export function isNull(value: any): boolean { + return value === null; +} + +/** + * Checks if a value is undefined + * @param value - The value to check + * @returns True if the value is undefined, false otherwise + */ +export function isUndefined(value: any): boolean { + return value === undefined; +} + +/** + * Checks if a value is a function + * @param value - The value to check + * @returns True if the value is a function, false otherwise + */ +export function isFunctionValue(value: any): boolean { + return typeof value === 'function'; +} + diff --git a/src/language-service/language-service.documentation.ts b/src/language-service/language-service.documentation.ts index 98679ab7..dd504303 100644 --- a/src/language-service/language-service.documentation.ts +++ b/src/language-service/language-service.documentation.ts @@ -153,6 +153,53 @@ export const BUILTIN_FUNCTION_DOCS: Record = { { name: 'a', description: 'Array to count.' } ] }, + reduce: { + 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: 'y', description: 'Initial accumulator value.' }, + { name: 'a', description: 'Input array.' } + ] + }, + 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.' } + ] + }, + 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.' } + ] + }, + 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.' } + ] + }, + unique: { + name: 'unique', + description: 'Returns a new array with duplicate values removed from array a.', + params: [ + { name: 'a', description: 'Input array.' } + ] + }, + distinct: { + name: 'distinct', + description: 'Alias for unique. Returns a new array with duplicate values removed from array a.', + params: [ + { name: 'a', description: 'Input array.' } + ] + }, clamp: { name: 'clamp', description: 'Clamps a value between a minimum and maximum.', @@ -310,6 +357,65 @@ export const BUILTIN_FUNCTION_DOCS: Record = { { name: 'obj', description: 'Input object.' }, { name: 'separator', description: 'Key separator (default: _).', optional: true } ] + }, + /** + * Type checking functions + */ + isArray: { + name: 'isArray', + description: 'Returns true if the value is an array.', + params: [ + { name: 'value', description: 'Value to check.' } + ] + }, + isObject: { + name: 'isObject', + description: 'Returns true if the value is an object (excluding null and arrays).', + params: [ + { name: 'value', description: 'Value to check.' } + ] + }, + isNumber: { + name: 'isNumber', + description: 'Returns true if the value is a number.', + params: [ + { name: 'value', description: 'Value to check.' } + ] + }, + isString: { + name: 'isString', + description: 'Returns true if the value is a string.', + params: [ + { name: 'value', description: 'Value to check.' } + ] + }, + isBoolean: { + name: 'isBoolean', + description: 'Returns true if the value is a boolean.', + params: [ + { name: 'value', description: 'Value to check.' } + ] + }, + isNull: { + name: 'isNull', + description: 'Returns true if the value is null.', + params: [ + { name: 'value', description: 'Value to check.' } + ] + }, + isUndefined: { + name: 'isUndefined', + description: 'Returns true if the value is undefined.', + params: [ + { name: 'value', description: 'Value to check.' } + ] + }, + isFunction: { + name: 'isFunction', + description: 'Returns true if the value is a function.', + params: [ + { name: 'value', description: 'Value to check.' } + ] } }; diff --git a/src/parsing/parser.ts b/src/parsing/parser.ts index a73315db..8f809c64 100644 --- a/src/parsing/parser.ts +++ b/src/parsing/parser.ts @@ -6,7 +6,7 @@ import { Expression } from '../core/expression.js'; import type { Value, VariableResolveResult, Values } from '../types/values.js'; import type { Instruction } from './instruction.js'; import type { OperatorFunction } from '../types/parser.js'; -import { atan2, condition, fac, filter, fold, gamma, hypot, indexOf, join, map, max, min, random, roundTo, sum, json, stringLength, isEmpty, stringContains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight, padBoth, slice, urlEncode, base64Encode, base64Decode, coalesceString, merge, keys, values, flatten, count, clamp } from '../functions/index.js'; +import { atan2, condition, fac, filter, fold, gamma, hypot, indexOf, join, map, max, min, random, roundTo, sum, json, stringLength, isEmpty, stringContains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight, padBoth, slice, urlEncode, base64Encode, base64Decode, coalesceString, merge, keys, values, flatten, count, clamp, reduce, find, some, every, unique, distinct, isArray, isObject, isNumber, isString, isBoolean, isNull, isUndefined, isFunctionValue } from '../functions/index.js'; import { add, sub, @@ -185,6 +185,12 @@ export class Parser { fac: fac, filter: filter, fold: fold, + reduce: reduce, + find: find, + some: some, + every: every, + unique: unique, + distinct: distinct, gamma: gamma, hypot: hypot, indexOf: indexOf, @@ -231,7 +237,16 @@ export class Parser { merge: merge, keys: keys, values: values, - flatten: flatten + flatten: flatten, + // Type checking functions + isArray: isArray, + isObject: isObject, + isNumber: isNumber, + isString: isString, + isBoolean: isBoolean, + isNull: isNull, + isUndefined: isUndefined, + isFunction: isFunctionValue }; this.numericConstants = { diff --git a/test/functions/functions-new-array.ts b/test/functions/functions-new-array.ts new file mode 100644 index 00000000..5014239d --- /dev/null +++ b/test/functions/functions-new-array.ts @@ -0,0 +1,156 @@ +/* global describe, it */ + +import assert from 'assert'; +import { Parser } from '../../index'; + +describe('New Array Functions TypeScript Test', function () { + describe('reduce(f, init, array)', function () { + it('should work as an alias for fold', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('reduce(max, -1, [1, 3, 5, 4, 2, 0])'), 5); + assert.strictEqual(parser.evaluate('reduce(min, 10, [1, 3, 5, 4, 2, 0, -2, -1])'), -2); + }); + it('should call self-defined functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(a, b) = a*b; reduce(f, 1, [1, 2, 3, 4, 5])'), 120); + }); + it('should return the initial value on an empty array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('reduce(max, 15, [])'), 15); + }); + it('should return undefined if undefined is passed as the array', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('f(a) = a*a; reduce(f, 3, undefined)'), undefined); + }); + }); + + describe('find(f, array)', function () { + it('should return the first matching element', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 2; find(f, [1, 2, 3, 4])'), 3); + assert.strictEqual(parser.evaluate('f(x) = x > 10; find(f, [5, 12, 8, 20])'), 12); + }); + it('should return undefined if no match is found', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 10; find(f, [1, 2, 3, 4])'), undefined); + }); + it('should return undefined on an empty array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 0; find(f, [])'), undefined); + }); + it('should work with built-in functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('find(not, [1, 2, 0, 3])'), 0); + }); + it('should work with index parameter', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(a, i) = i > 2; find(f, [10, 20, 30, 40, 50])'), 40); + }); + it('should return undefined if undefined is passed as the array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 0; find(f, undefined)'), undefined); + }); + }); + + describe('some(f, array)', function () { + it('should return true if at least one element matches', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 2; some(f, [1, 2, 3, 4])'), true); + assert.strictEqual(parser.evaluate('f(x) = x < 0; some(f, [1, -5, 3, 4])'), true); + }); + it('should return false if no elements match', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 10; some(f, [1, 2, 3, 4])'), false); + }); + it('should return false on an empty array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 0; some(f, [])'), false); + }); + it('should work with built-in functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('some(not, [1, 2, 0, 3])'), true); + assert.strictEqual(parser.evaluate('some(not, [1, 2, 3])'), false); + }); + it('should work with index parameter', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(a, i) = i > 5; some(f, [10, 20, 30])'), false); + assert.strictEqual(parser.evaluate('f(a, i) = i > 1; some(f, [10, 20, 30])'), true); + }); + it('should return undefined if undefined is passed as the array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 0; some(f, undefined)'), undefined); + }); + }); + + describe('every(f, array)', function () { + it('should return true if all elements match', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 0; every(f, [1, 2, 3, 4])'), true); + }); + it('should return false if any element does not match', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 2; every(f, [1, 2, 3, 4])'), false); + }); + it('should return true on an empty array (vacuous truth)', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 0; every(f, [])'), true); + }); + it('should work with built-in functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('every(not, [0, false, ""])'), true); + assert.strictEqual(parser.evaluate('every(not, [0, 1, false])'), false); + }); + it('should work with index parameter', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(a, i) = i < 5; every(f, [10, 20, 30])'), true); + assert.strictEqual(parser.evaluate('f(a, i) = i < 2; every(f, [10, 20, 30])'), false); + }); + it('should return undefined if undefined is passed as the array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x > 0; every(f, undefined)'), undefined); + }); + }); + + describe('unique(array)', function () { + it('should remove duplicate numbers', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('unique([1, 2, 2, 3, 3, 3, 4])'), [1, 2, 3, 4]); + }); + it('should remove duplicate strings', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('unique(["a", "b", "a", "c", "b"])'), ['a', 'b', 'c']); + }); + it('should work on empty arrays', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('unique([])'), []); + }); + it('should work on arrays with no duplicates', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('unique([1, 2, 3])'), [1, 2, 3]); + }); + it('should handle mixed types', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('unique([1, "1", 2, "2", 1])'), [1, '1', 2, '2']); + }); + it('should return undefined if undefined is passed as the array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('unique(undefined)'), undefined); + }); + }); + + describe('distinct(array)', function () { + it('should work as an alias for unique', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('distinct([1, 2, 2, 3, 3, 3, 4])'), [1, 2, 3, 4]); + assert.deepStrictEqual(parser.evaluate('distinct(["a", "b", "a", "c", "b"])'), ['a', 'b', 'c']); + }); + it('should work on empty arrays', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('distinct([])'), []); + }); + it('should return undefined if undefined is passed as the array', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('distinct(undefined)'), undefined); + }); + }); +}); diff --git a/test/functions/functions-type-checking.ts b/test/functions/functions-type-checking.ts new file mode 100644 index 00000000..d103e68f --- /dev/null +++ b/test/functions/functions-type-checking.ts @@ -0,0 +1,174 @@ +/* global describe, it */ + +import assert from 'assert'; +import { Parser } from '../../index'; + +describe('Type Checking Functions TypeScript Test', function () { + describe('isArray(value)', function () { + it('should return true for arrays', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isArray([])'), true); + assert.strictEqual(parser.evaluate('isArray([1, 2, 3])'), true); + assert.strictEqual(parser.evaluate('isArray(["a", "b"])'), true); + }); + it('should return false for non-arrays', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isArray(123)'), false); + assert.strictEqual(parser.evaluate('isArray("hello")'), false); + assert.strictEqual(parser.evaluate('isArray(true)'), false); + assert.strictEqual(parser.evaluate('isArray(null)'), false); + assert.strictEqual(parser.evaluate('isArray(undefined)'), false); + assert.strictEqual(parser.evaluate('isArray({a: 1})'), false); + }); + }); + + describe('isObject(value)', function () { + it('should return true for objects', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isObject({})'), true); + assert.strictEqual(parser.evaluate('isObject({a: 1, b: 2})'), true); + }); + it('should return false for arrays', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isObject([])'), false); + assert.strictEqual(parser.evaluate('isObject([1, 2, 3])'), false); + }); + it('should return false for null', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isObject(null)'), false); + }); + it('should return false for primitives', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isObject(123)'), false); + assert.strictEqual(parser.evaluate('isObject("hello")'), false); + assert.strictEqual(parser.evaluate('isObject(true)'), false); + assert.strictEqual(parser.evaluate('isObject(undefined)'), false); + }); + }); + + describe('isNumber(value)', function () { + it('should return true for numbers', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isNumber(123)'), true); + assert.strictEqual(parser.evaluate('isNumber(0)'), true); + assert.strictEqual(parser.evaluate('isNumber(-42.5)'), true); + assert.strictEqual(parser.evaluate('isNumber(3.14)'), true); + }); + it('should return false for non-numbers', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isNumber("123")'), false); + assert.strictEqual(parser.evaluate('isNumber(true)'), false); + assert.strictEqual(parser.evaluate('isNumber([])'), false); + assert.strictEqual(parser.evaluate('isNumber({})'), false); + assert.strictEqual(parser.evaluate('isNumber(null)'), false); + assert.strictEqual(parser.evaluate('isNumber(undefined)'), false); + }); + }); + + describe('isString(value)', function () { + it('should return true for strings', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isString("hello")'), true); + assert.strictEqual(parser.evaluate('isString("")'), true); + assert.strictEqual(parser.evaluate('isString("123")'), true); + }); + it('should return false for non-strings', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isString(123)'), false); + assert.strictEqual(parser.evaluate('isString(true)'), false); + assert.strictEqual(parser.evaluate('isString([])'), false); + assert.strictEqual(parser.evaluate('isString({})'), false); + assert.strictEqual(parser.evaluate('isString(null)'), false); + assert.strictEqual(parser.evaluate('isString(undefined)'), false); + }); + }); + + describe('isBoolean(value)', function () { + it('should return true for booleans', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isBoolean(true)'), true); + assert.strictEqual(parser.evaluate('isBoolean(false)'), true); + }); + it('should return false for non-booleans', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isBoolean(0)'), false); + assert.strictEqual(parser.evaluate('isBoolean(1)'), false); + assert.strictEqual(parser.evaluate('isBoolean("true")'), false); + assert.strictEqual(parser.evaluate('isBoolean([])'), false); + assert.strictEqual(parser.evaluate('isBoolean({})'), false); + assert.strictEqual(parser.evaluate('isBoolean(null)'), false); + assert.strictEqual(parser.evaluate('isBoolean(undefined)'), false); + }); + }); + + describe('isNull(value)', function () { + it('should return true for null', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isNull(null)'), true); + }); + it('should return false for non-null values', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isNull(0)'), false); + assert.strictEqual(parser.evaluate('isNull("")'), false); + assert.strictEqual(parser.evaluate('isNull(false)'), false); + assert.strictEqual(parser.evaluate('isNull([])'), false); + assert.strictEqual(parser.evaluate('isNull({})'), false); + assert.strictEqual(parser.evaluate('isNull(undefined)'), false); + }); + }); + + describe('isUndefined(value)', function () { + it('should return true for undefined', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isUndefined(undefined)'), true); + assert.strictEqual(parser.evaluate('isUndefined(x)', { x: undefined }), true); + }); + it('should return false for defined values', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isUndefined(0)'), false); + assert.strictEqual(parser.evaluate('isUndefined("")'), false); + assert.strictEqual(parser.evaluate('isUndefined(false)'), false); + assert.strictEqual(parser.evaluate('isUndefined([])'), false); + assert.strictEqual(parser.evaluate('isUndefined({})'), false); + assert.strictEqual(parser.evaluate('isUndefined(null)'), false); + }); + }); + + describe('isFunction(value)', function () { + it('should return true for functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('f(x) = x * 2; isFunction(f)'), true); + assert.strictEqual(parser.evaluate('isFunction(abs)'), true); + assert.strictEqual(parser.evaluate('isFunction(max)'), true); + }); + it('should return false for non-functions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('isFunction(123)'), false); + assert.strictEqual(parser.evaluate('isFunction("hello")'), false); + assert.strictEqual(parser.evaluate('isFunction(true)'), false); + assert.strictEqual(parser.evaluate('isFunction([])'), false); + assert.strictEqual(parser.evaluate('isFunction({})'), false); + assert.strictEqual(parser.evaluate('isFunction(null)'), false); + assert.strictEqual(parser.evaluate('isFunction(undefined)'), false); + }); + }); + + describe('Type checking combined usage', function () { + it('should work in conditional expressions', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('if(isArray([1,2]), "array", "not array")'), 'array'); + assert.strictEqual(parser.evaluate('if(isNumber(42), "number", "not number")'), 'number'); + assert.strictEqual(parser.evaluate('if(isString("test"), "string", "not string")'), 'string'); + }); + it('should work with filter', function () { + const parser = new Parser(); + assert.deepStrictEqual(parser.evaluate('filter(isNumber, [1, "a", 2, "b", 3])'), [1, 2, 3]); + }); + it('should work with some and every', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('some(isString, [1, 2, "hello", 3])'), true); + assert.strictEqual(parser.evaluate('every(isNumber, [1, 2, 3])'), true); + assert.strictEqual(parser.evaluate('every(isNumber, [1, "a", 3])'), false); + }); + }); +});