Jora is a powerful and expressive query language designed for querying and transforming JavaScript-like or JSON data. It's a superset of JSON5 and shares many similarities with JavaScript.
Major features:
undefined
* Extension methods may cause input data mutations, have side effects and throw an exception. However, that's an anti-pattern and outside of Jora's scope.
Related projects:
Jora is a powerful and expressive query language designed for querying and transforming JavaScript-like data or JSON data. It's a superset of JSON5, which means it supports all JSON5 syntax and adds additional features, making it even more versatile. Jora shares many similarities with JavaScript, providing a familiar environment for developers. Its core principles are simplicity, readability, flexibility, and performance, which make it easy to learn and use for a wide range of data processing tasks.
Concise syntax
Jora aims to provide a readable and concise syntax, which allows you to write complex queries with a minimal amount of code. It offers a compact syntax for common tasks, reducing boilerplate code and improving readability.
Tolerant to data structure
Jora is tolerant to data structure, meaning it doesn't fail on paths that don't exist and instead returns nothing. In other words, if there are no parse or compile errors for a query, it can be applied without errors to any data, and no exceptions will be raised (although the result might be incorrect). This makes it a robust language for handling various data structures.
Aggregates and eliminates duplicates
Jora automatically aggregates values across arrays and eliminates duplicates by default. This simplifies data queries and reduces the need for additional processing steps to handle duplicate values.
Immutability
Jora is an immutable language, which means it does not modify the original data. Instead, it creates new data based on the input. This ensures data consistency and reduces the risk of accidental data corruption.
Flexibility
Jora offers a variety of built-in methods and functions, allowing you to easily process and manipulate data to meet your specific needs. Additionally, you can extend Jora with custom methods and functions, further enhancing its capabilities.
Performance
Jora is designed to be fast and efficient, ensuring that even large and complex data sets can be processed quickly and without significant performance overhead.
Jora's syntax is designed to be both expressive and concise, allowing to easily perform complex data manipulation tasks. The similarities with JavaScript and JSON5 make Jora a natural choice for developers familiar with those languages, and its performance characteristics ensure it remains a powerful tool for data processing.
Here are some basic examples to help you get started:
posts[0].title
users.[age > 30]
users.(name.first + ' ' + name.last)
users.sort(name asc).group(=> name)
$oldestUser: users.max(age desc); $oldestUser.name
See Syntax overview for a comprehensive overview of Jora syntax.
First, download and install the jora with npm:
jora
npm
npm install jora
Then include it in your module:
// ESM import jora from 'jora'; // CommonJS const jora = require('jora');
For a browser two bundles are available:
dist/jora.js
dist/jora.esm.js
You may include jora by one of the following way:
<script src="node_modules/jora/dist/jora.js"></script> <script type="module"> import jora from 'node_modules/jora/dist/jora.esm.js'; </script> <!-- or use one of CDN --> <script src="https://cdn.jsdelivr.net/npm/jora/dist/jora.js"></script> <script type="module"> import jora from 'https://cdn.jsdelivr.net/npm/jora'; </script> <script src="https://unpkg.com/jora/dist/jora.js"></script>
In case a non ESM version is used, a global variable jora will become available.
Default export of jora is a factory function that converts a query (string) into a function.
import jora from 'jora'; typeof jora('query') // "function"
Such function takes a value and returns new value – a result of query performing.
jora('query')(data) // query result
See Jora library API for details.
Jora has no side effects and doesn't transform (mutate) input. Instead, it produces new data based on input. Jora query is never throw exception due to an input value or its structure, and may generate an output without input data at all.
Note: Side effects, input mutations and exception raising may occur due to custom methods. However, it should be avoided by method's athours.
jora('.foo.bar')({ a: 42 }) // undefined jora('2 + 2')() // 4
import jora from 'jora'; // Create a query const query = jora('foo.bar', { /* ...options */ }); // Perform the query const result = query(data, context);
Options:
methods
Type: Object<string, function | string>Default: undefined
Object<string, function | string>
Describes additional methods for use in queries. Accepts an object where each key represents a method name, and its value can be a function or a string (jora query) defining the action (see Custom methods and assertions). Note: Overriding built-in methods is not permitted and will result in an exception.
assertions
Specifies additional assertions for use in queries. It requires an object where each key is an assertion name, and its value is either a function or a string (jora query) for the assertion (see Custom methods and assertions). Similar to methods, overriding built-in assertions will trigger an exception.
debug
Type: Boolean or function(name, value)Default: false
Boolean
function(name, value)
false
Activates debug output. By default, it uses console.log(). If a function is provided, it will be used instead of console.log(), taking a section name and its value as arguments.
tolerant
Type: BooleanDefault: false
Enables a tolerant parsing mode that attempts to suppress parsing errors when feasible.
stat
Turns on stat mode. In this mode, instead of returning the query results, a query statistics interface is provided.
In scenarios where custom methods or assertions are required, it is recommended to use a custom query factory. This approach allows for consistent application of custom settings across various queries.
import jora from 'jora'; // Create a query factory with common settings const createQuery = jora.setup({ /* methods, assertions */ }); // Create a query const query = createQuery('foo.bar', { /* options as for jora() without "methods" and "assertions" */ }); // Perform the query const result = query(data, context);
Queries can be augmented using the methods and assertions options, allowing for custom extensions. Both options can accept definitions as either regular functions or strings (jora queries). There are distinct considerations for each type of definition:
Function definitions:
Within the function scope, this refers to a special object containing a context reference, which in turn refers to the query's context. Methods and assertions can be invoked using this.method(name, ...args) and this.assertion(name, ...methods), respectively. This approach allows for the integration of both built-in and custom definitions.
this
context
this.method(name, ...args)
this.assertion(name, ...methods)
String definitions (jora queries):
String definitions have access to special variables such as $, $$, @, and #. Additionally, all built-in methods and assertions, along with any custom elements, are accessible within these definitions.
$
$$
@
#
Note: The @ variable pertains to the entry value of a method or an assertion definition, and not to the entry value of the query itself.
There are two methods for enhancing queries in jora:
Using the setup() method for a query factory (recommended):
setup()
This approach involves creating a custom query factory using the setup() method. It allows for defining methods and assertions that are reused across all queries created with this factory. This method is more efficient, especially when dealing with multiple queries.
import jora from 'jora'; // Create a custom setup for queries const queryWithCustomMethods = jora.setup({ methods: { customMethod($) { /* implement custom logic here */ } }, assertions: { odd: '$ % 2 = 1' } }); // Use the custom query factory queryWithCustomMethods('foo.customMethod(is odd)')(data, context);
Defining extensions ad hoc on basic query factory call (less efficient):
In this method, extensions are defined directly on the query creation call of the basic setup. It's less efficient in some cases because extensions are not reused across different queries.
import jora from 'jora'; // Define extensions on a query created for basic setup const result = jora('foo.customMethod(is odd)', { methods: { customMethod($) { /* implement custom logic here */ } }, assertions: { odd: '$ % 2 = 1' } })(data, context);
Note: Extensions configuration is not available for queries created with a query factory made using the setup() method. The setup() method is designed to create extensions that are reused across all queries.
To introspect a query, it should be compiled in "stat" (statistic) mode by passing a stat option. In this case a result of the query evaluation will be a special API with encapsulated state instead of a value:
import jora from 'jora'; const query = jora('...query...', { stat: true }); const statApi = query(data); // { stat() { ... }, suggestion() { ... }, ... }
The returned API allows fetching the values which are passed through a location in a query (the stat() method) as well as a list of suggestions for a location (the suggestion() method):
stat()
suggestion()
import jora from 'jora'; const query = jora('.[foo=""]', { stat: true }); const statApi = query([{ id: 1, foo: "hello" }, { id: 2, foo: "world" }]); statApi.stat(3); // [ // { // context: 'path', // from: 2, // to: 5, // text: 'foo', // values: Set(2) { [Object], [Object] }, // related: null // } // ] statApi.suggestion(3); // .[f|oo=""] // [ // { // type: 'property', // from: 2, // to: 5, // text: 'foo', // suggestions: [ 'id', 'foo' ] // } // ] statApi.suggestion(7); // .[foo="|"] // [ // { // type: 'value', // from: 6, // to: 8, // text: '""', // suggestions: [ 'hello', 'world' ] // } // ]
That's an effective way to use stat mode together with tolerant mode for incomplete queries:
import jora from 'jora'; const query = jora('.[foo=]', { stat: true, tolerant: true // without the tolerant option a query compilation // will raise a parse error: // .[foo=] // ------^ }); const statApi = query([{ id: 1, foo: "hello" }, { id: 2, foo: "world" }]); statApi.suggestion(6); // .[foo=|] // [ // { // type: 'value', // from: 6, // to: 6, // text: '', // suggestions: [ 'hello', 'world' ] // }, // { // type: 'property', // from: 6, // to: 6, // text: '', // suggestions: [ 'id', 'foo' ] // } // ]
stat(pos: number, includeEmpty?: boolean)
Returns an array of ranges with all the values which are passed through pos during performing a query.
pos
Output format:
stat(): Array<{ context: 'path' | 'key' | 'value' | 'in-value' | 'value-subset' | 'var' | 'assertion', from: number, to: number, text: string, values: Set<any>, related: Set<any> | null }> | null
suggestion(pos: number, options?)
Returns suggesion values grouped by a type or null if there is no any suggestions. The following options are supported (all are optional):
null
options
limit
Infinity
"property"
"value"
"variable"
"assertion"
sort
0
filter
function
pattern => value => <expr>
patttern => value => String(value).toLowerCase().includes(pattern)
suggestion(): Array<{ type: 'property' | 'value' | 'variable' | 'assertion', from: number, to: number, text: string, suggestions: Array<string | number> }> | null
Jora is a query language designed for JSON-like data structures. It extends JSON5 and shares many similarities with JavaScript.
Jora expressions are the building blocks of Jora queries. Expressions can include comments, literals, operators, functions, and variables.
// single-line comment /* multi-line comment */
Jora supports literals, which include:
42
-3.14
6.022e23
"hello"
'world'
\
,
true
/regexp/flags
{ hello: 'world' }
[1, 2, 3]
=> …
NaN
See Literals
Jora supports most JavaScript operators, including:
+
-
*
/
%
=
!=
<
<=
>
>=
~=
and
or
not
no
??
is
in
not in
has
has no
?:
( )
|
See Operators
Jora provides notations for accessing properties and elements: dot, bracket and slice notations. Dot notation is similar to JavaScript's property access notation, using a period followed by the property name (e.g., $.propertyName). Bracket notation encloses the property name or index within square brackets (e.g., $['propertyName'] or $[0]), it's also possible to use functions to choose. Slice notation provides a concise syntax to slice elements with optional step (array[5:10:2] selects each odd element from 5th to 10th indecies).
$.propertyName
$['propertyName']
$[0]
array[5:10:2]
Jora provides a rich set of built-in methods for manipulating data, such as map(), filter(), group(), sort(), reduce(), and many others. You can also define custom functions using the => arrow function syntax, and use them as a method.
map()
filter()
group()
sort()
reduce()
=>
Jora has a concise syntax for mapping and filtering. The map(fn) method is equivalent to .(fn()), while the filter(fn) method is equivalent to .[fn()].
map(fn)
.(fn())
filter(fn)
.[fn()]
.(…)
..(…)
.[…]
Variables in Jora are helpful for storing intermediate results or simplifying complex expressions. To define a variable, use the $variableName: expression; syntax.
$variableName: expression;
See Variables
Literals in Jora are mostly the same as in JavaScript.
42 // integer number
4.22 // float number
1e3 // exponential number
1e-2 // exponential number
0xdecaf // hexadecimal number
There is no literal for negative numbers in Jora. However, using unary minus with a number gives a negative number:
-123 // negative number, the same result as -(123)
-0xC0FFEE // hexadecimal negative number
An underscore (_) can be used as a numerical separator:
_
1_000
1_345.678_901
0x12_34_56_78
"string"
'string'
`template string ${hello} ${world}`
Escape sequences are supported, as well as a new line escaping to continue a string on the next line:
"\u2013 This is \"a very long\" string which needs \ to wrap across multiple lines because \ otherwise, my code is unreadable\x21"
The same as in JavaScript. Supported flags: i, g, m, s and u.
i
g
m
s
u
/regexp/
/regexp/mi
Object literal syntax is the same as in JavaScript (see Object literals):
{ foo: 123, bar: true }
Array literal syntax is the same as in JavaScript (see Array literals):
[1, 'foo', { prop: 123 }]
A function defintion looks like an arrow function in JavaScript but without arguments (see Regular functions):
=> expr
There is also a syntax to define a comparator function (see Comparator functions):
name asc, age desc
The following keywords can be used with the same meaning as in JavaScript:
Jora offers a variety of operators to perform operations, comparisons, and boolean logic on data.
+ x
- x
+123 // 123 +'42' // 42 +'foo' // NaN
-123 // -123 -'42' // -42 -'foo' // NaN
x + y
x
y
x - y
x * y
x / y
x % y
// Add numbers 1 + 2 // 3 // Add arrays [1, 2, 3] + [2, 3, 4] // [1, 2, 3, 4] // Subtract numbers 10 - 5 // 5 // Subtract arrays [1, 2, 3] - [2, 3] // [1] // Subtract from array [1, 2, 3] - 2 // [1, 3] // Multiply numbers 2 * 3 // 6 // Divide numbers 10 / 2 // 5 // Modulo 7 % 3 // 1
x = y
Object.is(x, y)
x != y
!Object.is(x, y)
x < y
x <= y
x > y
x >= y
x ~= y
// Equals 1 = 1 // true 'a' = 'b' // false // Not equals 1 != 2 // true 'a' != 'a' // false // Less than 1 < 2 // true 2 < 1 // false // Less than or equal to 1 <= 1 // true 2 <= 1 // false // Greater than 2 > 1 // true 1 > 2 // false // Greater than or equal to 2 >= 2 // true 1 >= 2 // false // Match operator 'hello' ~= /l+/ // true 'world' ~= => size() > 3 // true 'foo' ~= null // true 'bar' ~= 123 // false
x or y
||
x and y
&&
not x
no x
!
x ?? y
x is assertion
x in [a, b, c]
[a, b, c] has x
x = a or x = b or x = c
x not in [a, b, c]
[a, b, c] has no x
x != a and x != b and x != c
// Logical OR true or false // true [] or false // false [1, 2] or false // [1, 2] // Logical AND true and false // false true and true // true {} and "ok" // {} // Logical NOT not true // false no false // true not [] // true not [1] // false // Nullish coalescing null ?? 1 // 1 undefined ?? 1 // 1 false ?? 1 // false 1234 ?? 1 // 1234 // IS operator [] is array // true [] is number // false {} is (boolean or string) // false // IN operator 1 in [1, 2, 3] // true 4 in [1, 2, 3] // false // HAS operator [1, 2, 3] has 1 // true [1, 2, 3] has 4 // false // NOT IN operator 1 not in [1, 2, 3] // false 4 not in [1, 2, 3] // true // HAS NO operator [1, 2, 3] has no 1 // false [1, 2, 3] has no 4 // true
The ternary operator requires three operands: a condition, followed by a question mark (?), an expression for a truthy condition, a colon (:), and an expression for a falsy condition. The condition is evaluated using the bool() method.
?
:
true ? 'yes' : 'no' // Result: 'yes'
false ? 'yes' : 'no' // Result: 'no'
Operands can be omitted. When omitted, $ is used as a default for the condition and the truthy expression, and undefined for the falsy expression.
?: // Equivalents to `$ ? $ : undefined`
A query to truncate strings in array longer than 10 characters:
['short', 'and a very long string'].(size() < 10 ?: `${slice(0, 10)}...`) // Result: ["short", "and a very..."]
If the falsy operand is omitted, the colon (:) can also be omitted. This is useful in conjunction with assertions and statistical or mathematical methods:
[{ x: 5 }, { x: 33 }, { x: 20 }].count(=> x > 10 ?) // Result: 2
[2, '2', 3, { foo: 4 }].sum(=> is number ? $ * $) // 2 * 2 + 3 * 3 // Result: 13
Parentheses ( ) serve as the grouping operator, allowing explicitly define the order in which operations should be executed, ensuring that specific calculations are performed before others. This is particularly useful when dealing with complex expressions, as it helps to avoid unexpected results due to the default operator precedence.
(1 + 2) * (3 + 4) // Result: 21
In this case, the addition operations are performed first, followed by the multiplication, resulting in the correct output of 21. Without the parentheses, the expression would be calculated as 1 + (2 * 3) + 4, giving a different result of 11.
1 + (2 * 3) + 4
Variable declarations are allowed within the parentheses:
($a: 1; $a + $a) // Result: 2
The pipeline operator | facilitates the simplification of queries by linking expressions in a chain. Its utility is especially evident when treating a query result as a scalar value or reusing the outcome of an extensive or resource-intensive subquery multiple times, without the need for storage in a variable. When using the pipeline operator, the value of $ on the right side of the operator becomes equal to the value of the left side.
1.5 | floor() + ceil() // Result: 3
The following examples demostrate how a query can be simplified using the pipeline operator:
Replacement for grouping operator:
(a + b).round()
a + b | round()
Simplify expressions to avoid using the mapping:
{ foo: 1, bar: 2, baz: 3 }.(foo + bar + baz) // Result: 6
{ foo: 1, bar: 2, baz: 3 } | foo + bar + baz // Result: 6
Reducing repetitions:
$a.bar + $a.baz
$a | bar + baz
Reusing result of a long or expensive subquery without saving it into a variable:
$result: very.expensive.query; $result ? $result.sum() / $result.size() : '–'
very.expensive.query | $ ? sum() / size() : '–'
The pipeline operator can be used in any place of query where any other operator is applicable as well as any number of pipeline operators can be used in sequence:
.({ $bar: num | floor() + ceil() | $ / 2; foo: $bar | a + b, baz: [1, $qux.min() | [$, $]] })
Variable declarations are allowed in the beginning of the right side of the pipeline operator:
{ a: 10, b: [2, 3, 4] } | $k: a; b.($ * $k) // Result: [20, 30, 40]
Any two independent syntactically correct queries can be joined with the pipeline operator, resulting in a syntactically correct query. But keep in mind that both queries will refer to the same variables @ (query input) and # (query context), which may not work in all cases.
The following table shows the operators' precedence from lowest to highest:
( … )
Assertions are functions used to evaluate values against specified criteria. They offer concise syntax and a dedicated context for suggestions, separate from methods.
expr is assertion
Note: Omitting expr is also valid, e.g., is assertion.
expr
is assertion
Assertions can include logical operators such as not, and, or, and parentheses for grouping:
is not assertion
is (assertion and assertion)
is (assertion or not assertion)
is not (assertion and assertion)
is (assertion and (assertion or assertion))
Jora comes with a set of built-in assertions which perform most common examinations on values.
typeof e === 'function'
symbol
typeof e === 'symbol'
primitive
e === null || (typeof value !== 'object' && typeof value !== 'function')
string
typeof e === 'string'
number
typeof e === 'number'
int
Number.isInteger(e)
finite
Number.isFinite(e)
nan
Number.isNaN(e)
infinity
e === Infinity || e === -Infinity
boolean
e === true || e === false
falsy
truthy
e === null
e === undefined
nullish
e === null || e === undefined
object
e !== null && typeof e === 'object' && e.constructor === Object
array
Array.isArray(e)
regexp
e.toString() === '[object RegExp]'
Variables storing functions can be used as assertions:
$odd: => $ % 2; [1, 2, 3, 4, 5].({ num: $, odd: is $odd }) // Result: [ // { "num": 1, "odd": true }, // { "num": 2, "odd": false }, // { "num": 3, "odd": true }, // { "num": 4, "odd": false }, // { "num": 5, "odd": true } // ]
Note: If a variable is not a function, a $var is not a function error will be thrown.
$var is not a function
Jora queries can be enchanced by defining custom methods (see Enhancing queries with custom methods and assertions):
import jora from 'jora'; // Setup query factory with custom assertions const createQueryWithCustomAssertions = jora.setup({ assertions: { mine($) { /* test a value */ } } }); // Create a query const queryWithMyAssertion = createQueryWithCustomAssertions('is mine');
Jora allows defining and using variables within queries. A value can be assigned to a variable only on its definition. Once a variable is defined, its value cannot be changed throughout the query. Variables are useful for storing intermediate results, improving readability, reusing expressions and preserving values across scopes.
There are two main forms for defining variables:
$ident: expression;
$ident;
An example of defining and using variables in various ways:
$foo: 123; // Define `$foo` variable $bar; // Shorthand for `$bar: bar;` $baz: $foo + $bar; // Definitions may be used in following expressions
In the following example, two variables are defining: $numbers and $multiplier. Then, the $multiplier is used within mapping to multiply each element of the $numbers array.
$numbers
$multiplier
$numbers: [1, 2, 3]; $multiplier: 2; $numbers.($ * $multiplier) // Result: [2, 4, 6]
Jora has several special variables that serve specific purposes:
Query input data and context are provided when executing a query: query(inputData, context).
query(inputData, context)
Examples of using $ and $$ in functions:
$fn: => { a: $, b: $$ }; 'hello'.$fn('world') // result: { a: 'hello', b: 'world' }
See Methods on details of methods usage
Scopes are created by certain constructions in Jora. Variables defined within a scope are only accessible within that scope and its nested scopes.
Constructions that allow defining variables include:
$foo: 'bar'; $foo // Result: 'bar'
{ a: 10, b: 20 }.($a; $b; $a + $b) // Result: 30
.[]
[1, 2, 3].[$num: $; $num % 2] // Result: [1, 3]
(…)
($a: 5; $b: 10; $a + $b) // Result: 15
{…}
{ $a: 3; $b: 4; c: $a * $b } // Result: { c: 12 }
[1, 2, 3] | $size: size(); .($ * $size) // Result: [3, 6, 9]
Variables in Jora can also be helpful for storing the current value ($) or its nested data before entering a nested scope, as $ might change within the nested scope. Storing the current value or its parts in a variable before entering the nested scope ensures that you can still access the original value or its parts in the new scope. Here are a couple of examples demonstrating this use case:
user.({ $username: name; $email; // The same as `$email: email;` ..., signedPosts: posts.({ author: $username, authorEmail: $email, title }) })
In this example, we store the name and email properties of the current user in variables $username and $email. This allows us to access these values within the nested scope created by the .() operation on the posts property, even though $ has changed to represent the current post.
name
email
$username
$email
.()
posts
$items: [ { id: 1, children: [2, 3] }, { id: 2, children: [4, 5] }, { id: 3, children: [6, 7] } ]; $items.( $item: $; children.({ parent: $item.id, child: $ }) ) // Result: // [ // { parent: 1, child: 2 }, // { parent: 1, child: 3 }, // { parent: 2, child: 4 }, // { parent: 2, child: 5 }, // { parent: 3, child: 6 }, // { parent: 3, child: 7 } // ]
In this example, we store the entire current item in the variable $item before entering the nested scope created by the .() operation on the children property. This allows us to access the parent value within the nested scope even though $ has changed to represent the current child.
$item
children
Jora reserves some names for future special use cases. These names cannot be used as variable names to ensure compatibility with future versions:
data
ctx
idx
index
Jora supports object literals as an integral part of its syntax. Object literals provide a convenient way to define and manipulate complex data structures.
Object literals in Jora follow the familiar syntax found in JSON5 and JavaScript. They are enclosed in curly braces {} and consist of key-value pairs, with keys being strings and values being any valid Jora expression. In Jora, keys in object literals don't need to be wrapped in quotes unless they contain special characters, spaces, or start with a digit. Here's an example of a simple object literal:
{}
{ "name": "John Doe", 'age': 30, isActive: true }
Jora supports computed properties in object literals, allowing you to create dynamic keys based on expressions. To use computed properties, wrap the key expression in square brackets []. Here's an example:
[]
$prefix: 'city'; { [$prefix + 'Code']: "NYC" } // Result: { cityCode: "NYC" }
When the value of an entry starts with an identifier, method call, or variable, the key name can be inferred and omitted:
$city: "New York"; { hello: 'world' } | { hello, $city, size() } // Result: { hello: 'world', city: 'New York', size: 1 }
This is equivalent to:
$city: "New York"; { hello: 'world' } | { hello: hello, city: $city, size: size() } // Result: { hello: 'world', city: 'New York', size: 1 }
Using shorthand syntax for methods is particularly useful when multiple aggregation functions need to be called on the same array:
[1, 3, 2] | { min(), max(), sum(), avg() } // Result: { min: 1, max: 3, sum: 6, avg: 2 }
Shorthand syntax can follow any expression, transforming from name expr into name: name | expr (see Pipeline Operator):
name expr
name: name | expr
{ foo.[x > 5], // equivalent to: `foo: foo | .[x > 5]` bar size() * 2, // equivalent to: `bar: bar | size() * 2` baz is number ?: 0, // equivalent to: `baz: baz | is number ?: 0` $var.size(), // equivalent to: `var: var | .size()` sort() reverse() // equivalent to: `sort: sort() | reverse()` }
Note that an expression can't start with an operator because it will cause a syntax error. However, starting with + (unary plus) and - (unary minus) does not lead to a syntax error but produces a scalar, discarding the base value:
[1, 2, 3] | { size() + 10 // equivalent to: `size: size() | +10` } // Result: { size: 10 }
The spread operator (...) is used in Jora to merge properties from one object into another. It can be used with a variable or an expression that evaluates to an object:
...
$foo: { a: 1, b: 2 }; $bar: { b: 3, c: 4 }; { ...$foo, ...$bar } // Result: { // "a": 1, // "b": 3, // "c": 4 // }
The spread operator can be used without an expression following it, which essentially means that it will spread the current value ($). In this case, the spread operator is equivalent to using ...$. This shorthand is helpful when you want to merge an object with the current value in a concise manner. Here's an example: suppose we have a list of users, and we want to add an additional property (active: true) to each user object. We can use the spread operator to achieve this:
...$
active: true
$users: [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]; $users.({ ..., // The same as ...$ active: true }) // Result: [ // { "id": 1, "name": "Alice", "active": true }, // { "id": 2, "name": "Bob", "active": true } // ]
In this example, the spread operator (...) without an expression following it represents the current value ($) within the mapping function, which is each user object in the $users array. The resulting object includes all the properties from the original user object along with the additional active: true property.
$users
The following methods are used to retrieve a list of entries from an object or to reconstruct an object from a list of entries:
entries()
Object.entries()
{ key, value }
key
value
{ a: 1, b: 2, c: 3 }.entries() // Result: [ // { "key": "a", "value": 1 }, // { "key": "b", "value": 2 }, // { "key": "c", "value": 3 } // ]
fromEntries()
Object.fromEntries()
[ { key: "a", value: 1 }, { key: "b", value: 2 }, { key: "c", value: 3 } ].fromEntries() // Result: { // "a": 1, // "b": 2, // "c": 3 // }
Array literals in Jora are similar to those in JavaScript. They allow you to create an array by enclosing a comma-separated list of values or expressions within square brackets []. Jora supports a wide range of values, including numbers, strings, objects, other arrays, and even Jora expressions.
[1, 2, 3, 4, 5]
You can nest arrays within other arrays to create multidimensional arrays. For example:
[ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]
This creates a 3x3 matrix represented as a 2-dimensional array.
Jora allows you to use expressions and variables within array literals to compute values dynamically:
$n: 5; [1, 1 + 1, $n > 1 ? 3 : 42, $n - 1, $n] // Result: [1, 2, 3, 4, 5]
Jora provides the slice() method and slice notation to extract a portion of an array:
slice()
$numbers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; $numbers.slice(3, 7) // Or using slice notation: $numbers[3:7] // Result: [3, 4, 5, 6]
Jora supports array concatenation using the spread operator ... and the + operator. Here's an example that concatenates two arrays:
$first: [1, 2, 3]; $second: [4, 5, 6]; [...$first, ...$second] // Or: $first + $second // Result: [1, 2, 3, 4, 5, 6]
The spread operator ... can be used in an array literal to include all elements of an existing array:
$first: [1, 2, 3]; $second: [4, 5, 6]; [...$first, ...$second] // Result: [1, 2, 3, 4, 5, 6]
In this example, the spread operator is used to merge $first and $second arrays into a new array containing all elements from both arrays.
$first
$second
$input: [1, 2, 3, 4, 5, 6, 7, 8, 9]; $input.reduce(=>$ % 2 ? [...$$, $] : $$, []) // Result: [1, 3, 5, 7, 9]
In this example, the reduce() method iterates over the $input array and checks if the current value ($) is odd (i.e., $ % 2). If the value is odd, it appends the value to the accumulator array (...$$, $); otherwise, it leaves the accumulator unchanged ($$). The initial value of the accumulator is an empty array [].
$input
$ % 2
...$$, $
Unlike JavaScript, spread operator in Jora inlines arrays only and left any other values "as is":
[...[1, 2], ...3, ..."45", ...{ "6": 7 }] // Result: [1, 2, 3, "45", { "6": 7 }]
As demonstrated above, the + operator can concatenate arrays and values. For example:
[1, 2, 3] + 4 + [5, 6] // Result: [1, 2, 3, 4, 5, 6]
The - operator, when the left operand is an array, produces a new array with the right operand value filtered out:
[1, 2, 3, 4, 5] - 3 - [1, 5] // Result: [2, 4]
Dot notation is one of the key features that make accessing object properties simple and straightforward. Similar to JavaScript, dot notation allows you to access a property by appending the property name after a period (.). Jora's property access works with own properties only and uses the .() (or .map()) method under the hood (see Mapping for details). For example, foo.bar is the same as .(foo).(bar).
.
.map()
foo.bar
.(foo).(bar)
person.name // Accesses the `name` property of the `person` object
Dot notation can be used to access nested properties within objects as well. In the following example, we have an object with nested properties:
$person: { name: { first: 'John', last: 'Doe' }, age: 30 }; $person.name.first // Result: 'John'
By chaining dot notation, you can access properties at any depth within an object.
In Jora, optional chaining is enabled by default for both dot and bracket notations. When accessing a property that does not exist, Jora returns undefined instead of throwing an error.
person.address.street // Accesses the `street` property of the `address` object, // returns `undefined` if `address` does not exist
Dot notation is also used for an array to get a value of a property for every object in the array. In the following example, we create a variable $fruits that holds an array of objects:
$fruits
$fruits: [ { id: 1, name: 'apple' }, { id: 2, name: 'banana' }, { id: 3, name: 'cherry' }, { id: 4, name: 'apple' } ]; $fruits.name // Result: ['apple', 'banana', 'cherry']
As you can see, when using dot notation on an array, Jora retrieves the values of the specified property for every object in the array, producing a new array with unique values.
Dot notation can be combined with other Jora features, such as filtering, mapping, or reducing, to create powerful and expressive queries:
$items: [ { name: 'foo', value: 1 }, { name: 'bar', value: 2 }, { name: 'baz', value: 3 }, { name: 'foo', value: 4 } ]; $items.[name = 'foo'].value.sum() // Result: 5
In this example, Jora first filters the items with the name 'foo', then extracts the 'value' property from each filtered item, and finally calculates the sum of these values.
Bracket notation enables convenient access to object properties and array elements. Bracket notation is especially useful when the property name is stored in a variable, computed dynamically, or when accessing array elements using indices. Jora supports bracket notation, similar to JavaScript, by enclosing the property name or index within square brackets ([]). You can also use bracket notation for the current value by using $['name'].
$['name']
person['name'] // Accesses the `name` property of the `person` object
items[0] // Accesses the first element of the `items` array
In Jora, optional chaining is enabled by default for both dot and bracket notations, meaning there is no special syntax required for optional chaining. This built-in functionality allows for more concise and error-free code when accessing properties or elements that may not exist.
Property access syntax in Jora is used for element access in arrays or characters in strings, making it versatile and consistent across different data types.
When a function is used with bracket notation, the notation returns the first value or element for which the function returns a truthy value. The context of a function varies depending on the data type:
Consider the following example:
$items: [ { id: 1, name: 'Foo' }, { id: 2, name: 'Bar' }, { id: 3, name: 'Baz' } ]; $items[=> id = 2] // Result: { id: 2, name: 'Bar' }
In this example, the function used with bracket notation returns the first object in the $items array with an id property equal to 2.
$items
id
Next query uses bracket notation to find the first object within $items that meets two conditions: the property key (entry key) matches the regular expression /^item/ and price property of the object (entry value) must be greater than or equal to 20.
/^item/
price
$items: { item1: { id: 1, name: 'First Item', price: 10 }, item2: { id: 2, name: 'Second Item', price: 20 }, item3: { id: 3, name: 'Third Item', price: 30 } }; $items[=> $$ ~= /^item/ and price >= 20] // Result: { id: 2, name: 'Second Item', price: 20 }
Jora supports negative indexing for arrays and strings, allowing you to access elements from the end of the array or string by specifying a negative index.
[1, 2, 3, 4, 5][-2] // Result: 4
In the example above, the negative index -2 accesses the second-to-last element in the array, which is the number 4.
-2
$prop: 'name'; person[$prop] // Accesses the `name` property of the `person` object
$matrix: [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]; $matrix[1][2] // Result: 6
$text: 'Jora'; $text[2] // Result: 'r'
$items: [ { category: 'A', value: 5 }, { category: 'B', value: 10 }, { category: 'A', value: 15 }, { category: 'C', value: 20 }, { category: 'A', value: 25 } ]; $items[=> category = 'A' and value >= 20] // Result: { category: 'A', value: 25 }
Slice notation is a convenient way to extract a portion of a sequence, such as an array or a string, in Jora. It allows you to define the start, end, and step of the slice. Slice notation is a complement to the slice() method, which is more verbose but works the same way. This notation is based on the slice notation proposal for ECMAScript.
The slice notation syntax is as follows:
sequence[from:to:step]
from
to
step
All values in the slice notation can be either positive or negative integers. Negative integers for from and to are interpreted as counting from the end of the sequence. Negative value for step reverses the order of the elements stepping from to to from.
Although all values are optional, it's essential to use at least one colon. Otherwise, if an expression is empty, it will be treated as bracket notation. In other words, $[:] denotes slice notation, while $[] indicates bracket notation.
$[:]
$[]
[1, 2, 3, 4, 5][1:4] // Result: [2, 3, 4]
[1, 2, 3, 4, 5][:3] // Result: [1, 2, 3]
[1, 2, 3, 4, 5][-2:] // Result: [4, 5]
[1, 2, 3, 4, 5][::2] // Result: [1, 3, 5]
[1, 2, 3, 4, 5][4:1] // the same as [1:4:-1] (see "Negative Step" below) // Result: [4, 3, 2]
Omitting all lower bound and upper bound value, produces a new copy of the array:
Note that a colon is essential here. Otherwise, it will be treated as bracket notation
['a', 'b', 'c', 'd'][:] // an alternative is to use [...array] // Result: ['a', 'b', 'c', 'd']
$value: ['a', 'b', 'c', 'd']; $value[:] = $value // Result: false
"hello"[1:4] // Result: "ell"
"hello"[:3] // Result: "hel"
"hello"[-2:] // Result: "lo"
"hello"[::2] // Result: "hlo"
"hello"[4:1] // the same as [1:4:-1] (see "Negative Step" below) // Result: "lle"
A negative step can be used to reverse the order of the elements in the slice:
[1, 2, 3, 4, 5][1:4:-1] // the same as [4:1] // Result: [4, 3, 2]
[1, 2, 3, 4, 5, 6, 7, 8][6:1:-2] // the same as [1:6:2] // Result: [2, 4, 6]
"hello"[1:4:-1] // Result: "lle"
1
to - 1
This algorithm ensures that the original sequence remains unmodified, and the output is a new sequence containing the desired elements based on the provided slice notation parameters.
There are two ways to define a function in Jora: a regular function and a comparator function (which used to compare a couple of values).
Jora's function syntax is akin to JavaScript's arrow functions but with distinct characteristics:
$foo
$bar
(...$args) => expression
($a, $b) => $ = $a and $$ = $b
$a
$b
Supported function forms in Jora include:
$arg => expr
() => expr
($arg) => expr
($arg1, $arg2) => expr
Functions are often used in place as method arguments:
[1, 2, 3, 4].group(=> $ % 2) // Result: [{ key: 1, value: [1, 3] }, { key: 0, value: [2, 4]}]
Functions can also be stored as local variables and used as regular methods:
$countOdd: => .[$ % 2].size(); [1, 2, 3, 4].$countOdd() // Result: 2
The special variables $ (first parameter) and $$ (second parameter) are always accessible in function scope:
$example: => [$, $$]; 1.$example(2) // Result: [1, 2]
Declaring arguments does not alter the behavior of $ and $$:
$example: ($a, $b) => [$a, $, $b, $$]; 1.$example(2) // Result: [1, 1, 2, 2]
Explicit argument declaration is beneficial when an argument is used in nested scopes within the function body:
$books: [ { id: 1, title: "To Kill a Mockingbird", author: "Harper Lee" }, { id: 2, title: "1984", author: "George Orwell" }, { id: 3, title: "The Great Gatsby", author: "F. Scott Fitzgerald" } ]; $getBook: $id => $books[=> id = $id]; 3 | $getBook() // Result: { id: 3, title: "The Great Gatsby", author: "F. Scott Fitzgerald" }
Here's how to sum up an array using the reduce() method and a function, where $ is the array element and $$ is the accumulator:
[1, 2, 3, 4].reduce(=> $$ + $, 0) // Result: 10
Note: In Jora, function arguments order is always $, $$, but in JavaScript's reduce(), the order is reversed. With explicit arguments a function for Jora's reduce will be ($current, $sum) => $sum + $current.
$, $$
($current, $sum) => $sum + $current
The JavaScript equivalent:
[1, 2, 3, 4].reduce(($$, $) => $$ + $, 0)
To use a function passed via context (#) or data (@), store it in a local variable and then use it as a method:
$functionFromContext: #.someFunction; someValue.$functionFromContext()
There is a syntax to define comparator functions – functions that return -1, 0, or 1, based on whether the first value in a pair is lesser, equal to, or greater than the second. The syntax produces a function which takes two arguments and compare the query result for each in the specified order (asc or desc):
-1
asc
desc
expr asc // Result (JS equivalent): (a, b) => expr(a) > expr(b) ? 1 : expr(a) < expr(b) ? -1 : 0
expr desc // Result (JS equivalent): (a, b) => expr(a) < expr(b) ? 1 : expr(a) > expr(b) ? -1 : 0
A comma-separated sequence defines a single function:
foo asc, bar.size() desc // Result (JS equivalent): // (a, b) => // a.foo > b.foo ? 1 : a.foo < b.foo ? -1 : // size(a.bar) < size(b.bar) ? 1 : size(a.bar) > size(b.bar) ? -1 : // 0
These functions are useful for built-in methods like sort(), min(), max() and others.
min()
max()
$input: [{ foo: 3 }, { foo: 1 }, { foo: 5 }]; $input.sort(foo desc) // Result: [{ foo: 5 }, { foo: 3 }, { foo: 1 }]
Before comparing a pair of values, a comparator function compares their types. When types are different, a comparator returns a result of types comparison, since comparing let say a number and an object makes no sense. In other words, an object value is always bigger than a number, and a boolean is always lower than a string:
There are some variations for asc and desc that provide additional comparison options:
ascN
descN
N
ascA
descA
A
ascAN
ascNA
descAN
descNA
AN
In Jora, methods are functions that are invoked in a functional way. This means that the left side value (the part before a method call, or $ if nothing is specified) is always passed as the first argument to the method. Jora has a set of built-in methods which can be extended with custom methods. Also functions can be used as a method.
expr.method(...args)
Note: expr can be omitted, i.e. .method(...args) or method(...args) are also valid forms
.method(...args)
method(...args)
Jora comes with a set of built-in methods which perform most common operations on data. There is an example of using group(), sort() and size() methods:
size()
group(=> name) .({ name: key, records: value }) .sort(records.size() desc)
See Built-in methods.
import jora from 'jora'; // Create a custom setup for queries const queryWithCustomMethods = jora.setup({ methods: { customMethod($) { /* implement custom logic here */ } } }); // Use the custom query factory queryWithCustomMethods('foo.customMethod()')(data, context);
The avg(getter) method calculates the arithmetic mean, also known as the average, of a collection of numbers. The arithmetic mean is computed by adding all the numbers in the collection and then dividing by the total count of numbers. This method is equivalent to the expressions numbers() | sum() / size() or sum() / count().
avg(getter)
numbers() | sum() / size()
sum() / count()
[1, 2, 3, 4].avg() // Result: 2.5
[{}, { a: 2 }, undefined, { a: 4 }].avg(=> a) // Result: 3
Similar to Boolean() in JavaScript, but treats empty arrays and objects with no keys as falsy.
Boolean()
123.bool() // Result: true
"".bool() // Result: false
[].bool() // Result: false
[false].bool() // Result: true
{}.bool() // Result: false
{ a: 42 }.bool() // Result: true
The count() method calculates the number of non-undefined values present in the input array. It processes each value in the array through a getter function (default function is => $). If the processed value is not undefined, it increments the count by 1. If the input is not an array, the method returns 0. This method is functionally equivalent to the expression numbers().size().
count()
getter
=> $
numbers().size()
[1, 2, 3].count() // Result: 3
[1, NaN, false, true, '123', { foo: 1 }, [5]].count() // Result: 7
[{ age: 10 }, { age: 20 }, {}, { foo: 1 }].count(=> age) // Result: 2
While the primary function of this method is to count non-undefined values in the input, it can also be utilized to count values that meet a specific condition, by converting falsy values to undefined:
[{ a: 1, b: 3}, { a: 5, b: 4 }, { a: 3, b: 8 }].count(=> a < b or undefined) // Result: 2
In the last example, the count() method returns the count of objects where the value of a is less than b.
a
b
Similar to Object.entries() in JavaScript, using { key, value } objects for entries instead of array tuples.
{ a: 42, b: 123 }.entries() // Result: [{ key: 'a', value: 42 }, { key: 'b', value: 123 }]
[1, 2].entries() // Result: [{ key: '0', value: 1 }, { key: '1', value: 2 }]
'abc'.entries() // Result: [{ key: '0', value: 'a' }, { key: '1', value: 'b' }, { key: '2', value: 'c' }]
123.entries() // Result: []
The same as Array#filter() in JavaScript, filter(fn) is equivalent to .[fn()] (see Filtering).
Array#filter()
[1, 2, 3, 4].filter(=> $ % 2) // Result: [1, 3]
$isOdd: => $ % 2; [1, 2, 3, 4].filter($isOdd) // Result: [1, 3]
Similar to Object.fromEntries() in JavaScript, expects array { key, value } objects as entries instead of array tuples.
[{ key: 'a', value: 42 }, { key: 'b', value: 123 }].fromEntries() // Result: { a: 42, b: 123 }
Group array items by a value fetched with the first getter and return an array of { key, value } entries (see Grouping).
Returns the first index of the specified value, starting the search at fromIndex. If fromIndex is not provided or cannot be converted to a number, the search starts from index 0. The method returns -1 if the value is not found or if the input doesn't implement the indexOf() method. Unlike JavaScript, this method supports index searching for NaN values.
fromIndex
indexOf()
[1, 2, 3, 1, 2, 3].indexOf(2) // Result: 1
[1, 2, 3, 1, 2, 3].indexOf(2, 3) // Result: 4
'abc abc'.indexOf('bc') // Result: 1
[1, NaN, 2, NaN, 3].indexOf(NaN) // Result: 1
The same as Array#join() in JavaScript. When separator is not specified, "," is used.
Array#join()
separator
","
[1, 2, 3].join() // Result: "1,2,3"
[undefined, null, 123, NaN, "str", [2, 3], {}].join(' / ') // Result: " / / 123 / NaN / str / 2,3 / [object Object]"
The same as Object.keys() in JavaScript.
Object.keys()
{ foo: 1, bar: 2 }.keys() // Result: ["foo", "bar"]
[2, 3, 4].keys() // Result: ["0", "1", "2"]
123.keys() // Result: []
Returns the first index of the specified value starting from the end at fromIndex. If fromIndex is not specified or cannot be converted to a number, it defaults to array or string length. The method returns -1 if the value is not found or if the input doesn't implement the lastIndexOf() method. Unlike JavaScript, this method supports index searching for NaN values.
lastIndexOf()
[1, 2, 3, 1, 2, 3].lastIndexOf(2) // Result: 4
[1, 2, 3, 1, 2, 3].lastIndexOf(2, 3) // Result: 1
'abc abc'.lastIndexOf('bc') // Result: 5
[1, NaN, 2, NaN, 3].lastIndexOf(NaN) // Result: 3
The same as Array#map() in JavaScript, is equivalent to .(fn()) (see Mapping).
Array#map()
[1, 2, 3, 4].map(=> $ * 2) // Result: [2, 4, 6, 8]
$getA: => a; [{ a: 1 }, { a: 2 }, { a: 1 }].map($getA) // Result: [1, 2]
Similar to String#match(). pattern might be a RegExp or string. When matchAll is truthy, returns an array of all occurrences of the pattern. Expressions match(/…/g) and match(/…/, true) are equivalent.
String#match()
pattern
matchAll
match(/…/g)
match(/…/, true)
'abcabc'.match('bc') // Result: { // matched: ['bc'], // start: 1, // end: 3, // input: 'abcabc', // groups: null, // }
'abcabc'.match('bc', true) // matchAll parameter is true // Result: [{ // matched: ['bc'], // start: 1, // end: 3, // input: 'abcabc', // groups: null, // }, { // matched: ['bc'], // start: 4, // end: 6, // input: 'abcabc', // groups: null, // }]
'abc123a45'.match(/a(bc)?(?<numbers>\d+)/) // Result: { // matched: ['abc123', 'bc', '123'], // start: 0, // end: 6, // input: 'abc123a45', // groups: { numbers: '123' }, // }
'abc123a45'.match(/a(bc)?(?<numbers>\d+)/g) // the RegExp has 'g' flag // Result: [{ // matched: ['abc123', 'bc', '123'], // start: 0, // end: 6, // input: 'abc123a45', // groups: { numbers: '123' }, // }, { // matched: ['a45', undefined, '45'], // start: 6, // end: 9, // input: 'abc123a45', // groups: { numbers: '45' }, // }]
The max(compare) method returns the maximum value from an array or a string, excluding undefined. It uses natural comparison for string values by default. When applied to an array, the method returns undefined if the array is empty or if a comparator function returns 0 for all values when compared with undefined.
max(compare)
The logic of the max() method is equivalent to the expressions sort().[$ != undefined][-1]. However, max() is more performant and memory efficient because it doesn't need to sort the entire array or string.
sort().[$ != undefined][-1]
Here are some examples of how to use the max() method:
// Find the maximum number in an array [1, 4, 2, 3].max() // Result: 4
// Find the maximum object in an array based on a property $input: [{ a: 10 }, { a: 42 }, {}, { a: 42, ok: 1 }, { a: 20 }]; $input.max(=> a) // Result: { a: 42, ok: 1 }
// Find the minimum object in an array based on a property $input: [{ a: 10 }, { a: 42 }, {}, { a: 20 }]; $input.max(a desc) // Result: { a: 10 }
// Find the maximum character in a string 'hello world'.max() // Result: 'w'
Computes the median (the second quartile) of the values in an array. It's a shortcut for percentile(50) or p(50) (see percentile()).
percentile(50)
p(50)
[4, 2, 1, 3, 5].median() // Result: 3
[4, 2, 1, 3, 6, 5].median() // Result: 3.5
The min() method returns the minimum value from an array or a string. It uses natural comparison for string values by default. When applied to an array, the method returns undefined if the array is empty or if a comparator function returns 0 for all values when compared with undefined.
The logic of the min() method is equivalent to the expression sort()[0]. However, min() is more performant and memory efficient because it doesn't need to sort the entire array or string.
sort()[0]
// Find the minimum number in an array [4, 1, 2, 3].min() // Result: 1
// Find the minimum object in an array based on a property using a function $input: [{ a: 10 }, { a: 5, ok: 1 }, {}, { a: 5 }, { a: 20 }]; $input.min(=> a) // Result: { a: 5, ok: 1 }
// Find the minimum object in an array based on a property using a compare function $input: [{ a: 10 }, { a: 42 }, {}, { a: 20 }]; $input.min(a desc) // Result: { a: 42 }
// Find the minimum character in a string 'hello world'.min() // Result: ' '
The numbers() method returns an array of numbers derived from the input values. It ignores undefined values returned by the getter function (the default getter function is => $, which returns the value itself). All other values are converted to numbers including NaN and Infinity. When converting a value to a number, any objects and arrays are converted to NaN.
numbers()
The numbers() method is utilized internally by statistical methods such as sum(), avg(), and count(). As such, it can be used to reveal the numbers that are being used to compute the result of these methods.
sum()
avg()
[1, NaN, false, true, '123', { foo: 1 }, [5]].numbers() // Result: [1, NaN, 0, 1, 123, NaN, NaN]
[1, 2, NaN, false, true, '123'].numbers() // Result: [1, 2, NaN, 0, 1, 123]
[{ age: 10 }, {}, { age: 20 }, null, { age: 10 }].numbers(=> age) // Result: [10, 20, 10]
Note: When applying a statistical computation to an array of objects, it is recommended to use a custom getter with the method rather than using dot notation or mapping. This is because dot notation and mapping ignore duplicate values. For instance, the query […].age.numbers() might return [10, 20] for the last example instead of the expected [10, 20, 10], which would be correctly returned by the query […].numbers(=> age).
[…].age.numbers()
[10, 20]
[10, 20, 10]
[…].numbers(=> age)
Alias for percentile() method.
percentile()
This function computes the percentile of values in an array. It returns undefined if the input is an empty array, not an array, or if the k parameter is not specified or falls outside the range of [0..100]. The function utilizes the same numbers as the numbers() method, given the same getter parameter. If the input (after processing through getter) contains a NaN, the function will always return NaN.
k
[0..100]
[4, 3, 5, 2, 1].percentile(75) // Result: 4
[4, 3, 5, 6, 2, 1].percentile(20) // Result: 2
[4, 3, 1].percentile() // k is not specified // Result: undefined
[4, 3, NaN, 1].percentile(50) // Result: NaN
Using custom getter:
[{ a: 1 }, { a: 3 }, undefined, { a: 2 }].percentile(75, => a) // Result: 2.5
Get a value by a key, index, or function. The method repeats behaviour of Bracket notation, i.e. expr.pick(…) is the same as expr[…].
expr.pick(…)
expr[…]
[1, 2, 3, 4].pick(2) // Result: 3
{ foo: 1, bar: 2 }.pick('bar') // Result: 2
The same as Array#reduce() in JS. Use $$ to access the accumulator and $ for the current value, e.g., find the max value:
Array#reduce()
[1, 5, 2, 3, 4].reduce(=>$ > $$ ? $ : $$) // Result: 5
The same as String#replaceAll() in JavaScript, but also works for arrays. When pattern is RegExp, a g flags adds automatically if omitted. When applying to arrays it's similar to .($ = pattern ? replacement : $), but without dropping duplicate values and inlining arrays.
String#replaceAll()
.($ = pattern ? replacement : $)
'abc123def123xyz'.replace('123', '_') // Result: "abc_def_xyz"
'abc123def45xyz'.replace(/[^\d]/, '_') // Result: "___123___45___"
'2023-07-14'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3-$2-$1') // Result: "14-07-2023"
'a 123 ... b 45'.replace( /([a-z]+)\s+(?<numbers>\d+)/, => `[numbers: ${groups.numbers} for '${matched[1]}']` ) // Result: "[numbers: 123 for 'a'] ... [numbers: 45 for 'b']"
[1, 2, 3, 3, 2, 1].replace(2, null) // Result: [1, null, 3, 3, null, 1]
Reverse order of elements in an array. Unlike JavaScript, doesn't modify input array but produce a new copy of it with the change (like Array#toReversed()). For any values other than an array, returns the input value.
Array#toReversed()
[1, 2, 5, 3].reverse() // Result: [3, 5, 2, 1]
'hello world'.reverse() // Result: 'hello world'
Returns count of entries in an object, otherwise returns length property value or 0 when the field is absent.
length
{ a: 42, b: 123 }.size() // Result: 2
[1, 2, 3, 4].size() // Result: 4
"Hello world".size() // Result: 11
123.size() // Result: 0
The same as Array#slice() and String#slice() in JavaScript (see also Slice notation).
Array#slice()
String#slice()
Sort an array by a value fetched with getter (fn). Can use sorting function definition syntax with asc and desc (see Sorting)
fn
The same as String#split() in JavaScript. pattern may be a string or regex.
String#split()
Returns the standard deviation (𝜎) of a population, is the square root of the variance.
𝜎
[2, 4, 6, 4].stdev() // Result: 1.4142135623730951
[{ a: 2 }, {}, undefined, { a: 4 }].stdev(=> a) // Result: 1
Computes the sum of the values in an array. It returns undefined for non-array values and empty arrays. The method uses the same numbers as numbers() method with the same getter parameter returns. The method employs the Kahan–Babuška summation algorithm to minimize numerical errors in the result.
[].sum() // Result: undefined
[1, 2, 3, 4].sum() // Result: 10
[1, 2, undefined, null, '3', 4].sum() // Result: 10
[1, 2, NaN, 4].sum() // Result: NaN
[0.1, 0.2, 0.3].sum() // Result: 0.6
Note: The sum() method returns 0.6 instead of 0.6000000000000001, which is the result of the expression 0.1 + 0.2 + 0.3. This is because the Kahan–Babuška summation algorithm is used to reduce numerical error.
0.6
0.6000000000000001
0.1 + 0.2 + 0.3
Using a custom getter function:
[1, 2, 3, 4].sum(=> $ * $) // Sum of number squares // Result: 30
[{ age: 10 }, {}, { age: 20 }, null, { age: 10 }].sum(=> age) // Result: 40
Since arrays are always converting to NaN. To summing array of arrays, a summation of sums should be used:
[[1, 2], [], [4]].sum() // Result: NaN
[[1, 2], [], null, [4], undefined].sum(=> sum()) // Result: 7
The same as String#toLocaleLowerCase() in JavaScript.
String#toLocaleLowerCase()
'Hello World!'.toLowerCase() // Result: "hello world!"
The same as String#toLocaleUpperCase() in JavaScript.
String#toLocaleUpperCase()
'Hello World!'.toUpperCase() // Result: "HELLO WORLD!"
The same as String#trim() in JavaScript.
String#trim()
' something in the middle '.trim() // Result: "something in the middle"
The same as Object.values() in JavaScript.
Object.values()
Returns the variance (𝜎²) of a population (the squared deviation from the mean).
𝜎²
[2, 4, 6, 4].variance() // Result: 2
[{ a: 2 }, {}, undefined, { a: 4 }].variance(=> a) // Result: 1
JavaScript's Math methods.
Math
Modifications from the standard JavaScript Math object include:
log()
log1p()
ln()
ln1p()
random()
Note: Keep in mind that the unary - operator has lower precedence than other operators. To apply a method to a negative scalar number, use the grouping operator, the pipeline operator, or store the number in a variable and then apply the method to it. For example, instead of -123.abs(), which is interpreted as -(123.abs()), you should use one of the following: (-123).abs() -123 | abs() $num = -123; $num.abs()
Note: Keep in mind that the unary - operator has lower precedence than other operators. To apply a method to a negative scalar number, use the grouping operator, the pipeline operator, or store the number in a variable and then apply the method to it. For example, instead of -123.abs(), which is interpreted as -(123.abs()), you should use one of the following:
-123.abs()
-(123.abs())
(-123).abs()
-123 | abs()
$num = -123; $num.abs()
Returns the absolute value of a number.
-123 | abs() // Result: 123
'hello world'.abs() // Result: NaN
Returns the arccosine of a number.
(-1).acos() // Result: 3.141592653589793
1.acos() // Result: 0
'hello world'.acos() // Result: NaN
Returns the hyperbolic arccosine of a number.
Returns the arcsine of a number.
Returns the hyperbolic arcsine of a number.
Returns the arctangent of a number.
Returns the arctangent of the quotient of its arguments, i.e. the angle in the plane (in radians) between the positive x-axis and the ray from (0, 0) to the point (x, y), for y.atan2(x).
(0, 0)
(x, y)
y.atan2(x)
Returns the hyperbolic arctangent of a number.
Returns the cube root of a number.
64.cbrt() // Result: 4
Returns the smallest integer greater than or equal to a number.
3.123.ceil() // Result: 4
Returns the number of leading zero bits of the 32-bit integer of a number.
Returns the cosine of a number.
Returns the hyperbolic cosine of a number.
Returns ex, where x is the argument, and e is Euler's number (2.718…, the base of the natural logarithm).
ex
e
2.exp() // Result: 7.38905609893065
(-1).exp() // Result: 0.36787944117144233
Returns subtracting 1 from exp(x), i.e. ex - 1.
exp(x)
ex - 1
2.expm1() // Result: 6.38905609893065
(-1).expm1() // Result: -0.6321205588285577
Returns the largest integer less than or equal to a number.
3.123.floor() // Result: 3
Returns the nearest 32-bit single precision float representation of a number.
5.5.fround() // Result: 5.5
5.05.fround() // Result: 5.050000190734863
Returns the square root of the sum of squares of its arguments.
FIXME: Must take an array of numbers like sum()
[3].hypot(4, 5) // Result: 7.0710678118654755
Returns the result of the C-like 32-bit integer multiplication of the two parameters.
3.imul(4) // Result: 12
0xffffffff.imul(5) // Result: -5
Returns the natural logarithm (loge or ln) of a number.
loge
ln
// 2^3 = 8 8.ln() / 2.ln() // Result: 3
Returns the base-10 logarithm of a number, i.e. log10(x).
log10(x)
2.log10() // Result: 0.3010299956639812
Returns the natural logarithm (loge or ln) of 1 + x for the number x.
1 + x
1.ln1p() // Result: 0.6931471805599453
Returns the base-2 logarithm of a number, i.e. log2(x).
log2(x)
2.log2() // Result: 1
Returns base x to the exponent power y, i.e. xy
xy
2.pow(3) // Result: 8
25.pow(0.5) // Result: 5
Returns the value of a number rounded to the nearest integer.
5.2.round() // Result: 5
5.5.round() // Result: 6
5.9.round() // Result: 6
Returns 1 or -1, indicating the sign of the number passed as argument. If the input is 0 or -0, it will be returned as-is.
-0
5.sign() // Result: 1
-42 | sign() // Result: -1
0.sign() // Result: 0
Returns the sine of a number.
Returns the hyperbolic sine of a number.
Returns the positive square root of a number.
25.sqrt() // Result: 5
Returns the tangent of a number.
Returns the hyperbolic tangent of a number.
Returns the integer part of a number by removing any fractional digits. It truncates (cuts off) the dot and the digits to the right of it, no matter whether the argument is a positive or negative number.
42.84.trunc() // Result: 42
-123.9 | trunc() // Result: -123
The mapping in Jora allows you to create a new array by transforming the elements of the given array with a provided function. This is achieved using the .(…) syntax or map() method.
Jora's mapping works not only with arrays but also with primitive types and objects. Note that the map method produces unique values, meaning that the resulting array might have a smaller length than the original array. If an expression returns an array, its result is concatenated with the overall result ignoring undefined values, possibly leading to a larger resulting array than the original.
.(expr)
Using map() method:
.map(fn) // or .map(=> expr)
Pick property values:
$input: [ { "baz": 1 }, { "baz": 2 }, { "baz": 3 } ]; $input.(baz) // Result: [ 1, 2, 3 ]
Alternatives for the query above: $input.map(=> baz) $input.baz
Alternatives for the query above:
$input.map(=> baz)
$input.baz
Rename property:
$input: [ { "a": 1 }, { "a": 2 }, { "a": 3 } ]; $input.({ answer: a }) // Result: // [ // { "answer": 1 }, // { "answer": 2 }, // { "answer": 3 } // ]
Pick object properties:
$input: [ { "foo": "bar", "baz": 1 }, { "foo": "bar", "baz": 2 }, { "foo": "bar", "baz": 3 } ]; $input.({ baz }) // Result: // [ // { "baz": 1 }, // { "baz": 2 }, // { "baz": 3 } // ]
The mapping can be applied to a primitive value (numbers, strings, etc.):
123.({ foo: $ }) // Result: { "foo": 123 }
Note: In the above example, $ references the current value.
Copying over the object with spread and computing additional properties:
{ "foo": 41 }.({ ..., answer: foo + 1 }) // Result: { "foo": 41, "answer": 42 }
{ "foo": 41 }.map(=> { ..., answer: foo + 1 }) // Result: { "foo": 41, "answer": 42 }
When using the mapping in Jora, it automatically returns unique values in the resulting array, which can lead to a smaller output array than the input array.
[ 1, 2, 2, 3, 3, 3 ].() // .() is equivalent to .($) // Result: [1, 2, 3]
If an expression of mapping returns an array, the resulting array will be concatenated with the overall result with dropping of duplicate values. This may lead to a larger output array than the input array.
Input
$input: [ { "values": [1, 2, 3] }, { "values": [3, 4] } ]; $input.(values) // Result: [ 1, 2, 3, 4 ]
The mapping in Jora automatically ignores undefined values when processing arrays. This feature can be useful when you want to filter out undefined values from the result while mapping an array of objects where some objects do not have a specified property.
In a simple array:
[ 1, undefined, 3 ].($) // Result: [ 1, 3 ]
In an array of objects:
[ { "a": 1 }, { }, { "a": 3 } ].(a) // Result: [ 1, 3 ]
In an array of nested objects:
$input: [ { "a": { "nested": 1 } }, { }, { "a": 3 } ]; $input.(a.nested) // Result: [ 1 ]
In the above examples, we can see how Jora's map method handles undefined values, effectively filtering them out of the output while preserving the values that are not undefined.
In some cases, you might want to preserve the same number of elements in the output array as in the input array. You can use a simple workaround by wrapping the result of the map method into an object. Let's consider an example:
[ 1, 2, 2, 3, 3, 3 ].({ value: $ }) // Result: // [ // { "value": 1 }, // { "value": 2 }, // { "value": 2 }, // { "value": 3 }, // { "value": 3 }, // { "value": 3 } // ]
In this example, we wrap the result of the map method into an object with a value property, which results in an output array with the same number of elements as the input array.
In general, .(…) is the preferred syntax because it is more concise. However, the map() method exists to allow mapping with a given function, for instance via a context (#) or defined in the query.
$myMapper: => { value: $ * 2 }; [1, 2, 3].map($myMapper) // Result: [{ value: 2 }, { value: 4 }, { value: 6 }]
In this case, the choice between .(…) and map() depends on the specific use case and the desired level of readability and flexibility. Both syntaxes can be used interchangeably for mapping purposes, equivalence of syntaxes:
.map(fn)
.map(=> expr)
Jora supports recursive mapping through its ..() syntax. This feature facilitates the traversal of nested data structures. Recursive mapping iteratively maps input values, appends unique mapped values to the result, and applies the mapping to these new values. This process continues until all values in the result are successfully mapped.
..()
..(expr)
For single property getter expressions, the parentheses are optional:
..property
The recursive mapping process involves the following steps:
Set
Consider the following JSON object representing a directory structure:
{ "name": "root", "type": "directory", "children": [ { "name": "folder1", "type": "directory", "children": [ { "name": "file1.txt", "type": "file" }, { "name": "file2.txt", "type": "file" } ] }, { "name": "folder2", "type": "directory", "children": [ { "name": "file3.txt", "type": "file" } ] } ] }
To extract all the objects from this dataset as a list, use recursive mapping:
..children // Result: // [ // { name: "folder1", type: "directory", children: [{…}, {…}] }, // { name: "folder2", type: "directory", children: [{…}] }, // { name: "file1.txt", type: "file" }, // { name: "file2.txt", type: "file" }, // { name: "file3.txt", type: "file" } // ]
Note that the original input is not included in the result, only the result of its mapping. To include the input values in the result, use explicit concatenation:
$ + ..children // Result: // [ // { name: "root", type: "directory", children: [{…}, {…}] }, // { name: "folder1", type: "directory", children: [{…}, {…}] }, // { name: "folder2", type: "directory", children: [{…}] }, // { name: "file1.txt", type: "file" }, // { name: "file2.txt", type: "file" }, // { name: "file3.txt", type: "file" } // ]
To apply additional operations to the result, wrap the concatenation in parentheses:
($ + ..children).name // Input[1] // Result: ["root", "folder1", "folder2", "file1.txt", "file2.txt", "file3.txt"]
The pipeline operator can also be used for the same result:
$ + ..children | name // Input[1] // Result: ["root", "folder1", "folder2", "file1.txt", "file2.txt", "file3.txt"]
Ensure that operations like filtering are performed after the recursive mapping is complete, to avoid missing some results. The following example returns only the names of files:
$ + ..children | .[type = "file"].name // Input[1] // Result: ["file1.txt", "file2.txt", "file3.txt"]
Recursive mapping adheres to the same rules as regular mapping:
Although recursive mapping is a powerful tool, it can potentially lead to an infinite loop (endless recursion). To circumvent an infinite loop, avoid creating new objects and arrays. Instead, apply transformation operations to the result of the recursive mapping.
{ example: 1 }..({ example: 1 }) // This leads to infinite recursion
To stop new values from being added to the result, use an empty array. The following example generates a series of numbers from 2 to 5:
1..($ < 5 ? $ + 1 : []) // Result: [2, 3, 4, 5]
In Jora, filtering allows to extract specific elements from an array based on a condition. This is achieved using the .[…] syntax or filter() method. Filtering returns an element in the result if the condition inside the filter evaluates to a truthy value. If the condition evaluates to a falsy value, the element will be excluded from the result.
Note: In Jora, empty arrays and objects with no entries are considered falsy.
.[block]
Using filter() method:
.filter(fn) // or .filter(=> expr)
Filtering an array of numbers:
[1, 2, 3, 4, 5].[$ >= 3] // Result: [3, 4, 5]
An alternative:
[1, 2, 3, 4, 5].filter(=> $ >= 3) // Result: [3, 4, 5]
Filtering an array of objects:
$input: [ { "title": "Book 1", "price": 5 }, { "title": "Book 2", "price": 15 }, { "title": "Book 3", "price": 7 }, { "title": "Book 4", "price": 12 } ]; $input.[price <= 10] // Result: // [ // { "title": "Book 1", "price": 5 }, // { "title": "Book 3", "price": 7 } // ]
Filtering an array of objects using a nested property:
$input: [ { "id": 1, "data": { "value": 42 } }, { "id": 2, "data": { "value": 17 } }, { "id": 3, "data": { "value": 99 } } ]; $input.[data.value > 20] // Result: // [ // { "id": 1, "data": { "value": 42 } }, // { "id": 3, "data": { "value": 99 } } // ]
In general, .[…] is the preferred syntax because it is more concise. However, the filter() method exists to allow filtering with a given function, for instance via a context (#) or defined in the query.
$myFilter: => data.value > 20; .filter($myFilter)
In this case, the choice between .[…] and filter() depends on the specific use case and the desired level of readability and flexibility. Both syntaxes can be used interchangeably for filtering purposes, equivalence of syntaxes:
.filter(fn)
.[expr]
.filter(=> expr)
The group() method allows to group elements of arrays based on specified properties or computed values. The method returns an array of objects containing a key and a value property. The key represents the grouping criterion, and the value is an array of unique input elements associated with that key. Note that keys can be any value, even an object.
The group() method takes parameters:
keyGetter
valueGetter
.group(keyFunction[, valueFunction])
Suppose you have an array of objects representing sales data, and you want to group the data by the region property while keeping only the sales values in the resulting groups.
region
[ { "region": "North", "sales": 100 }, { "region": "South", "sales": 200 }, { "region": "East", "sales": 150 }, { "region": "North", "sales": 300 }, { "region": "South", "sales": 250 } ]
Query
.group(=> region, => sales)
.group(=> region).({ key, value: value.(sales) })
Output
[ { "key": "North", "value": [100, 300] }, { "key": "South", "value": [200, 250] }, { "key": "East", "value": [150] } ]
You can also group data based on a computed property. In this example, we'll group an array of numbers into even and odd groups.
[ 1, 2, 3, 4, 5, 6 ]
.group(=> $ % 2 ? 'odd' : 'even')
[ { "key": "odd", "value": [1, 3, 5] }, { "key": "even", "value": [2, 4, 6] } ]
Suppose you have an array of objects representing products with multiple tags. You want to group products by each tag.
[ { "name": "Product A", "tags": ["Electronics", "Gadgets"] }, { "name": "Product B", "tags": ["Electronics", "Computers"] }, { "name": "Product C", "tags": ["Gadgets"] }, { "name": "Product D", "tags": ["Computers"] } ]
.group(=> tags)
[ { "key": "Electronics", "value": [ { "name": "Product A", "tags": ["Electronics", "Gadgets"] }, { "name": "Product B", "tags": ["Electronics", "Computers"] } ] }, { "key": "Gadgets", "value": [ { "name": "Product A", "tags": ["Electronics", "Gadgets"] }, { "name": "Product C", "tags": ["Gadgets"] } ] }, { "key": "Computers", "value": [ { "name": "Product B", "tags": ["Electronics", "Computers"] }, { "name": "Product D", "tags": ["Computers"] } ] } ]
Suppose you have an array of objects representing sales data with different currencies. You want to group sales data by the currency object.
const USD = { "code": "USD", "symbol": "$" }; const EUR = { "code": "EUR", "symbol": "€" }; const data = [ { "amount": 100, "currency": USD }, { "amount": 150, "currency": USD }, { "amount": 200, "currency": EUR }, { "amount": 250, "currency": EUR } ];
.group(=> currency)
[ { "key": { "code": "USD", "symbol": "$" }, "value": [ { "amount": 100, "currency": { "code": "USD", "symbol": "$" } }, { "amount": 150, "currency": { "code": "USD", "symbol": "$" } } ] }, { "key": { "code": "EUR", "symbol": "€" }, "value": [ { "amount": 200, "currency": { "code": "EUR", "symbol": "€" } }, { "amount": 250, "currency": { "code": "EUR", "symbol": "€" } } ] } ]
The group() method ensures that the elements in the value array are unique.
const a = { "category": "Electronics", "name": "Product A" }; const b = { "category": "Electronics", "name": "Product B" }; const c = { "category": "Gadgets", "name": "Product C" }; const input = [a, b, b, c, a];
.group(=> category)
[ { "key": "Electronics", "value": [ { "category": "Electronics", "name": "Product A" }, { "category": "Electronics", "name": "Product B" } ] }, { "key": "Gadgets", "value": [ { "category": "Gadgets", "name": "Product B" } ] } ]
Suppose you have an array of objects representing sales data, and you want to count the number of sales per region.
.group(=> region) .({ region: key, salesCount: value.size(), totalSales: value.reduce(=> $$ + sales, 0) // sum of sales })
[ { "region": "North", "salesCount": 2, "totalSales": 400 }, { "region": "South", "salesCount": 2, "totalSales": 450 }, { "region": "East", "salesCount": 1, "totalSales": 150 } ]
fromEntries() is a convenient method to convert an array of objects with { key, value } structure into an object. As the group() method returns an array of such objects, you can use fromEntries() directly to transform the grouped result into an object.
Suppose you have an array of objects representing sales data, and you want to group the data by region.
.group(=> region) .fromEntries()
{ "North": [ { "region": "North", "sales": 100 }, { "region": "North", "sales": 300 } ], "South": [ { "region": "South", "sales": 200 }, { "region": "South", "sales": 250 } ], "East": [ { "region": "East", "sales": 150 } ] }
Jora provides a powerful and flexible syntax for sorting data. The language allows you to define comparator functions using a concise syntax that specifies the sorting order (asc for ascending, desc for descending) and the properties to be sorted.
In Jora, a sorting function (comparator function) can be defined in several ways. These functions take two arguments and compare the query result for each in the specified order (asc or desc). Here are some examples:
expr asc // JS equivalent: (a, b) => expr(a) > expr(b) ? 1 : expr(a) < expr(b) ? -1 : 0
expr desc // JS equivalent: (a, b) => expr(a) < expr(b) ? 1 : expr(a) > expr(b) ? -1 : 0
foo asc, bar desc // JS equivalent: (a, b) => // a.foo > b.foo ? 1 : a.foo < b.foo ? -1 : // a.bar < b.bar ? 1 : a.bar > b.bar ? -1 : // 0
There are some modifiers for asc and desc that provide additional sorting options:
Suppose we have an array of objects representing products:
$products: [ { name: "Laptop", price: 1000 }, { name: "Smartphone", price: 800 }, { name: "Tablet", price: 600 } ];
We can sort the products by price in ascending order:
$products.sort(price asc)
Or in descending order:
$products.sort(price desc)
We can also sort by multiple properties. For example, suppose we have an array of objects representing users with a name and age property:
age
[ { "name": "Alice", "age": 30 }, { "name": "Bob", "age": 25 }, { "name": "Charlie", "age": 30 } ]
We can sort the users first by age in ascending order and then by name in descending order:
sort(age asc, name desc) // Input[1] // Result: [ // { "name": "Bob", "age": 25 }, // { "name": "Charlie", "age": 30 }, // { "name": "Alice", "age": 30 } // ]
Unlike JavaScript, Jora's sort() method does not modify the input array. Instead, it creates a new array with the ordered elements. Regular functions can also be used with the sort() method. Such functions should return a value that will be used for comparison, e.g. sort(=> name). Jora will use this function in the following way: fn(a) > fn(b) ? 1 : fn(a) < fn(b) ? -1 : 0. If a function in the sort() method returns an array, Jora compares array lengths first (in ascending order), then compares elements one by one (if lengths of arrays are equal). For example, sort(=> [name, age]) is equivalent to name asc, age asc.
sort(=> name)
fn(a) > fn(b) ? 1 : fn(a) < fn(b) ? -1 : 0
sort(=> [name, age])
name asc, age asc
Using functions instead of expr asc or expr desc is less powerful since you cannot specify the direction of sorting or other modifications like natural sorting. Sorting is always in ascending order. However, you can use the reverse() method afterward to achieve descending order, though it's less performant and produces two new arrays instead of one.
expr asc
expr desc
reverse()
Sorting functions created with asc / desc keywords can be stored in a variable for further usage, e.g., $orderByName: name asc; $orderByAgeAndName: age desc, name asc;.
$orderByName: name asc; $orderByAgeAndName: age desc, name asc;
This query calculates the average age of people grouped by their occupation, sorts the results by the average age in descending order, and returns an array of objects containing the occupation and the average age.
Assuming the data is structured like this:
{ people: [ { name: 'Alice', age: 34, occupation: 'Engineer' }, { name: 'Bob', age: 42, occupation: 'Doctor' }, { name: 'Charlie', age: 28, occupation: 'Engineer' }, { name: 'David', age: 50, occupation: 'Doctor' }, { name: 'Eve', age: 23, occupation: 'Student' } ] }
The Jora query would be:
people .group(=> occupation) .({ occupation: key, averageAge: value.avg(=> age) }) .sort(averageAge desc)
This query will return the following output:
[ { occupation: 'Doctor', averageAge: 46 }, { occupation: 'Engineer', averageAge: 31 }, { occupation: 'Student', averageAge: 23 } ]
Jora query that demonstrates calculating the percentage of people in each occupation who have a specific skill.
Let's assume the data structure is as follows:
const data = { people: [ { name: 'Alice', age: 30, occupation: 'Doctor', skills: ['A', 'B', 'C'] }, { name: 'Bob', age: 35, occupation: 'Doctor', skills: ['A', 'D'] }, { name: 'Charlie', age: 28, occupation: 'Engineer', skills: ['A', 'B'] }, { name: 'David', age: 26, occupation: 'Engineer', skills: ['C', 'D'] }, { name: 'Eva', age: 23, occupation: 'Student', skills: ['A', 'B', 'C'] }, { name: 'Frank', age: 22, occupation: 'Student', skills: ['D', 'E'] } ] };
people .group(=> occupation) .({ $skillCount: value.count(=> skills has #.skill?); $totalCount: value.size(); occupation: key, skill: #.skill, $skillCount, $totalCount, skillPercentage: $skillCount / $totalCount * 100 }) .sort(skillPercentage desc)
Given the data and context, this query will produce the following output:
// jora('...query...')(data, { skill: 'A' }) [ { occupation: 'Doctor', skill: 'A', skillCount: 2, totalCount: 2, skillPercentage: 100 }, { occupation: 'Engineer', skill: 'A', skillCount: 1, totalCount: 2, skillPercentage: 50 }, { occupation: 'Student', skill: 'A', skillCount: 1, totalCount: 2, skillPercentage: 50 } ]
The Jora query takes an input data object containing books, authors, tags, and reviews, and returns a list of books that match the specified tag filter. For each book, the query maps the title, author's name, list of tags, and the top review (based on rating and date). The top review text is truncated to 150 characters.
$authors; $tags; $reviews; books .({ $bookId: id; $authorId; $author: $authors[=> id = $authorId]; $tagIds; title, author: $author.name, tags: $tags.[id in $tagIds].name, topReview: $reviews .[bookId = $bookId] .min(rating desc, date desc) | { rating, text: `${text[0:150]}...` } }) .[tags has #.tagFilter] .sort(topReview.rating desc, title asc)
TypeScript definitions:
type InputData = { books: Book[]; authors: Author[]; tags: Tag[]; reviews: Review[]; }; type Author = { id: number; name: string; }; type Tag = { id: number; name: string; }; type Book = { id: number; title: string; authorId: number; tagIds: number[]; }; type Review = { bookId: number; rating: number; text: string; date: Date; };
Data example:
{ "books": [ { "id": 1, "title": "The Great Book", "authorId": 101, "tagIds": [201, 202] }, { "id": 2, "title": "A Fantastic Read", "authorId": 102, "tagIds": [202, 203] } ], "authors": [ { "id": 101, "name": "John Doe" }, { "id": 102, "name": "Jane Smith" } ], "tags": [ { "id": 201, "name": "Fiction" }, { "id": 202, "name": "Thriller" }, { "id": 203, "name": "Mystery" } ], "reviews": [ { "bookId": 1, "rating": 5, "text": "An amazing book! I loved every moment of it.", "date": "2023-01-01T00:00:00.000Z" }, { "bookId": 2, "rating": 4, "text": "A captivating story with great characters.", "date": "2023-01-15T00:00:00.000Z" } ] }
type QueryResult = ResultBook[]; type ResultBook = { title: string; author: string; tags: string[]; topReview: { rating: number; text: string; }; };
jora('...query...')(inputData, { tagFilter: 'Thriller' })
[ { "title": "The Great Book", "author": "John Doe", "tags": ["Fiction", "Thriller"], "topReview": { "rating": 5, "text": "An amazing book! I loved every moment of it...." } }, { "title": "A Fantastic Read", "author": "Jane Smith", "tags": ["Thriller", "Mystery"], "topReview": { "rating": 4, "text": "A captivating story with great characters...." } } ]
function getMappedBooks(inputData, tagFilter) { const { books, authors, tags, reviews } = inputData; const filteredBooks = books .map(book => { const author = authors.find(author => author.id === book.authorId); const bookTags = tags.filter(tag => book.tagIds.includes(tag.id)); const bookReviews = reviews.filter(review => review.bookId === book.id); const sortedReviews = bookReviews.sort((a, b) => { const ratingDiff = b.rating - a.rating; if (ratingDiff !== 0) { return ratingDiff; } return new Date(b.date) - new Date(a.date); }); const topReview = sortedReviews[0] && { rating: sortedReviews[0].rating, text: `${sortedReviews[0].text.slice(0, 150)}...` }; return { title: book.title, author: author.name, tags: bookTags.map(tag => tag.name), topReview: topReview }; }) .filter(mappedBook => tagFilter.some(tag => mappedBook.tags.includes(tag))) .sort((a, b) => { const ratingDiff = b.topReview.rating - a.topReview.rating; if (ratingDiff !== 0) { return ratingDiff; } return a.title.localeCompare(b.title); }); return filteredBooks; }
.books | map({ title: .title, author: (.authorId as $aid | .authors[] | select(.id == $aid).name), tags: (.tagIds | map(. as $tid | .tags[] | select(.id == $tid).name)), topReview: ( .id as $bid | .reviews | map(select(.bookId == $bid)) | sort_by(-.rating, .date) | .[0] | { rating: .rating, text: (.text | .[0:150] + "...") } ) }) | map(select(.tags | any(. as $t | .[] == $t))) | sort_by(.topReview.rating, .title)
How the query works:
$authors
$tags
$reviews
title
author
tags
topReview
This query generates a summary of events, grouped by month, including the total number of events and the count of unique users who participated in those events.
events .({ $userId; $user: @.users[=> id = $userId]; eventType, eventDate: timestamp, eventMonth: timestamp[0:7], userName: $user.name, userEmail: $user.email }) .group(=> eventMonth) .({ month: key, totalEvents: value.size(), uniqueUsers: value.userName.size() }) .sort(month asc)
type QueryInput = Event[]; type Event = { eventName: string; eventDetails: string; userId: number; timestamp: string; }; type User = { id: number; name: string; email: string; };
{ "events": [ { "eventName": "Workshop", "eventDetails": "Introduction to Programming", "userId": 1, "timestamp": "2023-01-15T14:00:00Z" }, { "eventName": "Conference", "eventDetails": "Tech Summit", "userId": 2, "timestamp": "2023-01-20T09:00:00Z" }, { "eventName": "Webinar", "eventDetails": "Web Development Basics", "userId": 1, "timestamp": "2023-02-05T18:00:00Z" }, { "eventName": "Workshop", "eventDetails": "Advanced Programming Techniques", "userId": 3, "timestamp": "2023-02-25T14:00:00Z" } ], "users": [ { "id": 1, "name": "Alice", "email": "alice@example.com" }, { "id": 2, "name": "Bob", "email": "bob@example.com" }, { "id": 3, "name": "Carol", "email": "carol@example.com" } ] }
[ { "month": "2023-01", "totalEvents": 2, "uniqueUsers": 2 }, { "month": "2023-02", "totalEvents": 2, "uniqueUsers": 2 } ]
function processEvents(events, users) { const eventsWithUserDetails = events.map(event => { const userId = event.userId; const user = users.find(user => user.id === userId); return { eventType: event.eventName, eventDate: event.timestamp, eventMonth: event.timestamp.slice(0, 7), userName: user.name, userEmail: user.email }; }); const groupedEvents = eventsWithUserDetails.reduce((acc, event) => { const month = event.eventMonth; if (!acc[month]) { acc[month] = []; } acc[month].push(event); return acc; }, {}); const result = Object.entries(groupedEvents).map(([month, events]) => { const uniqueUsers = new Set(events.map(event => event.userName)).size; return { month, totalEvents: events.length, uniqueUsers }; }); result.sort((a, b) => a.month.localeCompare(b.month)); return result; }
[ .events[] as $event | { userId: $event.userId, user: (.users[] | select(.id == $event.userId)), eventType: $event.eventName, eventDate: $event.timestamp, eventMonth: ($event.timestamp[0:7]) } ] | group_by(.eventMonth) | map({ month: .[0].eventMonth, totalEvents: length, uniqueUsers: (reduce .[].user.id as $id ({}; .[$id] |= . + 1) | length) }) | sort_by(.month)
The query takes an input dataset containing information about events and users. The events dataset has information about each event's eventName, eventDetails, userId, and timestamp. The users dataset has information about each user's id, name, and email.
eventName
eventDetails
userId
timestamp
The structure of the query is as follows:
events
eventType
eventDate
eventMonth
userName
userEmail
month
totalEvents
uniqueUsers
The output of the query is a list of objects representing a summary of events per month, including the total number of events and unique users who participated in those events.
A jora query that calculates the average rating for each product category and sorts the categories by the average rating.
products .group(=> category) .({ category: key, averageRating: value.avg(=> ratings.avg()), productCount: value.size() }) .sort(averageRating desc)
Data structure (TypeScript types):
type InputData = { products: Product[]; }; type Product = { id: string; name: string; category: string; ratings: number[]; };
JSON:
{ "products": [ { "id": "1", "name": "Product A", "category": "Electronics", "ratings": [4, 5, 4] }, { "id": "2", "name": "Product B", "category": "Electronics", "ratings": [4, 5, 5] }, { "id": "3", "name": "Product C", "category": "Books", "ratings": [3, 4, 3] }, { "id": "4", "name": "Product D", "category": "Books", "ratings": [4, 2, 2] }, { "id": "5", "name": "Product E", "category": "Clothing", "ratings": [4, 4, 5] } ] }
[ { "category": "Electronics", "averageRating": 4.5, "productCount": 2 }, { "category": "Clothing", "averageRating": 4.333333333333333, "productCount": 1 }, { "category": "Books", "averageRating": 3, "productCount": 2 } ]
function calculateAverageRatings(inputData) { const products = inputData.products; const categoryGroups = {}; products.forEach(product => { const category = product.category; if (!categoryGroups.hasOwnProperty(category)) { categoryGroups[category] = { ratingsSum: 0, ratingsCount: 0, productCount: 0 }; } const ratingsSum = product.ratings.reduce((a, b) => a + b, 0); const ratingsCount = product.ratings.length; categoryGroups[category].ratingsSum += ratingsSum; categoryGroups[category].ratingsCount += ratingsCount; categoryGroups[category].productCount++; }); const results = Object.entries(categoryGroups).map(([category, data]) => ({ category, averageRating: data.ratingsSum / data.ratingsCount, productCount: data.productCount })); results.sort((a, b) => b.averageRating - a.averageRating); return results; }
.products | group_by(.category) | map( { category: .[0].category, averageRating: (map(.ratings | add) | add) / (map(.ratings | length) | add), productCount: length } ) | sort_by(-.averageRating)
This Jora query performs the following operations:
category
$averageRating
productCount
The resulting output will be an array of objects containing the category, average rating, and product count, sorted by the average rating.
The following query groups products by the count of popular tags they match and sorts the groups in descending order.
$popularTags: products .group(=> tags) .sort(value.size() desc) .key[0:5]; products .({ ..., popularTagsMatchCount: tags.[$ in $popularTags].size() }) .sort(popularTagsMatchCount desc, category asc, price asc) .group(=> popularTagsMatchCount) .({ popularTagsCount: key, products: value.({ name, category, price }) })
type InputData = { products: Product[]; }; type Product = { id: number; name: string; category: string; price: number; tags: string[]; };
Data:
{ "products": [ { "id": 1, "name": "Product A", "category": "Electronics", "price": 200, "tags": ["trending", "smart", "wireless"] }, { "id": 2, "name": "Product B", "category": "Electronics", "price": 150, "tags": ["smart", "wireless"] }, { "id": 3, "name": "Product C", "category": "Clothing", "price": 50, "tags": ["trending", "fashion"] }, { "id": 4, "name": "Product D", "category": "Clothing", "price": 80, "tags": ["fashion", "casual"] }, { "id": 5, "name": "Product E", "category": "Electronics", "price": 100, "tags": ["trending", "smart"] } ] }
[ { "popularTagsCount": 3, "products": [ { "name": "Product A", "category": "Electronics", "price": 200 } ] }, { "popularTagsCount": 2, "products": [ { "name": "Product B", "category": "Electronics", "price": 150 }, { "name": "Product E", "category": "Electronics", "price": 100 }, { "name": "Product C", "category": "Clothing", "price": 50 } ] }, { "popularTagsCount": 1, "products": [ { "name": "Product D", "category": "Clothing", "price": 80 } ] } ]
function getProductsSortedByPopularTags(data) { const popularTags = data.products .flatMap(product => product.tags) .reduce((acc, tag) => { acc[tag] = (acc[tag] || 0) + 1; return acc; }, {}) .entries() .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(entry => entry[0]); const productsWithPopularTagsCount = data.products.map(product => { const popularTagsMatchCount = product.tags.filter(tag => popularTags.includes(tag)).length; return { ...product, popularTagsMatchCount }; }); const sortedProducts = productsWithPopularTagsCount.sort((a, b) => { if (b.popularTagsMatchCount !== a.popularTagsMatchCount) { return b.popularTagsMatchCount - a.popularTagsMatchCount; } if (a.category !== b.category) { return a.category.localeCompare(b.category); } return a.price - b.price; }); const groupedProducts = sortedProducts.reduce((acc, product) => { if (!acc[product.popularTagsMatchCount]) { acc[product.popularTagsMatchCount] = []; } acc[product.popularTagsMatchCount].push({ name: product.name, category: product.category, price: product.price }); return acc; }, {}); return Object.entries(groupedProducts).map(([popularTagsCount, products]) => ({ popularTagsCount: Number(popularTagsCount), products })); }
def popular_tags: group_by(.tags[]) | map({tag: .[0].tags[], count: length}) | sort_by(-.count) | .[0:5] | map(.tag); def product_info: { name, category, price }; def popular_tags_match_count(tags, popularTags): length(tags | map(select(. as $tag | popularTags | index($tag) != null))); def sorted_products(popularTags): map({ product: ., popularTagsMatchCount: (popular_tags_match_count(.tags, popularTags)) }) | sort_by(-.popularTagsMatchCount, .product.category, .product.price); { popularTags: (popular_tags), groupedProducts: (sorted_products(popular_tags)) | group_by(.popularTagsMatchCount) | map({ popularTagsCount: .[0].popularTagsMatchCount, products: [.[] | .product | product_info] }) }
The Jora query is composed of two main parts:
Calculate the top 5 popular tags in products. This expression calculates the popular tags by using the group() function with the tags property as the key. When a function in group() returns an array, a value will be added to several groups corresponding to each element in the array. The groups are then sorted by size in descending order. The top 5 tags are selected using slice notation [0:5]:
[0:5]
$popularTags: products .group(=> tags) .sort(value.size() desc) .key[0:5];
Add a popularTagsMatchCount field to each product by counting the number of popular tags it has, and then group the products by this count:
popularTagsMatchCount
products .({ ..., popularTagsMatchCount: tags.[$ in $popularTags].size() })
Sort products within each group by the popularTagsMatchCount in descending order, then by category in ascending order, and finally by price in ascending order:
.sort(popularTagsMatchCount desc, category asc, price asc)
Group the products by their popularTagsMatchCount, and for each group, create an object with the count and an array of products containing their name, category, and price:
.group(=> popularTagsMatchCount) .({ popularTagsCount: key, products: value.({ name, category, price }) })
($a, $a) => expr
comparator function
median()
variance()
stdev()
package.json
dist/*
0xa0b1_c2d3
{ sum(), // equavalent to: `sum: sum()` foo.[x > 5], // equavalent to: `foo: foo | .[x > 5]` baz is number ?: 0, // equavalent to: `baz: baz | is number ?: 0` $var.size() // equavalent to: `var: var | .size()` }
key: undefined
$[3:1]
$[1:3:-1]
$[5:1:-2]
$[1:5:2]
($a, $b) => expr
this.assertion(name, ...args)
| expr
{ $, prop: 1 }
pick()
bool()
<expr>
.has.and.is
has.and
{ null }
{ "null": $["null"] }
if
then
else
setup(methods)
setup({ methods })
jora()
jora(..., { assetions })
setup({ assertions })
query()
jora(query, { stat: true })().value
SortingFunction
CompareFunction
Unary
Prefix
Assertion
Postfix
replace()
p()
toLowerCase()
toUpperCase()
trim()
abs()
acos()
acosh()
asin()
asinh()
atan()
atan2()
atanh()
cbrt()
ceil()
clz32()
cos()
cosh()
exp()
expm1()
floor()
fround()
hypot()
imul()
Math.log()
log10()
Math.log1p()
log2()
pow()
round()
sign()
sin()
sinh()
sqrt()
tan()
tanh()
trunc()
expr ? : []
expr ? 1
expr?
$ ? $ : undefined
a + b desc
(a + b) desc
a + (b desc)
=> a | b
=> (a | b)
(=> a) | b
split()
NaN in [1, NaN, 3]
syntax.tokenize()
tolerantMode
()
'foo'
.[field='foo']
Pick
Block
\0
Indentifier
Placeholder
jora.setup()
Method "foo" is not defined
m.foo is not a function
match()
| in <string or number>
<string or number> has |
values
related
suggestion(): Array<{ type: 'property' | 'value' | 'variable', from: number, to: number, text: string, suggestions: Array<string | number> }> | null
^10.12.0 || ^12.20.0 || ^14.13.0 || >=15.0.0
jora.min.js
jora.js
jora.esm.js
jora.js.map
jora.esm.js.map
'hello\x20world'
\r\n
\u2028
\u2029
{ $a: 42; foo: $a * 2, $a }
{ foo: 84, a: 42 }
syntax.suggest(source, parseResult)
syntax.tokenize(source, tolerantMode = false)
Property
ObjectEntry
[...expr]
arg1
$method: => 123; $method() or path.$method()
syntax.walk()
.123
.5e-4
{ 1: 'ok', null: 'ok' }
Object.is()
===
!==
foo[expr]
group(=>)
)
mapToArray()
entries().({ nameProp: key, ...value })
["a", "b", 1, 2][$ in ["b", 2]]
"a"
foo | bar | ...
$a;.($a; ...)
query('...', { debug: (name, value) => /* ... */ })
$ foo
src/parser.js
jison
dist/parser.js
dist/version.json
syntax
syntax.parse(source, tolerantMode)
syntax.compile(ast, suggestRanges, statMode)
syntax.stringify(ast)
$str: '<foo>'; str[1:-1]
$ar:[1,2,3,4,5,6]; $ar[-3::-1]
[6,5,4]
slice(from, to)
split(pattern)
join(separator)
match(pattern, matchAll)
..method()
($a: 1; $a + $a)
=> body
sort(foo asc)
$sorting: foo desc; sort($sorting)
sort(foo desc, bar asc)
::self
get
map
<a > 20>
<(a > 20)>
<foo.[a > 20]>
current
.[
.(
..(
jora(query, methods?, debug?)
jora(query, options?)
{ methods?, debug? }
jora(query, { stat: true })
{ stat(), suggestion() }
jora(query, { tolerant: true })
version
{ $foo, bar: 1 }
{ foo: $foo, bar: 1 }
foo["bar"]
array.map(e => e[key])
.($foo:$.some.path.to.cache(); bar=$foo or baz=$foo)
a ? b : c
[expr]
.npmignore