Skip to content

Formula language

To calculate properties, we use a formula. Every formula will resolve to a value. A formula cannot return tasks or properties. It must resolve properties to a value.

Numeric constants can be used in the formula

Valid formats:

  • 123
  • 123.456
  • 0.456

Invalid formats: Format specifiers

  • Float 123f
  • Hexadecimal: 0x10
  • Binary: 10b
  • Leading zeros: 0123.456 (the 0.456 is valid since it’s not a leading zero)
  • Missing leading zero .456

Task selector will retrieve tasks

  • current: The current tasks.
  • before: The tasks directly before the current tasks.
  • after: The tasks directly after the current tasks.
  • allBefore: The tasks directly and indirectly before the current task.
  • allAfter: The tasks directly and indirectly after the current task.

taskSelector[<taskFilters>]

Task filters will filter tasks. They use boolean logic to determine if a task can included. By default all tasks are included.

  • done - Tasks must be done to be included
  • todo - Tasks must be incomplete to be included.

This can also be done with current, which will use the default value (not fallback value) for that property. This is useful if you want to determine time_to_finish, which will be: sum(before[todo].time_to_finish) + sum(current[todo].estimation). So that when you complete a task (and all blocked tasks) the outcome will be zero.

  1. Parentheses
  2. Logical not (! or not)
  3. Logical and (&&, or and)
  4. Logical or (|| or or)

taskSelector.<name-of-property>

A property selector will point to either a value-property or a calculated-property. The property must exist, if not an exception is thrown. The property selector is case-sensitive.

subject.propertySelector[<value-resolver>] Due to how properties work, we need a way to determine which value we want. A property can have a value, or a fallback value or might be undefined. The value resolver will resolve this by performing each the value resolvers in the order provided. If a resolver is unable to find a value, then value can’t be resolved, then the property (and thus the task) will be ignored. The value resolvers are used to ensure aggregators get values that they can use, since most aggregators will fail when they receive undefined.

  • value resolves the value set in the task
  • fallback Resolve to the fallback value.
  • default returns 0 for numeric values.

If no value resolver is provided the following is used: [value, fallback].

To make a comparison with javascript

// before.estimation[value, fallback, default]
before.map(task => task.estimation)
.map(property => property.value ?? property.fallback ?? 0)
.filter(v => v !== undefined)
// before.estimation[value, fallback]
before.map(task => task.estimation)
.map(property => property.value ?? property.fallback)
.filter(v => v !== undefined)
// before.estimation[value]
before.map(task => task.estimation)
.map(property => property.value)
.filter(v => v !== undefined)
// before.estimation[fallback]
before.map(task => task.estimation)
.map(property => property.fallback)
.filter(v => v !== undefined)

Will add all the values together; if no values are provided, then the result is equal to 0. It expects a numeric value. If any value is undefined then it will return undefined.

Will return the highest value. If no values are provided, then the result is 0. It expects a numeric value. If any value is undefined then it will return undefined.

Will return the lowest value. If no values are provided, then the result is 0. It expects a numeric value. If any value is undefined then it will return undefined.

Returns the number of values that were given. Can be used both on properties and tasks. If any value is undefined then it will return undefined.

// Using it on tasks (returns the total tasks before)
count(before)
// Counts the nodes before with a value or fallback (which might result in undefined)
count(before.property[value, fallback])

Returns the average. It expect a numeric value. If no values are provided then the result is equal to 0 instead of DivideByZero. If any value is undefined then it will return undefined.

The following operators are supported. In all cases the arguments of the operators must be a value. If the value is undefined then the operation will also return undefined.

  • Addition (a + b)
  • Subtraction (a - b)
  • Negation (-a) Inverts the sign of a value
  • Multiplication (a * b)
  • Division (a / b). Note that if b is 0 a DivisionByZeroError will occur.

The order of operators are:

  1. Parentheses
  2. Negation
  3. Multiplication and division
  4. Addition and subtraction
  • Division by zero (which is why sum(before.estimation) / count(before.estimation) should be avoided)
  • Circular calculated property.
    • When calculating a property the property that is being calculated can’t be used in the formula, direct or indirect.
  • A valid value
  • undefined, to indicate some value is missing.
  • error

Yes, but only if it contains a formula that will not (directly or indirectly) refers to an instance of itself. Note that below cp = means that the result of the formula is stored as task.cp. If for some reason this does happen, it will result in an error.

Valid:

  • cp = sum(before.v, after.v) since no calculated properties are used.
  • cp = sum(before.cp, current.v) is valid since the formula uses calculated properties that will not move back.
  • cp = sum(before.cp1, after.cp1) since the formula uses another calculated properties assuming those formula’s won’t use cp.

Invalid:

  • cp = sum(current.cp) since the formula points to itself.
  • cp1 = sum(current.cp2); cp2 = sum(current.cp1) since both cp1 and cp2 depend on one another.
  • cp = sum(before.cp, after.cp) since before.cp will vist current.cp which needs before.cp which needs current.cp and so on.
    • Note; that cp = sum(before.cp1, after.cp2) is valid.
  • cp1 = sum(before.cp2); cp2 = sum(after.cp1) since the formula’s have a direct circular reference
  • cp1 = sum(before.cp2); cp2 = sum(after.cp3); cp3 = sum(current.cp1) since the formula’s have an indirect circular reference. Basically: T1.cp1 -> T0.cp2 -> T1.cp3 -> T1.cp1(AGAIN)
    • NOTE: cp1 = sum(after.cp2); cp2 = sum(before.cp3); cp3 = sum(after.cp1) is valid since cp3 points to another cp1. Basically: T1.cp1 -> T2.cp2 -> T1.cp3 -> T2.cp1 -> T3.cp2 -> T2.cp3

Assume you have a lips dependency chain.

A

B

D

C

And we wish to calculate the total amount of tasks that need to be completed. Using a recursive before.cp will result in A being included multiple times.

Nodesum(before.cp) + 1count(allBefore) + 1
A1 (0+1)1 sum() +1
B2 (1+1)2 sum(1) + 1
C2 (1+1)2 sum(1) + 1
D6 (1+2+2+1)4 sum(1, 1, 1) + 1