date_values 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9128f65130329b58d9b750e411178c545086d0b46121ebe8a502fd3bb2774d9f
4
+ data.tar.gz: 39c349f3f8edb108890f1c3b267a75e3e16ef59f846fc5bebdc5198f38a89f31
5
+ SHA512:
6
+ metadata.gz: 30ceca8989f62e0fa958b02858ccefef45cff6d637e77f0fa7f4f3e87c6be562fb5d7c444873ebedb0f0ccf665f2662806ff7a5a0103459e47cce86e7753988e
7
+ data.tar.gz: d0c2a75cb71ae66715f1cf357602f948910d518d3d9901a57672f7457626e500fe6a91ebd18cb0d56738aeb6c2c66c74797043b92576f154d656fe4c138b9eae
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-03-19
4
+
5
+ - `DateValues::YearMonth` — year-month value object with arithmetic (`+`, `-`), `Range` support, and `Date` conversion
6
+ - `DateValues::MonthDay` — month-day value object with ISO 8601 `--MM-DD` format
7
+ - `DateValues::TimeOfDay` — time-of-day value object with optional seconds
8
+ - All classes are `Data.define`-based (immutable, value equality) and include `Comparable`
9
+ - `require 'date_values/rails'` registers ActiveModel types (`:year_month`, `:month_day`, `:time_of_day`)
10
+ - RBS type signatures included
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Keita Urashima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # DateValues
2
+
3
+ Value objects for `YearMonth`, `MonthDay`, and `TimeOfDay` — the date/time types Ruby is missing.
4
+
5
+ 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`.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bundle add date_values
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ruby
16
+ require 'date_values'
17
+ include DateValues
18
+ ```
19
+
20
+ ### YearMonth
21
+
22
+ ```ruby
23
+ ym = YearMonth.new(2026, 3)
24
+ ym.to_s # => "2026-03"
25
+ ym.to_date # => #<Date: 2026-03-01>
26
+
27
+ YearMonth.parse('2026-03') # => YearMonth[2026-03]
28
+
29
+ ym + 1 # => YearMonth[2026-04]
30
+ ym - 1 # => YearMonth[2026-02]
31
+ YearMonth.new(2026, 3) - YearMonth.new(2025, 1) # => 14
32
+
33
+ # Range support
34
+ (YearMonth.new(2026, 1)..YearMonth.new(2026, 3)).to_a
35
+ # => [YearMonth[2026-01], YearMonth[2026-02], YearMonth[2026-03]]
36
+ ```
37
+
38
+ ### MonthDay
39
+
40
+ ```ruby
41
+ md = MonthDay.new(3, 19)
42
+ md.to_s # => "--03-19"
43
+ md.to_date(2026) # => #<Date: 2026-03-19>
44
+
45
+ MonthDay.parse('--03-19') # => MonthDay[--03-19]
46
+ ```
47
+
48
+ ### TimeOfDay
49
+
50
+ ```ruby
51
+ tod = TimeOfDay.new(14, 30)
52
+ tod.to_s # => "14:30"
53
+
54
+ TimeOfDay.new(14, 30, 45).to_s # => "14:30:45"
55
+
56
+ TimeOfDay.parse('14:30') # => TimeOfDay[14:30]
57
+ ```
58
+
59
+ ## Rails Integration
60
+
61
+ Opt-in ActiveModel type casting for ActiveRecord attributes:
62
+
63
+ ```ruby
64
+ require 'date_values/rails'
65
+
66
+ class Shop < ApplicationRecord
67
+ attribute :billing_month, :year_month # string column "2026-03"
68
+ attribute :anniversary, :month_day # string column "--03-19"
69
+ attribute :opens_at, :time_of_day # string or time column
70
+ end
71
+ ```
72
+
73
+ ## License
74
+
75
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module DateValues
6
+ MonthDay = Data.define(:month, :day) do
7
+ include Comparable
8
+
9
+ def initialize(month:, day:)
10
+ raise ArgumentError, "invalid month: #{month}" unless (1..12).include?(month)
11
+ raise ArgumentError, "invalid day: #{day} for month: #{month}" unless (1..max_day(month)).include?(day)
12
+
13
+ super
14
+ end
15
+
16
+ def self.parse(str)
17
+ raise ArgumentError, "invalid MonthDay: #{str}" unless str.match?(/\A--\d{2}-\d{2}\z/)
18
+
19
+ month, day = str[2..].split('-').map(&:to_i)
20
+ new(month, day)
21
+ end
22
+
23
+ def <=>(other)
24
+ return nil unless other.is_a?(MonthDay)
25
+
26
+ [month, day] <=> [other.month, other.day]
27
+ end
28
+
29
+ def to_date(year)
30
+ Date.new(year, month, day)
31
+ end
32
+
33
+ def to_s
34
+ format('--%02d-%02d', month, day)
35
+ end
36
+
37
+ def inspect
38
+ "MonthDay[#{self}]"
39
+ end
40
+
41
+ private
42
+
43
+ def max_day(m)
44
+ case m
45
+ when 2 then 29
46
+ when 4, 6, 9, 11 then 30
47
+ else 31
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DateValues
4
+ module Rails
5
+ class MonthDayType < ActiveModel::Type::Value
6
+ def type
7
+ :month_day
8
+ end
9
+
10
+ def cast(value)
11
+ case value
12
+ when MonthDay then value
13
+ when String then MonthDay.parse(value)
14
+ when nil then nil
15
+ else raise ArgumentError, "can't cast #{value.class} to MonthDay"
16
+ end
17
+ end
18
+
19
+ def serialize(value)
20
+ value&.to_s
21
+ end
22
+
23
+ def deserialize(value)
24
+ return nil if value.nil?
25
+
26
+ MonthDay.parse(value)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DateValues
4
+ module Rails
5
+ class TimeOfDayType < ActiveModel::Type::Value
6
+ def type
7
+ :time_of_day
8
+ end
9
+
10
+ def cast(value)
11
+ case value
12
+ when TimeOfDay then value
13
+ when Time then TimeOfDay.new(value.hour, value.min, value.sec)
14
+ when String then TimeOfDay.parse(value)
15
+ when nil then nil
16
+ else raise ArgumentError, "can't cast #{value.class} to TimeOfDay"
17
+ end
18
+ end
19
+
20
+ def serialize(value)
21
+ value&.to_s
22
+ end
23
+
24
+ def deserialize(value)
25
+ case value
26
+ when nil then nil
27
+ when Time then TimeOfDay.new(value.hour, value.min, value.sec)
28
+ when String then TimeOfDay.parse(value)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DateValues
4
+ module Rails
5
+ class YearMonthType < ActiveModel::Type::Value
6
+ def type
7
+ :year_month
8
+ end
9
+
10
+ def cast(value)
11
+ case value
12
+ when YearMonth then value
13
+ when String then YearMonth.parse(value)
14
+ when nil then nil
15
+ else raise ArgumentError, "can't cast #{value.class} to YearMonth"
16
+ end
17
+ end
18
+
19
+ def serialize(value)
20
+ value&.to_s
21
+ end
22
+
23
+ def deserialize(value)
24
+ return nil if value.nil?
25
+
26
+ YearMonth.parse(value)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date_values'
4
+ require 'active_support'
5
+ require 'active_model/type'
6
+ require_relative 'rails/year_month_type'
7
+ require_relative 'rails/month_day_type'
8
+ require_relative 'rails/time_of_day_type'
9
+
10
+ ActiveModel::Type.register(:year_month, DateValues::Rails::YearMonthType)
11
+ ActiveModel::Type.register(:month_day, DateValues::Rails::MonthDayType)
12
+ ActiveModel::Type.register(:time_of_day, DateValues::Rails::TimeOfDayType)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DateValues
4
+ TimeOfDay = Data.define(:hour, :minute, :second) do
5
+ include Comparable
6
+
7
+ def initialize(hour:, minute:, second: 0)
8
+ raise ArgumentError, "invalid hour: #{hour}" unless (0..23).include?(hour)
9
+ raise ArgumentError, "invalid minute: #{minute}" unless (0..59).include?(minute)
10
+ raise ArgumentError, "invalid second: #{second}" unless (0..59).include?(second)
11
+
12
+ super
13
+ end
14
+
15
+ def self.parse(str)
16
+ parts = str.split(':')
17
+ raise ArgumentError, "invalid TimeOfDay: #{str}" unless [2, 3].include?(parts.size)
18
+
19
+ new(*parts.map(&:to_i))
20
+ end
21
+
22
+ def <=>(other)
23
+ return nil unless other.is_a?(TimeOfDay)
24
+
25
+ [hour, minute, second] <=> [other.hour, other.minute, other.second]
26
+ end
27
+
28
+ def to_s
29
+ if second.zero?
30
+ format('%02d:%02d', hour, minute)
31
+ else
32
+ format('%02d:%02d:%02d', hour, minute, second)
33
+ end
34
+ end
35
+
36
+ def inspect
37
+ "TimeOfDay[#{self}]"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DateValues
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module DateValues
6
+ YearMonth = Data.define(:year, :month) do
7
+ include Comparable
8
+
9
+ def initialize(year:, month:)
10
+ raise ArgumentError, "invalid month: #{month}" unless (1..12).include?(month)
11
+
12
+ super
13
+ end
14
+
15
+ def self.parse(str)
16
+ raise ArgumentError, "invalid YearMonth: #{str}" unless str.match?(/\A\d{4}-\d{2}\z/)
17
+
18
+ year, month = str.split('-').map(&:to_i)
19
+ new(year, month)
20
+ end
21
+
22
+ def <=>(other)
23
+ return nil unless other.is_a?(YearMonth)
24
+
25
+ [year, month] <=> [other.year, other.month]
26
+ end
27
+
28
+ def +(n)
29
+ total = (year * 12 + (month - 1)) + n
30
+ self.class.new(total / 12, total % 12 + 1)
31
+ end
32
+
33
+ def -(other)
34
+ case other
35
+ when Integer
36
+ self + (-other)
37
+ when YearMonth
38
+ (year * 12 + month) - (other.year * 12 + other.month)
39
+ else
40
+ raise TypeError, "#{other.class} can't be coerced into Integer or YearMonth"
41
+ end
42
+ end
43
+
44
+ def succ
45
+ self + 1
46
+ end
47
+
48
+ def to_date
49
+ Date.new(year, month, 1)
50
+ end
51
+
52
+ def to_s
53
+ format('%04d-%02d', year, month)
54
+ end
55
+
56
+ def inspect
57
+ "YearMonth[#{self}]"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'date_values/version'
4
+ require_relative 'date_values/year_month'
5
+ require_relative 'date_values/month_day'
6
+ require_relative 'date_values/time_of_day'
@@ -0,0 +1,44 @@
1
+ module DateValues
2
+ module Rails
3
+ class YearMonthType < ActiveModel::Type::Value
4
+ def type: () -> :year_month
5
+
6
+ def cast: (YearMonth value) -> YearMonth
7
+ | (String value) -> YearMonth
8
+ | (nil value) -> nil
9
+
10
+ def serialize: (YearMonth? value) -> String?
11
+
12
+ def deserialize: (String value) -> YearMonth
13
+ | (nil value) -> nil
14
+ end
15
+
16
+ class MonthDayType < ActiveModel::Type::Value
17
+ def type: () -> :month_day
18
+
19
+ def cast: (MonthDay value) -> MonthDay
20
+ | (String value) -> MonthDay
21
+ | (nil value) -> nil
22
+
23
+ def serialize: (MonthDay? value) -> String?
24
+
25
+ def deserialize: (String value) -> MonthDay
26
+ | (nil value) -> nil
27
+ end
28
+
29
+ class TimeOfDayType < ActiveModel::Type::Value
30
+ def type: () -> :time_of_day
31
+
32
+ def cast: (TimeOfDay value) -> TimeOfDay
33
+ | (Time value) -> TimeOfDay
34
+ | (String value) -> TimeOfDay
35
+ | (nil value) -> nil
36
+
37
+ def serialize: (TimeOfDay? value) -> String?
38
+
39
+ def deserialize: (Time value) -> TimeOfDay
40
+ | (String value) -> TimeOfDay
41
+ | (nil value) -> nil
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,73 @@
1
+ module DateValues
2
+ VERSION: String
3
+
4
+ class YearMonth
5
+ include Comparable
6
+
7
+ attr_reader year: Integer
8
+ attr_reader month: Integer
9
+
10
+ def initialize: (Integer year, Integer month) -> void
11
+
12
+ def self.parse: (String str) -> YearMonth
13
+
14
+ def <=>: (YearMonth other) -> Integer
15
+ | (untyped other) -> nil
16
+
17
+ def +: (Integer n) -> YearMonth
18
+
19
+ def -: (Integer n) -> YearMonth
20
+ | (YearMonth other) -> Integer
21
+
22
+ def succ: () -> YearMonth
23
+
24
+ def to_date: () -> Date
25
+
26
+ def to_s: () -> String
27
+
28
+ def inspect: () -> String
29
+ end
30
+
31
+ class MonthDay
32
+ include Comparable
33
+
34
+ attr_reader month: Integer
35
+ attr_reader day: Integer
36
+
37
+ def initialize: (Integer month, Integer day) -> void
38
+
39
+ def self.parse: (String str) -> MonthDay
40
+
41
+ def <=>: (MonthDay other) -> Integer
42
+ | (untyped other) -> nil
43
+
44
+ def to_date: (Integer year) -> Date
45
+
46
+ def to_s: () -> String
47
+
48
+ def inspect: () -> String
49
+
50
+ private
51
+
52
+ def max_day: (Integer m) -> Integer
53
+ end
54
+
55
+ class TimeOfDay
56
+ include Comparable
57
+
58
+ attr_reader hour: Integer
59
+ attr_reader minute: Integer
60
+ attr_reader second: Integer
61
+
62
+ def initialize: (Integer hour, Integer minute, ?Integer second) -> void
63
+
64
+ def self.parse: (String str) -> TimeOfDay
65
+
66
+ def <=>: (TimeOfDay other) -> Integer
67
+ | (untyped other) -> nil
68
+
69
+ def to_s: () -> String
70
+
71
+ def inspect: () -> String
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: date_values
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Keita Urashima
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ email:
13
+ - ursm@ursm.jp
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - CHANGELOG.md
19
+ - LICENSE.txt
20
+ - README.md
21
+ - Rakefile
22
+ - lib/date_values.rb
23
+ - lib/date_values/month_day.rb
24
+ - lib/date_values/rails.rb
25
+ - lib/date_values/rails/month_day_type.rb
26
+ - lib/date_values/rails/time_of_day_type.rb
27
+ - lib/date_values/rails/year_month_type.rb
28
+ - lib/date_values/time_of_day.rb
29
+ - lib/date_values/version.rb
30
+ - lib/date_values/year_month.rb
31
+ - sig/date_values.rbs
32
+ - sig/date_values/rails.rbs
33
+ homepage: https://github.com/ursm/date_values
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/ursm/date_values
38
+ source_code_uri: https://github.com/ursm/date_values
39
+ changelog_uri: https://github.com/ursm/date_values/blob/main/CHANGELOG.md
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.3.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 4.0.6
55
+ specification_version: 4
56
+ summary: Value objects for YearMonth, MonthDay, and TimeOfDay
57
+ test_files: []