Skip to content

Latest commit

 

History

History
142 lines (99 loc) · 4.45 KB

File metadata and controls

142 lines (99 loc) · 4.45 KB

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.