Skip to content

Commit 3b95a6e

Browse files
authored
Merge pull request #187 from adobe/newCoerceRules
Functions to accept arrays; update coercion rules
2 parents 70dbc40 + dcaad1a commit 3b95a6e

File tree

11 files changed

+1480
-546
lines changed

11 files changed

+1480
-546
lines changed

doc/spec.adoc

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ json-formula functions:
6464

6565
* expression: A string prefixed with an ampersand (`&`) character
6666

67+
This specification uses the term "scalar" to refer to any value that is not an array, object or expression. Scalars include numbers, strings, booleans, and null values.
68+
6769
=== Type Coercion
6870

6971
If the supplied data is not correct for the execution context, json-formula will attempt to coerce the data to the correct type. Coercion will occur in these contexts:
@@ -73,7 +75,8 @@ If the supplied data is not correct for the execution context, json-formula will
7375
* Operands of the union operator (`~`) shall be coerced to an array
7476
* The left-hand operand of ordering comparison operators (`>`, `>=`, `<`, `\<=`) must be a string or number. Any other type shall be coerced to a number.
7577
* If the operands of an ordering comparison are different, they shall both be coerced to a number
76-
* Parameters to functions shall be coerced to the expected type as long as the expected type is a single choice. If the function signature allows multiple types for a parameter e.g. either string or array, then coercion will not occur.
78+
* 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.
79+
* 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]`.
7780

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

@@ -114,19 +117,23 @@ In all cases except ordering comparison, if the coercion is not possible, a `Typ
114117
| string | array | create a single-element array with the string
115118
| boolean | array | create a single-element array with the boolean
116119
| object | array | Not supported
117-
| null | array | Empty array
120+
| null | array | Not supported
118121
| number | object | Not supported
119122
| string | object | Not supported
120123
| boolean | object | Not supported
121124
| array | object | Not supported. Use: `fromEntries(entries(array))`
122-
| null | object | Empty object
125+
| null | object | Not supported
123126
| number | boolean | zero is false, all other numbers are true
124127
| string | boolean | Empty string is false, populated strings are true
125128
| array | boolean | Empty array is false, populated arrays are true
126129
| object | boolean | Empty object is false, populated objects are true
127130
| null | boolean | false
128131
|===
129132

133+
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.
134+
135+
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.
136+
130137
[discrete]
131138
==== Examples
132139

