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
30 changes: 30 additions & 0 deletions docs/other-operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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__

`<expr1> ?: <expr2>`

__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__

`<expr1> ?? <expr2>`

__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.
Expand Down
80 changes: 80 additions & 0 deletions docs/programming.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -68,6 +70,84 @@ __Examples__
]</div>
</div>

### 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__

<div class="jsonata-ex">
<div>Account.Order.Product.{
`Product Name`: $.'Product Name',
`Category`: $.Category ?: "Uncategorized"
}</div>
<div>[
{
"Product Name": "Bowler Hat",
"Category": "Uncategorized"
},
{
"Product Name": "Trilby hat",
"Category": "Uncategorized"
},
{
"Product Name": "Bowler Hat",
"Category": "Uncategorized"
},
{
"Product Name": "Cloak",
"Category": "Uncategorized"
}
]</div>
</div>

### 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__

<div class="jsonata-ex">
<div>Account.Order.{
"OrderID": OrderID,
Rating": ($sum(Product.Rating) / $count(Product.Rating)) ?? 0
}</div>
<div>[
{
"OrderID": "order101",
"Rating": 5
},
{
"OrderID": "order102",
"Rating": 3
},
{
"OrderID": "order103",
"Rating": 4
},
{
"OrderID": "order104",
"Rating": 2
}
]</div>
</div>

## 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).
Expand Down
2 changes: 1 addition & 1 deletion src/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
35 changes: 35 additions & 0 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const parser = (() => {
'<=': 40,
'>=': 40,
'~>': 40,
'?:': 40,
'??': 40,
'and': 30,
'or': 25,
'in': 40,
Expand Down Expand Up @@ -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++;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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';
Expand Down
6 changes: 6 additions & 0 deletions test/test-suite/groups/coalescing-operator/case000.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"expr": "bar ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": 98
}
6 changes: 6 additions & 0 deletions test/test-suite/groups/coalescing-operator/case001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"expr": "foo.bar ?? 98",
"dataset": "dataset0",
"bindings": {},
"result": 42
}
6 changes: 6 additions & 0 deletions test/test-suite/groups/coalescing-operator/case002.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"expr": "foo.blah[0].baz.fud ?? 98",
"dataset": "dataset0",
"bindings": {},
"result": "hello"
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case003.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "undefined property uses default number on rhs",
"expr": "baz ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": 42
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case004.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "property missing on object uses default number on rhs",
"expr": "foo.baz ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": 42
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case005.json
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case006.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "null is used",
"expr": "null ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": null
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case007.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "false is used",
"expr": "false ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": false
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case008.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "true is used",
"expr": "true ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": true
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case009.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "0 is used",
"expr": "0 ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": 0
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case010.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "empty array is used",
"expr": "[] ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": []
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case011.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "empty object is used",
"expr": "{} ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": {}
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/coalescing-operator/case012.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "empty string is used",
"expr": "\"\" ?? 42",
"dataset": "dataset0",
"bindings": {},
"result": ""
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/default-operator/case000.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "true is used",
"expr": "true ?: 42",
"dataset": "dataset0",
"bindings": {},
"result": true
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/default-operator/case001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "false is not used",
"expr": "false ?: 42",
"dataset": "dataset0",
"bindings": {},
"result": 42
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/default-operator/case002.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "1 is used",
"expr": "1 ?: 42",
"dataset": "dataset0",
"bindings": {},
"result": 1
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/default-operator/case003.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "0 is not used",
"expr": "0 ?: 42",
"dataset": "dataset0",
"bindings": {},
"result": 42
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/default-operator/case004.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "hello is used",
"expr": "\"hello\" ?: 42",
"dataset": "dataset0",
"bindings": {},
"result": "hello"
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/default-operator/case005.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "empty string is not used",
"expr": "\"\" ?: 42",
"dataset": "dataset0",
"bindings": {},
"result": 42
}
9 changes: 9 additions & 0 deletions test/test-suite/groups/default-operator/case006.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"description": "[1] is used",
"expr": "[1] ?: 42",
"dataset": "dataset0",
"bindings": {},
"result": [
1
]
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/default-operator/case007.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "[0] is not used",
"expr": "[0] ?: 42",
"dataset": "dataset0",
"bindings": {},
"result": 42
}
7 changes: 7 additions & 0 deletions test/test-suite/groups/default-operator/case008.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "empty array is not used",
"expr": "[] ?: 42",
"dataset": "dataset0",
"bindings": {},
"result": 42
}
Loading