fixed_decimal.cr

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 an Int128.
  • UFixed(N) is unsigned and wraps a UInt128.

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

  1. Add the dependency to your shard.yml:

    dependencies:
      fixed_decimal:
        github: plambert/fixed_decimal.cr
    
  2. 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 Int on either side. Convert explicitly to mix fraction widths: Fixed(32).new(some_fixed_16).
  • Comparison is exact across all fixed-point types, Int and Float, and equal values hash equally.

Conversions

  • Constructors take any Int, Float, String or fixed-point value. .parse? returns nil instead of raising.
  • #to_i8 through #to_u128 truncate toward zero and raise OverflowError; the ? variants return nil and the ! variants wrap.
  • #to_f64 and #to_f32 are correctly rounded; #to_f32 raises when the value is beyond Float32::MAX, #to_f32? returns nil, 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 from BigInt, BigRational, BigDecimal and BigFloat. 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

  1. Fork it (https://github.com/plambert/fixed_decimal.cr/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

Repository

fixed_decimal.cr

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 1
  • about 8 hours ago
  • September 24, 2026
License

MIT License

Links
Synced at

Thu, 24 Sep 2026 03:03:49 GMT

Languages