github.com/plambert/fixed_decimal.cr
main / published Sep 24, 2026 / repository
A fixed decimal implementation for Crystal
fixed_decimal
Binary fixed-point numbers for Crystal, backed by 128-bit integers.
Fixed(N)is signed and wraps anInt128.UFixed(N)is unsigned and wraps aUInt128.
The low N bits are the fraction and the remaining 128 - N bits are the
integer part (including the sign for Fixed). N is checked at compile time:
0..127 for Fixed, 0..128 for UFixed.
Installation
-
Add the dependency to your
shard.yml:dependencies: fixed_decimal: github: plambert/fixed_decimal.cr -
Run
shards install
Usage
require "fixed_decimal"
price = Fixed(64).new("19.99")
total = price * 3 # => 59.97
total.format(10, 2, '0') # => "0000059.97"
total.format("%+.4f") # => "+59.9700"
(Fixed(16).new(1) / 3).to_s # => "0.33333"
UFixed(128).new("0.75").to_f64 # => 0.75
Fixed(8).new(300).to_u8? # => nil
Semantics
- Rounding is always round-half-to-even: multiplication, division, parsing,
conversion from floats and between fixed-point types, and
#format. - Addition, subtraction, and multiplication or division by an integer are exact when the result is in range.
- Any result that is out of range raises
OverflowError, as the built-in integer types do.&+and&-wrap instead. - Division by zero raises
DivisionByZeroError. - Arithmetic works between values of the same type and with any
Inton either side. Convert explicitly to mix fraction widths:Fixed(32).new(some_fixed_16). - Comparison is exact across all fixed-point types,
IntandFloat, and equal values hash equally.
Conversions
- Constructors take any
Int,Float,Stringor fixed-point value..parse?returnsnilinstead of raising. #to_i8through#to_u128truncate toward zero and raiseOverflowError; the?variants returnniland the!variants wrap.#to_f64and#to_f32are correctly rounded;#to_f32raises when the value is beyondFloat32::MAX,#to_f32?returnsnil, and#to_f32!returns infinity.require "fixed_decimal/big"adds exact#to_big_r,#to_big_d,#to_big_f, truncating#to_big_i, and constructors fromBigInt,BigRational,BigDecimalandBigFloat. It needs libgmp.
Strings
#to_s prints the shortest decimal that parses back to the same value, with
at least one fraction digit ("1.0") unless N is zero.
Parsing accepts an optional sign, digits with single underscores between
them, an optional fraction, and an optional exponent: "-1_000.25", ".5",
"6.02e23".
#format(width, decimal, padding) and #format(spec) imitate sprintf:
| Call | Result |
|---|---|
x.format(8, 2) | " -3.25" |
x.format(-8, 2) | "-3.25 " |
x.format(8, 3, '0') | "-003.250" |
x.format("%+010.3f") | "-00003.250" |
x.format("%f") | "-3.250000" |
x.format("8") | " -3.25" |
In a spec, the % and the f are optional. The flags are -, +, space,
0 and #. Without f, a missing precision means the shortest form rather
than six digits.
Development
shards install
crystal spec
The specs check every operation against an independent BigRational
reference across a range of fraction widths, using boundary and seeded
pseudo-random values.
Contributing
- Fork it (https://github.com/plambert/fixed_decimal.cr/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Paul M. Lambert - creator and maintainer
API
- Fixed(N)
A signed binary fixed-point number stored in an
Int128. - FixedDecimal
Binary fixed-point numbers backed by 128-bit integers.
- Float
Float is the base type of all floating point numbers.
- Int
Int is the base type of all integer types.
- UFixed(N)
An unsigned binary fixed-point number stored in a
UInt128.