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.
Constants
Section titled “Constants”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
(the0.456
is valid since it’s not a leading zero) - Missing leading zero
.456
Task selector
Section titled “Task selector”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.
Task filters
Section titled “Task filters”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 includedtodo
- 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.
Task filter operators
Section titled “Task filter operators”- Parentheses
- Logical not (
!
ornot
) - Logical and (
&&
, orand
) - Logical or (
||
oror
)
Property selector
Section titled “Property selector”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.
Value Resolver
Section titled “Value Resolver”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 taskfallback
Resolve to the fallback value.default
returns0
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)
Aggregator
Section titled “Aggregator”sum(…)
Section titled “sum(…)”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
.
max(…)
Section titled “max(…)”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
.
min(…)
Section titled “min(…)”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
.
count(…)
Section titled “count(…)”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])
avg(…)
Section titled “avg(…)”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
.
Operators
Section titled “Operators”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 ifb
is 0 a DivisionByZeroError will occur.
The order of operators are:
- Parentheses
- Negation
- Multiplication and division
- Addition and subtraction
Errors that can occur
Section titled “Errors that can occur”- 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.
What are the valid outcomes of a formula?
Section titled “What are the valid outcomes of a formula?”- A valid value
undefined
, to indicate some value is missing.error
Can we mix before and after?
Section titled “Can we mix before and after?”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 usecp
.
Invalid:
cp = sum(current.cp)
since the formula points to itself.cp1 = sum(current.cp2); cp2 = sum(current.cp1)
since bothcp1
andcp2
depend on one another.cp = sum(before.cp, after.cp)
sincebefore.cp
will vistcurrent.cp
which needsbefore.cp
which needscurrent.cp
and so on.- Note; that
cp = sum(before.cp1, after.cp2)
is valid.
- Note; that
cp1 = sum(before.cp2); cp2 = sum(after.cp1)
since the formula’s have a direct circular referencecp1 = 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 sincecp3
points to anothercp1
. Basically:T1.cp1 -> T2.cp2 -> T1.cp3 -> T2.cp1 -> T3.cp2 -> T2.cp3
- NOTE:
Why do we have indirect and direct?
Section titled “Why do we have indirect and direct?”Assume you have a lips dependency chain.
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.
Node | sum(before.cp) + 1 | count(allBefore) + 1 |
---|---|---|
A | 1 (0+1) | 1 sum() +1 |
B | 2 (1+1) | 2 sum(1) + 1 |
C | 2 (1+1) | 2 sum(1) + 1 |
D | 6 (1+2+2+1) | 4 sum(1, 1, 1) + 1 |