# Calculation Engine {#calculation-engine}



## Overview {#overview}


The calculation engine powers all expression evaluation across the MQUAD platform. It handles five expression types using a unified parser and evaluator:

| Expression Type | Purpose | Reactivity |
|---|---|---|
| `calculation` | Computed field values | Reactive — re-evaluated on every change |
| `constraint` | Validation rules | Reactive — checked on every change |
| `relevant` | Skip logic / visibility | Reactive — re-evaluated on every change |
| `choice_filter` | Dynamic choice filtering | Reactive — re-evaluated on every change |
| `default` | Initial field values | Init-only — evaluated once at form start |


### Three Implementations, One Spec {#three-implementations-one-spec}


All three runtimes implement the same grammar:

| Runtime | File | Language |
|---|---|---|
| Server | `form-service/.../xpath_evaluator.py` | Python |
| Web | `frontend/.../calculation-engine.ts` | TypeScript |
| Mobile | `mobile/.../expression/` (tokenizer, parser, evaluator, functions) | Dart |

A 114-test compatibility harness enforces identical behavior across all platforms. Every function added to one engine must be added to all three.


## Architecture {#architecture}



### Pipeline {#pipeline}


```
Raw Expression String
        │
        ▼
   ┌─────────┐
   │  LEXER   │  tokenize() → Token[]
   │         │  Single-pass, left-to-right scanner
   └────┬────┘
        │
        ▼
   ┌─────────┐
   │  PARSER  │  parse() → AST
   │         │  Recursive descent, operator precedence
   └────┬────┘
        │
        ▼
   ┌──────────┐
   │VALIDATOR  │  validate() → ValidationResult
   │          │  Variable existence, function names, parens balance
   └────┬─────┘
        │
        ▼
   ┌──────────┐
   │EVALUATOR  │  evaluate() → Value
   │          │  Walk AST, resolve variables, call functions
   └──────────┘
```


### Dependency Graph {#dependency-graph}


The platform uses a directed acyclic graph (DAG) to manage expression evaluation order:

1. **Node**: Each expression keyed by `{type}:{question_name}` (e.g., `calculation:total_score`)
2. **Edge**: `A → B` means "A depends on B" (B must be evaluated before A)
3. **Topological Sort**: Kahn's algorithm determines evaluation order
4. **Partial Recalculation**: When a field changes, `getAffectedByChange()` returns only the transitive dependents that need re-evaluation
5. **Cycle Detection**: DFS with recursion stack; self-references are ignored


## Expression Syntax {#expression-syntax}



### Literals {#literals}


| Type | Examples |
|---|---|
| Number | `42`, `-3.14`, `0` |
| String | `'hello'`, `"world"` |
| Boolean | `true()`, `false()` |
| Null | `null` |


### Variable References {#variable-references}


```
${question_name}
```

References another question's answer by its `name` field. The dot `.` represents the current question's value.


### Operators {#operators}


| Category | Operators | Precedence |
|---|---|---|
| Logical OR | `or` | 1 (lowest) |
| Logical AND | `and` | 2 |
| Comparison | `=`, `==`, `!=`, `<`, `>`, `<=`, `>=` | 3 |
| Addition | `+`, `-` | 4 |
| Multiplication | `*`, `div`, `mod` | 5 |
| Unary | `-`, `not`, `!` | 6 (highest) |


### Ternary {#ternary}


```
if(condition, thenValue, elseValue)
```


## Function Reference {#function-reference}



### Numeric Functions {#numeric-functions}


| Function | Signature | Description |
|---|---|---|
| `int(v)` | `any → int?` | Truncate to integer |
| `number(v)` | `any → num?` | Convert to number |
| `round(v [, n])` | `num, int? → num` | Round to n decimal places |
| `abs(v)` | `num → num` | Absolute value |
| `min(a, b, ...)` | `num... → num?` | Minimum value |
| `max(a, b, ...)` | `num... → num?` | Maximum value |
| `sum(a, b, ...)` | `num... → num?` | Sum of values |


### String Functions {#string-functions}


| Function | Signature | Description |
|---|---|---|
| `concat(a, b, ...)` | `any... → string` | Concatenate values |
| `join(sep, a, b, ...)` | `string, any... → string` | Join with separator |
| `string(v)` | `any → string` | Convert to string |
| `string-length(v)` | `string → int` | Length of string |
| `substring(s, start [, end])` | `string, int, int? → string` | Extract substring |
| `contains(s, sub)` | `string, string → bool` | Check for substring |
| `starts-with(s, prefix)` | `string, string → bool` | Check prefix |
| `ends-with(s, suffix)` | `string, string → bool` | Check suffix |
| `replace(s, search, repl)` | `string, string, string → string` | Replace all occurrences |
| `regex(v, pattern)` | `string, string → bool?` | Test regex match |


### Date Functions {#date-functions}


| Function | Signature | Description |
|---|---|---|
| `today()` | `→ string` | Current date (yyyy-MM-dd) |
| `now()` | `→ string` | Current datetime (ISO 8601) |
| `date(v)` | `string → string?` | Parse date string |
| `date(y, m, d)` | `int, int, int → string?` | Construct date from components |
| `format-date(d, fmt)` | `string, string → string` | Format date with pattern |
| `days-between(d1, d2)` | `string, string → int?` | Days between dates |
| `today-offset(n)` | `int → string` | Date offset from today |
| `year(d)` | `string → int?` | Extract year |
| `month(d)` | `string → int?` | Extract month (1-12) |
| `day(d)` | `string → int?` | Extract day (1-31) |


### Selection Functions {#selection-functions}


| Function | Signature | Description |
|---|---|---|
| `selected(v, choice)` | `array, string → string` | Check choice selected |
| `count-selected(v)` | `array → int` | Count selected choices |
| `selected-at(v, i)` | `array, int → string` | Choice at index |


### Conditional Functions {#conditional-functions}


| Function | Signature | Description |
|---|---|---|
| `if(cond, t, e)` | `bool, any, any → any` | Conditional |
| `coalesce(a, b, ...)` | `any... → any?` | First non-empty value |
| `not(v)` | `any → bool` | Logical NOT |


### Utility Functions {#utility-functions}


| Function | Signature | Description |
|---|---|---|
| `count(v)` | `any → int` | Count items |
| `uuid()` | `→ string` | Generate UUID v4 |


## Evaluation Lifecycle {#evaluation-lifecycle}



### Form Start {#form-start}


```
1. Load schema + prefill data
2. Resolve default values (date/static eagerly, refs in order)
3. Build dependency graph from all expressions
4. Evaluate ALL calculation expressions (topological order)
5. Render UI
```


### On Field Change {#on-field-change}


```
1. Update field value in answers
2. Query dependency graph for affected nodes
3. Re-evaluate only affected calculation expressions
4. Re-check constraints for affected questions
5. Re-evaluate relevance for affected questions
6. Re-compute choice filters for affected questions
7. Update UI
```


## Compatibility {#compatibility}


All functions and operators are tested across all three platforms with 114 canonical test cases. See `ODK_COMPATIBILITY_MATRIX.md` for the current status and `SUPPORTED_FUNCTIONS.md` for the complete function registry.


## Reference Documentation {#reference-documentation}


- `SUPPORTED_FUNCTIONS.md` — Complete function registry with signatures and examples
- `DEPENDENCY_GRAPH_SPEC.md` — Dependency graph data structures and algorithms
- `EXPRESSION_ENGINE_SPEC.md` — Unified expression engine architecture
- `ODK_COMPATIBILITY_MATRIX.md` — Cross-platform function support tracking
- `default-values.md` — Default value expression syntax and resolution rules
