Numbers
Numbers have two namespaces: integer operations take values built on
Int, and decimal operations take values built on Decimal. The
operators / and % are in
Expressions and operators;
these operations do what an operator can't.
| Operation | Gives | Fails | SQL form |
|---|---|---|---|
integer.divide(dividend, divisor, rounding:) |
Int (Expressions and operators) |
every mode: div and mod with a CASE |
|
integer.abs(value) |
Int, at least 0 |
overflow on the smallest Int is a defect |
abs(x) |
integer.min(a, b), integer.max(a, b) |
the smaller or larger, keeping their named type (both the same, as + needs) |
LEAST(a, b), GREATEST(a, b) |
|
integer.from_decimal(value, rounding:) |
Int, value rounded to a whole number |
overflow is a defect | round(x)::bigint for half_up; a CASE on the dropped digits for the others |
decimal.divide(dividend, divisor, rounding:) |
Decimal (Expressions and operators) |
every mode: as integer.divide, on the values scaled to the dividend's places |
|
decimal.abs(value) |
Decimal, at least 0, the same scale |
abs(x) |
|
decimal.min(a, b), decimal.max(a, b) |
as for integer |
LEAST(a, b), GREATEST(a, b) |
|
decimal.round(value, scale:, rounding:) |
Decimal with at most scale places; scale: a whole number written out, at least 0 |
x when it has at most s places, where round would add zeros; otherwise round(x, s) for half_up, trunc(x, s) for down and a CASE on the dropped digits for the others |
|
decimal.sqrt(value, scale:, rounding:) |
Decimal; value never negative |
none | |
decimal.power(value, exponent) |
Decimal, exact; exponent a whole number, never negative |
overflow is a defect | none |
decimal.exp(value, scale:, rounding:), decimal.ln(value, scale:, rounding:) |
Decimal; ln's value above 0 |
ln: out_of_range |
none |
decimal.sin(radians, scale:, rounding:), decimal.cos(radians, scale:, rounding:) |
Decimal |
none | |
decimal.atan2(y, x, scale:, rounding:) |
Decimal, radians |
none | |
decimal.pi(scale:, rounding:) |
Decimal |
none |
- Rounding to a type's scale needs no call. Building a value of a
type with a fixed
scalerounds it half to even (Conditions as proof), sodecimal.roundis for another rounding mode, or for fewer places than the type keeps. - The rounding modes.
Roundinghas four:half_eventakes a half to the even digit,half_uptakes a half away from zero,uprounds away from zero, anddownrounds toward zero, its SQL formtrunc. There is nofloor,ceilingorhalf_down. - An
Intbecomes aDecimalby multiplying, as incount * 1.00, since*may mix the two (Expressions and operators). There is nodecimal.from_integer. - Parsing and formatting work as for every standard type, through
the contract
(Every standard type meets one contract):
text.parse(raw, as: Quantity)andtext.format(n, locale:). Parsing takes only the form a number has on the wire: no grouping, no decimal comma, no exponent. A spreadsheet's1.234,50is a job for a cell decoder, a package'sdecode:. - Formatting for people uses CLDR's decimal format for the locale,
with its digits, grouping and decimal separator, and shows every place
the value keeps, as a translation's
{n}writes it (Using a message):1234.50is1,234.50inen_gband1.234,50inde_de. It takes no options, sozone:orformat:istype.temporal_option_unused. ABoolhas no format for people. minandmaxtake two values of one type, as+does, and give one of them as it is. Two named types that aren't one type after widening aretype.mismatch. Where one widens to the other, the result has the wider type (When one type can stand in for another). A literal takes the other value's type and is checked against all its rules, since the result may be the literal. A plain number, which carries no type of its own, makes the result the plain representation, and so do two literals. Of two equal values,decimal.minanddecimal.maxgive the first, sodecimal.min(1.50, 1.5)keeps1.50's places.scale:is a whole number from 0 to 18, the range a type'sscaletakes (Representation types), written out at the call. A value there istype.mismatch, as it is fortext.parse'sas:. A literal below 0 istype.argument_negative, and any other literal that isn't a whole number up to 18 istype.mismatch.decimal.roundgives at mostscaleplaces, so it keeps a value that has no more places thanscale:as it is. The maths operations give exactlyscaleplaces:decimal.sqrt(4.00, scale: 2, ...)is2.00.- What the checker knows about results. These facts let it prove a
result fits a type without a check
(Conditions as proof).
absis at least 0, as the table says.minandmaxcarry their arguments' bounds: the smaller of two values is never above either's top.integer.from_decimalanddecimal.roundkeep the sign of a value bounded at zero, since no mode rounds across zero.sqrtandexpare at least 0, andpiis at least 3. The checker knows nothing finer: a rounded value's other bounds, and anything built fromsinorcos, which carry no bounds, need a check. - Arguments that must be proved.
sqrt's value andpower's exponent are never negative, proved as a count is (never_negative), or the call istype.argument_negative.lnfailsout_of_range, and that is settled by proof, as a construction's rule is. A literal above 0, or a value whose type has aminabove 0, can't fail, so its call takes noattempt; one written with anattemptisfailure.attempt_infallible. Any other value may fail, and the call mapsout_of_rangeorinvalid:; its failure isinvalid_value. - Edges of the maths.
atan2(0, 0)is 0, where no angle is meant, rather than a failure.atan2(0, x)forxbelow 0 is π, asDecimalhas no negative zero to make it -π.power(value, 0)is 1 atvalue's scale,0^0included, and every other power keeps the places repeated*keeps:1.50squared is2.25. Apowerpast 38 digits or 38 places, or anexppast 38 digits, is overflow, a defect. Anexpso far below 0 that it is under half of 10^-18 gives 0, or, withup, which rounds away from zero, one unit of the last place. - The same digits on every target. The maths works on
Decimal, as there is noFloat(Representation types). Jadpo's own decimal code rounds each result correctly toscalewith the named rounding, so it is the same wherever the program runs. - In SQL, PostgreSQL's
roundties away from zero, which ishalf_up. The other modes are spelled out with aCASE, never left to the database.
A bakery's counts, weights and prices, rounded and compared:
type Portions = Int { min: 0}type TrayCount = Int { min: 1 max: 12}type LoafCount = Int {}type FlourWeight = Mass { min: 0g}type Hydration = Decimal { scale: 3 min: 0.000}type LoafPrice = Decimal { scale: 2 min: 0.00}/// How far a counted batch is from its recipe, either way. `abs` is never/// below 0, so building `Portions` from it needs no arm.function drift(in_batch: Portions, recipe: Portions) -> Portions { return Portions(integer.abs(in_batch - recipe))}/// The trays to bake: what was ordered, but never more than the oven/// holds. `min` gives one of its two values as it is, still a `TrayCount`.function trays_to_bake(requested: TrayCount, oven: TrayCount) -> TrayCount { return integer.min(requested, oven)}/// Whole loaves a sack of flour makes at 450g each: part of a loaf isn't/// one, so the count rounds down, toward zero. Dividing a weight by a weight/// gives a plain `Decimal`.function loaves_from(flour: FlourWeight) -> LoafCount { return LoafCount(integer.from_decimal(flour / 450g, rounding: Rounding.down))}/// A loaf's price at a fifth off, never below the lowest price.function sale_price(price: LoafPrice, lowest: LoafPrice) -> LoafPrice { return decimal.max(LoafPrice(price * 0.80), lowest)}/// A dough's hydration to one place, for its label.function label_hydration(hydration: Hydration) -> Hydration { return Hydration(decimal.round(hydration, scale: 1, rounding: Rounding.half_up))}/// The price as a customer reads it, in their own locale.function price_label(price: LoafPrice, customer: Locale) -> PresentationText { return text.format(price, locale: customer)}
A literal passed to min or max takes the other value's type and is
checked against it, so integer.min(requested, 20) is an error, because
TrayCount stops at 12. A plain number, such as requested + 1, makes
the result a plain Int. The tests check these functions, and each
rounding mode to one place:
test "number operations give what the page says" { assert drift(980, 1000) == 20 assert drift(1020, 1000) == 20 assert trays_to_bake(10, 8) == 8 assert trays_to_bake(3, 8) == 3 assert loaves_from(10kg) == 22 assert loaves_from(449g) == 0 assert sale_price(4.20, 3.50) == 3.50 assert sale_price(5.00, 3.50) == 4.00 assert label_hydration(1.250) == 1.3 assert label_hydration(1.249) == 1.2 assert integer.max(3, 8) == 8 assert decimal.abs(-2.50) == 2.50 assert decimal.min(4.20, 3.80) == 3.80 assert price_label(1234.50, Locale.en_gb) == "1,234.50" assert price_label(1234.50, Locale.de_de) == "1.234,50"}test "every rounding mode, to one place" { assert decimal.round(2.25, scale: 1, rounding: Rounding.half_even) == 2.2 assert decimal.round(2.35, scale: 1, rounding: Rounding.half_even) == 2.4 assert decimal.round(2.25, scale: 1, rounding: Rounding.half_up) == 2.3 assert decimal.round(-2.25, scale: 1, rounding: Rounding.half_up) == -2.3 assert decimal.round(2.21, scale: 1, rounding: Rounding.up) == 2.3 assert decimal.round(-2.21, scale: 1, rounding: Rounding.up) == -2.3 assert decimal.round(2.29, scale: 1, rounding: Rounding.down) == 2.2 assert decimal.round(-2.29, scale: 1, rounding: Rounding.down) == -2.2 assert integer.from_decimal(2.5, rounding: Rounding.half_even) == 2 assert integer.from_decimal(-2.5, rounding: Rounding.half_up) == -3 assert integer.divide(7, 2, rounding: Rounding.down) == 3}
A scale: that isn't a whole number from 0 to 18 written out at the
call, as a type's scale is, is type.mismatch, and one below 0 is
type.argument_negative (Errors).
The maths operations, from sqrt to pi, on the bakery's tins,
starters and deliveries. Each result is the true value rounded to
scale places by the mode named, so the same call gives the same digits
on every target:
type Centimetres = Decimal { scale: 2 min: 0.00}type SquareCentimetres = Decimal { scale: 2 min: 0.00}type Tier = Int { min: 0 max: 6}type RecipeScale = Decimal { scale: 4}type Growth = Decimal { scale: 4 min: 0.0000}type Rise = Decimal { scale: 6}type ProvingTime = Decimal { scale: 1 min: 0.0}type Metres = Decimal { scale: 1}type Course = Decimal { scale: 6}type NeverRose = Failure { kind: FailureKind.invalid_value}/// The surface of a round tin, π r².function tin_area(radius: Centimetres) -> SquareCentimetres { return SquareCentimetres(decimal.pi(scale: 6, rounding: Rounding.half_even) * radius * radius)}/// The side of a square tin the same size, rounded down so the cake/// fits. A size is never negative, so its square root needs no check.function square_side(surface: SquareCentimetres) -> Centimetres { return Centimetres(decimal.sqrt(surface, scale: 2, rounding: Rounding.down))}/// How much more each tier below the surface needs, at half again a tier:/// exact, as `1.5 * 1.5 * 1.5` is.function tier_scale(tier: Tier) -> RecipeScale { return RecipeScale(decimal.power(1.5, tier))}/// How much a starter grows over `proving`, rising all the while by/// `rise` an hour. `e` to any power is never below 0, so `Growth` needs no arm.function risen(rise: Rise, proving: ProvingTime) -> Growth { return Growth(decimal.exp(rise * proving, scale: 4, rounding: Rounding.half_even))}/// How fast a starter rose an hour, from how much bigger it had `grown`/// in a day. Growth of 0 has no logarithm: a starter that shrank to/// nothing never rose.function hourly_rise(grown: Growth) fails NeverRose -> Rise { var daily: Decimal = attempt decimal.ln(grown, scale: 6, rounding: Rounding.half_even) { out_of_range: NeverRose } return Rise(daily / 24.0)}/// The direction of a customer at `northing` and `easting` from the/// bakery, in radians clockwise from north.function course(northing: Metres, easting: Metres) -> Course { return Course(decimal.atan2(easting, northing, scale: 6, rounding: Rounding.half_even))}/// Where a van is, east and north of the bakery, `driven` metres on/// `course`.function easting_after(driven: Metres, course: Course) -> Metres { return Metres(driven * decimal.sin(course, scale: 6, rounding: Rounding.half_even))}function northing_after(driven: Metres, course: Course) -> Metres { return Metres(driven * decimal.cos(course, scale: 6, rounding: Rounding.half_even))}
sqrt's value and power's exponent must be proved never negative, as
a count is, or the call is type.argument_negative. ln fails with
out_of_range unless its value is proved above 0, by a literal above 0
or a type whose min is above 0. Growth allows 0, so hourly_rise
maps the failure. A result past 38 digits, such as
decimal.exp(100.0, ...), is a defect, as overflow is for *. The tests
check these functions, and each mode on the true value:
test "the maths gives what the page says" { assert tin_area(10.00) == 314.16 assert square_side(314.16) == 17.72 assert tier_scale(3) == 3.375 assert tier_scale(0) == 1.0 assert risen(0.100000, 6.0) == 1.8221 var rise: Rise = attempt hourly_rise(2.0000) assert rise == 0.028881 assert hourly_rise(0.0000) fails NeverRose assert course(100.0, 100.0) == 0.785398 assert course(-100.0, 0.0) == 3.141593 assert easting_after(1000.0, 0.523599) == 500.0 assert northing_after(1000.0, 0.523599) == 866.0}test "each mode rounds the true value" { assert decimal.sqrt(2.0, scale: 3, rounding: Rounding.down) == 1.414 assert decimal.sqrt(2.0, scale: 3, rounding: Rounding.up) == 1.415 assert decimal.pi(scale: 4, rounding: Rounding.half_even) == 3.1416 assert decimal.pi(scale: 4, rounding: Rounding.down) == 3.1415 assert decimal.exp(1.0, scale: 5, rounding: Rounding.half_up) == 2.71828 assert decimal.ln(10.0, scale: 5, rounding: Rounding.up) == 2.30259}
Markdown for agents: /docs/numbers.md.Built from Jadpo main at a707efb.