0.0
No release in over 3 years
Value objects for YearMonth, MonthDay, and TimeOfDay
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

DateValues

Value objects for YearMonth, MonthDay, and TimeOfDay — the date/time types Ruby is missing.

Ruby has Date, Time, and DateTime, but no way to represent "March 2026" without picking a day, "March 19" without picking a year, or "14:30" without picking a date. DateValues fills that gap with immutable, Comparable value objects built on Data.define.

Installation

bundle add date_values

Usage

require 'date_values'
include DateValues

YearMonth

ym = YearMonth.new(2026, 3)
ym.to_s                                          # => "2026-03"
ym.to_date                                       # => #<Date: 2026-03-01>
ym.days                                          # => 31

YearMonth.from(Date.today)                       # => #<DateValues::YearMonth 2026-03>
YearMonth.parse('2026-03')                       # => #<DateValues::YearMonth 2026-03>
YearMonth.parse('2026/3')                        # also works

ym + 1                                           # => #<DateValues::YearMonth 2026-04>
ym - 1                                           # => #<DateValues::YearMonth 2026-02>
YearMonth.new(2026, 3) - YearMonth.new(2025, 1)  # => 14

ym.advance(years: 1, months: 2)                  # => #<DateValues::YearMonth 2027-05>
ym.change(year: 2025)                            # => #<DateValues::YearMonth 2025-03>

# Range support
(YearMonth.new(2026, 1)..YearMonth.new(2026, 3)).to_a
# => [#<DateValues::YearMonth 2026-01>, #<DateValues::YearMonth 2026-02>, #<DateValues::YearMonth 2026-03>]

MonthDay

String representation uses ISO 8601 --MM-DD format (year omitted):

md = MonthDay.new(3, 19)
md.to_s                  # => "--03-19"
md.to_date(2026)         # => #<Date: 2026-03-19>

MonthDay.from(Date.today) # => #<DateValues::MonthDay --03-20>
MonthDay.parse('--03-19') # => #<DateValues::MonthDay --03-19>
MonthDay.parse('3/19')    # also works (month/day order by default)

md.change(month: 12)     # => #<DateValues::MonthDay --12-19>

# Range membership
summer = MonthDay.new(6, 1)..MonthDay.new(8, 31)
summer.cover?(MonthDay.new(7, 15))    # => true

TimeOfDay

tod = TimeOfDay.new(14, 30)
tod.to_s                                        # => "14:30"

TimeOfDay.new(14, 30, 45).to_s                  # => "14:30:45"

TimeOfDay.from(Time.now)                        # => #<DateValues::TimeOfDay 14:30>
TimeOfDay.parse('14:30')                        # => #<DateValues::TimeOfDay 14:30>

tod + 3600                                       # => #<DateValues::TimeOfDay 15:30>
tod - 1800                                       # => #<DateValues::TimeOfDay 14:00>
TimeOfDay.new(17, 0) - TimeOfDay.new(9, 0)       # => 28800 (seconds)
tod.advance(hours: 2, minutes: 15)                # => #<DateValues::TimeOfDay 16:45>
tod.change(minute: 0)                             # => #<DateValues::TimeOfDay 14:00>
tod.to_seconds                                    # => 52200
TimeOfDay.from_seconds(52200)                     # => #<DateValues::TimeOfDay 14:30>

# Wraps at 24h boundaries
TimeOfDay.new(23, 30) + 3600                     # => #<DateValues::TimeOfDay 00:30>

# Range membership
business_hours = TimeOfDay.new(9, 0)..TimeOfDay.new(17, 0)
business_hours.cover?(TimeOfDay.new(12, 30))    # => true

Pattern Matching

Built on Data.define, so pattern matching works out of the box:

case YearMonth.new(2026, 3)
in { year: 2026, month: (1..3) }
  puts 'Q1 2026'
end

case MonthDay.new(12, 25)
in { month: 12, day: 25 }
  puts 'Christmas'
end

case TimeOfDay.new(14, 30)
in { hour: (9..17) }
  puts 'Business hours'
end

JSON

All classes implement #as_json, returning the same string as #to_s:

YearMonth.new(2026, 3).as_json   # => "2026-03"
MonthDay.new(3, 19).as_json      # => "--03-19"
TimeOfDay.new(14, 30).as_json    # => "14:30"

Configuration

MonthDay parse order

By default, MonthDay.parse interprets ambiguous formats like "3/19" as month/day. For day/month (European convention):

DateValues.config.month_day_order = :day_first

MonthDay.parse('19/3')   # => #<DateValues::MonthDay --03-19>

ISO 8601 format (--MM-DD) is always month/day regardless of this setting.

Rails Integration

See date_values-rails for ActiveModel/ActiveRecord type casting, validation, I18n, and ActiveJob support.

License

The gem is available as open source under the terms of the MIT License.