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.fmt.fixedrenders one to a set number of places.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.+,-,*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.
Integer division truncates
7 / 2 # 3
7.0 / 2 # 3.5
float(7) / 2 # 3.5
A common surprise. Convert one operand when you want a fraction.
JSON
JSON has no exact-decimal type. json_encode emits a decimal as a plain number in its canonical text, and decoding gives back a float. A round trip through JSON is float-precision - if exactness must survive transport, send a string.
Maths
std.math has the constants and functions: pi, e, sqrt, sin, log, floor, clamp, gcd, isclose and the rest.