A decision table lookup rarely returns a single number. When a table has several OUT columns, or when the
context matches more than one row, higson.getValue(...) returns a matrix — a sub-matrix of the table
containing every row that matched the context.
This page is a complete reference for reading that matrix inside a function body. For the basics of writing functions — available objects, type converters, calling other functions — see Best Practices for Writing Function Code.
Objects#
Everything you do with a decision table result involves three objects. Knowing which one you are holding tells you which methods are available:
| Object | You get it from | Represents | Key methods |
|---|---|---|---|
higson |
always available in the function body | access point to the engine | getValue, getString/getNumber/…, call/callValue |
| matrix | higson.getValue(table, ctx) |
all rows matching the context | size, isEmpty, isBlank, row(i), rows(), get(...) |
| row | matrix.row(i) or an element of matrix.rows() |
one row and its OUT columns | size, get(...), typed getters, array/list getters, getEnum |
The distinction matters in practice: array and list getters, getEnum and getDatetime exist only on a row,
not on the matrix. To read a multi-value column you must go through row(i) first.
Retrieving the Matrix#
The context supplies the values for the table’s IN columns. There are four ways to build it, all returning the same matrix object:
// 1. the function's own context
higson.getValue('demo.motor.price', ctx)
// 2. the function's context with selected paths overridden by the passed tokens
higson.getValue('demo.motor.price', ctx, 'car.model.year', '2020')
// 3. a context built on the fly from key/value pairs, without ctx
higson.getValue('demo.motor.price', 'car.model.type', 'BMW', 'car.model.year', '2020')
// 4. a reusable context object
def standalone = higson.createContext('car.model.type', 'BMW', 'car.model.year', '2020')
def inherited = higson.createContext(ctx, 'car.model.year', '2020') // inherits ctx, overrides one path
def matrix = higson.getValue('demo.motor.price', inherited)
Variants 2–4 are the ones to reach for when a single function queries the same table for several scenarios — for example pricing the same risk for three different coverage variants — instead of building a fresh context each time.
Guarding Against No Match#
When no row matches the context, getValue does not throw. It returns an empty matrix, and an empty matrix
behaves differently from a populated one:
def matrix = higson.getValue('demo.motor.price', ctx)
if (matrix == null || matrix.isEmpty()) {
return null // or a business default
}
Why this check is not optional:
- On an empty matrix,
row()androw(i)returnnullinstead of throwing. Without the guard, the failure surfaces later as aNullPointerExceptionon the nextget(...)— several lines away from the real cause. - On a populated matrix, an out-of-range index behaves the other way round:
row(99)throwsINVALID_ROW_INDEXand names the available range in the message.
Three methods describe the state of a matrix:
| Method | Returns |
|---|---|
matrix.size() |
number of matched rows |
matrix.isEmpty() |
true when no row matched |
matrix.isBlank() |
true when rows exist but every cell in them is empty |
isEmpty() and isBlank() are not opposites and not mutually exclusive: a row of nulls is
blank but not empty, and an empty matrix is also blank (there is no non-blank row in
it). Test isEmpty() first when you need to tell the two apart.
Matrix Dimensions#
size() means something different on each object:
int rowCount = matrix.size() // number of rows
int colCount = matrix.row(0).size() // number of OUT columns in that row
Reading a Single Cell#
Columns can be addressed by index (0-based, in OUT column order) or by name (the OUT column name from the table definition).
Methods without a row number always operate on row 0:
matrix.get() // row 0, column 0
matrix.get(5) // row 0, column 5
matrix.get('model') // row 0, column named 'model'
Methods with a row number take it as the first argument:
matrix.get(0, 5) // row 0, column 5
matrix.get(1, 5) // row 1, column 5
matrix.get(1, 'model') // row 1, column named 'model'
Both forms are valid, but get('model') and get(1, 'model') are different methods, and in a long function it
is easy to misread which argument is the row and which is the column. Prefer the explicit form, which cannot be
misread:
matrix.row(0).get('model')
matrix.row(1).get('model')
To have Higson cast the value for you, pass the target class:
matrix.get(0, 'model', String.class)
matrix.row(1).get('model', String.class)
Type Conversion#
get(...) returns the raw value. The typed getters convert it. They are available on both the matrix (acting on
row 0) and on any row, each accepting a column index, a column name, or nothing at all (column 0):
| Method | Returns | Note |
|---|---|---|
getString(...) |
String |
|
getNumber(...) |
double |
primitive — an empty cell silently becomes 0.0 |
getDecimal(...) |
BigDecimal |
preferred for monetary values |
getBigDecimal(...) |
BigDecimal |
alias of getDecimal |
getInteger(...) |
Integer |
|
getLong(...) |
Long |
|
getDate(...) |
java.util.Date |
|
getBoolean(...) |
boolean |
primitive — an empty cell silently becomes false |
def brand = matrix.getString('brand') // row 0
def price = matrix.getDecimal('price') // row 0
def power = matrix.row(1).getInteger('power')
def valid = matrix.row(1).getDate('validFrom')
For nullable columns choose getDecimal, getInteger or getLong — they return objects and preserve null.
getNumber and getBoolean return primitives and cannot represent a missing value, so they substitute a
default instead: an empty cell reads as 0.0 and false respectively, with no error. A missing rate therefore
prices as zero rather than failing, which is why a nullable column is safer read through getDecimal and
checked explicitly.
Row-Only Getters#
def row = matrix.row(0)
row.getDatetime('createdAt') // java.util.Date including time
row.getEnum('status', MyStatus.class) // enum constant
Multi-Value Columns#
A column declared as an array holds several values in one cell. These getters exist only on a row:
| Array form | List form | Element type |
|---|---|---|
getStringArray(...) |
getStringList(...) |
String |
getIntegerArray(...) |
getIntegerList(...) |
Integer |
getLongArray(...) |
— | Long |
getNumberArray(...) |
getNumberList(...) |
double / Double |
getDecimalArray(...) / getBigDecimalArray(...) |
getDecimalList(...) |
BigDecimal |
getDateArray(...) |
getDateList(...) |
java.util.Date |
getArray(...) |
— | ValueHolder (unconverted) |
def tags = matrix.row(0).getStringList('tags')
if ('PREMIUM' in tags) {
premium = premium * 1.1
}
Value Holders#
A holder is the cell before conversion — it carries the Higson type assigned to the column in the table definition, and unlike the typed getters it keeps “the cell was empty” and “the cell was zero” apart:
def holder = matrix.getHolder('model')
holder.getValue() // the underlying value
holder.getClass().simpleName // StringHolder, IntegerHolder, DateHolder, ...
def holder = matrix.getHolder('premium')
if (holder.isNull()) {
return null // the cell really was empty
}
holder.getBigDecimal() // null-preserving
holder.doubleValue() // 0.0 for an empty cell
getHolder accepts the same addressing forms as get: getHolder(), getHolder(col),
getHolder('name'), getHolder(row, col), getHolder(row, 'name'). The full holder
surface is described in
Built-in Objects in a Function.
Iterating Over Rows#
// 1. index loop — toStringInline() prints a whole row on one line, useful when logging
for (int i = 0; i < matrix.size(); i++) {
log.info("row {}: {}", i, matrix.row(i).toStringInline())
}
// 2. for-in, backed by the matrix iterator
for (def row in matrix) {
def price = row.getDecimal('price')
}
// 3. rows() with the index
matrix.rows().eachWithIndex { row, i ->
log.info("row {}: {}", i, row.getString('brand'))
}
// 4. columns of one row — reads a table whose column names you do not know yet
def row = matrix.row(0)
for (int c = 0; c < row.size(); c++) {
log.info("column[{}] = {} ({})", c, row.get(c), row.getHolder(c)?.getClass()?.simpleName)
}
The last loop is the fastest way to inspect an unfamiliar table from the Tester: it prints every column with its index, value and declared type.
The Matrix as a Collection#
matrix.rows() returns a java.util.List, so the full Groovy collection API applies to a decision table result.
This is usually shorter and clearer than an index loop:
def matrix = higson.getValue('demo.motor.price', ctx)
// one column across all rows
def allPrices = matrix.rows().collect { it.getDecimal('price') }
// filter rows on a business condition
def available = matrix.rows().findAll { it.getBoolean('isAvailable') }
// first row satisfying a condition, null when there is none
def cheapest = matrix.rows().find { it.getDecimal('price') < 50000 }
// aggregate — the elvis operator protects against a null cell
def total = matrix.rows().sum { it.getDecimal('price') ?: BigDecimal.ZERO }
// extreme row
def topRow = matrix.rows().max { it.getDecimal('price') }
// sort — ALWAYS pass false; see the warning below
def sorted = matrix.rows().sort(false) { it.getDecimal('price') } // false = work on a copy
// group rows into buckets
def byBrand = matrix.rows().groupBy { it.getString('brand') }
// rebuild rows as maps — the usual return shape for a function exposed over Runtime REST
def result = matrix.rows().collect { row ->
[ brand: row.getString('brand'), price: row.getDecimal('price') ]
}
rows() is a live view of the matrix, not a copy. Sorting it in place — rows().sort { }
without the false argument — permanently reorders the matrix, so every later row(i) and
get(rowNo, ...) sees the new order. Pass sort(false) { } unless you mean to reorder it.
Raw Access#
When a result has to be handed to Java code that expects the engine’s own types, unwrap it:
matrix.row(0).unwrap() // Object[] — raw values of one row, no holders
matrix.row(0).raw() // MultiValue — the engine's row object
matrix.unwrap() // ParamValue — the engine's matrix object
Reading a Cell Without Choosing a Type#
higson.get(table, ctx) reads the first cell and hands back whatever the column holds —
a numeric column arrives as double, an integer column as long, anything else as the
raw object. Prefer the typed getters unless you genuinely do not know the column type.
Functions Returning a Matrix#
A function can return a matrix just like a table does. Retrieve it with callValue:
def matrix = higson.callValue('demo.motor.price.variants', ctx)
def variants = matrix.rows().collect { it.getString('code') }
A matrix from callValue guards differently from one from getValue. callValue
always wraps the result in an ordinary matrix, never the empty one, so on a zero-row result
row(0) throws INVALID_ROW_INDEX rather than returning null. Check size() or
isEmpty() before addressing a row — the row() == null idiom does not apply here.
The other call* methods are listed in
Calling Other Functions.
Calling a Flow#
A flow can be invoked from a function body too. It returns a map of the variables the flow marks for return:
def result = higson.flow('demo.pricing.flow', ctx)
def premium = result.premium
def withArgs = higson.flow('demo.pricing.flow', ctx, 'FULL', 3)
This is the function-body route. The $w notation described in
Using Functions in Cascade Expressions
is the configuration-level one — same flows, different call site.
Quick Reference#
def m = higson.getValue('table.code', ctx) // matrix
if (m == null || m.isEmpty()) return null // always guard
m.size() // row count
m.row(0).size() // column count
m.get('col') // row 0, by name
m.get(1, 'col') // row 1, by name
m.row(1).get('col') // same, preferred form
m.row(1).getDecimal('col') // converted
m.row(0).getStringList('tags') // multi-value column (row only)
m.rows().findAll { it.getBoolean('active') } // matrix as a collection