RFC-023: Quantities in Law YAML (Money, Percentages, and Dimensioned Values)

ProposedImplemented
Depends on
RFC-001 (YAML Schema Design Decisions)
Required by
RFC-024 (Precision and Rounding in Law YAML (Modeling Statutory Rounding))

Summary

Today a law YAML stores money, ratios and percentages as bare numbers, so 3971900 or 0.01896 is indistinguishable from any other number, a factor-of-100 faithfulness risk. This RFC makes a value’s quantity-kind an explicit label.

Decided (YAML/modeling layer):

  1. Declare quantity-kind with type + type_spec.unit, on inputs, outputs, parameters and definitions constants.
  2. Extend the type_spec.unit enum with euro, ratio, percentage (alongside existing eurocent, years, weeks, months, days).
  3. ratio and percentage are distinct labels; any division by 100 is an explicit value operation, never implied by the label.
  4. A unit is a label, not a computational constraint; tagging a value never changes it.
  5. definitions entries may be optionally structured to carry type/type_spec; the bare naam: 123 form stays valid.

Out of scope (deferred): how the engine checks, infers or reconciles units across laws. Rounding is RFC-024. See In practice for worked YAML.

Context

The question is narrow: how does a law YAML say what a number means? Today it often can’t. The situations below, all from the current corpus, are what the language must be able to express.

1. The corpus already encodes quantities, but ambiguously. wet_op_de_zorgtoeslag stores, in one definitions block, both a money amount and a ratio as bare numbers:

# corpus/regulation/nl/wet/wet_op_de_zorgtoeslag/2025-01-01.yaml (definitions, ~L56) drempelinkomen_alleenstaande: value: 3971900 # eurocent, i.e. € 39.719,00. Nothing here says "eurocent". percentage_drempelinkomen_alleenstaande: value: 0.01896 # a 0–1 ratio (1,896%). Nothing here says "ratio, not percent".

Guess wrong and you are off by a factor of 100, in either direction.

2. The same legal concept is stored in two incompatible ways. Where zorgtoeslag uses decimal ratios, the Diemen afstemmingsverordening_participatiewet stores percentages as whole integers, and these are bare, untyped constants in the schema-free definitions block: no type, no unit, nothing to disambiguate them:

# corpus/regulation/nl/gemeentelijke_verordening/diemen/ # afstemmingsverordening_participatiewet/2015-01-01.yaml (definitions, ~L69) definitions: # schema-free: `{ "type": "object", "additionalProperties": true }` PERCENTAGE_CATEGORIE_1: value: 5 # 5 percent (a bare integer, no type, no unit) PERCENTAGE_CATEGORIE_2: value: 30 # 30 percent (indistinguishable from a raw multiplier 30)

There is no way today to tell “30 percent” from “raw multiplier 30.” The type: number declarations that do appear elsewhere in this file sit on parameters and input/output fields, never on these constants, which by construction cannot carry a type (see situation 5).

3. Quantities cross law boundaries. A consuming law pulls another law’s output via source.regulation + output, e.g. zorgtoeslag consumes toetsingsinkomen from the AWIR (algemene_wet_inkomensafhankelijke_regelingen). Both ends are already labeled today: the AWIR output and the zorgtoeslag input each declare type: amount + type_spec.unit: eurocent. So the YAML can already express this; what is missing is checking that producer and consumer agree, which is an engine concern (see Implementation Notes), not a gap in the language.

4. Money is routinely sub-cent. The corpus is social-domain today, but fiscal law is coming, and it is dense with sub-cent amounts, reached two different ways:

  • A scaled base unit. Excise duty (accijns) is denominated per 1.000 liter at two decimals, e.g. € 844,69 / 1.000 L (enacted 2026). The printed figure is tidy, but the per-liter value it implies is € 0,84469 = 84,469 eurocent per liter, not a whole number of cents.
  • A tariff printed directly at sub-cent precision. Energy tax (energiebelasting, first bracket electricity) is € 0,09161 / kWh (2026, excl. VAT): a five-decimal per-kWh rate set straight in the legislation, no scaling trick involved.

Different mechanisms, same modeling consequence: a money value is not always a whole number of cents.

