Skip to content

Commit 9e1523d

Browse files
feat: add default/elvis and coalescing operator (#748)
* Merge default operator (?:) feature from markmelville/jsonata Add support for the default (Elvis) operator (?:) which provides a default value when the left-hand side expression is falsy. Examples: - order ?: 'default value' - age ?: 42 - missing ?: other ?: 0 (chainable) Co-authored-by: Mark Melville <[email protected]> * feat: add default and coalescing operators * docs * add more testcases * add more tests for elvis * use existing logic for new operators * fix $boolean for functions --------- Co-authored-by: Mark Melville <[email protected]>
1 parent 0159fe9 commit 9e1523d

31 files changed

+336
-1
lines changed

docs/other-operators.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,36 @@ __Example__
2525

2626
`Price < 50 ? "Cheap" : "Expensive"`
2727

28+
## `?:` (Default/Elvis)
29+
30+
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`).
31+
32+
__Syntax__
33+
34+
`<expr1> ?: <expr2>`
35+
36+
__Example__
37+
38+
`foo.bar ?: 'default'` => `'default'` (if `foo.bar` is evaluates to Boolean `false`)
39+
40+
## `??` (Coalescing)
41+
42+
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 `''`.
43+
44+
__Syntax__
45+
46+
`<expr1> ?? <expr2>`
47+
48+
__Example__
49+
50+
`foo.bar ?? 42` => `42` (if `foo.bar` is undefined)
51+
52+
`foo.bar ?? 'default'` => `'default'` (if `foo.bar` is undefined)
53+
54+
`0 ?? 1` => `0`
55+
56+
`'' ?? 'fallback'` => `''`
57+
2858
## `:=` (Variable binding)
2959

3060
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.

docs/programming.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ Produces [this](http://try.jsonata.org/ryYn78Q0m), if you're interested!
4040

4141
## Conditional logic
4242

43+
### Ternary operator (`? :`)
44+
4345
If/then/else constructs can be written using the ternary operator "? :".
4446

4547
`predicate ? expr1 : expr2`
@@ -68,6 +70,84 @@ __Examples__
6870
]</div>
6971
</div>
7072

73+
### Elvis/Default operator (`?:`)
74+
75+
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.
76+
77+
`expr1 ?: expr2`
78+
79+
This is equivalent to:
80+
81+
`expr1 ? expr1 : expr2`
82+
83+
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.
84+
85+
__Examples__
86+
87+
<div class="jsonata-ex">
88+
<div>Account.Order.Product.{
89+
`Product Name`: $.'Product Name',
90+
`Category`: $.Category ?: "Uncategorized"
91+
}</div>
92+
<div>[
93+
{
94+
"Product Name": "Bowler Hat",
95+
"Category": "Uncategorized"
96+
},
97+
{
98+
"Product Name": "Trilby hat",
99+
"Category": "Uncategorized"
100+
},
101+
{
102+
"Product Name": "Bowler Hat",
103+
"Category": "Uncategorized"
104+
},
105+
{
106+
"Product Name": "Cloak",
107+
"Category": "Uncategorized"
108+
}
109+
]</div>
110+
</div>
111+
112+
### Coalescing operator (`??`)
113+
114+
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.
115+
116+
`expr1 ?? expr2`
117+
118+
This is equivalent to:
119+
120+
`$exists(expr1) ? expr1 : expr2`
121+
122+
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.
123+
124+
__Examples__
125+
126+
<div class="jsonata-ex">
127+
<div>Account.Order.{
128+
"OrderID": OrderID,
129+
Rating": ($sum(Product.Rating) / $count(Product.Rating)) ?? 0
130+
}</div>
131+
<div>[
132+
{
133+
"OrderID": "order101",
134+
"Rating": 5
135+
},
136+
{
137+
"OrderID": "order102",
138+
"Rating": 3
139+
},
140+
{
141+
"OrderID": "order103",
142+
"Rating": 4
143+
},
144+
{
145+
"OrderID": "order104",
146+
"Rating": 2
147+
}
148+
]</div>
149+
</div>
150+
71151
## Variables
72152

73153
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).

src/functions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1420,7 +1420,7 @@ const functions = (() => {
14201420
if (arg !== 0) {
14211421
result = true;
14221422
}
1423-
} else if (arg !== null && typeof arg === 'object') {
1423+
} else if (arg !== null && typeof arg === 'object' && !isFunction(arg)) {
14241424
if (Object.keys(arg).length > 0) {
14251425
result = true;
14261426
}

src/parser.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const parser = (() => {
4040
'<=': 40,
4141
'>=': 40,
4242
'~>': 40,
43+
'?:': 40,
44+
'??': 40,
4345
'and': 30,
4446
'or': 25,
4547
'in': 40,
@@ -198,6 +200,16 @@ const parser = (() => {
198200
position += 2;
199201
return create('operator', '~>');
200202
}
203+
if (currentChar === '?' && path.charAt(position + 1) === ':') {
204+
// ?: default / elvis operator
205+
position += 2;
206+
return create('operator', '?:');
207+
}
208+
if (currentChar === '?' && path.charAt(position + 1) === '?') {
209+
// ?? coalescing operator
210+
position += 2;
211+
return create('operator', '??');
212+
}
201213
// test for single char operators
202214
if (Object.prototype.hasOwnProperty.call(operators, currentChar)) {
203215
position++;
@@ -566,6 +578,20 @@ const parser = (() => {
566578
prefix("-"); // unary numeric negation
567579
infix("~>"); // function application
568580

581+
// coalescing operator
582+
infix("??", operators['??'], function (left) {
583+
this.type = 'condition';
584+
this.condition = {
585+
type: 'function',
586+
value: '(',
587+
procedure: { type: 'variable', value: 'exists' },
588+
arguments: [left]
589+
};
590+
this.then = left;
591+
this.else = expression(0);
592+
return this;
593+
});
594+
569595
infixr("(error)", 10, function (left) {
570596
this.lhs = left;
571597

@@ -849,6 +875,15 @@ const parser = (() => {
849875
return this;
850876
});
851877

878+
// elvis/default operator
879+
infix("?:", operators['?:'], function (left) {
880+
this.type = 'condition';
881+
this.condition = left;
882+
this.then = left;
883+
this.else = expression(0);
884+
return this;
885+
});
886+
852887
// object transformer
853888
prefix("|", function () {
854889
this.type = 'transform';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "bar ?? 42",
3+
"dataset": "dataset0",
4+
"bindings": {},
5+
"result": 98
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "foo.bar ?? 98",
3+
"dataset": "dataset0",
4+
"bindings": {},
5+
"result": 42
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expr": "foo.blah[0].baz.fud ?? 98",
3+
"dataset": "dataset0",
4+
"bindings": {},
5+
"result": "hello"
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"description": "undefined property uses default number on rhs",
3+
"expr": "baz ?? 42",
4+
"dataset": "dataset0",
5+
"bindings": {},
6+
"result": 42
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"description": "property missing on object uses default number on rhs",
3+
"expr": "foo.baz ?? 42",
4+
"dataset": "dataset0",
5+
"bindings": {},
6+
"result": 42
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"description": "out of bounds index uses default number on rhs",
3+
"expr": "foo.blah[9].baz.fud ?? 42",
4+
"dataset": "dataset0",
5+
"bindings": {},
6+
"result": 42
7+
}

0 commit comments

Comments
 (0)