diff --git a/docs/other-operators.md b/docs/other-operators.md index c56f7931..ab8f627a 100644 --- a/docs/other-operators.md +++ b/docs/other-operators.md @@ -25,6 +25,36 @@ __Example__ `Price < 50 ? "Cheap" : "Expensive"` +## `?:` (Default/Elvis) + +The default (or "elvis") operator returns the left-hand side if it has an effective Boolean value of `true`, otherwise it returns the right-hand side. This is useful for providing fallback values when an expression may evaluate to a value with an effective Boolean value of `false` (such as `null`, `false`, `0`, `''`, or `undefined`). + +__Syntax__ + +` ?: ` + +__Example__ + +`foo.bar ?: 'default'` => `'default'` (if `foo.bar` is evaluates to Boolean `false`) + +## `??` (Coalescing) + +The coalescing operator returns the left-hand side if it is defined (not `undefined`), otherwise it returns the right-hand side. This is useful for providing fallback values only when the left-hand side is missing or not present (empty sequence), but not for other values with an effective Boolean value of `false` like `0`, `false`, or `''`. + +__Syntax__ + +` ?? ` + +__Example__ + +`foo.bar ?? 42` => `42` (if `foo.bar` is undefined) + +`foo.bar ?? 'default'` => `'default'` (if `foo.bar` is undefined) + +`0 ?? 1` => `0` + +`'' ?? 'fallback'` => `''` + ## `:=` (Variable binding) The variable binding operator is used to bind the value of the RHS to the variable name defined on the LHS. The variable binding is scoped to the current block and any nested blocks. It is an error if the LHS is not a `$` followed by a valid variable name. diff --git a/docs/programming.md b/docs/programming.md index d09aab58..039fa9e1 100644 --- a/docs/programming.md +++ b/docs/programming.md @@ -40,6 +40,8 @@ Produces [this](http://try.jsonata.org/ryYn78Q0m), if you're interested! ## Conditional logic +### Ternary operator (`? :`) + If/then/else constructs can be written using the ternary operator "? :". `predicate ? expr1 : expr2` @@ -68,6 +70,84 @@ __Examples__ ] +### Elvis/Default operator (`?:`) + +The default (or "elvis") operator is syntactic sugar for a common pattern using the ternary operator. It returns the left-hand side if it has an effective Boolean value of `true`, otherwise it returns the right-hand side. + +`expr1 ?: expr2` + +This is equivalent to: + +`expr1 ? expr1 : expr2` + +The elvis operator is useful for providing fallback values when an expression may evaluate to a value with an effective Boolean value of `false`, without having to repeat the expression twice as you would with the ternary operator. + +__Examples__ + +
+
Account.Order.Product.{ + `Product Name`: $.'Product Name', + `Category`: $.Category ?: "Uncategorized" +}
+
[ + { + "Product Name": "Bowler Hat", + "Category": "Uncategorized" + }, + { + "Product Name": "Trilby hat", + "Category": "Uncategorized" + }, + { + "Product Name": "Bowler Hat", + "Category": "Uncategorized" + }, + { + "Product Name": "Cloak", + "Category": "Uncategorized" + } +]
+
+ +### Coalescing operator (`??`) + +The coalescing operator is syntactic sugar for a common pattern using the ternary operator with the `$exists` function. It returns the left-hand side if it is defined (not `undefined`), otherwise it returns the right-hand side. + +`expr1 ?? expr2` + +This is equivalent to: + +`$exists(expr1) ? expr1 : expr2` + +The coalescing operator is useful for providing fallback values only when the left-hand side is missing or not present (empty sequence), but not for other values with an effective Boolean value of `false` like `0`, `false`, or `''`. It avoids having to evaluate the expression twice and explicitly use the `$exists` function as you would with the ternary operator. + +__Examples__ + +
+
Account.Order.{ + "OrderID": OrderID, + Rating": ($sum(Product.Rating) / $count(Product.Rating)) ?? 0 +}
+
[ + { + "OrderID": "order101", + "Rating": 5 + }, + { + "OrderID": "order102", + "Rating": 3 + }, + { + "OrderID": "order103", + "Rating": 4 + }, + { + "OrderID": "order104", + "Rating": 2 + } +]
+
+ ## Variables Any name that starts with a dollar '$' is a variable. A variable is a named reference to a value. The value can be one of any type in the language's [type system](processing#the-jsonata-type-system). diff --git a/src/functions.js b/src/functions.js index c3fff7a4..3b28e943 100644 --- a/src/functions.js +++ b/src/functions.js @@ -1420,7 +1420,7 @@ const functions = (() => { if (arg !== 0) { result = true; } - } else if (arg !== null && typeof arg === 'object') { + } else if (arg !== null && typeof arg === 'object' && !isFunction(arg)) { if (Object.keys(arg).length > 0) { result = true; } diff --git a/src/parser.js b/src/parser.js index 9cffaf3d..a6d4223e 100644 --- a/src/parser.js +++ b/src/parser.js @@ -40,6 +40,8 @@ const parser = (() => { '<=': 40, '>=': 40, '~>': 40, + '?:': 40, + '??': 40, 'and': 30, 'or': 25, 'in': 40, @@ -198,6 +200,16 @@ const parser = (() => { position += 2; return create('operator', '~>'); } + if (currentChar === '?' && path.charAt(position + 1) === ':') { + // ?: default / elvis operator + position += 2; + return create('operator', '?:'); + } + if (currentChar === '?' && path.charAt(position + 1) === '?') { + // ?? coalescing operator + position += 2; + return create('operator', '??'); + } // test for single char operators if (Object.prototype.hasOwnProperty.call(operators, currentChar)) { position++; @@ -566,6 +578,20 @@ const parser = (() => { prefix("-"); // unary numeric negation infix("~>"); // function application + // coalescing operator + infix("??", operators['??'], function (left) { + this.type = 'condition'; + this.condition = { + type: 'function', + value: '(', + procedure: { type: 'variable', value: 'exists' }, + arguments: [left] + }; + this.then = left; + this.else = expression(0); + return this; + }); + infixr("(error)", 10, function (left) { this.lhs = left; @@ -849,6 +875,15 @@ const parser = (() => { return this; }); + // elvis/default operator + infix("?:", operators['?:'], function (left) { + this.type = 'condition'; + this.condition = left; + this.then = left; + this.else = expression(0); + return this; + }); + // object transformer prefix("|", function () { this.type = 'transform'; diff --git a/test/test-suite/groups/coalescing-operator/case000.json b/test/test-suite/groups/coalescing-operator/case000.json new file mode 100644 index 00000000..546e66ad --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case000.json @@ -0,0 +1,6 @@ +{ + "expr": "bar ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": 98 +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case001.json b/test/test-suite/groups/coalescing-operator/case001.json new file mode 100644 index 00000000..cbb70772 --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case001.json @@ -0,0 +1,6 @@ +{ + "expr": "foo.bar ?? 98", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case002.json b/test/test-suite/groups/coalescing-operator/case002.json new file mode 100644 index 00000000..79480e1e --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case002.json @@ -0,0 +1,6 @@ +{ + "expr": "foo.blah[0].baz.fud ?? 98", + "dataset": "dataset0", + "bindings": {}, + "result": "hello" +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case003.json b/test/test-suite/groups/coalescing-operator/case003.json new file mode 100644 index 00000000..6e1a9a59 --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case003.json @@ -0,0 +1,7 @@ +{ + "description": "undefined property uses default number on rhs", + "expr": "baz ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case004.json b/test/test-suite/groups/coalescing-operator/case004.json new file mode 100644 index 00000000..40624b6f --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case004.json @@ -0,0 +1,7 @@ +{ + "description": "property missing on object uses default number on rhs", + "expr": "foo.baz ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case005.json b/test/test-suite/groups/coalescing-operator/case005.json new file mode 100644 index 00000000..4e56001f --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case005.json @@ -0,0 +1,7 @@ +{ + "description": "out of bounds index uses default number on rhs", + "expr": "foo.blah[9].baz.fud ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case006.json b/test/test-suite/groups/coalescing-operator/case006.json new file mode 100644 index 00000000..802b7383 --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case006.json @@ -0,0 +1,7 @@ +{ + "description": "null is used", + "expr": "null ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": null +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case007.json b/test/test-suite/groups/coalescing-operator/case007.json new file mode 100644 index 00000000..b00c8449 --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case007.json @@ -0,0 +1,7 @@ +{ + "description": "false is used", + "expr": "false ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": false +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case008.json b/test/test-suite/groups/coalescing-operator/case008.json new file mode 100644 index 00000000..df6d590e --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case008.json @@ -0,0 +1,7 @@ +{ + "description": "true is used", + "expr": "true ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": true +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case009.json b/test/test-suite/groups/coalescing-operator/case009.json new file mode 100644 index 00000000..e812d733 --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case009.json @@ -0,0 +1,7 @@ +{ + "description": "0 is used", + "expr": "0 ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": 0 +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case010.json b/test/test-suite/groups/coalescing-operator/case010.json new file mode 100644 index 00000000..32515763 --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case010.json @@ -0,0 +1,7 @@ +{ + "description": "empty array is used", + "expr": "[] ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": [] +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case011.json b/test/test-suite/groups/coalescing-operator/case011.json new file mode 100644 index 00000000..246a6127 --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case011.json @@ -0,0 +1,7 @@ +{ + "description": "empty object is used", + "expr": "{} ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": {} +} \ No newline at end of file diff --git a/test/test-suite/groups/coalescing-operator/case012.json b/test/test-suite/groups/coalescing-operator/case012.json new file mode 100644 index 00000000..ec13c52b --- /dev/null +++ b/test/test-suite/groups/coalescing-operator/case012.json @@ -0,0 +1,7 @@ +{ + "description": "empty string is used", + "expr": "\"\" ?? 42", + "dataset": "dataset0", + "bindings": {}, + "result": "" +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case000.json b/test/test-suite/groups/default-operator/case000.json new file mode 100644 index 00000000..1bb14602 --- /dev/null +++ b/test/test-suite/groups/default-operator/case000.json @@ -0,0 +1,7 @@ +{ + "description": "true is used", + "expr": "true ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": true +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case001.json b/test/test-suite/groups/default-operator/case001.json new file mode 100644 index 00000000..6aab9ca2 --- /dev/null +++ b/test/test-suite/groups/default-operator/case001.json @@ -0,0 +1,7 @@ +{ + "description": "false is not used", + "expr": "false ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case002.json b/test/test-suite/groups/default-operator/case002.json new file mode 100644 index 00000000..9ccd3a86 --- /dev/null +++ b/test/test-suite/groups/default-operator/case002.json @@ -0,0 +1,7 @@ +{ + "description": "1 is used", + "expr": "1 ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 1 +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case003.json b/test/test-suite/groups/default-operator/case003.json new file mode 100644 index 00000000..51788d39 --- /dev/null +++ b/test/test-suite/groups/default-operator/case003.json @@ -0,0 +1,7 @@ +{ + "description": "0 is not used", + "expr": "0 ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case004.json b/test/test-suite/groups/default-operator/case004.json new file mode 100644 index 00000000..0d648fa5 --- /dev/null +++ b/test/test-suite/groups/default-operator/case004.json @@ -0,0 +1,7 @@ +{ + "description": "hello is used", + "expr": "\"hello\" ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": "hello" +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case005.json b/test/test-suite/groups/default-operator/case005.json new file mode 100644 index 00000000..5e641cc6 --- /dev/null +++ b/test/test-suite/groups/default-operator/case005.json @@ -0,0 +1,7 @@ +{ + "description": "empty string is not used", + "expr": "\"\" ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case006.json b/test/test-suite/groups/default-operator/case006.json new file mode 100644 index 00000000..a8e4910c --- /dev/null +++ b/test/test-suite/groups/default-operator/case006.json @@ -0,0 +1,9 @@ +{ + "description": "[1] is used", + "expr": "[1] ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": [ + 1 + ] +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case007.json b/test/test-suite/groups/default-operator/case007.json new file mode 100644 index 00000000..1a9bc7af --- /dev/null +++ b/test/test-suite/groups/default-operator/case007.json @@ -0,0 +1,7 @@ +{ + "description": "[0] is not used", + "expr": "[0] ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case008.json b/test/test-suite/groups/default-operator/case008.json new file mode 100644 index 00000000..a3713d86 --- /dev/null +++ b/test/test-suite/groups/default-operator/case008.json @@ -0,0 +1,7 @@ +{ + "description": "empty array is not used", + "expr": "[] ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case009.json b/test/test-suite/groups/default-operator/case009.json new file mode 100644 index 00000000..769791df --- /dev/null +++ b/test/test-suite/groups/default-operator/case009.json @@ -0,0 +1,9 @@ +{ + "description": "object with property is used", + "expr": "{ \"a\": 1 } ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": { + "a": 1 + } +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case010.json b/test/test-suite/groups/default-operator/case010.json new file mode 100644 index 00000000..321bbcb2 --- /dev/null +++ b/test/test-suite/groups/default-operator/case010.json @@ -0,0 +1,7 @@ +{ + "description": "empty object is not used", + "expr": "{} ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case011.json b/test/test-suite/groups/default-operator/case011.json new file mode 100644 index 00000000..0d1e8829 --- /dev/null +++ b/test/test-suite/groups/default-operator/case011.json @@ -0,0 +1,7 @@ +{ + "description": "function is not used", + "expr": "function(){true} ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case012.json b/test/test-suite/groups/default-operator/case012.json new file mode 100644 index 00000000..e2437f63 --- /dev/null +++ b/test/test-suite/groups/default-operator/case012.json @@ -0,0 +1,7 @@ +{ + "description": "42 is used", + "expr": "Account.blah ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file diff --git a/test/test-suite/groups/default-operator/case013.json b/test/test-suite/groups/default-operator/case013.json new file mode 100644 index 00000000..e2437f63 --- /dev/null +++ b/test/test-suite/groups/default-operator/case013.json @@ -0,0 +1,7 @@ +{ + "description": "42 is used", + "expr": "Account.blah ?: 42", + "dataset": "dataset0", + "bindings": {}, + "result": 42 +} \ No newline at end of file