Skip to content

Instantly share code, notes, and snippets.

@JoelQ
Last active November 10, 2023 04:17
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JoelQ/70e2810af465fb679da888389ba4c481 to your computer and use it in GitHub Desktop.
Save JoelQ/70e2810af465fb679da888389ba4c481 to your computer and use it in GitHub Desktop.
require "date"
class Month
include Comparable
MONTHS_PER_YEAR = 12
def self.from_date(date)
self.from_parts(date.year, date.month)
end
def self.from_parts(year, month)
months_since_jan = month - 1
months_since_one_bce = (year * MONTHS_PER_YEAR) + months_since_jan
new(months_since_one_bce)
end
def initialize(since_one_bce)
# months since Jan, 1BCE (there is no year zero)
# https://en.wikipedia.org/wiki/Year_zero
@since_one_bce = since_one_bce
end
# Accessors
# We need to use integer division and modulo to break down our single internal
# value into different granularities for human consumption.
def month
# we want Jan to be month 1, not 0 to match the Ruby Date#month API
(since_one_bce % MONTHS_PER_YEAR) + 1
end
def year
(since_one_bce / MONTHS_PER_YEAR)
end
# <=> and succ required to work with ranges
def <=>(other)
since_one_bce <=> other.since_one_bce
end
def succ
self.class.new(since_one_bce.succ)
end
# Since this is a value object, we want eql?, hash, and == to match when the
# internal value of two month objects is the same. We get == for free from
# Comparable but need to implement the others ourselves.
def hash
[self.class, since_one_bce].hash
end
alias_method :eql?, :==
def inspect
"#{Date::MONTHNAMES[month]} #{year}"
end
protected
attr_reader :since_one_bce
end
require_relative "month"
require "date"
RSpec.describe Month do
describe ".from_date" do
it "correctly pulls the year and month" do
date = Date.new(2020, 6)
month = Month.from_date(date)
expect(month.year).to eq 2020
expect(month.month).to eq 6
end
it "treats january as the first month, not zero" do
date = Date.new(2020, 1)
month = Month.from_date(date)
expect(month.month).to eq 1
end
end
describe ".from_parts" do
it "correctly pulls the year and month" do
month = Month.from_parts(2020, 6)
expect(month.year).to eq 2020
expect(month.month).to eq 6
end
it "treats january as the first month, not zero" do
month = Month.from_parts(2020, 1)
expect(month.month).to eq 1
end
end
describe "#<=>" do
it "is 0 for another instance of the same value" do
m1 = Month.from_parts(2020, 2)
m2 = Month.from_parts(2020, 2)
expect(m1 <=> m2).to eq 0
end
it "is -1 for another instance that is later" do
m1 = Month.from_parts(2020, 2)
m2 = Month.from_parts(2020, 3)
expect(m1 <=> m2).to eq(-1)
end
it "is 1 for another instance that is earlier" do
m1 = Month.from_parts(2020, 2)
m2 = Month.from_parts(2020, 1)
expect(m1 <=> m2).to eq 1
end
end
describe "#succ" do
it "generates the next month" do
m1 = Month.from_parts(2020, 1)
m2 = m1.succ
expect(m2.year).to eq 2020
expect(m2.month).to eq 2
end
it "correctly wraps around the year boundary" do
m1 = Month.from_parts(2020, 12)
m2 = m1.succ
expect(m2.year).to eq 2021
expect(m2.month).to eq 1
end
end
describe "#hash" do
it "is the same as another instance of the same value" do
m1 = Month.from_parts(2020, 1)
m2 = Month.from_parts(2020, 1)
expect(m1.hash).to eq m2.hash
end
it "is the different from another instance of a different value" do
m1 = Month.from_parts(2020, 1)
m2 = Month.from_parts(2020, 2)
expect(m1.hash).not_to eq m2.hash
end
end
describe "#eql?" do
it "two instances are equal if they have the same value" do
m1 = Month.from_parts(2020, 1)
m2 = Month.from_parts(2020, 1)
expect(m1).to be_eql m2
end
it "two instances are not equal if they have a different value" do
m1 = Month.from_parts(2020, 1)
m2 = Month.from_parts(2020, 2)
expect(m1).not_to be_eql m2
end
end
describe "#==" do
it "two instances are equal if they have the same value" do
m1 = Month.from_parts(2020, 1)
m2 = Month.from_parts(2020, 1)
expect(m1).to eq m2
end
it "two instances are not equal if they have a different value" do
m1 = Month.from_parts(2020, 1)
m2 = Month.from_parts(2020, 2)
expect(m1).not_to eq m2
end
end
describe "#equal?" do
it "is equal if two object share the same identity" do
m1 = Month.from_parts(2020, 1)
expect(m1).to be_equal m1
end
it "is not equal if two different objects share the same value" do
m1 = Month.from_parts(2020, 1)
m2 = Month.from_parts(2020, 1)
expect(m1).not_to be_equal m2
end
end
end
@mthadley
Copy link

Loved the blog post!

FYI, looks like the describe block name for from_parts is blank.

@JoelQ
Copy link
Author

JoelQ commented Apr 22, 2022

Good catch @mthadley! Fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment