Numbers
Three numeric types, each for a different job.
| type | what it is | use for |
|---|---|---|
int | 64-bit signed, checked | counting, indexing |
float | IEEE-754 double | scientific, approximate |
decimal | exact base-10, ~28 digits | money |
int overflow is an error
9223372036854775807 + 1 # runtime error, not a wraparound
Silent wraparound is a bug that surfaces far from its cause. Ecko refuses at the operation.
float literals take an exponent
let tolerance = 2.5e-9
let avogadro = 6.022e23
let big = 1e3 # 1000.0, a float - not the integer 1000
e or E, with an optional sign, and at least one digit after it. That last part is what keeps e usable as a variable: 2 * e is arithmetic, because there are no digits following.
An exponent always produces a float, whatever the mantissa looked like. It is also the only practical way to reach the ends of the range - the largest float is about 1.8e308, and nobody writes that as digits.
ecko fmt leaves the spelling you chose alone, the same way it leaves strings alone. Expanding 6.022e23 into twenty-four digits would be the identical value and a far worse line.
float equality is exact IEEE
0.1 + 0.2 == 0.3 # false
approx(0.1 + 0.2, 0.3) # true
approx(a, b, 0.01) # explicit epsilon
== on floats does what IEEE says, which is the honest answer. approx has a default epsilon of 1e-9.
NaN does not equal itself, which is part of that same honesty:
import std.math
n = math.sqrt(-1.0)
n == n # false
contains([n], n) # false - equality is asked here too
math.isnan(n) # true - this is the question to ask
It catches people out in every language that has floats, so it is worth saying plainly: a NaN inside a list can never be found by searching for it, and a NaN compared against anything, itself included, is false. math.isnan is the only reliable test.
decimal for money
price = 19.99m
qty = 3
subtotal = price * qty # 59.97 exactly
tax_rate = decimal("0.0825")
total = subtotal + subtotal * tax_rate
print(0.1m + 0.2m) # 0.3
print(0.1 + 0.2) # 0.30000000000000004
The rules, chosen so money behaves:
decimalmixes withintand the result isdecimal.5m == 5istrue.int,round,floorandceilaccept adecimaland return anint, exactly - nothing routes through a float on the way.inttruncates toward zero;roundtakes halves away from zero, not the half-to-even a statistics library would use, because money does not work that way.float(aDecimal)stays rejected, for the same reasondecimal(aFloat)is.round(x, places, mode?)rounds to places and keeps the type. A decimal comes back a decimal at exactly that scale, soround(12.345m, 2)is12.35mandround(12.3m, 2)is12.30m. Halves go away from zero unless you pick a mode:half_up(the default),half_even,half_down,up,down,ceilingorfloor.round(2.345m, 2, "half_even")is2.34m. A float rounds the same way but in binary, so round money as a decimal.fmt.fixedrenders a number to a set number of places as text.decimalwithfloatis a hard error. A binary float has already lost precision; letting it into an exact calculation would silently corrupt the result. Convert explicitly if you truly mean to.decimal(aFloat)is rejected. Pass a string -decimal("0.1")- to state the exact value you want.decimal(x)does accept anintor a numeric string. It is lenient about the string, asintis: surrounding whitespace is trimmed and exponent notation is read (decimal("1e5")is100000). For money typed in by a person, where"1e5"is more likely a mistake than an amount, parse with themoneypackage, which is strict.+,-,*preserve scale, so cents are never dropped:19.99m + 0.01mis20.00./and%return the exact quotient normalized:decimal(10) / 4is2.5.- Overflow and divide-by-zero raise, as with
int.
/ divides, // floors
7 / 2 # 3.5 - always a float for two ints
6 / 3 # 2.0
7 // 2 # 3 - floor division, an int for two ints
(0 - 7) // 2 # -4 - floor, not truncation
7.5 // 2 # 3.0
/ never drops the fraction, so an average or a percentile cannot quietly come out as a whole number. Use // when you want the whole number of times one value fits in another - a page count, a midpoint index, a day number. It rounds toward negative infinity, as in Python, and % is its remainder, with the sign of the divisor: -7 % 2 is 1, so a == (a // b) * b + a % b always holds and a weekday or a minute-of-hour computed with % is right for a negative time too. % mixes a float and an int the way the other operators do. A decimal stays a decimal under both: decimal(10) / 4 is 2.5 and decimal(10) // 4 is 2.
A / where you meant // does not always fail: x = x / 2 makes x a whole Float, and if it only feeds more arithmetic the answer stays right while every step after it runs in floating point. ecko check flags the clear case, a / inside an x % 2 == 0 branch, and the case that always fails: a / as a range bound or a count, such as take(xs, len(xs) / 2).
JSON
JSON has no exact-decimal type, so a decimal is written as a plain number in its exact text (12.50m is 12.50). By default decoding gives back a float; decode with json.decode(text, decimal: true) and every non-integer number comes back as an exact decimal instead, so a money field survives the round trip. See std.json.
Maths
std.math has the constants and functions: pi, e, sqrt, sin, log, floor, clamp, gcd, isclose and the rest.