Skip to content

Instantly share code, notes, and snippets.

@aars
Created March 21, 2017 14:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aars/5d9ee899628f8f357d609dca40ed4fcf to your computer and use it in GitHub Desktop.
Save aars/5d9ee899628f8f357d609dca40ed4fcf to your computer and use it in GitHub Desktop.

SureBase Expressions

Can be defined in most any structural notation/language. We only use JSON.

JSON - common structure

A few keywords are shared among expressions:

meta

Use meta keyword for everything descriptive. There is no validation on the content of meta, and can thus contain any data deemed useful.

opts

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.

args

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.

example JSON
{
  meta: { 
    name: 'A Name', 
    description: 'Something descriptive' 
  },
  opts: {
    numeric: true,
    closest: -1
  },
  args: []
}

The example opts shown here belong to a table operator.

Dataset

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.

Chain value

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.

Example

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

Initial chain value

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.

Expression arguments

For most calculation expressions the following is true:

  • Operate on chain value and only argument, or
  • Operate on all arguments, ignore chain value.
Examples
// 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' }] } 

Expressions

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.

Expressions::Operators

  • keyword: op
example JSON:
{ op: 'add', args: [1, 2] } // This will result in ... *drum roll* ... 3. Magic.

Expressions::Operators::Math


add

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

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

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

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

max

Maximize (limit) chain value to only argument.

  • required args(1)
  • options: none
{ op: 'max', args: [100] } // maximize chain value to 100

round

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

floor

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

ceil

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

Expressions::Operators::Logic


eq

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?

lt

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?

lte

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?

gt

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?

gte

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?

if

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

Expressions::Operators::Table


table

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

examples

String lookup (exact match only)
{ 
  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!'
Numeric lookup (exact match only)
{ 
  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
Numeric lookup (interpolate)
{ 
  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
Numeric lookup (closest match)
{ 
  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

Expressions::Functions

  • keyword: fn

example JSON:

{ fn: 'year', args: ['1982-10-13T00:00:00.000Z'] } // Results in 1982. I know. Magic.

count

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

year

Returns the year of of a iso8601 formatted timestamp.

  • required args(1)
  • options: none
{ fn: 'year', args: ['1982-10-13T20:12:01.000Z'] } // 1982

years_ago, months_ago, weeks_ago, days_ago

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

Expressions::Reference

Reference a dataset value using regular JSON notation.

  • keyword: ref
example dataset
{ 
  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
    }
  }
}
example references
{ 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

Expression::Set

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)
example JSON
{ 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'

Expression::Get

Get a previously set variable.

  • keyword: get
  • no args
  • options: none
example JSON
{ 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::Chain

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).
example use-cases (referenced data from Expression::Reference example dataset)
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.
]

Expression::Value

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment