lib/decimal.bx
The helpers a money language is judged on.
use "lib/decimal.bx";
Burxt’s headline is that $0.10 / 3 is a question you have to answer rather than one the machine answers wrongly for you. This file is where that promise gets cashed: money_split splits a total into parts whose sum is the total, exactly, on every input — the canonical exact-money problem, and the first function a reader comes looking for.
What is in it
| Name | Kind | What it answers |
|---|---|---|
decimal2_cents |
function | The number of pennies in value. decimal2_cents($19.99) is 1999, and decimal2_cents(-$0.01) is -1. |
decimal2_from_cents |
function | The inverse: a count of pennies as money. decimal2_from_cents(1999) is $19.99. |
decimal2_is_zero |
function | Is this exactly zero? $0.00, and nothing else — there is no epsilon here and there will not be one, because there is n |
decimal2_abs |
function | The distance from zero. requires the value is not the most negative one, for the reason math_abs states: the two |
decimal2_sign |
function | -1, 0 or 1. No precondition and none needed: nothing is negated, so the most negative value answers -1 rather than trapp |
decimal2_round_to |
function | value rounded to places decimal places, still typed Decimal<2>. places is 0, 1 or 2: whole currency units, tenth |
decimal2_percent_of |
function | rate is a FRACTION, so a bare number here is a hundred times too big and nothing refuses it. Measured, reported by |
money_split |
function | * parts larger than the penny count. money_split($0.02, 5) is |
decimal4_ticks |
function | The count of ten-thousandths. decimal4_ticks(8.25%) is 825. |
decimal4_from_ticks |
function | — |
decimal4_is_zero |
function | — |
decimal4_abs |
function | — |
decimal4_sign |
function | — |
decimal6_ticks |
function | The count of millionths. |
decimal6_from_ticks |
function | — |
decimal6_is_zero |
function | — |
decimal7_ticks |
function | The count of ten-millionths. The integer magnitude_of_squared searches for by hand; this is the same search named once |
decimal7_from_ticks |
function | — |
decimal7_is_zero |
function | — |
decimal7_abs |
function | — |
decimal7_sign |
function | — |
divide_round_half_even |
function | n / step, rounded to the nearest whole, ties to the EVEN one. divide_round_half_even(15, 10) is 2 and `divide_round_ |
Functions
decimal2_cents
pure function decimal2_cents(value: Decimal<2>) -> Int
The number of pennies in value. decimal2_cents($19.99) is 1999, and decimal2_cents(-$0.01) is -1.
Exact for every Decimal<2> including both extremes of the range. See the header for why this is a search rather than a cast, and why the sign is decided before the descent starts.
decimal2_from_cents
pure function decimal2_from_cents(cents: Int) -> Decimal<2>
The inverse: a count of pennies as money. decimal2_from_cents(1999) is $19.99.
The penny * n shape lib/json.bx already uses, named so the round trip has two ends a reader can see. This is the only multiplication in the file that can trap, and only when cents is the full i64 range — which is exactly when the answer does not exist.
decimal2_is_zero
pure function decimal2_is_zero(value: Decimal<2>) -> Bool
Is this exactly zero? $0.00, and nothing else — there is no epsilon here and there will not be one, because there is no float to wobble. That is the entire argument for this language, so a tolerance would be a lie about the arithmetic.
decimal2_abs
pure function decimal2_abs(value: Decimal<2>) -> Decimal<2>
The distance from zero. requires the value is not the most negative one, for the reason math_abs states: the two’s-complement range is asymmetric, so the magnitude of the smallest Decimal<2> is not a Decimal<2>. A precondition rather than a wrong answer.
The clause compares against decimal2_from_cents(INT_MIN) — one multiply — rather than against decimal2_cents(value), which would run the sixty-three-step search on every call to say the same thing. A contract clause is code and costs what code costs.
decimal2_sign
pure function decimal2_sign(value: Decimal<2>) -> Int
-1, 0 or 1. No precondition and none needed: nothing is negated, so the most negative value answers -1 rather than trapping. The Int return matches math_sign.
decimal2_round_to
pure function decimal2_round_to(value: Decimal<2>, places: Int) -> Decimal<2>
value rounded to places decimal places, still typed Decimal<2>. places is 0, 1 or 2: whole currency units, tenths, or the identity.
HALF-EVEN, which is RoundHalfEven — the language’s own contract, the one financial reporting asks for, and the one that does not drift upward over a long ledger. To one place, $0.05 is $0.00 and $0.15 is $0.20: both are exact ties, and the tie goes to the EVEN neighbour rather than always upward. Half-up would round both up, and a ledger full of ties rounded one way gains a penny per tie.
Why this exists when the language has Decimal<2, RoundHalfEven>: a contract governs an operation that NARROWS a scale, and there is no narrowing here — the answer stays at scale 2, it just has zeroes at the end. $1.005 is not a Decimal<2> in the first place. So the language’s rounding cannot express this, and the arithmetic is done on the penny count where the tie rule is four visible lines instead of a keyword.
It also keeps the contract off the caller. A Decimal<2, RoundHalfEven> cannot be passed to anything typed plain Decimal<2> — array_sum_money would refuse it — so a helper that returned one would poison every value that passed through it. Every function here answers a plain Decimal<2> for that reason.
Within a penny of the top of the range, rounding UP has nowhere to go, and this traps — burxt runtime error: arithmetic overflow, measured, in tests/panic/round_to_at_the_ceiling.bx. That is the language’s own trap and it is the right answer: the rounded value does not exist as a Decimal<2>, so the alternatives are wrapping to a large negative number or clamping to a value nobody asked for. No requires guards it, because stating the condition would mean naming both the step and the leftover in the signature, and the trap already says the same thing at the same moment with a better message.
decimal2_percent_of
pure function decimal2_percent_of(amount: Decimal<2>, rate: Decimal<4>) -> Decimal<2>
rate is a FRACTION, so a bare number here is a hundred times too big and nothing refuses it. Measured, reported by star-burxt from a real example:
decimal2_percent_of(80.00, 12.5%) -> 10.00 what was meant
decimal2_percent_of(80.00, 12.5) -> 1000.00 accepted, and 100x too much
The % carries the whole meaning and the type system carries none of it: 12.5% IS 0.125, so the two spellings are the same value by the time this function sees them and no contract can tell them apart. requires rate < 1 would catch 12.5 and would also refuse 150%, which is a legitimate markup — the information is destroyed at the literal, and only a distinct TYPE could keep it.
That makes this the one place on the money surface where Burxt accepts a silent wrong answer, in the direction of charging somebody a hundred times too much. It is written down here rather than left for the next person to find, and the fix is a design decision on the flagship claim rather than a patch: see docs/limitations.md. Write the literal. 12.5%, never 0.125 and never 12.5 — the literal is the only spelling that says what it means.
money_split
pure function money_split(total: Decimal<2>, parts: Int) -> [Decimal<2>]
partslarger than the penny count.money_split($0.02, 5)is
`[$0.01, $0.01, $0.00, $0.00, $0.00]`. Zero is a legal share; three payees getting nothing
is the truthful answer to "split two pennies five ways".
- A total of zero gives
partszeroes, and they sum to zero. *partsof 1 gives the total back, which the remainder handles without a special case:
the remainder of anything by 1 is 0.
partsof 0 is refused byrequires. “Split this nought ways” has no answer, and
`lib/array.bx`'s `array_min` records the same call for the same reason.
decimal4_ticks
pure function decimal4_ticks(value: Decimal<4>) -> Int
The count of ten-thousandths. decimal4_ticks(8.25%) is 825.
decimal4_from_ticks
pure function decimal4_from_ticks(ticks: Int) -> Decimal<4>
decimal4_is_zero
pure function decimal4_is_zero(value: Decimal<4>) -> Bool
decimal4_abs
pure function decimal4_abs(value: Decimal<4>) -> Decimal<4>
decimal4_sign
pure function decimal4_sign(value: Decimal<4>) -> Int
decimal6_ticks
pure function decimal6_ticks(value: Decimal<6>) -> Int
The count of millionths.
decimal6_from_ticks
pure function decimal6_from_ticks(ticks: Int) -> Decimal<6>
decimal6_is_zero
pure function decimal6_is_zero(value: Decimal<6>) -> Bool
decimal7_ticks
pure function decimal7_ticks(value: Decimal<7>) -> Int
The count of ten-millionths. The integer magnitude_of_squared searches for by hand; this is the same search named once, and lib/vector.bx predates it.
decimal7_from_ticks
pure function decimal7_from_ticks(ticks: Int) -> Decimal<7>
decimal7_is_zero
pure function decimal7_is_zero(value: Decimal<7>) -> Bool
decimal7_abs
pure function decimal7_abs(value: Decimal<7>) -> Decimal<7>
decimal7_sign
pure function decimal7_sign(value: Decimal<7>) -> Int
divide_round_half_even
pure function divide_round_half_even(n: Int, step: Int) -> Int
n / step, rounded to the nearest whole, ties to the EVEN one. divide_round_half_even(15, 10) is 2 and divide_round_half_even(25, 10) is also 2 — both ties, and both go to the even neighbour rather than both going up.
Written once, in Int, and shared by every scale — which is the payoff of converting to the unscaled count first: the rounding rule exists in exactly one place, so there is one thing to read and one thing to be wrong.
It answers the QUOTIENT, not the rounded multiple, because both callers want the quotient and only one of them wants it multiplied back up. Returning the multiple was the first version and it made decimal2_percent_of($100.00, 8.25%) answer $82,500.00 — a factor of 10,000, which is exactly the kind of mistake a fixture catches and a reading does not.
divide_floor rather than truncation here, and unlike money_split that is the right choice: flooring makes the leftover non-negative for negative n too, so “is this a tie” is one comparison instead of two. -5 with step 10 gives quotient -1 and leftover 5, a tie, and the even neighbour is 0 — which is decimal2_round_to(-$0.05, 1) being $0.00, the mirror of the positive case. Half-even is symmetric about zero, and tests/pass/decimal_helpers.bx checks that over every penny in a range rather than on the four examples above.