Skip to content

Instantly share code, notes, and snippets.

@vlado
Last active November 7, 2022 14:59
Show Gist options
  • Save vlado/fe847c59954fc4c637b5fb57237cd4ef to your computer and use it in GitHub Desktop.
Save vlado/fe847c59954fc4c637b5fb57237cd4ef to your computer and use it in GitHub Desktop.
Value Objects in Ruby (and Rails)
# create_table :products do |t|
# t.integer :price_in_cents
# ...
# end
class Product < ApplicationRecord
VAT = 25 # %
def price_in_eur
price_in_cents / 100.0
end
def price_with_vat_in_eur
(price_in_cents + (price_in_cents * VAT / 100.0).round(2)) / 100.0
end
end
module ProductsHelper
HRK_EXCHANGE_RATE = 7.53450
def display_product_price_in_hrk(product)
hrk = (product.price_in_eur * HRK_EXCHANGE_RATE).round(2)
number_to_currency(price_in_hrk, currency: "HRK")
end
def display_product_price_with_vat_in_hrk(product)
hrk = (product.price_with_vat_in_eur * HRK_EXCHANGE_RATE).round(2)
number_to_currency(hrk, currency: "HRK")
end
def total_price(products)
products.sum { |product| product.price_in_eur }
end
def total_price_with_vat(products)
products.sum { |product| product.price_with_vat_in_eur }
end
end
class Price
include Comparable
HRK_EXCHANGE_RATE = 7.53450
VAT = 25 # %
attr_reader :cents
def initialize(cents)
@cents = cents
end
def eur
cents / 100.0
end
def hrk
(eur * HRK_EXCHANGE_RATE).round(2)
end
def with_vat
cents_with_vat = cents + (cents * VAT / 100.0).round(2)
Price.new(cents_with_vat)
end
def +(other_price)
Price.new(cents + other_price.cents)
end
def <=>(other_price)
cents <=> other_price.cents
end
end
# create_table :products do |t|
# t.integer :price_in_cents
# ...
# end
class Product < ApplicationRecord
attr_accessor :price_in_cents
def price
Price.new(price_in_cents)
end
def price=(new_price)
self.price_in_cents = new_price.cents
end
end
price = Price.new(10_000)
puts price.eur # => 100.00
puts price.hrk # => 753.45
hundred_euros = Price.new(10_000)
p1 = Product.new(price: hundred_euros)
puts p1.price.eur # => 100.00
puts p1.price.hrk # => 753.45
puts p1.price.with_vat.eur # => 125.00
puts p1.price.with_vat.hrk # => 941.81
thousand_euros = Price.new(100_000)
p2 = Product.new(price: thousand_euros)
puts p2.price > p1.price # => true
total_price = p1.price + p2.price
p total_price # => #<Price:0x00007fcdcf85a940 @cents=110000>
puts total_price.with_vat.eur # => 1375.00
hundred_euros = Price.new(10_000)
seven_hundred_euros = Price.new(70_000)
thousand_euros = Price.new(100_000)
hundred_euros == thousand_euros # => false
hundred_euros == Price.new(10_000) # => true
hundred_euros > seven_hundred_euros # => false
seven_hundred_euros < thousand_euros # => true
seven_hundred_euros.between?(hundred_euros, thousand_euros) # => true
[seven_hundred_euros, thousand_euros, hundred_euros].sort
# => [#<Price:0x00007fcdcf85a940 @cents=10000>, #<Price:0x00007fcdcf85a941 @cents=70000>, #<Price:0x00007fcdcf85a942 @cents=100000>]
class Address
attr_reader :city, :zip, :street, :house_no
def initialize(city, zip, street, house_no)
@city, @zip, @street, @house_no = city, zip, street, house_no
end
def ==(other_address)
city == other_address.city && zip == other_address.zip && street == other_address.street && house == other_address.house_no
end
def same_city?(other_address)
city == other_address.city
end
end
Price = Data.define(:amount, :currency) do
def with_vat
Price.new(amount * 1.25, currency)
end
end
hundred_euros = Price.new(100, "EUR")
thousand_euros = Price.new(1000, "EUR")
date1 = Date.new(2022, 9, 13)
date2 = Date.new(2022, 9, 13)
puts date1.object_id # => 60
puts date2.object_id # => 80
puts date1.equal?(date2) # => false
puts date1.eql?(date2) # => true
puts date1 == date2 # => true
class Money
# Remove setters (replace attr_accessor with attr_reader)
attr_reader :amount, :currency
def initialize(amount, currency)
@amount = amount
@currency = currency
end
# If you really need a setter initialize a new value object
# instead of modifying the current one
def with_amount(new_amount)
Money.new(new_amount, currency)
end
end
eur = Money.new(10, "EUR")
p eur # => #<Money:0x00007fd89f84ecc8 @amount=10, @currency="EUR">
other_eur = eur.with_amount(20)
p other_eur #<Money:0x00007fcb55872558 @amount=20, @currency="EUR">
class Money
include Comparable
attr_reader :cents
def initialize(cents)
@cents = cents
end
def <=>(other_money)
cents <=> other_money.cents
end
end
two_euros = Money.new(200)
four_euros = Money.new(400)
puts four_euros > two_euros # => true
p [four_euros, two_euros].sort # => [#<Money:0x00007f837905d9e0 @cents=200>, #<Money:0x00007f837905d968 @cents=400>]
class Money
attr_accessor :amount, :currency
def initialize(amount, currency)
@amount = amount
@currency = currency
end
def to_s
"#{amount} #{currency}"
end
end
eur = Money.new(10, "EUR")
p eur # => #<Money:0x00007fd89f84ecc8 @amount=10, @currency="EUR">
eur.amount = 20
p eur # => #<Money:0x00007fd89f84ecc8 @amount=20, @currency="EUR">
puts eur # => "10 EUR"
puts :ruby.object_id # => 707228
puts :ruby.object_id # => 707228
puts :ruby.equal?(:ruby) # => true
puts nil.object_id # => 8
puts nil.object_id # => 8
class Temperature
OPTIMAL = 22
def initialize(degrees)
@degrees = degrees
end
def cold?
@degrees < OPTIMAL
end
def hot?
@degrees > OPTIMAL
end
def optimal?
@degrees == OPTIMAL
end
end
t1 = Temperature.new(20)
t2 = Temperature.new(22)
puts t1.optimal? # => false
puts t1.hot? # => false
puts t1.cold? # => true
puts t2.optimal? # => true
# create_table :events do |t|
# t.datetime :start_time
# t.datetime :end_time
# ...
# end
class Event < ApplicationRecord
attr_accessor :start_time, :end_time
def formatted_time
from_str = start_time.strftime("%d.%m.%y %H:%M")
till_str = end_time.strftime("%d.%m.%y %H:%M")
"#{from_str} - #{till_str}"
end
def duration
end_time - start_time
end
def multi_day?
start_time.to_date != end_time.to_date
end
def include_date?(date)
date >= start_time.to_date && date <= end_time.to_date
end
def overlap_time_period?(other_start_time, other_end_time)
start_time <= other_start_time && other_end_time <= end_time
end
end
# VS
class TimePeriod
attr_reader :start_time, :end_time
def initialize(start_time, end_time)
@start_time = start_time
@end_time = end_time
end
def to_s
from_str = start_time.strftime("%d.%m.%y %H:%M")
till_str = end_time.strftime("%d.%m.%y %H:%M")
"#{from_str} - #{till_str}"
end
def duration
end_time - start_time
end
def multi_day?
start_time.to_date != end_time.to_date
end
def include_date?(date)
date >= start_time.to_date && date <= end_time.to_date
end
def overlap_time_period?(other_time_period)
start_time <= other_time_period.start_time && other_time_period.end_time <= end_time
end
end
class Event
def time_period
TimePeriod.new(start_time, end_time)
end
end
event = Event.new(start_time: Time.current, end_time: 4.hours.from_now)
event.time_period.to_s
# => "06.11.2022 11:09 - 06.11.2022 15:09"
event.time_period.duration
# => 14399.99786
event.time_period.multi_day?
# => false
event.time_period.include_date?(Time.zone.today)
# => true
other_event = Event.new(start_time: Time.current, end_time: 2.hours.from_now)
event.time_period.overlap_time_period?(other_event.time_period)
# => true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment