fasti 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.
- checksums.yaml +7 -0
- data/.mcp.json +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +82 -0
- data/.rubocop_todo.yml +89 -0
- data/.serena/project.yml +68 -0
- data/.simplecov +31 -0
- data/.yardopts +9 -0
- data/AGENTS.md +60 -0
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +416 -0
- data/RELEASING.md +202 -0
- data/Rakefile +34 -0
- data/TODO.md +11 -0
- data/benchmark/holiday_cache_benchmark.rb +111 -0
- data/benchmark/memory_benchmark.rb +86 -0
- data/docs/agents/git-pr.md +298 -0
- data/docs/agents/languages.md +388 -0
- data/docs/agents/rubocop.md +55 -0
- data/docs/plans/positional-arguments.md +303 -0
- data/docs/plans/structured-config.md +232 -0
- data/examples/config.rb +80 -0
- data/exe/fasti +6 -0
- data/lib/fasti/calendar.rb +292 -0
- data/lib/fasti/calendar_transition.rb +266 -0
- data/lib/fasti/cli.rb +550 -0
- data/lib/fasti/config/schema.rb +36 -0
- data/lib/fasti/config/types.rb +66 -0
- data/lib/fasti/config.rb +125 -0
- data/lib/fasti/error.rb +6 -0
- data/lib/fasti/formatter.rb +234 -0
- data/lib/fasti/style_parser.rb +211 -0
- data/lib/fasti/version.rb +6 -0
- data/lib/fasti.rb +21 -0
- data/mise.toml +5 -0
- data/sig/fasti.rbs +4 -0
- metadata +181 -0
@@ -0,0 +1,292 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require "holidays"
|
5
|
+
|
6
|
+
module Fasti
|
7
|
+
# Represents a calendar for a specific month and year with configurable start of week.
|
8
|
+
#
|
9
|
+
# This class provides calendar structure functionality including calendar grid generation,
|
10
|
+
# day calculations, and formatting support. It handles different week start preferences
|
11
|
+
# (Sunday vs Monday) and integrates with country-specific holiday detection via the holidays gem.
|
12
|
+
#
|
13
|
+
# @example Creating a calendar for January 2024
|
14
|
+
# calendar = Calendar.new(2024, 1, country: :jp, start_of_week: :sunday)
|
15
|
+
# calendar.days_in_month #=> 31
|
16
|
+
# calendar.month_year_header #=> "January 2024"
|
17
|
+
#
|
18
|
+
# @example Getting calendar grid for display
|
19
|
+
# grid = calendar.calendar_grid
|
20
|
+
# # Returns: [[nil, 1, 2, 3, 4, 5, 6], [7, 8, 9, ...], ...]
|
21
|
+
#
|
22
|
+
# @example Working with different week starts
|
23
|
+
# sunday_calendar = Calendar.new(2024, 1, country: :us, start_of_week: :sunday)
|
24
|
+
# monday_calendar = Calendar.new(2024, 1, country: :jp, start_of_week: :monday)
|
25
|
+
class Calendar
|
26
|
+
# @return [Integer] The year of the calendar
|
27
|
+
# @return [Integer] The month of the calendar (1-12)
|
28
|
+
# @return [Symbol] The start of week preference (:sunday, :monday, :tuesday, etc.)
|
29
|
+
# @return [Symbol] The country code for holiday context
|
30
|
+
attr_reader :year, :month, :start_of_week, :country
|
31
|
+
|
32
|
+
# Full weekday names for internal reference (as symbols)
|
33
|
+
WEEK_DAYS = %i[sunday monday tuesday wednesday thursday friday saturday].freeze
|
34
|
+
private_constant :WEEK_DAYS
|
35
|
+
|
36
|
+
# Abbreviated day names for calendar headers
|
37
|
+
DAY_ABBREVS = %w[Su Mo Tu We Th Fr Sa].freeze
|
38
|
+
private_constant :DAY_ABBREVS
|
39
|
+
|
40
|
+
# Creates a new calendar instance.
|
41
|
+
#
|
42
|
+
# @param year [Integer] The year (must be positive)
|
43
|
+
# @param month [Integer] The month (1-12)
|
44
|
+
# @param country [Symbol] Country code for holiday context (e.g., :jp, :us)
|
45
|
+
# @param start_of_week [Symbol] Week start preference (:sunday, :monday, :tuesday, etc.)
|
46
|
+
# @raise [ArgumentError] If parameters are invalid
|
47
|
+
#
|
48
|
+
# @example Standard calendar
|
49
|
+
# Calendar.new(2024, 6, country: :jp)
|
50
|
+
#
|
51
|
+
# @example Monday-start calendar
|
52
|
+
# Calendar.new(2024, 6, country: :us, start_of_week: :monday)
|
53
|
+
def initialize(year, month, country:, start_of_week: :sunday)
|
54
|
+
@year = year
|
55
|
+
@month = month
|
56
|
+
@start_of_week = start_of_week.to_sym
|
57
|
+
@country = country
|
58
|
+
@holidays_for_month = nil
|
59
|
+
@calendar_transition = CalendarTransition.new(@country)
|
60
|
+
|
61
|
+
validate_inputs
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the number of days in the calendar month.
|
65
|
+
#
|
66
|
+
# @return [Integer] Number of days in the month (28-31)
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# Calendar.new(2024, 2, country: :jp).days_in_month #=> 29 (leap year)
|
70
|
+
# Calendar.new(2023, 2, country: :jp).days_in_month #=> 28
|
71
|
+
def days_in_month
|
72
|
+
Date.new(year, month, -1).day
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the first day of the calendar month.
|
76
|
+
#
|
77
|
+
# @return [Date] The first day of the month
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# Calendar.new(2024, 6, country: :jp).first_day_of_month
|
81
|
+
# #=> #<Date: 2024-06-01>
|
82
|
+
def first_day_of_month
|
83
|
+
@calendar_transition.create_date(year, month, 1)
|
84
|
+
rescue ArgumentError
|
85
|
+
# If day 1 is in a gap (very rare), try day 2, then 3, etc.
|
86
|
+
(2..31).each do |day|
|
87
|
+
return @calendar_transition.create_date(year, month, day)
|
88
|
+
rescue ArgumentError
|
89
|
+
next
|
90
|
+
end
|
91
|
+
# Fallback to standard Date if all fails
|
92
|
+
Date.new(year, month, 1)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns the last day of the calendar month.
|
96
|
+
#
|
97
|
+
# @return [Date] The last day of the month
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# Calendar.new(2024, 6, country: :jp).last_day_of_month
|
101
|
+
# #=> #<Date: 2024-06-30>
|
102
|
+
def last_day_of_month
|
103
|
+
# Start from the theoretical last day and work backwards
|
104
|
+
max_days = Date.new(year, month, -1).day
|
105
|
+
max_days.downto(1).each do |day|
|
106
|
+
return @calendar_transition.create_date(year, month, day)
|
107
|
+
rescue ArgumentError
|
108
|
+
next
|
109
|
+
end
|
110
|
+
# Fallback to standard Date if all fails
|
111
|
+
Date.new(year, month, -1)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the day of the week for the first day of the month.
|
115
|
+
#
|
116
|
+
# @return [Integer] Day of week (0=Sunday, 1=Monday, ..., 6=Saturday)
|
117
|
+
#
|
118
|
+
# @example
|
119
|
+
# calendar = Calendar.new(2024, 6, country: :jp)
|
120
|
+
# calendar.first_day_wday #=> 6 (if June 1st, 2024 is Saturday)
|
121
|
+
def first_day_wday
|
122
|
+
first_day_of_month.wday
|
123
|
+
end
|
124
|
+
|
125
|
+
# Generates a 2D grid representing the calendar layout.
|
126
|
+
#
|
127
|
+
# The grid is an array of weeks (rows), where each week is an array of 7 days.
|
128
|
+
# Days are represented as integers (1-31) or nil for empty cells.
|
129
|
+
# The grid respects the start_of_week preference.
|
130
|
+
#
|
131
|
+
# @return [Array<Array<Integer, nil>>] 2D array of calendar days
|
132
|
+
#
|
133
|
+
# @example Sunday-start June 2024
|
134
|
+
# calendar = Calendar.new(2024, 6, country: :jp, start_of_week: :sunday)
|
135
|
+
# grid = calendar.calendar_grid
|
136
|
+
# # Returns: [[nil, nil, nil, nil, nil, nil, 1],
|
137
|
+
# # [2, 3, 4, 5, 6, 7, 8], ...]
|
138
|
+
def calendar_grid
|
139
|
+
grid = []
|
140
|
+
current_row = []
|
141
|
+
|
142
|
+
# Add leading empty cells for days before month starts
|
143
|
+
leading_empty_days.times do
|
144
|
+
current_row << nil
|
145
|
+
end
|
146
|
+
|
147
|
+
# Add only existing days (skip gap days) for continuous display
|
148
|
+
(1..days_in_month).each do |day|
|
149
|
+
# Only add days that actually exist (not in transition gaps)
|
150
|
+
next unless to_date(day)
|
151
|
+
|
152
|
+
current_row << day
|
153
|
+
|
154
|
+
# Start new row on end of week
|
155
|
+
if current_row.length == 7
|
156
|
+
grid << current_row
|
157
|
+
current_row = []
|
158
|
+
end
|
159
|
+
# Skip gap days completely - they don't take up space in the grid
|
160
|
+
end
|
161
|
+
|
162
|
+
# Add trailing empty cells and final row if needed
|
163
|
+
if current_row.any?
|
164
|
+
current_row << nil while current_row.length < 7
|
165
|
+
grid << current_row
|
166
|
+
end
|
167
|
+
|
168
|
+
grid
|
169
|
+
end
|
170
|
+
|
171
|
+
# Calculates the number of empty cells needed before the first day.
|
172
|
+
#
|
173
|
+
# This accounts for the start_of_week preference to properly align
|
174
|
+
# the first day of the month in the calendar grid.
|
175
|
+
#
|
176
|
+
# @return [Integer] Number of empty cells (0-6)
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# # If June 1st, 2024 falls on Saturday and we start weeks on Sunday:
|
180
|
+
# calendar = Calendar.new(2024, 6, country: :jp, start_of_week: :sunday)
|
181
|
+
# calendar.leading_empty_days #=> 6
|
182
|
+
def leading_empty_days
|
183
|
+
# Calculate offset based on start of week preference
|
184
|
+
start_wday = WEEK_DAYS.index(start_of_week) || 0
|
185
|
+
|
186
|
+
(first_day_wday - start_wday) % 7
|
187
|
+
end
|
188
|
+
|
189
|
+
# Returns day abbreviations arranged according to start_of_week preference.
|
190
|
+
#
|
191
|
+
# @return [Array<String>] Array of day abbreviations (Su, Mo, Tu, etc.)
|
192
|
+
#
|
193
|
+
# @example Sunday start
|
194
|
+
# Calendar.new(2024, 6, country: :jp, start_of_week: :sunday).day_headers
|
195
|
+
# #=> ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
|
196
|
+
#
|
197
|
+
# @example Monday start
|
198
|
+
# Calendar.new(2024, 6, country: :jp, start_of_week: :monday).day_headers
|
199
|
+
# #=> ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
200
|
+
#
|
201
|
+
# @example Wednesday start
|
202
|
+
# Calendar.new(2024, 6, country: :jp, start_of_week: :wednesday).day_headers
|
203
|
+
# #=> ["We", "Th", "Fr", "Sa", "Su", "Mo", "Tu"]
|
204
|
+
def day_headers
|
205
|
+
# Rotate headers based on start of week
|
206
|
+
start_wday = WEEK_DAYS.index(start_of_week) || 0
|
207
|
+
|
208
|
+
DAY_ABBREVS.rotate(start_wday)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Returns a formatted month and year header string.
|
212
|
+
#
|
213
|
+
# @return [String] Formatted month and year (e.g., "June 2024")
|
214
|
+
#
|
215
|
+
# @example
|
216
|
+
# Calendar.new(2024, 6, country: :jp).month_year_header #=> "June 2024"
|
217
|
+
# Calendar.new(2024, 12, country: :jp).month_year_header #=> "December 2024"
|
218
|
+
def month_year_header
|
219
|
+
date = first_day_of_month
|
220
|
+
date.strftime("%B %Y")
|
221
|
+
end
|
222
|
+
|
223
|
+
# Converts a day number to a Date object for this calendar's month/year.
|
224
|
+
#
|
225
|
+
# @param day [Integer, nil] Day of the month (1-31) or nil
|
226
|
+
# @return [Date, nil] Date object for the specified day, or nil if day is nil
|
227
|
+
#
|
228
|
+
# @example
|
229
|
+
# calendar = Calendar.new(2024, 6, country: :jp)
|
230
|
+
# calendar.to_date(15) #=> #<Date: 2024-06-15>
|
231
|
+
# calendar.to_date(nil) #=> nil
|
232
|
+
def to_date(day)
|
233
|
+
return nil unless day
|
234
|
+
return nil unless (1..days_in_month).cover?(day)
|
235
|
+
|
236
|
+
begin
|
237
|
+
@calendar_transition.create_date(year, month, day)
|
238
|
+
rescue ArgumentError
|
239
|
+
# Date falls in calendar transition gap (non-existent)
|
240
|
+
nil
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Checks if a specific day in this calendar month is a holiday.
|
245
|
+
#
|
246
|
+
# @param day [Integer, nil] Day of the month (1-31) or nil
|
247
|
+
# @return [Boolean] true if the day is a holiday, false otherwise
|
248
|
+
#
|
249
|
+
# @example
|
250
|
+
# calendar = Calendar.new(2024, 1, country: :jp)
|
251
|
+
# calendar.holiday?(1) #=> true (New Year's Day in Japan)
|
252
|
+
# calendar.holiday?(15) #=> false (regular day)
|
253
|
+
# calendar.holiday?(nil) #=> false
|
254
|
+
def holiday?(day)
|
255
|
+
date = to_date(day)
|
256
|
+
return false unless date
|
257
|
+
|
258
|
+
holidays_for_month.key?(date)
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns a hash of holidays for the current month, keyed by date
|
262
|
+
#
|
263
|
+
# @return [Hash<Date, Hash>] Hash mapping holiday dates to holiday information
|
264
|
+
private def holidays_for_month
|
265
|
+
@holidays_for_month ||= begin
|
266
|
+
# Use standard Date creation for holidays lookup to avoid recursion
|
267
|
+
start_date = Date.new(year, month, 1)
|
268
|
+
end_date = Date.new(year, month, -1)
|
269
|
+
|
270
|
+
begin
|
271
|
+
holidays = Holidays.between(start_date, end_date, country)
|
272
|
+
holidays.each_with_object({}) {|holiday, hash| hash[holiday[:date]] = holiday }
|
273
|
+
rescue Holidays::InvalidRegion
|
274
|
+
warn "Warning: Unknown country code '#{country}' for holiday detection"
|
275
|
+
{}
|
276
|
+
rescue => e
|
277
|
+
warn "Warning: Holiday detection failed: #{e.message}"
|
278
|
+
{}
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
private def validate_inputs
|
284
|
+
raise ArgumentError, "Invalid year: #{year}" unless year.is_a?(Integer) && year.positive?
|
285
|
+
raise ArgumentError, "Invalid month: #{month}" unless (1..12).cover?(month)
|
286
|
+
|
287
|
+
return if WEEK_DAYS.include?(start_of_week)
|
288
|
+
|
289
|
+
raise ArgumentError, "Invalid start_of_week: #{start_of_week}. Must be one of: #{WEEK_DAYS.join(", ")}"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module Fasti
|
6
|
+
# Manages Julian to Gregorian calendar transitions for a specific country.
|
7
|
+
#
|
8
|
+
# This class provides country-specific calendar transition data and validation
|
9
|
+
# methods to handle historical calendar reforms. It uses Julian Day Numbers (JDN)
|
10
|
+
# for precise date calculations and gap management during transition periods.
|
11
|
+
#
|
12
|
+
# @example Basic usage
|
13
|
+
# transition = CalendarTransition.new(:gb)
|
14
|
+
# transition.gregorian_start_jdn #=> 2361222
|
15
|
+
# transition.valid_date?(Date.new(1752, 9, 10)) #=> false (in gap)
|
16
|
+
#
|
17
|
+
# @example Date creation with country-specific transitions
|
18
|
+
# gb = CalendarTransition.new(:gb)
|
19
|
+
# gb.create_date(1752, 9, 2) #=> Date object with British transition
|
20
|
+
# it = CalendarTransition.new(:it)
|
21
|
+
# it.create_date(1582, 10, 4) #=> Date object with Italian transition
|
22
|
+
class CalendarTransition
|
23
|
+
# Calendar transition data mapping countries to their Gregorian adoption dates.
|
24
|
+
#
|
25
|
+
# Each entry contains the Julian Day Number when the country switched to
|
26
|
+
# the Gregorian calendar. This corresponds to Ruby's Date class constants.
|
27
|
+
#
|
28
|
+
# Key transitions:
|
29
|
+
# - Italy (1582): October 4 (Julian) → October 15 (Gregorian) - 10 day gap
|
30
|
+
# - Great Britain (1752): September 2 (Julian) → September 14 (Gregorian) - 11 day gap
|
31
|
+
# - Russia (1918): January 31 (Julian) → February 14 (Gregorian) - 13 day gap
|
32
|
+
# - Greece (1923): February 15 (Julian) → March 1 (Gregorian) - 13 day gap
|
33
|
+
TRANSITIONS = {
|
34
|
+
# Italy - October 15, 1582 (Gregorian start)
|
35
|
+
it: Date::ITALY, # 2299161
|
36
|
+
|
37
|
+
# Great Britain - September 14, 1752 (Gregorian start)
|
38
|
+
gb: Date::ENGLAND, # 2361222
|
39
|
+
|
40
|
+
# Common countries using Italian transition (Catholic countries)
|
41
|
+
es: Date::ITALY, # Spain - same as Italy (1582-10-15)
|
42
|
+
fr: 2_299_227, # France - 1582-12-20
|
43
|
+
pt: Date::ITALY, # Portugal - same as Italy (1582-10-15)
|
44
|
+
pl: Date::ITALY, # Poland - same as Italy (1582-10-15)
|
45
|
+
at: Date::ITALY, # Austria - same as Italy (1582-10-15)
|
46
|
+
|
47
|
+
# Countries using British transition (British influence)
|
48
|
+
us: Date::ENGLAND, # United States - followed British calendar
|
49
|
+
ca: Date::ENGLAND, # Canada - followed British calendar
|
50
|
+
au: Date::ENGLAND, # Australia - followed British calendar
|
51
|
+
nz: Date::ENGLAND, # New Zealand - followed British calendar
|
52
|
+
in: Date::ENGLAND, # India - under British rule
|
53
|
+
|
54
|
+
# Germany (complex - varied by region, using common date)
|
55
|
+
de: Date::ITALY, # Most German states adopted in 1582-1584
|
56
|
+
|
57
|
+
# Nordic countries (varied transitions)
|
58
|
+
se: 2_361_391, # Sweden - March 1, 1753 (complex gradual transition)
|
59
|
+
dk: 2_342_032, # Denmark - March 1, 1700
|
60
|
+
no: 2_342_032, # Norway - same as Denmark (March 1, 1700)
|
61
|
+
|
62
|
+
# Eastern European countries (much later adoption)
|
63
|
+
ru: 2_421_639, # Russia - February 14, 1918
|
64
|
+
gr: 2_423_410, # Greece - March 1, 1923
|
65
|
+
|
66
|
+
# Netherlands (complex regional adoption - using latest Protestant adoption)
|
67
|
+
nl: 2_342_164, # Netherlands - July 12, 1700 (Gelderland, last province)
|
68
|
+
|
69
|
+
# Additional European countries from historical table
|
70
|
+
be: 2_299_245, # Belgium (Spanish Netherlands) - January 6, 1583
|
71
|
+
ch: 2_342_150, # Switzerland (Protestant cantons) - January 23, 1700
|
72
|
+
bg: 2_421_344, # Bulgaria - April 14, 1916
|
73
|
+
ro: 2_421_972, # Romania - April 14, 1919
|
74
|
+
rs: 2_421_972, # Serbia (Yugoslavia) - April 14, 1919
|
75
|
+
hr: 2_421_972, # Croatia (Yugoslavia) - April 14, 1919
|
76
|
+
si: 2_421_972, # Slovenia (Yugoslavia) - April 14, 1919
|
77
|
+
tr: 2_424_858, # Turkey (civil calendar) - January 1, 1927
|
78
|
+
|
79
|
+
# The following countries never used Julian calendar before Gregorian adoption
|
80
|
+
# Use proleptic Gregorian calendar (Date::GREGORIAN) for computational consistency
|
81
|
+
|
82
|
+
# Recent adopters from lunar calendars
|
83
|
+
sa: Date::GREGORIAN, # Saudi Arabia - adopted 2016-10-01 (from Islamic/Hijri calendar)
|
84
|
+
|
85
|
+
# Asian countries: transitioned from lunisolar calendars
|
86
|
+
jp: Date::GREGORIAN, # Japan adopted Gregorian calendar on 1873-01-01 (Meiji 6)
|
87
|
+
cn: Date::GREGORIAN, # China adopted Gregorian calendar on 1912-01-01 (Republic of China establishment)
|
88
|
+
tw: Date::GREGORIAN, # Taiwan (same historical root as mainland China)
|
89
|
+
kr: Date::GREGORIAN, # Korea adopted Gregorian calendar on 1896-01-01 (Korean Empire, Geonyang 1)
|
90
|
+
vn: Date::GREGORIAN, # Vietnam adopted Gregorian calendar in 1967
|
91
|
+
th: Date::GREGORIAN # Thailand adopted Gregorian calendar on 1888-01-01 (Rattanakosin 107)
|
92
|
+
}.freeze
|
93
|
+
private_constant :TRANSITIONS
|
94
|
+
|
95
|
+
# Default transition for countries not explicitly listed
|
96
|
+
DEFAULT_TRANSITION = Date::ITALY
|
97
|
+
private_constant :DEFAULT_TRANSITION
|
98
|
+
|
99
|
+
# Returns list of supported countries with transition dates.
|
100
|
+
#
|
101
|
+
# @return [Array<Symbol>] List of supported country codes
|
102
|
+
def self.supported_countries
|
103
|
+
TRANSITIONS.keys.sort
|
104
|
+
end
|
105
|
+
|
106
|
+
# Creates a new CalendarTransition instance for the specified country.
|
107
|
+
#
|
108
|
+
# @param country [Symbol] Country code (e.g., :gb, :us, :it)
|
109
|
+
def initialize(country)
|
110
|
+
@country = country
|
111
|
+
@transition_jdn = TRANSITIONS[@country] || DEFAULT_TRANSITION
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the country code for this transition.
|
115
|
+
#
|
116
|
+
# @return [Symbol] Country code
|
117
|
+
attr_reader :country
|
118
|
+
|
119
|
+
# Returns the Julian Day Number when this country adopted the Gregorian calendar.
|
120
|
+
#
|
121
|
+
# @return [Integer] Julian Day Number of Gregorian calendar adoption
|
122
|
+
#
|
123
|
+
# @example
|
124
|
+
# gb = CalendarTransition.new(:gb)
|
125
|
+
# gb.gregorian_start_jdn #=> 2361222
|
126
|
+
#
|
127
|
+
# it = CalendarTransition.new(:it)
|
128
|
+
# it.gregorian_start_jdn #=> 2299161
|
129
|
+
#
|
130
|
+
# unknown = CalendarTransition.new(:unknown)
|
131
|
+
# unknown.gregorian_start_jdn #=> 2299161 (default)
|
132
|
+
def gregorian_start_jdn
|
133
|
+
@transition_jdn
|
134
|
+
end
|
135
|
+
|
136
|
+
# Creates a Date object using the appropriate calendar system for this country.
|
137
|
+
#
|
138
|
+
# This method automatically selects Julian or Gregorian calendar based on
|
139
|
+
# the date and country's transition point, ensuring appropriate calendar
|
140
|
+
# date representation.
|
141
|
+
#
|
142
|
+
# @param year [Integer] Year
|
143
|
+
# @param month [Integer] Month (1-12)
|
144
|
+
# @param day [Integer] Day of month
|
145
|
+
# @return [Date] Date object with appropriate calendar system
|
146
|
+
# @raise [ArgumentError] If the date falls in the transition gap (non-existent)
|
147
|
+
#
|
148
|
+
# @example
|
149
|
+
# gb = CalendarTransition.new(:gb)
|
150
|
+
# # Before British transition - uses Julian
|
151
|
+
# gb.create_date(1752, 9, 2)
|
152
|
+
#
|
153
|
+
# # After British transition - uses Gregorian
|
154
|
+
# gb.create_date(1752, 9, 14)
|
155
|
+
#
|
156
|
+
# # In gap - raises ArgumentError
|
157
|
+
# gb.create_date(1752, 9, 10) # => ArgumentError
|
158
|
+
def create_date(year, month, day)
|
159
|
+
# Try creating the date with Gregorian first (most common case)
|
160
|
+
gregorian_date = Date.new(year, month, day, Date::GREGORIAN)
|
161
|
+
|
162
|
+
if gregorian_date.jd >= @transition_jdn
|
163
|
+
# Date is on or after transition - use Gregorian
|
164
|
+
gregorian_date
|
165
|
+
else
|
166
|
+
# Date is before transition - use Julian
|
167
|
+
julian_date = Date.new(year, month, day, Date::JULIAN)
|
168
|
+
|
169
|
+
# Check if this date would fall in the gap
|
170
|
+
if julian_date.jd >= @transition_jdn
|
171
|
+
raise ArgumentError,
|
172
|
+
"Date #{year}-#{month.to_s.rjust(2, "0")}-#{day.to_s.rjust(2, "0")} " \
|
173
|
+
"does not exist in #{@country.upcase} due to calendar transition"
|
174
|
+
end
|
175
|
+
|
176
|
+
julian_date
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Checks if a date exists in this country's calendar system.
|
181
|
+
#
|
182
|
+
# During calendar transitions, certain dates were skipped and never existed.
|
183
|
+
# This method validates whether a specific date is valid for this country.
|
184
|
+
#
|
185
|
+
# @param date [Date] Date to validate
|
186
|
+
# @return [Boolean] true if date exists, false if it falls in transition gap
|
187
|
+
#
|
188
|
+
# @example
|
189
|
+
# gb = CalendarTransition.new(:gb)
|
190
|
+
# date1 = Date.new(1752, 9, 10) # In British gap
|
191
|
+
# gb.valid_date?(date1) #=> false
|
192
|
+
#
|
193
|
+
# date2 = Date.new(1752, 9, 2) # Before British gap
|
194
|
+
# gb.valid_date?(date2) #=> true
|
195
|
+
def valid_date?(date)
|
196
|
+
# If we're dealing with the default transition (Italy), Ruby handles it correctly
|
197
|
+
return true if @transition_jdn == DEFAULT_TRANSITION && date.jd != @transition_jdn - 1
|
198
|
+
|
199
|
+
# For other countries, check if the date falls in a gap
|
200
|
+
# We need to check both Julian and Gregorian representations
|
201
|
+
begin
|
202
|
+
julian_version = Date.new(date.year, date.month, date.day, Date::JULIAN)
|
203
|
+
gregorian_version = Date.new(date.year, date.month, date.day, Date::GREGORIAN)
|
204
|
+
|
205
|
+
# If both versions have the same JDN, there's no ambiguity
|
206
|
+
return true if julian_version.jd == gregorian_version.jd
|
207
|
+
|
208
|
+
# Check if either version is valid for this country
|
209
|
+
julian_valid = julian_version.jd < @transition_jdn
|
210
|
+
gregorian_valid = gregorian_version.jd >= @transition_jdn
|
211
|
+
|
212
|
+
julian_valid || gregorian_valid
|
213
|
+
rescue ArgumentError
|
214
|
+
# Invalid date in both calendar systems
|
215
|
+
false
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns information about this country's calendar transition.
|
220
|
+
#
|
221
|
+
# @return [Hash] Transition information including JDN and gap details
|
222
|
+
#
|
223
|
+
# @example
|
224
|
+
# gb = CalendarTransition.new(:gb)
|
225
|
+
# gb.transition_info
|
226
|
+
# #=> {
|
227
|
+
# # country: :gb,
|
228
|
+
# # gregorian_start_jdn: 2361222,
|
229
|
+
# # gregorian_start_date: #<Date: 1752-09-14>,
|
230
|
+
# # julian_end_date: #<Date: 1752-09-02>,
|
231
|
+
# # gap_days: 11
|
232
|
+
# # }
|
233
|
+
def transition_info
|
234
|
+
# Use explicit calendar system to avoid implicit Italian transition
|
235
|
+
gregorian_start = Date.jd(@transition_jdn, Date::GREGORIAN)
|
236
|
+
julian_end = Date.jd(@transition_jdn - 1, Date::JULIAN)
|
237
|
+
|
238
|
+
# Calculate actual gap in calendar dates, not just JDN difference
|
239
|
+
# For example: Oct 4 (Julian) -> Oct 15 (Gregorian) has 10 gap days (5-14)
|
240
|
+
if @transition_jdn == DEFAULT_TRANSITION
|
241
|
+
# For Italy, Ruby's Date class already handles the gap
|
242
|
+
gap_days = 10 # Known historical gap for Italy
|
243
|
+
else
|
244
|
+
# For other countries, calculate based on calendar date differences
|
245
|
+
# The gap is the difference between the date numbers minus 1
|
246
|
+
gap_days = gregorian_start.day - julian_end.day - 1
|
247
|
+
|
248
|
+
# Handle cross-month transitions (like Denmark Dec 21 -> Jan 1)
|
249
|
+
if gap_days < 0
|
250
|
+
# When crossing months, need to account for days in the previous month
|
251
|
+
# This is a simplified calculation - may need refinement for complex cases
|
252
|
+
prev_month_days = Date.new(julian_end.year, julian_end.month, -1, Date::JULIAN).day
|
253
|
+
gap_days = (prev_month_days - julian_end.day) + gregorian_start.day - 1
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
{
|
258
|
+
country: @country,
|
259
|
+
gregorian_start_jdn: @transition_jdn,
|
260
|
+
gregorian_start_date: gregorian_start,
|
261
|
+
julian_end_date: julian_end,
|
262
|
+
gap_days:
|
263
|
+
}
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|