Numbers
A 64-bit signed integer, an IEEE-754 double, and an exact scaled decimal for money. Nothing converts implicitly.
Integers
An Int is a signed 64-bit integer. Division truncates toward zero, and
% takes the sign of the dividend: it is a remainder, not a modulus.
println('7 / 2 = ${7 / 2}')
println('-7 / 2 = ${-7 / 2}')
println('7 % 3 = ${7 % 3}')
println('-7 % 3 = ${-7 % 3}')
64-bit means 64-bit: arithmetic wraps at the ends of the range rather than promoting to
a wider type. int.max_value + 1 is int.min_value.
int.abs(int.min_value) saturates to int.max_value, because the
true magnitude is not representable. Small integers live in the value word and large
ones spill to the heap; that boundary is invisible to arithmetic and printing.
Bitwise
scarlet/int carries the bitwise operations:
bitwise_and, bitwise_or, bitwise_xor,
bitwise_not, bitwise_shift_left, and
bitwise_shift_right. They are functions rather than operators, so their
precedence against == and the arithmetic operators is never a thing to
remember. All six work on the full 64 bits, and all six are total: every input has a
defined answer.
// A permission bitfield: set two flags, test one, clear one.
flags = int.bitwise_or(int.bitwise_shift_left(1, 3), int.bitwise_shift_left(1, 40))
println('bit 3 set: ${int.bitwise_and(flags, int.bitwise_shift_left(1, 3)) != 0}')
println(
'bit 3 cleared: ${int.bitwise_and(flags, int.bitwise_not(int.bitwise_shift_left(1, 3)))}',
)
// WebSocket frame masking: XOR by the key byte, and XOR again to undo it.
masked = int.bitwise_xor(200, 222)
println('masked: ${masked} back: ${int.bitwise_xor(masked, 222)}')
// A count at or past the width shifts every bit out; the count is not masked.
println('1 shifted left 64: ${int.bitwise_shift_left(1, 64)}')
// The right shift is arithmetic, so the sign survives.
println('-8 shifted right 1: ${int.bitwise_shift_right(-8, 1)}')
A shift count of 64 or more shifts every bit out. Scarlet does not mask the count to
6 bits the way C and most hardware shift instructions do, so
bitwise_shift_left(1, 64) is 0, not 1. A negative count shifts the other
way by the same magnitude. bitwise_shift_right is arithmetic: the sign
bit propagates, so bitwise_shift_right(-8, 1) is -4, a negative value
never becomes positive, and a right shift past the width leaves -1 rather than 0.
Mask first to get logical behaviour.
bitwise_not complements the sign bit along with the other 63, so
bitwise_not(0) is -1 and bitwise_not(n) equals
0 - n - 1. An Int is signed and has no unsigned counterpart,
so complementing a small non-negative number gives a negative one; mask the result
with bitwise_and(bitwise_not(n), 255) to keep it inside a byte. Integer
literals are decimal, so masks are written as numbers.
Floats
A Float is an IEEE-754 double. Nothing converts implicitly:
float.from_int is the way in, and floor, ceil,
round, and truncate are the ways out. round
breaks ties away from zero. && and || short-circuit,
which is what makes a guard like this safe.
fn safe_div(a Int, b Int) Bool {
b != 0 && a / b > 1
}
Money
A double cannot represent 0.1 exactly, so money never touches Float. A
Decimal from scarlet/decimal is
units * 10^(-scale), an exact scaled integer: new(1999, 2) is
19.99, and 0.1 + 0.2 is 0.3. The scale is carried through arithmetic. Addition aligns
scales; multiplication adds them. The only operations that discard digits are
div and round, and both take an explicit target scale and
rounding mode.
type Usd {
amount Decimal
}
fn cents(n Int) Usd {
Usd(decimal.new(n, 2))
}
// Multiply exactly, so the scale grows to hold every digit, then round back
// to cents. decimal.round is banker's rounding (half to even), the default
// throughout scarlet/decimal.
fn tax(u Usd, rate Decimal) Usd {
Usd(decimal.round(decimal.mul(u.amount, rate), 2))
}
The six rounding modes are HalfEven, HalfUp,
Down, Up, Floor, and Ceiling.
HalfEven is the default: ties go to the even neighbour, so repeated
rounding does not drift. Splitting a total is the standard trap; divide and multiply
back and money is lost or invented. Allocate the remainder instead.
// Split a total into n shares that sum back to exactly the total: every share
// gets the rounded-down amount, and the leftover cents go one each to the
// first shares. Dividing and multiplying back would either lose or invent
// money; this remainder-preserving allocation is the standard fix.
fn split(total Usd, n Int) Array(Usd) {
base = decimal.div_with(total.amount, decimal.from_int(n), 2, Down) or decimal.from_int(0)
leftover = decimal.units(decimal.sub(total.amount, decimal.mul(base, decimal.from_int(n))))
shares(base, leftover, n)
}
Built-in == compares representation, so 1.50 == 1.5 is
False. Compare numerically with decimal.eq or
decimal.compare, which returns an Order.
parse and from_float return Err(Nil) on inputs
that would overflow rather than wrapping.