This forces a principle the rest of this RFC relies on: a unit is a label, not a computational constraint. The arithmetic and the literal values determine the sum, so tagging a value never changes it by itself. The one way a unit: eurocent tag could corrupt a correct sum is by triggering implicit rounding, which is why labeling and rounding are kept strictly separate (rounding lives in RFC-024).

5. The place where misreading originates cannot carry a label at all. The definitions block, the home of every constant in points 1–2, is schema-free: in the schema it is { "type": "object", "additionalProperties": true }. Inputs and outputs can carry type / type_spec; constants cannot. That is backwards: constants are raw magic numbers with no surrounding context to disambiguate them.

Decision

A value’s quantity-kind becomes a first-class, explicit label in the law representation. The table maps each situation above to the YAML expressiveness it requires and the modeling primitive that provides it.

#Situation in legislation (grounded)What the YAML must expressModeling primitive (YAML layer)
1Money in whole cents (zorgtoeslag 3971900)“this is money, denominated in eurocent”type: amount + type_spec.unit: eurocent (exists)
2Sub-cent tariffs (accijns, energiebelasting)“this money value is not a whole number of cents; the label must not force whole cents”a money label usable without implying integer cents (euro / eurocent), paired with type_spec.precision (in the schema; issue #444) to state decimal places. Rounding is out of scope (see RFC-024)
3Percentage stored as ratio (0.01896, 0.137)“this is a 0–1 ratio, not a 0–100 percent, not a bare multiplier”type_spec.unit: ratio
4Percentage stored as whole percent (5, 30)“this is a 0–100 percent”type_spec.unit: percentage
5Constants in definitions carry no type”a constant can declare its quantity-kind”make definitions entries optionally structured so a constant carries type + type_spec; the bare naam: 123 form stays valid

(Durations and cross-law unit flow are already expressible with existing vocabulary and are not decisions of this RFC; see Already expressible below.)

Concretely, this RFC decides, at the YAML/modeling layer:

  1. Quantity-kind is an explicit, first-class label, declared with type + type_spec.unit, available on inputs, outputs, parameters, and definitions/constants, closing the schema-free definitions gap (situation 5).
  2. Extend the type_spec.unit enum with euro, ratio, and percentage, alongside the existing eurocent, years, weeks, months, days. Adding euro does not deprecate eurocent; both are valid money labels and the choice is the modeler’s, reflecting how the law denominates the value (cf. accijns per 1.000 L).
  3. ratio and percentage are distinct labels for the same dimension, and the corpus keeps both. We do not force a single canonical form, because some texts state “1,896” as a ratio and others state “30 procent” literally. Whether a percentage is divided by 100 is a value concern, expressed by an explicit DIVIDE … 100 in the YAML where the value is applied, never something the label silently performs. The corpus already works this way, and across a law boundary: the Diemen afstemmingsverordening_participatiewet stores the verlagingspercentages as whole percents (5, 30) and uses them directly in its IF cases, while the national participatiewet that consumes that verlaging_percentage (via the art. 8 open term) performs the … * verlaging_percentage / 100 at the point of application (participatiewet/2022-03-15.yaml:432).
  4. A unit is a label, not a computational constraint. As a normative rule: tagging a value with a unit must never, by itself, change its numeric value (no implicit rounding or scaling). This is the deliberate repudiation of coupling a unit to a coercion (e.g. unit: eurocent → round_to_integer, proposed in RFC-015); where rounding belongs is decided in RFC-024.
  5. definitions entries become optionally structured. The bare form naam: 123 remains valid (backward-compatible); the structured form lets a constant carry type and type_spec, reusing the existing field shape so there is one way to describe a quantity everywhere.

This RFC does not decide how the engine checks, infers, or reconciles units, only what the YAML may say. See Implementation Notes.

In practice

The zorgtoeslag constants and a cross-law input, labeled per this RFC (compare the bare forms in Context situations 1–2):

definitions: # money constant (was a bare 3971900) drempelinkomen_alleenstaande: value: 3971900 type: amount type_spec: unit: eurocent # a 0–1 ratio, explicitly not a percentage percentage_drempelinkomen_alleenstaande: value: 0.01896 type: number type_spec: unit: ratio # a whole percent; the /100 happens where it is applied, never via the label PERCENTAGE_CATEGORIE_2: value: 30 type: number type_spec: unit: percentage input: # an input states the unit it expects to receive from another law - name: toetsingsinkomen type: amount source: regulation: algemene_wet_inkomensafhankelijke_regelingen output: toetsingsinkomen type_spec: unit: eurocent

The bare naam: 123 form stays valid, so labeling is incremental: a constant is upgraded only when someone annotates it.

Already expressible

Two situations the corpus raises are already covered by existing modeling vocabulary. They are not decisions of this RFC; they are recorded here so authors model them with the existing primitive instead of reaching for a bare type: number:

  • Durations. A value meant as months / years / days is modeled with the existing time units years, weeks, months, days (RFC-001), not a bare type: number. No new unit is required.
  • Cross-law unit flow. The consuming source input already declares its expected type_spec.unit, and producing laws label their outputs too (Context situation 3: zorgtoeslag’s toetsingsinkomen and the AWIR output both carry unit: eurocent). Reconciling a producer/consumer mismatch is an engine concern (see Implementation Notes), not a YAML-layer addition.

Why

Benefits

  • A labeled value states its quantity-kind explicitly, removing the factor-of-100 ambiguity at the source. The label is not self-enforcing (at the YAML layer it is inert until a consumer acts on it; engine unit-checks are deferred, see Implementation Notes), but a reader, human or tool, no longer has to guess whether 3971900 is euros or cents.
  • The label is consumed by more than the engine: the editor renders the correct input control from a value’s type/type_spec (a boolean switch, a euro field; see PR #748), so a single YAML-layer decision pays off across the engine, the editor UI, and scenario inputs.
  • Cross-law flows become checkable once the engine reconciles units (deferred): the consuming law states the unit it expects to receive.
  • It closes the definitions blind spot, where the factor-of-100 ambiguity actually originates.
  • It is opt-in and backward-compatible: bare constants and unlabeled values keep working, so adoption is gradual and no corpus-wide rewrite is forced.

Tradeoffs

  • Opt-in labeling means un-annotated laws stay ambiguous until someone annotates them. Acceptable: the alternative (mandatory labels) would force a corpus-wide rewrite and block on it.
  • Keeping both ratio and percentage preserves a known representational inconsistency. We accept it deliberately: collapsing to one canonical form would distort faithful transcription of laws that literally say “30 procent” versus those that state a ratio.

Alternatives Considered

Alternative 1: a single canonical percentage form (ratio only). Force every percentage to a 0–1 ratio. Rejected: it distorts faithful modeling. A law that says “30 procent” should be transcribable as 30 with a percentage label, not silently rewritten to 0.30.

Alternative 2: drop eurocent, use euro floats everywhere. Rejected: much of the corpus and Belastingdienst convention is cent-denominated, and forcing euro floats invites the very precision loss this work is trying to prevent.

Alternative 3: bundle rounding into the unit (the RFC-015 coercion). Let unit: eurocent imply round-to-integer. Rejected: empirically false for sub-cent tariffs (accijns, energiebelasting), and it hides a legal decision inside a unit label. Rounding must be a law-modeled instruction (see RFC-024).

Alternative 4: leave definitions schema-free and rely on naming conventions. Rejected: that is the status quo that produces the factor-of-100 ambiguity in situations 1–2.

Implementation Notes

This RFC is intentionally silent on the engine. The unit algebra (which unit pairs are valid under each operation), unit inference over expression trees, the static-validation and runtime mismatch checks, the error type for a mismatch, and cross-law unit reconciliation are engine concerns, specified and implemented separately. A prototype of that engine machinery already exists in PR #729 (units of measurement on values), which originally drafted itself as “RFC-019” (a number since assigned on main to an unrelated RFC, Law End Dates). This RFC supersedes the modeling decisions in that draft, reframed situation-first; the Rust code in PR #729 can serve as the implementation of this RFC’s deferred engine track. The only concrete artifact this RFC implies is a future schema version that adds the euro/ratio/percentage units and the optional structured definitions form (schema versions are immutable; the current version is v0.5.3).

References

RegelRecht

An exploration by Bureau Architectuur of the Dutch Ministry of the Interior into the possibilities of transparent, executable legislation.

Links

GitHub repository
How it works
Stay informed
Documentation

Contact

regelrecht@minbzk.nl

Part of

Bureau Architectuur
Ministry of the Interior and Kingdom Relations