Last active
September 2, 2024 18:44
-
-
Save JoelQ/70e2810af465fb679da888389ba4c481 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Loved the blog post!
FYI, looks like the
describe
block name forfrom_parts
is blank.