@@ -135,7 +142,7 @@ In all cases except ordering comparison, if the coercion is not possible, a `Typ
135142
eval("\"$123.00\" + 1", {}) -> TypeError
136143
eval("truth is " & `true`, {}) -> "truth is true"
137144
eval(2 + `true`, {}) -> 3
138-
eval(avg("20"), {}) -> 20
145+
eval(avg(["20", "30"]), {}) -> 25
139146
----
140147

141148
=== Date and Time Values
@@ -433,7 +440,7 @@ The numeric and concatenation operators (`+`, `-`, `{asterisk}`, `/`, `&`) have
433440

434441
* When both operands are arrays, a new array is returned where the elements are populated by applying the operator on each element of the left operand array with the corresponding element from the right operand array
435442
* If both operands are arrays and they do not have the same size, the shorter array is padded with null values
436-
* If one operand is an array and one is a scalar value, a new array is returned where the operator is applied with the scalar against each element in the array
443+
* If one operand is an array and one is a scalar value, the scalar operand will be converted to an array by repeating the value to the same size array as the other operand
437444

438445
[source%unbreakable]
439446
----
@@ -452,7 +459,7 @@ The union operator (`~`) returns an array formed by concatenating the contents o
452459
eval(a ~ b, {"a": [[0,1,2]], "b": [[3,4,5]]}) -> [[0,1,2],[3,4,5]]
453460
eval(a[] ~ b[], {"a": [[0,1,2]], "b": [[3,4,5]]}) -> [0,1,2,3,4,5]
454461
eval(a ~ 10, {"a": [0,1,2]}) -> [0,1,2,10]
455-
eval(a ~ `null`, {"a": [0,1,2]}) -> [0,1,2]
462+
eval(a ~ `null`, {"a": [0,1,2]}) -> [0,1,2,null]
456463
----
457464

458465
=== Boolean Operators
@@ -735,8 +742,9 @@ operator follows these processing steps:
735742
* The result array is returned as a <<Projections,projection>>
736743

737744
Once the flattening operation has been performed, subsequent operations
738-
are projected onto the flattened array. The difference between a bracketed wildcard (`[{asterisk}]`) and flatten (`[]`) is that
739-
flatten will first merge sub-arrays.
745+
are projected onto the flattened array.
746+
747+
A bracketed wildcard (`[{asterisk}]`) and flatten (`[]`) behave similarly in that they produce a projection from an array. The only difference is that a bracketed wildcard preserves the original array structure while flatten collapses one level of array structure.
740748

741749
[discrete]
742750
==== Examples
@@ -1194,27 +1202,55 @@ output.
11941202
return_type function_name2(type1|type2 $argname)
11951203
----
11961204

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

12001210
As a shorthand, the type `any` is used to indicate that the function argument can be
1201-
of any of (`array|object|number|string|boolean|null`).
1211+
any of (`array|object|number|string|boolean|null`).
12021212

12031213
The expression type, (denoted by `&expression`), is used to specify an
12041214
expression that is not immediately evaluated. Instead, a reference to that
1205-
expression is provided to the function being called. The function can then apply the expression reference as needed. It is semantically similar
1215+
expression is provided to the function. The function can then apply the expression reference as needed. It is semantically similar
12061216
to an https://en.wikipedia.org/wiki/Anonymous_function[anonymous function]. See the <<_sortBy, sortBy()>> function for an example of the expression type.
12071217

1208-
The result of the `functionExpression` is the result returned by the
1209-
function call. If a `functionExpression` is evaluated for a function that
1210-
does not exist, a `FunctionError` error is raised.
1211-
1212-
Functions can either have a specific arity or be variadic with a minimum
1218+
Function parameters can either have a specific arity or be variadic with a minimum
12131219
number of arguments. If a `functionExpression` is encountered where the
12141220
arity does not match, or the minimum number of arguments for a variadic function
12151221
is not provided, or too many arguments are provided, then a
12161222
`FunctionError` error is raised.
12171223

1224+
The result of the `functionExpression` is the result returned by the
1225+
function call. If a `functionExpression` is evaluated for a function that
1226+
does not exist, a `FunctionError` error is raised.
1227+
1228+
==== Array Parameters
1229+
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.
1230+
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)]`.
1231+
1232+
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.
1233+
1234+
`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`.
1235+
1236+
Non-aggregating functions will return the same array hierarchy. e.g.
1237+
1238+
`upper("a", ["b"]]) => ["A", ["B"]]`
1239+
`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]] `
1240+
1241+
These array balancing rules apply when any parameter is an array:
1242+
1243+
* All parameters will be treated as arrays
1244+
* Any scalar parameters will be converted to an array by repeating the scalar value to the length of the longest array
1245+
* All array parameters will be padded to the length of the longest array by adding null values
1246+
* 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.
1247+
1248+
With nested arrays:
1249+
* Nested arrays will be flattened for aggregating functions
1250+
* Non-aggregating functions will preserve the array hierarchy and will apply the balancing rules to each element of the nested arrays
1251+
1252+
=== Function evaluation
1253+
12181254
Functions are evaluated in applicative order:
12191255
- Each argument must be an expression
12201256
- Each argument expression must be evaluated before evaluating the

src/TreeInterpreter.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export default class TreeInterpreter {
7373
this.debug = debug;
7474
this.language = language;
7575
this.visitFunctions = this.initVisitFunctions();
76+
// track the identifier name that started the chain
77+
// so that we can use it in debug hints
78+
this.debugChainStart = null;
7679
}
7780

7881
search(node, value) {
@@ -85,22 +88,22 @@ export default class TreeInterpreter {
8588
if (value !== null && (isObject(value) || isArray(value))) {
8689
const field = getProperty(value, node.name);
8790
if (field === undefined) {
88-
debugAvailable(this.debug, value, node.name);
91+
debugAvailable(this.debug, value, node.name, this.debugChainStart);
8992
return null;
9093
}
9194
return field;
9295
}
93-
debugAvailable(this.debug, value, node.name);
96+
debugAvailable(this.debug, value, node.name, this.debugChainStart);
9497
return null;
9598
}
9699

97100
initVisitFunctions() {
98101
return {
99102
Identifier: this.field.bind(this),
100103
QuotedIdentifier: this.field.bind(this),
101-
102104
ChainedExpression: (node, value) => {
103105
let result = this.visit(node.children[0], value);
106+
this.debugChainStart = node.children[0].name;
104107
for (let i = 1; i < node.children.length; i += 1) {
105108
result = this.visit(node.children[1], result);
106109
if (result === null) return null;
@@ -322,7 +325,9 @@ export default class TreeInterpreter {
322325

323326
UnionExpression: (node, value) => {
324327
let first = this.visit(node.children[0], value);
328+
if (first === null) first = [null];
325329
let second = this.visit(node.children[1], value);
330+
if (second === null) second = [null];
326331
first = matchType([TYPE_ARRAY], first, 'union', this.toNumber, this.toString);
327332
second = matchType([TYPE_ARRAY], second, 'union', this.toNumber, this.toString);
328333
return first.concat(second);

0 commit comments

Comments
 (0)