Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 77 additions & 17 deletions src/functions/array/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,30 @@
* Handles array manipulation and processing operations
*/

/**
* Get a user-friendly type name for a value
*/
function getTypeName(value: unknown): string {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
}

export function filter(f: Function, a: any[] | undefined): any[] | undefined {
if (a === undefined) {
return undefined;
}
if (typeof f !== 'function') {
throw new Error('First argument to filter is not a function');
throw new Error(
`filter(predicate, array) expects a function as first argument, got ${getTypeName(f)}.\n` +
'Example: filter(x => x > 0, [1, -2, 3])'
);
}
if (!Array.isArray(a)) {
throw new Error('Second argument to filter is not an array');
throw new Error(
`filter(predicate, array) expects an array as second argument, got ${getTypeName(a)}.\n` +
'Example: filter(x => x > 0, [1, -2, 3])'
);
}
return a.filter(function (x: any, i: number): any {
return f(x, i);
Expand All @@ -23,10 +38,16 @@ export function fold(f: Function, init: any, a: any[] | undefined): any {
return undefined;
}
if (typeof f !== 'function') {
throw new Error('First argument to fold is not a function');
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])'
);
Comment on lines +41 to +44
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduce() is an alias for fold(), so any argument-type errors triggered via reduce(...) will now surface an error message that starts with fold(reducer, initial, array) .... That’s confusing for users who called reduce. Consider having reduce() do its own argument validation / error formatting (or catch and rethrow) so the message references reduce(reducer, initial, array) and provides a reduce(...) example.

Copilot uses AI. Check for mistakes.
}
if (!Array.isArray(a)) {
throw new Error('Second argument to fold is not an array');
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])'
);
}
return a.reduce(function (acc: any, x: any, i: number): any {
return f(acc, x, i);
Expand All @@ -38,7 +59,10 @@ export function indexOf(target: any, s: string | any[] | undefined): number | un
return undefined;
}
if (!(Array.isArray(s) || typeof s === 'string')) {
throw new Error('Second argument to indexOf is not a string or array');
throw new Error(
`indexOf(target, arrayOrString) expects a string or array as second argument, got ${getTypeName(s)}.\n` +
'Example: indexOf("b", ["a", "b", "c"]) or indexOf("o", "hello")'
);
}

return s.indexOf(target);
Expand All @@ -49,7 +73,10 @@ export function join(sep: string | undefined, a: any[] | undefined): string | un
return undefined;
}
if (!Array.isArray(a)) {
throw new Error('Second argument to join is not an array');
throw new Error(
`join(separator, array) expects an array as second argument, got ${getTypeName(a)}.\n` +
'Example: join(", ", ["a", "b", "c"])'
);
}

