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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ Specification / Reference: [HTML](https://opensource.adobe.com/json-formula/doc/
[JavaScript API](./doc/output/JSDOCS.md)

[Developer Instructions](./DEVELOPMENT.md)

# Beta 2.0.0 Documentation
[HTML](https://opensource.adobe.com/json-formula/doc/output/json-formula-specification-2.0.0-beta.1.html)
[PDF](https://opensource.adobe.com/json-formula/doc/output/json-formula-specification-2.0.0-beta.1.pdf)

8,278 changes: 8,278 additions & 0 deletions doc/output/json-formula-specification-2.0.0-beta.1.html

Large diffs are not rendered by default.

Binary file not shown.
19 changes: 8 additions & 11 deletions doc/spec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ If the supplied data is not correct for the execution context, json-formula will
* The left-hand operand of ordering comparison operators (`>`, `>=`, `<`, `\<=`) must be a string or number. Any other type shall be coerced to a number.
* If the operands of an ordering comparison are different, they shall both be coerced to a number
* Parameters to functions shall be coerced when there is a single viable coercion available. For example, if a null value is provided to a function that accepts a number or string, then coercion shall not happen, since a null value can be coerced to both types. Conversely if a string is provided to a function that accepts a number or array of numbers, then the string shall be coerced to a number, since there is no supported coercion to convert it to an array of numbers.
* When functions accept a typed array, the function rules determine whether coercion may occur. Some functions (e.g. `avg()`) ignore array members of the wrong type. Other functions (e.g. `abs()`) coerce array members. If coercion may occur, then any provided array will have each of its members coerced to the expected type. e.g., if the input array is `[2,3,"6"]` and an array of numbers is expected, the array will be coerced to `[2,3,6]`.

The equality and inequality operators (`=`, `==`, `!=`, `<>`) do **not** perform type coercion. If operands are different types, the values are considered not equal.

Expand All @@ -94,7 +93,7 @@ In all cases except ordering comparison, if the coercion is not possible, a `Typ
eval([1,2,3] ~ 4, {}) -> [1,2,3,4]
eval(123 < "124", {}) -> true
eval("23" > 111, {}) -> false
eval(avg(["2", "3", "4"]), {}) -> 3
eval(avgA(["2", "3", "4"]), {}) -> 3
eval(1 == "1", {}) -> false
----

Expand Down Expand Up @@ -130,7 +129,7 @@ In all cases except ordering comparison, if the coercion is not possible, a `Typ
| null | boolean | false
|===

An array may be coerced to another type of array as long as there is a supported coercion for the array content. For examples, just as a string can be coerced to a number, an array of strings may be coerced to an array of numbers.
An array may be coerced to another type of array as long as there is a supported coercion for the array content. For example, just as a string can be coerced to a number, an array of strings may be coerced to an array of numbers.

Note that while strings, numbers and booleans may be coerced to arrays, they may not be coerced to a different type within that array. For example, a number cannot be coerced to an array of strings -- even though a number can be coerced to a string, and a string can be coerced to an array of strings.

Expand All @@ -142,7 +141,7 @@ Note that while strings, numbers and booleans may be coerced to arrays, they may
eval("\"$123.00\" + 1", {}) -> TypeError
eval("truth is " & `true`, {}) -> "truth is true"
eval(2 + `true`, {}) -> 3
eval(avg(["20", "30"]), {}) -> 25
eval(minA(["20", "30"]), {}) -> 20
----

=== Date and Time Values
Expand Down Expand Up @@ -1204,8 +1203,7 @@ output.

=== Function parameters

Functions support the set of standard json-formula <<Data Types, data types>>. If the resolved arguments cannot be coerced to
match the types specified in the signature, a `TypeError` error occurs.
Functions support the set of standard json-formula <<Data Types, data types>>. If the parameters cannot be coerced to match the types specified in the signature, a `TypeError` error occurs.

As a shorthand, the type `any` is used to indicate that the function argument can be
any of (`array|object|number|string|boolean|null`).
Expand All @@ -1229,14 +1227,12 @@ does not exist, a `FunctionError` error is raised.
Many functions that process scalar values also allow for the processing of arrays of values. For example, the `round()` function may be called to process a single value: `round(1.2345, 2)` or to process an array of values: `round([1.2345, 2.3456], 2)`. The first call will return a single value, the second call will return an array of values.
When processing arrays of values, and where there is more than one parameter, each parameter is converted to an array so that the function processes each value in the set of arrays. From our example above, the call to `round([1.2345, 2.3456], 2)` would be processed as if it were `round([1.2345, 2.3456], [2, 2])`, and the result would be the same as: `[round(1.2345, 2), round(2.3456, 2)]`.

Functions that accept array parameters will also accept nested arrays. With nested arrays, aggregating functions (min(), max(), avg(), sum() etc.) will flatten the arrays. e.g.

`avg([2.1, 3.1, [4.1, 5.1]])` will be processed as `avg([2.1, 3.1, 4.1, 5.1])` and return `3.6`.
Functions that accept array parameters will also accept nested arrays. Aggregating functions (`min()`, `max()`, `avg()`, `sum()`, etc.) will flatten nested arrays. e.g. `avg([2.1, 3.1, [4.1, 5.1]])` will be processed as `avg([2.1, 3.1, 4.1, 5.1])` and return `3.6`.

Non-aggregating functions will return the same array hierarchy. e.g.

`upper("a", ["b"]]) => ["A", ["B"]]`
`round([2.12, 3.12, [4.12, 5.12]], 1)` will be processed as `round([2.12, 3.12, [4.12, 5.12]], [1, 1, [1, 1]])` and return `[2.1, 3.1, [4.1, 5.1]] `
`upper(["a", ["b"]]) => ["A", ["B"]]`
`round([2.12, 3.12, [4.12, 5.12]], 1)` will be processed as `round([2.12, 3.12, [4.12, 5.12]], [1, 1, [1, 1]])` and return `[2.1, 3.1, [4.1, 5.1]]`

These array balancing rules apply when any parameter is an array:

Expand All @@ -1246,6 +1242,7 @@ These array balancing rules apply when any parameter is an array:
* The function will return an array which is the result of iterating over the elements of the arrays and applying the function logic on the values at the same index.

With nested arrays:

* Nested arrays will be flattened for aggregating functions
* Non-aggregating functions will preserve the array hierarchy and will apply the balancing rules to each element of the nested arrays

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/json-formula",
"version": "1.1.2",
"version": "2.0.0-beta.1",
"description": "json-formula Grammar and implementation",
"main": "src/json-formula.js",
"type": "module",
Expand Down
148 changes: 84 additions & 64 deletions src/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,10 +440,10 @@ export default function functions(

/**
* Finds the average of the elements in an array.
* Non-numeric values (text, boolean, null etc) are ignored.
* Non-numeric values (text, boolean, null, object) are ignored.
* If there are nested arrays, they are flattened.
* If the array is empty, an evaluation error is thrown
* @param {number[]} elements array of numeric values
* @param {any[]} elements array of values
* @return {number} average value
* @function avg
* @example
Expand All @@ -467,7 +467,7 @@ export default function functions(

/**
* Finds the average of the elements in an array, converting strings and booleans to number.
* If any conversions to number fail, an type error is thrown.
* If any conversions to number fail, a type error is thrown.
* If there are nested arrays, they are flattened.
* If the array is empty, an evaluation error is thrown
* @param {number[]} elements array of numeric values
Expand Down Expand Up @@ -1200,7 +1200,7 @@ export default function functions(
* @returns {string|string[]} the lower case value of the input string
* @function lower
* @example
* lower("E. E. Cummings") // returns e. e. cummings
* lower("E. E. Cummings") // returns "e. e. cummings"
*/
lower: {
_func: args => evaluate(args, a => toString(a).toLowerCase()),
Expand Down Expand Up @@ -1230,10 +1230,10 @@ export default function functions(

/**
* Calculates the largest value in the input numbers.
* Any values that are not numbers (e.g. null, boolean, strings, objects) will be ignored.
* Any values that are not numbers (null, boolean, strings, objects) will be ignored.
* If any parameters are arrays, the arrays will be flattened.
* If no numbers are provided, the function will return zero.
* @param {...(number[]|number)} collection values/array(s) in which the maximum
* @param {...(array|any)} collection values/array(s) in which the maximum
* element is to be calculated
* @return {number} the largest value found
* @function max
Expand Down Expand Up @@ -1263,7 +1263,7 @@ export default function functions(
* Calculates the largest value in the input values, coercing parameters to numbers.
* Null values are ignored.
* If any parameters cannot be converted to a number,
* the function will fail with an type error.
* the function will fail with a type error.
* If any parameters are arrays, the arrays will be flattened.
* If no numbers are provided, the function will return zero.
* @param {...(any)} collection values/array(s) in which the maximum
Expand Down Expand Up @@ -1379,10 +1379,10 @@ export default function functions(

/**
* Calculates the smallest value in the input numbers.
* Any values that are not numbers (e.g. null, boolean, strings, objects) will be ignored.
* Any values that are not numbers (null, boolean, string, object) will be ignored.
* If any parameters are arrays, the arrays will be flattened.
* If no numbers are provided, the function will return zero.
* @param {...(number[]|number)} collection
* @param {...(any[]|any)} collection
* Values/arrays to search for the minimum value
* @return {number} the smallest value found
* @function min
Expand Down Expand Up @@ -1410,10 +1410,10 @@ export default function functions(
* Calculates the smallest value in the input values, coercing parameters to numbers.
* Null values are ignored.
* If any parameters cannot be converted to a number,
* the function will fail with an type error.
* the function will fail with a type error.
* If any parameters are arrays, the arrays will be flattened.
* If no numbers are provided, the function will return zero.
* @param {...(any)} collection values/array(s) in which the maximum
* @param {...(any[]|any)} collection values/array(s) in which the maximum
* element is to be calculated
* @return {number} the largest value found
* @function minA
Expand Down Expand Up @@ -2148,13 +2148,13 @@ export default function functions(
},

/**
* Find the square root of a number
* @param {number|number[]} num source number
* @return {number|number[]} The calculated square root value
* @function sqrt
* @example
* sqrt(4) // returns 2
*/
* Find the square root of a number
* @param {number|number[]} num source number
* @return {number|number[]} The calculated square root value
* @function sqrt
* @example
* sqrt(4) // returns 2
*/
sqrt: {
_func: args => evaluate(args, arg => validNumber(Math.sqrt(arg), 'sqrt')),
_signature: [
Expand Down Expand Up @@ -2185,7 +2185,7 @@ export default function functions(
* then compute the standard deviation using [stdevp]{@link stdevp}.
* Non-numeric values (text, boolean, null etc) are ignored.
* If there are nested arrays, they are flattened.
* @param {number[]} numbers The array of numbers comprising the population.
* @param {any[]} values The array containing numbers comprising the population.
* Array size must be greater than 1.
* @returns {number} [Standard deviation](https://en.wikipedia.org/wiki/Standard_deviation)
* @function stdev
Expand Down Expand Up @@ -2252,7 +2252,9 @@ export default function functions(
* `stdevp` assumes that its arguments are the entire population.
* If your data represents a sample of the population,
* then compute the standard deviation using [stdev]{@link stdev}.
* @param {number[]} numbers The array of numbers comprising the population.
* Non-numeric values (text, boolean, null etc) are ignored.
* If there are nested arrays, they are flattened.
* @param {any[]} values The array containing numbers comprising the population.
* An empty array is not allowed.
* @returns {number} Calculated standard deviation
* @function stdevp
Expand Down Expand Up @@ -2359,7 +2361,9 @@ export default function functions(
/**
* Calculates the sum of the provided array.
* An empty array will produce a return value of 0.
* @param {number[]} collection array of numbers
* Any values that are not numbers (null, boolean, strings, objects) will be ignored.
* If any parameters are arrays, the arrays will be flattened.
* @param {any[]} collection array of values
* @return {number} The computed sum
* @function sum
* @example
Expand All @@ -2368,13 +2372,18 @@ export default function functions(
sum: {
_func: resolvedArgs => {
let sum = 0;
resolvedArgs[0].flat(Infinity).forEach(arg => {
sum += arg * 1;
});
resolvedArgs[0]
.flat(Infinity)
.filter(a => getType(a) === TYPE_NUMBER)
.forEach(arg => {
sum += arg * 1;
});

return sum;
},
_signature: [{ types: [TYPE_ARRAY_NUMBER] }],
_signature: [{ types: [TYPE_ARRAY] }],
},

/**
* Computes the tangent of a number in radians
* @param {number|number[]} angle A number representing an angle in radians.
Expand Down Expand Up @@ -2517,11 +2526,14 @@ export default function functions(
},

/**
* Converts the provided arg to a number as per
* the <<_type_coercion_rules,type coercion rules>>.
* Converts the provided arg to a number.
* The conversions follow the <<_type_coercion_rules,type coercion rules>> but will also:
* * Convert non-numeric strings to zero
* * Convert arrays to arrays of numbers
*
* @param {any} arg to convert to number
* @param {integer} [base=10] If the input `arg` is a string, the use base to convert to number.
* @param {any|any[]} value to convert to number
* @param {integer|integer[]} [base=10] If the input `arg` is a string,
* the base to use to convert to number.
* One of: 2, 8, 10, 16. Defaults to 10.
* @return {number} The resulting number. If conversion to number fails, return null.
* @function toNumber
Expand All @@ -2535,49 +2547,57 @@ export default function functions(
*/
toNumber: {
_func: resolvedArgs => {
const num = valueOf(resolvedArgs[0]);
const base = resolvedArgs.length > 1 ? toInteger(resolvedArgs[1]) : 10;
if (getType(num) === TYPE_STRING && base !== 10) {
let digitCheck;
if (base === 2) digitCheck = /^\s*(\+|-)?[01.]+\s*$/;
else if (base === 8) digitCheck = /^\s*(\+|-)?[0-7.]+\s*$/;
else if (base === 16) digitCheck = /^\s*(\+|-)?[0-9A-Fa-f.]+\s*$/;
else throw evaluationError(`Invalid base: "${base}" for toNumber()`);

if (num === '') return 0;
if (!digitCheck.test(num)) {
debug.push(`Failed to convert "${num}" base "${base}" to number`);
return null;
}
const parts = num.split('.').map(p => p.trim());
const toNumberFn = (value, base) => {
const num = valueOf(value);
if (getType(num) === TYPE_STRING && base !== 10) {
let digitCheck;
if (base === 2) digitCheck = /^\s*(\+|-)?[01.]+\s*$/;
else if (base === 8) digitCheck = /^\s*(\+|-)?[0-7.]+\s*$/;
else if (base === 16) digitCheck = /^\s*(\+|-)?[0-9A-Fa-f.]+\s*$/;
else throw evaluationError(`Invalid base: "${base}" for toNumber()`);

if (num === '') return 0;
if (!digitCheck.test(num)) {
debug.push(`Failed to convert "${num}" base "${base}" to number`);
return null;
}
const parts = num.split('.').map(p => p.trim());

let decimal = 0;
if (parts.length > 1) {
decimal = parseInt(parts[1], base) * base ** -parts[1].length;
}
let decimal = 0;
if (parts.length > 1) {
decimal = parseInt(parts[1], base) * base ** -parts[1].length;
}

const result = parseInt(parts[0], base) + decimal;
if (parts.length > 2 || Number.isNaN(result)) {
debug.push(`Failed to convert "${num}" base "${base}" to number`);
const result = parseInt(parts[0], base) + decimal;
if (parts.length > 2 || Number.isNaN(result)) {
debug.push(`Failed to convert "${num}" base "${base}" to number`);
return null;
}
return result;
}
try {
return toNumber(num);
} catch (e) {
const errorString = arg => {
const v = toJSON(arg);
return v.length > 50 ? `${v.substring(0, 20)} ...` : v;
};

debug.push(`Failed to convert "${errorString(num)}" to number`);
return null;
}
return result;
}
try {
return toNumber(num);
} catch (e) {
const errorString = arg => {
const v = toJSON(arg);
return v.length > 50 ? `${v.substring(0, 20)} ...` : v;
};

debug.push(`Failed to convert "${errorString(num)}" to number`);
return null;
};
let base = 10;
if (resolvedArgs.length > 1) {
base = Array.isArray(resolvedArgs[1])
? resolvedArgs.map(toInteger)
: toInteger(resolvedArgs[1]);
}
return evaluate([resolvedArgs[0], base], toNumberFn);
},
_signature: [
{ types: [TYPE_ANY] },
{ types: [TYPE_NUMBER], optional: true },
{ types: [TYPE_NUMBER, TYPE_ARRAY_NUMBER], optional: true },
],
},

Expand Down
16 changes: 14 additions & 2 deletions test/functions.json
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,19 @@
},
{
"expression": "sum(array)",
"error": "TypeError"
"result": 11
},
{
"expression": "sum(toNumber(array))",
"result": 111
},
{
"expression": "toNumber(array, 16)",
"result": [-1, 3, 4, 5, 10, 256]
},
{
"expression": "sum(toNumber(array, 16))",
"result": 277
},
{
"expression": "sum(array[].toNumber(@))",
Expand Down Expand Up @@ -1037,7 +1049,7 @@
},
{
"expression": "toNumber(`[0]`)",
"result": null
"result": [0]
},
{
"expression": "toNumber(`{\"foo\": 0}`)",
Expand Down
Loading