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.
@@ -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