# Numbers and money Until now the archive norms answered "yes", "no", and "not established". But copy fees, overdue fines, and instalments are numbers, and numbers have their own rules: a page counter does not add up with a share, money does not divide by a whole number, and rounding is never implied. In the corpus, comparison stands in 5,518 rules across 293 packages, arithmetic in 3,944 across 210, money and percent literals in 228 across 28 (measured 05.09.2026). The tariffs on this page are for teaching: 150 tenge a page, 200 a day for overdue items, and a 5,000 cap — example numbers, not enacted law. ```law language "law.core" version "0.2"; package tutorial.archive version "0.13.0"; namespace "urn:law:tutorial:archive"; entity Person; entity CopyOrder; relation copy_order(o: CopyOrder, p: Person, pages: Integer) kind empirical { key(o); } relation researcher(p: Person) kind institutional; relation days_overdue(p: Person, days: Integer) kind empirical { key(p); } relation restoration_cost(p: Person, cost: Money) kind empirical { key(p); } relation reader_contribution(p: Person, paid: Money) kind empirical { key(p); } relation reader_discount(p: Person, rate: Decimal) kind empirical { key(p); } relation copy_fee(o: CopyOrder, amount: Money) kind institutional { key(o); } relation researcher_fee(o: CopyOrder, amount: Money) kind institutional { key(o); } relation bulk_order(o: CopyOrder) kind institutional; relation late_fee(p: Person, amount: Money) kind institutional { key(p); } relation instalment(o: CopyOrder, amount: Money) kind institutional { key(o); } relation contribution_share(p: Person, share: Decimal) kind institutional { key(p); } relation discounted_fee(o: CopyOrder, amount: Money) kind institutional { key(o); } const PAGE_RATE: Money = 150 KZT; const RESEARCHER_RATE: Decimal = 75 percent; const BULK_PAGES: Integer = 100; const DAILY_LATE_FEE: Money = 200 KZT; const LATE_FEE_CAP: Money = 5000 KZT; const INSTALMENTS: Decimal = 3.0; ``` ## Four kinds of numbers | Kind | Literal | What it is | |---|---|---| | `Integer` | `7`, `100` | a counter: pages, days, items | | `Decimal` | `0.9`, `3.0`, `75 percent` | an exact decimal fraction; a percent is `Decimal` `0.75` | | `Rational` | no literal: only `7 / 2` | an exact fraction; produced by dividing two integers | | `Money` | `150 KZT`, `5000 KZT` | an amount with a currency; the space is mandatory | The core does not convert kinds implicitly. `Integer` and `Decimal` differ: `7` does not equal `7.0`, and when a whole `n` is compared with `100.5`, the compiler refuses. Kinds meet only in multiplication: whole and decimal numbers act as dimensionless scalars and multiply anything. ## Tariff: a counter times money ```law rule CopyFee strict { for o: CopyOrder; for p: Person; for n: Integer; when copy_order(o, p, n); then copy_fee(o, n * PAGE_RATE); } ``` | Order | `copy_fee` | |---|---| | 7 pages | `1050 KZT` | The test from this page, byte for byte: ```law test "семь страниц по 150 тенге — 1050" { given { context { decision_time @2026-04-20T09:00:00+05:00; knowledge_time @2026-04-20T09:00:00+05:00; legal_time @2026-04-20; timezone "Asia/Almaty"; } assert copy_order(entity_ref("urn:tutorial:order1"), entity_ref("urn:tutorial:ivanova"), 7) { id "assert-order"; origin case_input; } } evaluate truth(copy_fee(entity_ref("urn:tutorial:order1"), 1050 KZT)); expect truth_status == TRUE_ONLY; expect evaluation_status == COMPUTED; } ``` The test name reads: "Seven pages at 150 tenge — 1050." The rule multiplies the page count by the tariff: a whole number scales money and does not change the kind. A `truth` query asks whether exactly this value is established: ask `copy_fee(order1, 1000 KZT)` and you get `NEITHER`. "How much" can also be asked directly, with an expression query, covered below. ## Rate and rounding Researchers pay three quarters of the tariff. `75 percent` is the `Decimal` `0.75`, and money times a decimal fraction is money of the same currency. The result must be rounded explicitly: `round` takes three arguments, none implied. ```law rule ResearcherFee strict { for o: CopyOrder; for p: Person; for n: Integer; for a: Money; when copy_order(o, p, n) and researcher(p) and copy_fee(o, a); then researcher_fee(o, round(a * RESEARCHER_RATE, 2, "HALF_UP")); } ``` | Order | `researcher_fee` | |---|---| | 7 pages, researcher | `787.50 KZT` | The core never rounds by itself — neither money nor shares. If a norm's result may come out inexact, it must name the precision and the mode; otherwise the engine will not answer "about 787" but refuse. ## Threshold and cap Comparison is a body guard, and both its operands share one kind: a whole page count against a whole constant. The fine cap is `min` over two sums of one currency. ```law rule BulkOrder strict { for o: CopyOrder; for p: Person; for n: Integer; when copy_order(o, p, n) and n > BULK_PAGES; then bulk_order(o); } rule LateFee strict { for p: Person; for d: Integer; when days_overdue(p, d); then late_fee(p, min(d * DAILY_LATE_FEE, LATE_FEE_CAP)); } ``` | Facts | Answer | |---|---| | 7 pages | `bulk_order`: `NEITHER` | | 120 pages | `bulk_order`: `TRUE_ONLY` | | 3 days overdue | `late_fee`: `600 KZT` | | 40 days overdue | `late_fee`: `5000 KZT` | Below the threshold the rule stays silent and answers `NEITHER`, not "false". Comparison does not bind a variable but checks it; candidates are enumerated by the positive `copy_order` literal, and without it the compiler refuses. ## Division Division is arranged most strictly: the specification lists the pairs that divide, and the list is closed. Money divides by `Decimal` and only by it; the engine does not silently round an inexact quotient but rejects it. One term divides under a policy — `div_round`: it rounds the exact quotient. ```law rule Instalment strict { for o: CopyOrder; for a: Money; when copy_fee(o, a); then instalment(o, div_round(a, INSTALMENTS, 2, "HALF_UP")); } ``` | Expression | Answer | |---|---| | `1050 KZT / 3.0` | `350 KZT` | | `1000 KZT / 3.0` | issue `INEXACT_DIVISION` | | `div_round(1000 KZT, 3.0, 2, "HALF_UP")` | `333.33 KZT` | | `1050 KZT / 3` | issue `TYPE_ERROR` | | `7 / 2` | an exact `Rational` fraction | | `250 KZT / 1000 KZT` | `0.25` — a share, not money | The fourth row catches most often: a money divisor is written `3.0`, not `3`. A whole number does not act as a scalar here, because the scalar rule applies only in multiplication. The sixth row divides two sums of one currency: the quotient is dimensionless and suits the proportions set by the act itself. ```law rule ContributionShare strict { for p: Person; for c: Money; for paid: Money; when restoration_cost(p, c) and reader_contribution(p, paid); then contribution_share(p, paid / c); } ``` | Facts | `contribution_share` | |---|---| | restoration 4,000, reader contribution 1,000 | `0.25` | There is a catch: statics does not know which kind a `Money / Money` quotient has, and treats it as `Unknown`, because the kind depends on the value — a decimal fraction with a 10-smooth denominator, else `Rational`. `check` will not catch an error around such an expression; it will arrive at runtime. ## Key: one value per order `key(o)` on `copy_order` says: the page count is a function of the order. Two different counts on one order are not two facts but a contradiction: | Facts | `copy_fee(order1, 1050 KZT)` | Document | |---|---|---| | an order for 7 pages and the same order for 9 | `TRUE_ONLY` | issue `KEY_CONFLICT` | A key deletes and picks nothing: both facts remain, the tariff is derived from each, and the discrepancy gets a name. Without a key the predicate would answer one question with two sums and stay silent about it. That is why computed heads also carry keys — `copy_fee`, `late_fee`, `instalment`: this is the corpus-wide convention for all relations whose value a calculation derives. ## A fraction that became a whole A reader discount is a `Decimal`. Record it in a test as `0.9` and all agrees; record the full rate as `1` and you get a runtime refusal with clean `parse`, `check`, and `lower`. ```law rule DiscountedFee strict { for o: CopyOrder; for p: Person; for n: Integer; for a: Money; for r: Decimal; when copy_order(o, p, n) and copy_fee(o, a) and reader_discount(p, r); then discounted_fee(o, round(a * r, 2, "HALF_UP")); } ``` | Discount fact | `discounted_fee` | Document | |---|---|---| | `reader_discount(Ivanova, 0.9)` | `945.00 KZT` | — | | `reader_discount(Ivanova, 1)` | `NEITHER` | issue `TYPE_ERROR` | The literal `1` reads as `Integer`, the parameter is declared `Decimal`, and the core does not mix them. The trap is insidious because the CLIR canon strips the fractional tail off `Decimal`: a `1.0` value from the case prints as `1`, and text reproduced from the document verbatim changes kind. Write `1.0`; with money the currency suffix keeps the kind, and that trouble does not exist. ## Asking a number directly An expression's value is asked without a rule — by an expression query with an `expect value` expectation. Such tests live in the page file; here — in text, together with the runtime refusals, which are checked by observing `issue`: ```text evaluate 7 * 150 KZT; → 1050 KZT evaluate 25 percent; → 0.25 evaluate -7 mod 3; → 2 (евклидов остаток) evaluate 1050 KZT + 0.5; → issue(TYPE_ERROR) evaluate 1 + (1 / 2); → issue(TYPE_ERROR): счётчик и доля не складываются evaluate 1000 KZT + 5 USD; → issue(CURRENCY_MISMATCH) ``` The parenthetical notes read: "(Euclidean remainder)" and "a counter and a share do not add up". Runtime refuses where the compiler stays silent: `check` lets `1000 KZT + 5 USD` through, because currency lives in the value. Guard such places with a test. ## Compiler refusals Comparison does not bind a variable. Remove the `copy_order` literal from `BulkOrder`, leaving one guard: ```text error LDC-E4101: BulkOrder: head использует несвязанные переменные [o] — правило не range-restricted (§190: связывание только позитивным established-конъюнктом конечного источника) ``` The diagnostic reads: "'BulkOrder': head uses unbound variables [o] — the rule is not range-restricted: a variable must be bound by a positive established conjunct of a finite source". A threshold of the wrong kind — `n > 100.5` with a whole `n`: ```text error LDC-E2108: «>»: несравнимые виды Integer и Decimal — §57/§48 требуют совпадения видов, неявных конверсий нет ``` The diagnostic reads: "'`>`': incomparable kinds Integer and Decimal — kinds must match, no implicit conversions". Rounding without a policy — `round(a * RESEARCHER_RATE)`: ```text error LDC-E2116: round: объявленная арность стандартного терма — 3; передано 1. Арность задана §257 и разделом семантики самого терма (§50/§58/§59/§84/§86); догадки по числу аргументов нет ``` The diagnostic reads: "'round': the declared arity of the standard term is 3; 1 passed. Arity is set by the term's own semantic section; no guessing by argument count". All three refusals belong to the language: the compiler rejects such variants. Note where statics stops: the compiler does not trace the kind of the `PAGE_RATE` constant in an expression, and `PAGE_RATE / n` passes `check` to fall with `TYPE_ERROR` at runtime. A literal and a variable the compiler catches earlier; a constant is guarded by a test. ## Next One number from one fact is half of calculations. The other half is a sum over many: a reader's orders for a month, the count of unpaid ones, the average cheque. Those are aggregates, with their own trap: two identical sums that count as one, and an empty set that does not equal zero — [the next tutorial](/tutorials/aggregates/). The exercise for this page is [/tutorials/exercise-numbers/](/tutorials/exercise-numbers/).