Coercion
Grid is loosely typed at the value level, like a spreadsheet: a formula may receive a number where it expected text, a blank where it expected a number, or a boolean in an arithmetic…
Coercion
Grid is loosely typed at the value level, like a spreadsheet: a formula may receive a number where it expected text, a blank where it expected a number, or a boolean in an arithmetic expression. Coercion is the set of rules that decide what happens.
The rules below describe Grid's value conversions.
For the kinds and tags these rules operate on, see
reference.md. For how errors flow
(they do not coerce — they propagate), see errors.md.
1. Where Coercion Happens
There are four places a value may be coerced:
- Operators — arithmetic, comparison, concatenation, logical.
- Function arguments — a function that expects a number coerces what it can and errors on what it can't.
- Type-tagged assignments —
A1 is currency = "100"coerces the string to a number before the tag check. - Rule-action read-modify-write —
add/sub/mul/divcoerce to number;concat/prependcoerce to text.
Coercion never mutates a stored value: it produces a converted value for the operation at hand. The binding keeps whatever kind its formula produced.
2. Coercion To Number
Used by arithmetic (+ - * / // ^ %% MOD), numeric comparison, and any
function that wants a number.
| From | Result |
|---|---|
| number | the number |
| boolean | TRUE → 1, FALSE → 0 |
| blank | 0 |
empty string "" |
0 |
| numeric string | parsed (whitespace trimmed): "5" → 5, " 3.14 " → 3.14 |
| non-numeric string | #VALUE! (e.g. "abc") |
| complex with zero imaginary part | the real part |
| anything else (array, object, date) | not a scalar number — function-specific |
"5" + 3 # 8 (numeric string coerces)
TRUE + 1 # 2 (TRUE = 1)
5 + BLANK # 5 (blank = 0)
"abc" + 1 # #VALUE! (non-numeric string)
Blank as zero is what makes self-referential accumulators settle:
E1 = E1 + 1 resolves to 1 on first fire because E1 is blank
(→ 0) before it exists.
3. Coercion To Boolean
Used by IF, AND/OR/XOR/NOT, conditions in WHEN, and the
THEN … ELSE guard.
| From | Truthiness |
|---|---|
| boolean | itself |
| number | TRUE iff non-zero |
| string | TRUE iff non-empty |
| blank | FALSE |
| error | FALSE |
| complex | TRUE iff real or imaginary part is non-zero |
| array / date / object | TRUE |
IF(0, "y", "n") # "n" (0 is falsy)
IF("", "y", "n") # "n" (empty string is falsy)
IF("text", "y", "n") # "y" (non-empty string is truthy)
IF(BLANK, "y", "n") # "n"
4. Coercion To Text
Used by concatenation (&), string functions, and interpolation.
| From | Text |
|---|---|
| string | itself |
| number | its decimal form (42 → "42", 3.5 → "3.5") |
| boolean | its text form |
| blank | "" (empty string) |
| date | its ISO form |
| error | the error literal (e.g. "#DIV/0!") |
| complex | "re+imi" |
| array / object | not a scalar — use the relevant function |
"foo" & "bar" # "foobar"
"count: " & 42 # "count: 42" (number coerces to text)
`net={A1}` # interpolation coerces A1 to text
For controlled formatting (currency, thousands separators, fixed
decimals), use TEXT(value, "pattern") or an interpolation format spec
(`{A1:"$#,##0.00"}`) rather than relying on default text coercion.
5. Comparison Across Kinds
Grid operator equality (=, <>/!=) is type-sensitive and case-sensitive:
it does not coerce text to a number, and text values compare by exact code
points. The type boundary matches Excel operator equality—a number is not
equal to numeric-looking text—but Excel's operator text equality is
case-insensitive. Use EXACT when spelling the shared Grid/Excel
case-sensitive operation explicitly. Coerce explicitly when numeric text
should compare as a number. Exact numeric values remain interoperable with
compatible ordinary numbers because both are numeric representations.
5 = 5 # TRUE
"5" = 5 # FALSE — string and number are different kinds
"Grid" = "grid" # FALSE — Grid operator equality is case-sensitive
EXACT("Grid", "grid") # FALSE — explicit and portable case-sensitive test
VALUE("5") = 5 # TRUE — conversion is explicit
BIGINT(5) = 5 # TRUE — exact and ordinary numeric representations
Ordering (<, <=, >, >=) compares two text operands lexically, even
when both strings look numeric. For different kinds, loose mode coerces both
sides to number where possible. If they cannot be compared numerically, Grid
uses Excel's cross-kind ordering:
number < text < boolean
So any text sorts greater than any number, and any boolean greater than any text:
"10" < "2" # TRUE (same-kind lexical comparison)
"5" < 6 # TRUE (different kinds; loose numeric coercion)
BIGINT(2) < "1" # FALSE (numeric text is promoted into the exact domain)
"a" > 1 # TRUE (text outranks number)
Exact ordering never widens a noninteger binary float into an exact value:
BIGINT(2) < 1.5 and BIGINT(2) < "1.5" return #VALUE!. Under
coercion warn, a successful numeric-text promotion retains the loose result
and emits one warning; under coercion strict, it returns #TYPE!.
For case-insensitive text comparison, use ILIKE or fold case with
LOWER/UPPER first.
6. Operator Coercion Summary
| Operator family | Coerces operands to | On bad input |
|---|---|---|
Arithmetic + - * / // ^ |
number | #VALUE! for non-numeric text |
Modulo %% MOD |
number | #VALUE! / #DIV/0! |
Numeric comparison < <= > >= |
number (else cross-kind order) | cross-kind ordering |
Equality = <> != |
type- and case-sensitive value equality | — |
Concatenation & |
text | — (everything has a text form) |
Logical AND OR XOR NOT && || |
boolean (truthiness) | — |
Bitwise BITAND … |
integer (truncates) | #VALUE! |
6.1 Strict Mode Rejects Silent Coercion
The table above describes loose, the default. A model headed with
strict (see reference.md)
turns a lossy coercion in an arithmetic or comparison operand into a
TYPE_ERROR instead of converting silently:
strict
A2 = A1 + 1 # #TYPE! when A1 is blank or "5"
A2 = 40 + 2 # 42 — no coercion happened
Only the silent conversion is rejected. A genuine number flows through
untouched, so strict never changes the result of arithmetic that had no
coercion to begin with. strict except coercion keeps this axis loose
while still tightening dimensions. To set this axis by itself — including
the warn middle state the umbrella cannot spell — use the per-axis
header below.
6.2 The Per-Axis coercion Header
coercion strict|warn|loose|off sets the coercion axis alone, without
touching dimensions (mirror of the dimensions header for units; see
reference.md):
| Mode | Effect on the conversions in §2–§5 |
|---|---|
coercion strict |
Lossy blank/string→number coercions in arithmetic and comparison become #TYPE! |
coercion warn |
Results stay loose; lossy coercions are flagged as diagnostics |
coercion loose |
Spreadsheet coercion, the default (same as omitting the header) |
coercion off |
Alias for loose |
Directives combine last-wins per axis, so a coercion header after the
umbrella overrides just this axis. Rejecting on this axis alone:
coercion strict
A1 = BLANK
A2 = A1 + 1 # #TYPE! — blank → 0 is rejected on this axis alone
And keeping results loose while surfacing the conversions:
coercion warn
A1 = "5"
A2 = A1 + 3 # 8 — the lossy conversion is flagged, not rejected
coercion strict enforcement matches the umbrella's coercion axis today
(scalar arithmetic and comparison operands). warn records the mode with
the model and its diagnostics surface incrementally — not every lossy
site emits its warning yet.
7. Type-Tag Coercion On Assignment
A type tag declares a semantic meaning over a kind. Under
strict, on assignment the
produced value is checked for kind compatibility with the tag's
representation root, after the same value coercions above:
strict
A1 is currency = 100 # OK — number is currency-compatible
A1 is currency = "100" # OK — string coerces to a number first
A1 is currency = "hello" # #TYPE! — not coercible to a number
A1 is percentage = 21pct # OK — 0.21, percentage-tagged
The check is part of the coercion axis — loose (the default) applies
the tag as an overlay without checking, and strict except coercion also
skips it. Either way, tags do not change arithmetic: a currency-tagged
1000 is still the number 1000 in A1 * 1.1. See
reference.md and
assignments.md.
7.1 Unit Conversion
INTO / TYPE_TAG overlays a semantic tag and participates in dimensional
checking, but it does not rescale a number. Use CONVERT(value, from, to) for numeric unit conversion:
CONVERT(3, "m", "ft") # 9.842519685...
CONVERT(10, "m/s", "km/h") # 36
CONVERT(0, "C", "K") # 273.15
CONVERT(10, "delta_C", "delta_F") # 18
The conversion catalog understands ISO currency unit tags, SI/derived units,
rational exponents, absolute temperature units (C, F, K), and explicit
temperature delta units (delta_C, delta_F, delta_K). Absolute temperatures
convert with offsets; deltas convert by scale only. Converting between absolute
and delta temperature units returns #VALUE!.
Model-scoped custom units declared with unit <name> = ... participate in the
same catalog for compiled models, as long as they are scale-only aliases over
linear units.
Currency conversion never uses ambient FX. CONVERT(10, "USD", "EUR") returns
#VALUE!; currency conversion must be explicit with FX_RATE or with a
model-owned rate value declared by fx_rate <target> = BASE/QUOTE and populated
from model data, a table, or an explicit connector input.
8. Function Argument Coercion
Most built-ins coerce each argument to the kind they need, using the rules in §2–§4, and aggregate functions are lenient about non-numeric cells in a range:
SUM(1, "2", TRUE, BLANK) # 4 (numeric strings, TRUE=1, blank=0)
SUM([1, "x", 3]) # 4 (range aggregation skips non-numeric)
ROUND("3.14159", 2) # 3.14 (numeric string coerces)
A scalar function argument that cannot be coerced returns #VALUE!,
while range aggregations (SUM, AVERAGE, COUNT, …) generally
skip cells that aren't numeric rather than erroring. Use AGGREGATE
with an error-ignoring mode when a range may contain error values:
AGGREGATE(9, 6, B1:B10) # SUM, ignoring errors
See functions.md for each function's contract.
9. Errors Do Not Coerce
Error values are never coerced into a "normal" value by an operator or function — they propagate. The first error in a positional argument list wins:
A1 = #N/A
A2 = A1 + 5 # #N/A
A3 = SUM(A1, 1, 2) # #N/A
A4 = IFERROR(A1, 0) # 0 (explicit error-handling function)
Only dedicated error-handling forms (IFERROR, IFNA, TRY,
DEFAULT/??, ?=, ISERROR, …) consume an error. Structural collection
predicates are a narrow inspection exception: IN/HAS, SUBSET/SUPERSET,
OVERLAPS, and ordered CONTAINS compare an error member by code without
coercing it; diagnostic message text is not identity. Ordinary arithmetic and
functions still pass errors through. Full rules: errors.md.
10. Gotchas
10.1 Blank is not empty string
A1 = "" # empty string
A2 = BLANK # blank
ISBLANK(A1) # FALSE
ISBLANK(A2) # TRUE
"" + 1 # 1 (empty string → 0 in arithmetic)
Both coerce to 0 in arithmetic and to falsy in boolean context, but
ISBLANK distinguishes them.
10.2 Numeric strings coerce, words don't
"5" + 3 # 8
"5 apples" + 3 # #VALUE! (not a clean number)
10.3 Boolean arithmetic is intentional
SUMPRODUCT((region = "EU") * amount) # TRUE/FALSE → 1/0 enables masks
10.4 Don't rely on default number-to-text formatting
"Total: " & 1234.5 # "Total: 1234.5"
"Total: " & TEXT(1234.5, "$#,##0.00") # "Total: $1,234.50"
11. See Also
reference.md— kinds and type tags.assignments.md— tag checks.errors.md— error propagation and#TYPE!.functions.md— per-function argument contracts.