return a.join(sep);
Expand All @@ -60,10 +87,16 @@ export function map(f: Function, a: any[] | undefined): any[] | undefined {
return undefined;
}
if (typeof f !== 'function') {
throw new Error('First argument to map is not a function');
throw new Error(
`map(mapper, array) expects a function as first argument, got ${getTypeName(f)}.\n` +
'Example: map(x => x * 2, [1, 2, 3])'
);
}
if (!Array.isArray(a)) {
throw new Error('Second argument to map is not an array');
throw new Error(
`map(mapper, array) expects an array as second argument, got ${getTypeName(a)}.\n` +
'Example: map(x => x * 2, [1, 2, 3])'
);
}
return a.map(function (x: any, i: number): any {
return f(x, i);
Expand All @@ -75,7 +108,10 @@ export function sum(array: (number | undefined)[] | undefined): number | undefin
return undefined;
}
if (!Array.isArray(array)) {
throw new Error('Sum argument is not an array');
throw new Error(
`sum(array) expects an array as argument, got ${getTypeName(array)}.\n` +
'Example: sum([1, 2, 3, 4])'
);
}
if (array.includes(undefined)) {
return undefined;
Expand All @@ -91,7 +127,10 @@ export function count(array: any[] | undefined): number | undefined {
return undefined;
}
if (!Array.isArray(array)) {
throw new Error('Count argument is not an array');
throw new Error(
`count(array) expects an array as argument, got ${getTypeName(array)}.\n` +
'Example: count([1, 2, 3, 4])'
);
}
return array.length;
}
Expand All @@ -106,10 +145,16 @@ export function find(f: Function, a: any[] | undefined): any {
return undefined;
}
if (typeof f !== 'function') {
throw new Error('First argument to find is not a function');
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])'
);
}
if (!Array.isArray(a)) {
throw new Error('Second argument to find is not an array');
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])'
);
}
return a.find(function (x: any, i: number): any {
return f(x, i);
Expand All @@ -121,10 +166,16 @@ export function some(f: Function, a: any[] | undefined): boolean | undefined {
return undefined;
}
if (typeof f !== 'function') {
throw new Error('First argument to some is not a function');
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])'
);
}
if (!Array.isArray(a)) {
throw new Error('Second argument to some is not an array');
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])'
);
}
return a.some(function (x: any, i: number): any {
return f(x, i);
Expand All @@ -136,10 +187,16 @@ export function every(f: Function, a: any[] | undefined): boolean | undefined {
return undefined;
}
if (typeof f !== 'function') {
throw new Error('First argument to every is not a function');
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])'
);
}
if (!Array.isArray(a)) {
throw new Error('Second argument to every is not an array');
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])'
);
}
return a.every(function (x: any, i: number): any {
return f(x, i);
Expand All @@ -151,7 +208,10 @@ export function unique(a: any[] | undefined): any[] | undefined {
return undefined;
}
if (!Array.isArray(a)) {
throw new Error('Argument to unique is not an array');
throw new Error(
`unique(array) expects an array as argument, got ${getTypeName(a)}.\n` +
'Example: unique([1, 2, 2, 3, 3, 3])'
);
Comment on lines +211 to +214
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

distinct() is a public alias for unique(), but any argument-type errors triggered via distinct(...) will now mention unique(array) ..., which can be confusing. Consider validating/rethrowing inside distinct() so the message uses the distinct(array) signature (and example) while still delegating the implementation to unique().

Copilot uses AI. Check for mistakes.
}
// Use Set to remove duplicates, then convert back to array
return Array.from(new Set(a));
Expand Down
175 changes: 175 additions & 0 deletions test/functions/functions-array-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/* global describe, it */

import { Parser } from '../../index';
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 () {
const parser = new Parser();
expect(() => parser.evaluate('filter(42, [1, 2, 3])')).toThrow(
/filter\(predicate, array\) expects a function as first argument, got number/
);
expect(() => parser.evaluate('filter(42, [1, 2, 3])')).toThrow(/Example:/);
Comment on lines +10 to +13
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Example: assertion re-evaluates the same expression a second time (which re-parses and re-runs the evaluation). To keep tests faster and avoid any unintended side effects, consider capturing the thrown error once and asserting on its .message for both the main text and the Example: line.

Copilot uses AI. Check for mistakes.
});

it('should provide user-friendly error when second argument is not an array', 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('f(x) = x > 0; filter(f, "not an array")')).toThrow(/Example:/);
});
});

describe('map()', function () {
it('should provide user-friendly error when first argument is not a function', 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/
);
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 () {
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/
);
});
Comment on lines +34 to +39
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test validates the main part of the map(...) error, but it doesn’t assert that the message includes the usage Example: line. Since the PR’s goal is to add signature + example to these errors, consider asserting Example: is present here too to prevent regressions.

Copilot uses AI. Check for mistakes.
});

describe('fold()', function () {
it('should provide user-friendly error when first argument is not a function', 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/
);
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 () {
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/
);
});
Comment on lines +51 to +56
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test validates the main part of the fold(...) error, but it doesn’t assert that the message includes the usage Example: line. Since the PR adds examples to these errors, consider asserting Example: is present here too.

Copilot uses AI. Check for mistakes.
});

