ruby 50 lines · 8 steps

Building an immutable Money value object in Ruby

A frozen, comparable Money class that stores cents and currency and behaves like a proper value type.

Explained by highlit
1class Money
2 include Comparable
3 
4 CURRENCY_SYMBOLS = { usd: "$", eur: "€", gbp: "£" }.freeze
5 
6 Value = Struct.new(:cents, :currency)
7 
8 attr_reader :value
9 
10 def initialize(cents, currency = :usd)
11 raise ArgumentError, "unknown currency #{currency}" unless CURRENCY_SYMBOLS.key?(currency)
12 
13 @value = Value.new(Integer(cents), currency).freeze
14 freeze
15 end
16 
17 def cents = value.cents
18 def currency = value.currency
19 
20 def +(other)
21 ensure_same_currency!(other)
22 self.class.new(cents + other.cents, currency)
23 end
24 
25 def <=>(other)
26 return nil unless other.is_a?(self.class) && other.currency == currency
27 
28 cents <=> other.cents
29 end
30 
31 def eql?(other)
32 other.is_a?(self.class) && value == other.value
33 end
34 
35 def hash
36 value.hash
37 end
38 
39 def to_s
40 format("%s%.2f", CURRENCY_SYMBOLS.fetch(currency), cents / 100.0)
41 end
42 
43 private
44 
45 def ensure_same_currency!(other)
46 return if other.currency == currency
47 
48 raise ArgumentError, "cannot combine #{currency} with #{other.currency}"
49 end
50end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Storing money as integer cents avoids the rounding errors of floating-point amounts.
  2. 2Freezing the object and its inner state makes a value type safe to share and use as a hash key.
  3. 3Implementing <=>, eql?, and hash together gives a class correct ordering, equality, and hashing semantics.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building an immutable Money value object in Ruby — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code