MetricCalendar 1.0.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/metric_calendar.rb +146 -0
  3. metadata +45 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7985b53d95778cdfde8aced371201714c3b58366bb6e34290354dc56f2d8280b
4
+ data.tar.gz: 79ecf8c72a6cb27feb886a309b6e13d28bd96bf275d8f82256d803c794376c73
5
+ SHA512:
6
+ metadata.gz: 1b3c02405c6e619c9213a10188946afac7cc6863ffae7dc198ca6363ba4d99abfea543312ed9fc882ee2cd9c81500530ed031479b4133132c88f8b6c9de9ae1d
7
+ data.tar.gz: 79ad2eb397818ced0299c1cd475b7373dcdfe6d11ffff486a82af52e143fa03134040f32b2fc06ea78163f892b26db433ec73fa7b0f5c41ec93840c84fb9fcff
@@ -0,0 +1,146 @@
1
+ require 'date'
2
+
3
+ module MetricCalendar
4
+ DAY_NAMES = %w[Primday Duoday Triday Quadday Quintday Hexday Septday Octday Novday Decday].freeze
5
+ MONTH_NAMES = %w[Unil Duil Tril Quadril Quintil Sextil Septil Octil Novil Decil Undecil Duodecil].freeze
6
+ TURNING_DAY_NAMES = %w[Vigil Balance Dawn].freeze
7
+ YULE_DAY_NAMES = ['Yule Eve', 'Midwinter', 'Kindling'].freeze
8
+
9
+ # Struct for a Metric Calendar date. Fields:
10
+ # year - Metric year (Year 0 = spring equinox 1970)
11
+ # month - 1-12, 0 for Turning/Yule
12
+ # month_name - e.g. "Unil", "" for Turning/Yule
13
+ # day - 1-30, 0 for Turning/Yule
14
+ # week_day - 1-10, 0 for Turning/Yule
15
+ # day_name - e.g. "Primday", "" for Turning/Yule
16
+ # week - 1-36, 0 for Turning/Yule
17
+ # season_index - 0-3, -1 for Turning/Yule
18
+ # is_leap_year - true if this metric year has 3 Yule days
19
+ # is_turning - true during The Turning (3 days at spring equinox)
20
+ # is_yule - true during Yule
21
+ # is_midsummer - true on Quadril 1 (summer solstice)
22
+ # is_spiral - true on Quintil 18 (golden angle day)
23
+ # is_rest - true on days 8-10 of any 10-day week
24
+ # special_day - "Vigil", "Balance", "Dawn", "Yule Eve", "Midwinter", "Kindling", or ""
25
+ MetricDate = Struct.new(
26
+ :year, :month, :month_name, :day, :week_day, :day_name,
27
+ :week, :season_index, :is_leap_year, :is_turning, :is_yule,
28
+ :is_midsummer, :is_spiral, :is_rest, :special_day,
29
+ keyword_init: true
30
+ )
31
+
32
+ def self.leap_year?(gregorian_year)
33
+ gregorian_year % 4 == 0 && (gregorian_year % 100 != 0 || gregorian_year % 400 == 0)
34
+ end
35
+ private_class_method :leap_year?
36
+
37
+ # Convert a Gregorian Date to a MetricDate.
38
+ # @param date [Date] the Gregorian date to convert
39
+ # @return [MetricDate]
40
+ def self.gregorian_to_metric(date)
41
+ year = date.year
42
+ equinox = Date.new(year, 3, 20)
43
+ days_from_equinox = (date - equinox).to_i
44
+
45
+ if days_from_equinox >= 0
46
+ metric_year = year - 1970
47
+ day_of_year = days_from_equinox + 1
48
+ else
49
+ metric_year = year - 1 - 1970
50
+ prev_equinox = Date.new(year - 1, 3, 20)
51
+ day_of_year = (date - prev_equinox).to_i + 1
52
+ end
53
+
54
+ leap = leap_year?(metric_year + 1971)
55
+ yule_day_count = leap ? 3 : 2
56
+
57
+ base = {
58
+ year: metric_year, month: 0, month_name: '', day: 0, week_day: 0,
59
+ day_name: '', week: 0, season_index: -1,
60
+ is_leap_year: leap, is_turning: false, is_yule: false,
61
+ is_midsummer: false, is_spiral: false, is_rest: false, special_day: ''
62
+ }
63
+
64
+ # The Turning (days 1-3)
65
+ if day_of_year <= 3
66
+ return MetricDate.new(**base.merge(is_turning: true, special_day: TURNING_DAY_NAMES[day_of_year - 1]))
67
+ end
68
+
69
+ adjusted = day_of_year - 3
70
+
71
+ if adjusted <= 270
72
+ m = (adjusted - 1) / 30 + 1
73
+ d = (adjusted - 1) % 30 + 1
74
+ elsif adjusted <= 270 + yule_day_count
75
+ return MetricDate.new(**base.merge(is_yule: true, special_day: YULE_DAY_NAMES[adjusted - 271]))
76
+ else
77
+ post_yule = adjusted - 270 - yule_day_count
78
+ m = 9 + (post_yule - 1) / 30 + 1
79
+ d = (post_yule - 1) % 30 + 1
80
+ end
81
+
82
+ week_day = (d - 1) % 10 + 1
83
+ week = (m - 1) * 3 + (d - 1) / 10 + 1
84
+
85
+ MetricDate.new(
86
+ year: metric_year,
87
+ month: m, month_name: MONTH_NAMES[m - 1],
88
+ day: d, week_day: week_day, day_name: DAY_NAMES[week_day - 1],
89
+ week: week, season_index: (m - 1) / 3,
90
+ is_leap_year: leap,
91
+ is_turning: false, is_yule: false,
92
+ is_midsummer: (m == 4 && d == 1),
93
+ is_spiral: (m == 5 && d == 18),
94
+ is_rest: week_day >= 8,
95
+ special_day: ''
96
+ )
97
+ end
98
+
99
+ # Convert a Metric Calendar date back to a Gregorian Date.
100
+ # @param year [Integer] Metric year
101
+ # @param period_type [String] "turning", "month", or "yule"
102
+ # @param period_value [Integer] 0-indexed turning/yule (0-2), or 1-12 for month
103
+ # @param day_of_month [Integer] 1-30, used only when period_type is "month"
104
+ # @return [Date]
105
+ def self.metric_to_gregorian(year, period_type, period_value, day_of_month = 1)
106
+ equinox_year = year + 1970
107
+ leap = leap_year?(year + 1971)
108
+ yule_day_count = leap ? 3 : 2
109
+
110
+ offset = case period_type
111
+ when 'turning'
112
+ raise ArgumentError, 'turning period_value must be 0-2' unless (0..2).include?(period_value)
113
+ period_value
114
+ when 'month'
115
+ m, d = period_value, day_of_month
116
+ raise ArgumentError, 'month must be 1-12' unless (1..12).include?(m)
117
+ raise ArgumentError, 'day must be 1-30' unless (1..30).include?(d)
118
+ if m <= 9
119
+ 3 + (m - 1) * 30 + (d - 1)
120
+ else
121
+ 3 + 270 + yule_day_count + (m - 10) * 30 + (d - 1)
122
+ end
123
+ when 'yule'
124
+ raise ArgumentError, 'Kindling only occurs in leap years' if period_value == 2 && !leap
125
+ raise ArgumentError, 'yule period_value must be 0-2' unless (0..2).include?(period_value)
126
+ 3 + 270 + period_value
127
+ else
128
+ raise ArgumentError, "Unknown period_type: #{period_type.inspect}"
129
+ end
130
+
131
+ Date.new(equinox_year, 3, 20) + offset
132
+ end
133
+
134
+ # Returns true if the given date is a rest day (days 8-10 of any 10-day week).
135
+ # @param date [Date]
136
+ # @return [Boolean]
137
+ def self.is_rest_day(date)
138
+ gregorian_to_metric(date).is_rest
139
+ end
140
+
141
+ # Returns the current Metric Calendar date.
142
+ # @return [MetricDate]
143
+ def self.today
144
+ gregorian_to_metric(Date.today)
145
+ end
146
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: MetricCalendar
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Rabarts
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A Ruby library for working with the Metric Calendar — a rational decimal
14
+ calendar system with 10-day weeks and 12 months of 30 days.
15
+ email: []
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/metric_calendar.rb
21
+ homepage: https://metricweek.com
22
+ licenses:
23
+ - MIT
24
+ metadata:
25
+ source_code_uri: https://github.com/alexrabarts/metric-calendar
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '2.5'
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubygems_version: 3.0.3.1
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Metric Calendar date conversion
45
+ test_files: []