describe('reduce()', function () {
it('should provide user-friendly error from fold when first argument is not a function', 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/
Comment on lines +60 to +63
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test codifies reduce(...) raising an error message that starts with fold(...). Since reduce is a user-facing function, it would be clearer if the thrown message referenced reduce(reducer, initial, array) (and included a reduce(...) example) rather than leaking the alias implementation detail.

Suggested change
it('should provide user-friendly error from fold when first argument is not a function', 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/
it('should provide user-friendly error when first argument is not a function', function () {
const parser = new Parser();
expect(() => parser.evaluate('reduce(true, 0, [1, 2, 3])')).toThrow(
/reduce\(reducer, initial, array\) expects a function as first argument, got boolean/

Copilot uses AI. Check for mistakes.
);
});
});

describe('find()', function () {
it('should provide user-friendly error when first argument is not a function', 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/
);
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 () {
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/
);
});
Comment on lines +77 to +82
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test validates the main part of the find(...) error, but it doesn’t assert that the message includes the usage Example: line. Adding an Example: assertion here would better cover the new behavior introduced by this PR.

Copilot uses AI. Check for mistakes.
});

describe('some()', function () {
it('should provide user-friendly error when first argument is not a function', 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/
);
});
Comment on lines +85 to +91
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of the some(...) error tests asserts that the message contains the Example: line. Since this PR adds examples to these errors, consider asserting Example: is included for both cases to prevent regressions.

Copilot uses AI. Check for mistakes.

it('should provide user-friendly error when second argument is not an array', 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/
);
});
});

describe('every()', function () {
it('should provide user-friendly error when first argument is not a function', 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/
);
});
Comment on lines +101 to +107
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of the every(...) error tests asserts that the message contains the Example: line. Since this PR adds examples to these errors, consider asserting Example: is included for both cases to prevent regressions.

Copilot uses AI. Check for mistakes.

it('should provide user-friendly error when second argument is not an array', 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/
);
});
});

describe('indexOf()', function () {
it('should provide user-friendly error when second argument is not an array or string', function () {
const parser = new Parser();
expect(() => parser.evaluate('indexOf(1, 123)')).toThrow(
/indexOf\(target, arrayOrString\) expects a string or array as second argument, got number/
);
expect(() => parser.evaluate('indexOf(1, 123)')).toThrow(/Example:/);
});
});

describe('join()', function () {
it('should provide user-friendly error when second argument is not an array', function () {
const parser = new Parser();
expect(() => parser.evaluate('join(", ", "not array")')).toThrow(
/join\(separator, array\) expects an array as second argument, got string/
);
expect(() => parser.evaluate('join(", ", "not array")')).toThrow(/Example:/);
});
});

describe('sum()', function () {
it('should provide user-friendly error when argument is not an array', function () {
const parser = new Parser();
expect(() => parser.evaluate('sum(42)')).toThrow(
/sum\(array\) expects an array as argument, got number/
);
expect(() => parser.evaluate('sum(42)')).toThrow(/Example:/);
});
});

describe('count()', function () {
it('should provide user-friendly error when argument is not an array', function () {
const parser = new Parser();
expect(() => parser.evaluate('count("string")')).toThrow(
/count\(array\) expects an array as argument, got string/
);
expect(() => parser.evaluate('count("string")')).toThrow(/Example:/);
});
});

describe('unique()', function () {
it('should provide user-friendly error when argument is not an array', function () {
const parser = new Parser();
expect(() => parser.evaluate('unique(123)')).toThrow(
/unique\(array\) expects an array as argument, got number/
);
expect(() => parser.evaluate('unique(123)')).toThrow(/Example:/);
});
});

describe('distinct()', function () {
it('should provide user-friendly error from unique when argument is not an array', function () {
const parser = new Parser();
expect(() => parser.evaluate('distinct({a: 1})')).toThrow(
/unique\(array\) expects an array as argument, got object/
);
Comment on lines +168 to +172
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test currently expects distinct(...) to throw an error message that starts with unique(array) .... Since distinct is user-facing, it would be clearer if the error message used the distinct(array) signature (and included an Example: line) instead of exposing the alias target.

Suggested change
it('should provide user-friendly error from unique when argument is not an array', function () {
const parser = new Parser();
expect(() => parser.evaluate('distinct({a: 1})')).toThrow(
/unique\(array\) expects an array as argument, got object/
);
it('should provide user-friendly error when argument is not an array', function () {
const parser = new Parser();
expect(() => parser.evaluate('distinct({a: 1})')).toThrow(
/distinct\(array\) expects an array as argument, got object/
);
expect(() => parser.evaluate('distinct({a: 1})')).toThrow(/Example:/);

Copilot uses AI. Check for mistakes.
});
});
});
Loading