Can be defined in most any structural notation/language. We only use JSON.
A few keywords are shared among expressions:
Use meta
keyword for everything descriptive. There is no validation on the content of meta
, and can thus contain any data deemed useful.
Some expression behaviour can be configured through options. Options are only checked by the expressions that use them. Options are stored under the opts
keyword.
Expression arguments are stored/nested under the args
keyword. Strictly speaking this is an optional keyword, some expressions can operate without any arguments (i.e. The set
and get
expressions). Though most require arguments. args
must be defined as an array.
{
meta: {
name: 'A Name',
description: 'Something descriptive'
},
opts: {
numeric: true,
closest: -1
},
args: []
}
The example opts
shown here belong to a table
operator.
Expressions usually operate with a dataset containing variables required for calculation, tables for lookups, etc. A dataset is an arbitrary piece of JSON, variables are referenced using regular JSON notation.
A set of expressions is called a chain
. Within a chain of expression a chain value
is kept; the input and output of all expressions.
- The outer set of expression is an implicit chain
- The outer implicit expression chain is initialized with
0
- The input of an expression is the current chain value
- The output of an expression is the new chain value
- With one "exception", the
private
chain expression, which always outputs it's original input, effectively not touching the chain value.
- With one "exception", the
This is a contrived example to demonstrate chain value modifications. The third expression here ignores and thus replaces the chain value, for demonstration purposes only.
// initial chain value: 0
[
// input is 0
{ op: 'add', [2] }, // add 2 to input.
// output is new chain value: 2
{ op: 'multiply', [5] }, // multiply chain value by 5.
// output is new chain value: 10,
{ op: 'add', [3, 5] }, // add 3 and 5. Ignores chain value because of multiple args.
// output is new chain value: 8.
// start a new chain, input is chain value: 8.
{
// but this chain is private, and will always output 8.
opts: { private: true },
chain: [
{ op: 'if', args: [
// if 'some.referenced.value' is truthy...
{ ref: 'some.referenced.value' },
// multiply chain value by 20
{ op: 'multiply', args: [20] },
// or return (private) chain value
]
},
// private chain value here is 8*20 if first 'some.referenced.value' was truthy.
// Store (private) chain value as 'variable_name'
{ set: 'variable_name' } // set expression returns it's value...
// but this chain is private and will only output it's input, 8.
]
},
// output is still 8, chain was private.
//
// etc...
]
The implicit outer expression chain will start with a chain value of 0
. Though nothing in SureBase Expression prevents alternative initial values (since the outer implicit chain is simply another expression chain). Current use-cases do not require control over the initial chain value. We have no interface providing control here.
For most calculation expressions the following is true:
- Operate on
chain value
andonly argument
, or - Operate on
all arguments
, ignore chain value.
// Add 1 and chain value.
{ op: 'add', [1] }
// Add 2 and 3. Ignore chain value.
{ op: 'add', [2, 3] }
// Divide chain value by 12
{ op: 'divide', [12] }
// Divide 12 by the referenced 'some.number'. Ignore chain value.
{ op: 'divide', [12, { ref: 'some.number' }] }
Note: Expressions are seperated into groups, like "Operators" and "Functions", though this is mostly arbitrairily picked by the developer and does not convey any meaningful seperation. This document groups the expressions accordingly.
Without further ado, the expressions.
- keyword:
op
{ op: 'add', args: [1, 2] } // This will result in ... *drum roll* ... 3. Magic.
Add chain value and only argument, or all arguments.
- required
args
- options:
none
{ op: 'add', args: [1] } // Add 1 to chain value
{ op: 'add', args: [2, 3, 4] } // add 2 and 3 and 4
Subtract only argument from chain value, or subtract all arguments.
- required
args
- options:
none
{ op: 'subtract', args: [1] } // subtract 1 from chain value
{ op: 'subtract', args: [10, 2, 4] // subtract 4 from (2 from 10)
Multiply chain value and only argument, or all arguments.
- required
args
- options:
none
{ op: 'multiply', args: [1.4] } // multiply chain value by 1.4
{ op: 'multiply', args: [0.3, 5.2] } // multiply 0.3 by 5.2
Divide chain value by only argument, or all arguments.
- required
args
- options:
none
{ op: 'divide', args: [2] }
{ op: 'divide', args: [10, 2, 1.5] } // (divide 10 by 2) by 1.5
Maximize (limit) chain value to only argument.
- required
args(1)
- options:
none
{ op: 'max', args: [100] } // maximize chain value to 100
Round chain value to only argument, or 1.
- optional
args
- options:
none
{ op: 'round' } // round chain value to nearest integer
{ op: 'round', args: [100] } // round chain value to nearest 100
Round chain value down to only argument, or 1.
- optional
args
- options:
none
{ op: 'floor' } // round down chain value to nearest integer
{ op: 'floor', args: [100] } // round down chain value to nearest 100
Round chain value up to only argument, or 1.
- optional
args
- options:
none
{ op: 'ceil' } // round up chain value to nearest integer
{ op: 'ceil', args: [100] } // round up chain value to nearest 100
Compare for equality, chain value and first argument, or first and second argument.
- required
args(1...2)
- options:
none
- returns:
1|0
{ op: 'eq', args: [0] } // is chain value equal to 0?
{ op: 'eq', args: [1, 1] } // is 1 equal to 1?
Is the chain value less than the first argument, or first argument less than second argument.
- required
args(1..2)
- options:
none
- returns:
1|0
{ op: 'lt', args: [12] } // is chain value less than 12?
{ op: 'lt', args: [11, 12] } // is 11 less than 12?
Is the chain value less than or equal to the first argument, or first argument less than or equal to second argument.
- required
args(1..2)
- options:
none
- returns:
1|0
{ op: 'lte', args: [12] } // is chain value less than or equal to 12?
{ op: 'lte', args: [11, 12] } // is 11 less than or equal to 12?
Is the chain value greater than the first argument, or first argument greater than second argument.
- required
args(1..2)
- options:
none
- returns:
1|0
{ op: 'gt', args: [12] } // is chain value greater than 12?
{ op: 'gt', args: [11, 12] } // is 11 greater than 12?
Is the chain value greater than or equal to the first argument, or first argument greater than or equal to second argument.
- required
args(1..2)
- options:
none
- returns:
1|0
{ op: 'gte', args: [12] } // is chain value greater than or equal to 12?
{ op: 'gte', args: [11, 12] } // is 11 greater than or equal to 12?
Returns the last argument, if all other arguments are truthy.
- required
args(2..)
- options:
none
{ op: 'if', args: [1, 2] } // If 1 is truthy (it is), return 2
{ op: 'if', args: [0, 'some string'] } // If 0 is truthy (it's not), return 'some string'
{ op: 'if',
args: [
{ ref: 'some.referenced.value' },
12,
{ chain: [ //... ] }
]
} // If 'some.referenced.value' and 12 are truthy, return/perform nested chain
Perform a table lookup in the last argument, using first argument or chain value as lookup key (query).
- required
args(1..2)
- options:
numeric: true|false (default: false)
true
: Numeric query.false
: String query. Only exact matches. No other options apply.
interpolate: true|false (default: false)
true
: Interpolate numeric query when no exact match if found. No other options apply.false
: Only exact matches.
closest: -1|0|1 (default: 0)
-1
: Return lower/previous key when no exact match is found.0
: Only exact matches.1
: Return higher/next key when no exact match is found.
out_of_range: true|false (default: true)
true
: Return highest/lowest key when lookup key is out of table range.false
: Only exact matches.
extrapolate: true|false (default: false)
(this feature does not exist. It does. But is doesn't. Stop worrying about it.)
{
op: 'table',
args: [
'find_me', // lookup key
{ // lookup table
some: 12,
keys: 130,
stuff: 'thing',
find_me: 'you found me!',
other: 'values'
}
]
} // Returns 'you found me!'
{
op: 'table',
options: { numeric: true, out_of_range: false },
args: [
2, // lookup key
{ // lookup table
0: 100,
1: 200,
2: 300,
3: 400
}
]
} // Returns 300
{
op: 'table',
options: { numeric: true, out_of_range: false },
args: [
5, // lookup key
{ // lookup table
0: 100,
1: 200,
2: 300,
3: 400
}
]
} // Returns null, because out_of_range: false
{
op: 'table',
options: { numeric: true, out_of_range: true },
args: [
5, // lookup key
{ // lookup table
0: 100,
1: 200,
2: 300,
3: 400
}
]
} // Returns 400, because out_of_range: true
{
op: 'table',
options: { numeric: true, interpolate: true },
args: [
2.5, // lookup key
{ // lookup table
0: 100,
1: 200,
2: 300,
3: 400
}
]
} // Returns 350, interpolated between 300 and 400
{
op: 'table',
options: { numeric: true, closest: 0 },
args: [
2.5, // lookup key
{ // lookup table
0: 100,
1: 200,
2: 300,
3: 400
}
]
} // Returns null, not looking for closest match
{
op: 'table',
options: { numeric: true, closest: -1 },
args: [
2.5, // lookup key
{ // lookup table
0: 100,
1: 200,
2: 300,
3: 400
}
]
} // Returns 300, because 2 was the closest key looking down
{
op: 'table',
options: { numeric: true, closest: 1 },
args: [
2.5, // lookup key
{ // lookup table
0: 100,
1: 200,
2: 300,
3: 400
}
]
} // Returns 400, because 3 was the closest key looking up
- keyword:
fn
{ fn: 'year', args: ['1982-10-13T00:00:00.000Z'] } // Results in 1982. I know. Magic.
Returns the length of the first argument. (array length, string length, whatever)
TODO: Support chain value as argument.
- required
args(1)
- options:
none
{ fn: 'count', args: ['one thing', 'another thing'] } // 2
Returns the year of of a iso8601 formatted timestamp.
- required
args(1)
- options:
none
{ fn: 'year', args: ['1982-10-13T20:12:01.000Z'] } // 1982
Returns the number of years|months|weeks|days
between first and second argument, or now. Rounded down (floor).
TODO: Support chain value as argument.
- required
args(1..2)
- options:
none
{ fn: 'years_ago', args: ['1982-10-13T20:12:01.000Z', '2000-10-14T00:00:00.000Z'] } // 18
{ fn: 'months_ago', args: ['1982-10-13T20:12:01.000Z', '2000-10-14T00:00:00.000Z'] } // 216
{ fn: 'weeks_ago', args: ['1982-10-13T20:12:01.000Z', '2000-10-14T00:00:00.000Z'] } // dunno..
{ fn: 'days_ago', args: ['1982-10-13T20:12:01.000Z', '2000-10-14T00:00:00.000Z'] } // a lot...
Reference a dataset value using regular JSON notation.
- keyword:
ref
{
people: [
{ name: 'Mr. Personman', date_of_birth: '1982-10-13T00:00:00.000Z' },
{ name: 'Mrs. Personman', date_of_birth: '1991-12-29T00:00:00.000Z' }
],
car: {
make: 'Telsa',
model: 'T',
fuel_type: 'electricity'
},
tables: {
fuel_type_deduction: {
gas: 0,
petrol: 0,
lpg: 10,
electricity: 1000
}
}
}
{ ref: 'people' } // returns entire array under key 'people', we could count them:
{ fn: 'count', args: [{ ref: 'people'}] } // 2!
{ ref: 'people[1].date_of_birth' } // returns dob for second 'people', we could calculate age:
{ fn: 'years_ago', args: [{ref: 'people[1].date_of_birth'}] } // year(now)-1991
// We can do table lookups using referenced data:
{ op: 'table', args: [{ ref: 'car.fuel_type' }, { ref: 'tables.fuel_type_deduction' }] } // 1000
Set a variable.
Useful to store intermediate results for later and/or multiple use.
Variables are stored as a simple key->value
Hash/Object, nesting or deep references are not supported. Variables are only retrievable through get
expressions.
- keyword:
set
- optional
args(1)
- options:
none
- returns set value (passthrough)
{ set: 'keep_this_for_later' } // Store chain value as 'keep_this_for_later'
{ set: 'variable_name', args: ['value'] } // Store 'value' as 'variable_name'
{
set: 'complicated_calculation',
args: [
{ chain: [ /* a whole bunch of complicated calculation */ ] }
]
} // Store result of entire chain as 'complicated_calculation'
Get a previously set
variable.
- keyword:
get
- no
args
- options:
none
{ get: 'keep_this_for_later' } // Get variable 'keep_this_for_later'
// How we might use a stored variable as an argument for another expression.
{ op: 'multiply', args: [1, { get: 'complicated_calculation' }] }
Expression chains provide flow control.
- keyword:
chain
- type:
Array
- no
args
- options:
private: true|false (default false)
- A private chain will always output it's input, effectively not touching the outer chain value (see example use-cases).
outer_chain = [
{ op: 'multiply', args: [2, 5] }, // chain value is now 10...
// We might want to set a (couple of) variable(s) for repeated later use, without
// interfering with the current chain value. A private chain would be useful here.
{ // input is chain value: 10
options: { private: true },
chain: [
// Store the age of the first person.
{ set: 'age', args: [
{ fn: 'years_ago', args: [{ ref: 'people[1].date_of_birth' }] }
] }, // since set returns the value, chain value is now the same as 'age'
// Store result of some table lookup
{ set: 'car_fuel_type_deduction', args: [
{ op: 'table', args: [{ ref: 'car.fuel_type' }, { ref: 'tables.fuel_type_deduction' }] }
] } // again, chain value is updated with this result.
]
}, // But since this chain is private, it returns the original chain value of 10.
// We can now use `age` and `car_fuel_type_deduction` in following expressions.
{ op: 'if', args: [
{ op: 'gte', args: [{ get: 'age' }, 18] }, // If person is 18 or older
{ op: 'multiply', args: [{ get: 'car_fuel_type_deduction' }, 1.04] } // whatever
] },
// Another use-case is controlling the flow and order of the expressions.
// Imagine we want to do the following:
// input / (1 - 15 * table_lookup(100 * table_lookup(some_key)))
// One might express this like so:
{
op: 'divide',
args: [
{ op: 'subtract', args: [
1,
{ op: 'multiply', args: [
{ op: 'table', args: [
{ op: 'multiply', args: [
100,
{ op: 'table', args: ['some_key', { ref: 'some_table' }] }
] },
{ ref: 'some_other_table' }
] },
15
] }
] }
]
},
// This is already difficult to follow, and can very easily become unreadable.
// If we introduce a chain we can make use of the chain value it introduces,
// allowing us to write the same expression a bit more readable:
{
op: 'divide',
args: [
{ op: 'subtract', args: [
1,
{ chain: [ // Let's make use of the internal chain value here
{ op: 'table', args: ['some_key', { ref: 'some_table' }] },
{ op: 'multiply', args: [100] },
{ op: 'table', args: ['some_other_table'] },
{ op: 'multiply', args: [15] }
] }
] }
]
} // There... readable-er.
]
Every node that is not a recognized expression is transformed into a Expression::Value
object, a simple wrapper to represent the original value through the common SureBase Expressions API.