This RFC answers a modeling question: how does a law YAML express the rounding and precision that the law text prescribes? As with RFC-023, the starting point is the legislative situation, not the engine. The catalog is drawn from the current corpus.
1. Rounding is written in the law text, and we currently cannot execute it. The AWIR says “Het
bedrag van de tegemoetkoming wordt rekenkundig afgerond op hele euro’s”
(algemene_wet_inkomensafhankelijke_regelingen/2025-01-01.yaml:298); the Zorgverzekeringswet says
“Het op grond van het eerste of tweede lid berekende bedrag wordt afgerond op hele euro’s”
(zorgverzekeringswet/2025-01-01.yaml:537). Neither is executed. The clearest case is
test_untranslatables/2025-01-01.yaml, where “naar boven afgerond op hele euro’s” sits as an
unaccepted untranslatable:
RFC-012 names rounding as the canonical untranslatable and suggests adding a rounding operation. This RFC is the situation-first mechanism that resolves it, in keeping with RFC-012’s own rule that the vocabulary grows only when real legal texts demand it.
2. Rounding has both a direction and a target. Directions seen in or expected from Dutch law: rekenkundig (half-up), naar boven (ceil), naar beneden / afkapping (floor). Targets: whole euro, whole cent, or a number of decimals. “Round to whole euros, half-up” and “round up to whole euros” are different instructions and must be distinguishable.
3. Thresholds and caps are rounding-adjacent and under-modeled. The AWIR continues: “Een
tegemoetkoming wordt niet toegekend indien deze minder dan € 24 zou bedragen” (:299), a
below-threshold-to-nothing rule that is not modeled today. Zero-floors (MAX(0, …) in zorgtoeslag)
and wealth caps (vermogensgrens, via LESS_THAN_OR_EQUAL) are modeled with existing operations.
4. Intermediate precision matters, and intermediate values must not be rounded. The
Zorgverzekeringswet computes a day-based pro-rata fraction (“vermenigvuldigd met een breuk waarvan de
teller gelijk is aan het aantal dagen … en de noemer aan het aantal dagen in het … kalenderjaar”,
:522) and only then rounds the result to whole euros (:537). Two distinct rules apply, from two
distinct sources (researched in issue #299). First, intermediate calculations are not rounded:
standing Belastingdienst/Toeslagen administrative policy, not a court ruling. Second, rekenkundig
(half-up) is the default rounding method unless a law says otherwise: established by the Hoge Raad
(HR 15 May 2009, ECLI:NL:HR:2009:BI1980, itself a VAT case). Double-rounding an intermediate value
shifts a legally binding amount.
5. The engine currently rounds money implicitly, which is the wrong place for the decision. Today
the engine rounds eurocent outputs at the result boundary regardless of what the law says, and
RFC-015 proposes formalizing that as a unit: eurocent → round_to_integer coercion.
But “currency must be whole cents” is empirically false (sub-cent accijns and energiebelasting tariffs;
see RFC-023 §Context). The law, not a unit-coercion policy, must decide whether and how a value is
rounded. This RFC supplies the law-level vocabulary that lets that implicit engine behavior become
law-controlled (issue #304).
Rounding becomes an explicit, modeled legal instruction. The table maps each situation to the YAML expressiveness it requires and the primitive that provides it.
| # | Situation in legislation (grounded) | What the YAML must express | Modeling primitive |
|---|---|---|---|
| 1 | ”rekenkundig afgerond op hele euro’s” (AWIR, zvw) | round to nearest, half-up, at a named point | ROUND + precision (half-up is its default) |
| 2 | ”naar boven afgerond op hele euro’s” (the open untranslatable) | round up | CEIL + precision (flips the accepted: false entry to expressible) |
| 3 | ”naar beneden” / “afkapping” | round down | FLOOR + precision (toward-zero TRUNCATE is deferred; see Alternative 4) |
| 4 | Round to whole cent / to N decimals (sub-cent tariffs, e.g. 5 dp) | the granularity to round to | the precision field (decimal places) on any of these operations |
| 5 | Pro-rata fraction then round (zvw art. 21) | full precision through the intermediate DIVIDE; round only at the named step | principle: no implicit intermediate rounding |
(Below-threshold-to-nothing and zero-floor/cap need no new operation; they are existing patterns, see Already expressible below. Banker’s rounding / half-even is not in scope; see decision 2.)
Concretely, this RFC decides, at the YAML/modeling layer:
Rounding is an explicit, modeled legal instruction, never an implicit side effect of a type or unit. This is the direct counter to the current boundary rounding and to RFC-015’s unit coercion.
Introduce three distinct rounding operations: ROUND (round to nearest), CEIL (round up),
and FLOOR (round down). Each is a unary operation: one operand
in value: (mirroring the unary NOT), plus a precision, the number of decimal places to
round to, in the value’s own unit (RFC-023). precision is a field on the operation itself,
deliberately not read implicitly from the operand’s type_spec, so the rounding instruction is
always visible where it applies. precision: 0 rounds to whole units, precision: 5 to five
decimals; rounding to whole euros is precision: 0 on a euro-denominated value (and precision: -2
on one held in eurocent). This is the abstract granularity knob; no money-specific “whole euro”
keyword in the language.
Distinct functions get distinct names, consistent with RFC-004’s ADD/SUBTRACT and MIN/MAX
and its stated philosophy that “the operation name describes what happens.” CEIL, FLOOR
and round-to-nearest are genuinely different functions, not modes of one, so they are
not collapsed into a single mode-parametrised operation (see Alternatives Considered).
ROUND is round-to-nearest with half-up as its default tie-break (the HR-2009 default). A
different tie-break, banker’s rounding / half-even, is not part of this RFC: no corpus text
demands it yet, so per RFC-012 it is left to a future optional tie: field on ROUND alone
(CEIL/FLOOR have no tie-break). RFC-014 already anticipates rounding
joining the Core conformance level “when added.”
Worked example: the AWIR’s “rekenkundig afgerond op hele euro’s” (:298), rounding the
full-precision result of an earlier SUBTRACT only at the named step:
The open untranslatable “naar boven afgerond op hele euro’s” (situation 2) is then a CEIL:
And rounding to a number of decimals is just a non-zero precision: here an energiebelasting line
rounded half-up to whole cents (two decimals of a euro), or to five decimals for a sub-cent tariff:
Half-up (rekenkundig) is ROUND’s documented default tie-break (per the Hoge Raad’s 2009 VAT
ruling; issue #299), but rounding is never applied implicitly. A law that rounds must say so
with an explicit rounding operation. Where the law is silent, no rounding occurs (matching the
“don’t round intermediates” policy).
Intermediate values carry full precision. As a normative rule: nothing rounds a value between steps; only an explicit rounding operation changes precision. (Grounded in zvw art. 21 and issue #299.)
This resolves the rounding untranslatable. Once these rounding operations exist, the
test_untranslatables “naar boven afgerond” entry (a CEIL) flips from accepted: false to
expressible, the concrete legal text that, per RFC-012, justifies growing the vocabulary.
This RFC does not specify the rounding algorithm or its edge cases, only the modeling vocabulary and the legal-default policy. See Implementation Notes.
These rounding-adjacent situations need no new operation: existing RFC-004 operations already cover them. They are not decisions of this RFC; they are recorded as patterns (with a worked example) so authors model them instead of leaving them implicit:
:299) is an IF
with a LESS_THAN comparison. The AWIR € 24 floor is the worked example authors should follow.MAX(0, …), the vermogensgrens cap) uses
the existing MIN / MAX operations, no rounding primitive involved.Alternative 1: keep rounding as an untranslatable. Rejected: rounding is pervasive and statutorily explicit; perpetual untranslatability blocks faithful execution of the most common money operation in the corpus.
Alternative 2: rounding via the unit-coercion policy (RFC-015) or the engine’s boundary rounding. Rejected: empirically wrong for sub-cent tariffs, and it hides a legal decision in engine configuration. The law text must control rounding.
Alternative 3: a single ROUND operation with a mode field (mode: half_up|ceil|floor).
One appeal is that the rounding direction becomes one comparable field and new variants slot in without
new operations. Rejected: ceil, floor and round-to-nearest are genuinely different
functions, not spellings of one, and RFC-004 already names different functions distinctly
(ADD/SUBTRACT, MIN/MAX), with the stated philosophy that “the operation name describes what
happens.” A single mode-parametrised ROUND would be the only such special case in the vocabulary.
(The IF/SWITCH unification is not a precedent here: those were two syntaxes of one control-flow
operation, whereas the rounding directions are distinct functions. RFC-004 is also not a flat enum:
it groups operations by purpose with distinct operand fields, so “matching the enum” argues nothing.)
Keeping the operations distinct also keeps each schema trivial: only ROUND ever needs a tie-break
field; CEIL/FLOOR need none.
Alternative 4: also ship TRUNCATE (round toward zero) now.
Rejected for this RFC: TRUNCATE and FLOOR differ only for negative values (FLOOR(-2.3) = -3,
TRUNCATE(-2.3) = -2); for non-negative values they are identical. The amounts Dutch law rounds
“naar beneden” / “afkapping” (bedragen, tarieven, breuken) are non-negative, so FLOOR already
covers every grounded corpus case, and no corpus text demands toward-zero rounding of a negative.
Per RFC-012’s rule that the vocabulary grows only when a real legal text demands it, TRUNCATE is
deferred until such a text appears; it can then be added as a fourth unary operation with no change to
the others.
Alternative 5: only round-to-nearest (ROUND), no CEIL/FLOOR.
Rejected: the corpus already needs ceil (“naar boven”) and floor (“naar beneden” / “afkapping”), and
shipping CEIL without its directional mirror FLOOR would leave the up/down/nearest set lopsided.
Alternative 6: encode rounding with arithmetic tricks (e.g. FLOOR(x + 0.5) via existing ops).
Rejected: RFC-012 explicitly flags “arithmetic chains that approximate rounding” as a workaround to be
caught in review. An explicit primitive is auditable and self-documenting.
This RFC is silent on engine internals. The rounding algorithm, the float-versus-decimal representation question, half-even tie-breaking, division-by-zero in pro-rata fractions, and the removal or replacement of the engine’s current boundary rounding are engine concerns specified elsewhere: the conformance hooks in RFC-014, the engine-policy track in RFC-015, and the open PR #791 (which moves boundary rounding to a once-only, precision-driven step). This RFC decides the modeling vocabulary and the legal-default policy; the engine track decides the computation.
ROUND/CEIL/FLOOR joinprecision is relative totype_spec (the
type_spec.precision field a rounding result targets lives in the schema; see issue #444)type_spec.precision parsed but not enforcedAn exploration by Bureau Architectuur of the Dutch Ministry of the Interior into the possibilities of transparent, executable legislation.
Bureau Architectuur
Ministry of the Interior and Kingdom Relations