Class: Fasti::Calendar

Inherits:
Object
  • Object
show all
Defined in:
lib/fasti/calendar.rb

Overview

Represents a calendar for a specific month and year with configurable start of week.

This class provides calendar structure functionality including calendar grid generation, day calculations, and formatting support. It handles different week start preferences (Sunday vs Monday) and integrates with country-specific holiday detection via the holidays gem.

Examples:

Creating a calendar for January 2024

calendar = Calendar.new(2024, 1, country: :jp, start_of_week: :sunday)
calendar.days_in_month  #=> 31
calendar.month_year_header  #=> "January 2024"

Getting calendar grid for display

grid = calendar.calendar_grid
# Returns: [[nil, 1, 2, 3, 4, 5, 6], [7, 8, 9, ...], ...]

Working with different week starts

sunday_calendar = Calendar.new(2024, 1, country: :us, start_of_week: :sunday)
monday_calendar = Calendar.new(2024, 1, country: :jp, start_of_week: :monday)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(year, month, country:, start_of_week: :sunday) ⇒ Calendar

Creates a new calendar instance.

Examples:

Standard calendar

Calendar.new(2024, 6, country: :jp)

Monday-start calendar

Calendar.new(2024, 6, country: :us, start_of_week: :monday)

Parameters:

  • year (Integer)

    The year (must be positive)

  • month (Integer)

    The month (1-12)

  • country (Symbol)

    Country code for holiday context (e.g., :jp, :us)

  • start_of_week (Symbol) (defaults to: :sunday)

    Week start preference (:sunday, :monday, :tuesday, etc.)

Raises:

  • (ArgumentError)

    If parameters are invalid



53
54
55
56
57
58
59
60
61
62
# File 'lib/fasti/calendar.rb', line 53

def initialize(year, month, country:, start_of_week: :sunday)
  @year = year
  @month = month
  @start_of_week = start_of_week.to_sym
  @country = country
  @holidays_for_month = nil
  @calendar_transition = CalendarTransition.new(@country)

  validate_inputs
end

Instance Attribute Details

#countryInteger, Symbol (readonly)

Returns:

  • (Integer)

    The year of the calendar

  • (Integer)

    The month of the calendar (1-12)

  • (Symbol)

    The start of week preference (:sunday, :monday, :tuesday, etc.)

  • (Symbol)

    The country code for holiday context



30
31
32
# File 'lib/fasti/calendar.rb', line 30

def country
  @country
end

#monthInteger, Symbol (readonly)

Returns:

  • (Integer)

    The year of the calendar

  • (Integer)

    The month of the calendar (1-12)

  • (Symbol)

    The start of week preference (:sunday, :monday, :tuesday, etc.)

  • (Symbol)

    The country code for holiday context



30
31
32
# File 'lib/fasti/calendar.rb', line 30

def month
  @month
end

#start_of_weekInteger, Symbol (readonly)

Returns:

  • (Integer)

    The year of the calendar

  • (Integer)

    The month of the calendar (1-12)

  • (Symbol)

    The start of week preference (:sunday, :monday, :tuesday, etc.)

  • (Symbol)

    The country code for holiday context



30
31
32
# File 'lib/fasti/calendar.rb', line 30

def start_of_week
  @start_of_week
end

#yearInteger, Symbol (readonly)

Returns:

  • (Integer)

    The year of the calendar

  • (Integer)

    The month of the calendar (1-12)

  • (Symbol)

    The start of week preference (:sunday, :monday, :tuesday, etc.)

  • (Symbol)

    The country code for holiday context



30
31
32
# File 'lib/fasti/calendar.rb', line 30

def year
  @year
end

Instance Method Details

#calendar_gridArray<Array<Integer, nil>>

Generates a 2D grid representing the calendar layout.

The grid is an array of weeks (rows), where each week is an array of 7 days. Days are represented as integers (1-31) or nil for empty cells. The grid respects the start_of_week preference.

Examples:

Sunday-start June 2024

calendar = Calendar.new(2024, 6, country: :jp, start_of_week: :sunday)
grid = calendar.calendar_grid
# Returns: [[nil, nil, nil, nil, nil, nil, 1],
#           [2, 3, 4, 5, 6, 7, 8], ...]

Returns:

  • (Array<Array<Integer, nil>>)

    2D array of calendar days



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/fasti/calendar.rb', line 138

def calendar_grid
  grid = []
  current_row = []

  # Add leading empty cells for days before month starts
  leading_empty_days.times do
    current_row << nil
  end

  # Add only existing days (skip gap days) for continuous display
  (1..days_in_month).each do |day|
    # Only add days that actually exist (not in transition gaps)
    next unless to_date(day)

    current_row << day

    # Start new row on end of week
    if current_row.length == 7
      grid << current_row
      current_row = []
    end
    # Skip gap days completely - they don't take up space in the grid
  end

  # Add trailing empty cells and final row if needed
  if current_row.any?
    current_row << nil while current_row.length < 7
    grid << current_row
  end

  grid
end

#day_headersArray<String>

Returns day abbreviations arranged according to start_of_week preference.

Examples:

Sunday start

Calendar.new(2024, 6, country: :jp, start_of_week: :sunday).day_headers
#=> ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]

Monday start

Calendar.new(2024, 6, country: :jp, start_of_week: :monday).day_headers
#=> ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]

Wednesday start

Calendar.new(2024, 6, country: :jp, start_of_week: :wednesday).day_headers
#=> ["We", "Th", "Fr", "Sa", "Su", "Mo", "Tu"]

Returns:

  • (Array<String>)

    Array of day abbreviations (Su, Mo, Tu, etc.)



204
205
206
207
208
209
# File 'lib/fasti/calendar.rb', line 204

def day_headers
  # Rotate headers based on start of week
  start_wday = WEEK_DAYS.index(start_of_week) || 0

  DAY_ABBREVS.rotate(start_wday)
end

#days_in_monthInteger

Returns the number of days in the calendar month.

Examples:

Calendar.new(2024, 2, country: :jp).days_in_month  #=> 29 (leap year)
Calendar.new(2023, 2, country: :jp).days_in_month  #=> 28

Returns:

  • (Integer)

    Number of days in the month (28-31)



71
72
73
# File 'lib/fasti/calendar.rb', line 71

def days_in_month
  Date.new(year, month, -1).day
end

#first_day_of_monthDate

Returns the first day of the calendar month.

Examples:

Calendar.new(2024, 6, country: :jp).first_day_of_month
#=> #<Date: 2024-06-01>

Returns:

  • (Date)

    The first day of the month



82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/fasti/calendar.rb', line 82

def first_day_of_month
  @calendar_transition.create_date(year, month, 1)
rescue ArgumentError
  # If day 1 is in a gap (very rare), try day 2, then 3, etc.
  (2..31).each do |day|
    return @calendar_transition.create_date(year, month, day)
  rescue ArgumentError
    next
  end
  # Fallback to standard Date if all fails
  Date.new(year, month, 1)
end

#first_day_wdayInteger

Returns the day of the week for the first day of the month.

Examples:

calendar = Calendar.new(2024, 6, country: :jp)
calendar.first_day_wday  #=> 6 (if June 1st, 2024 is Saturday)

Returns:

  • (Integer)

    Day of week (0=Sunday, 1=Monday, …, 6=Saturday)



121
122
123
# File 'lib/fasti/calendar.rb', line 121

def first_day_wday
  first_day_of_month.wday
end

#holiday?(day) ⇒ Boolean

Checks if a specific day in this calendar month is a holiday.

Examples:

calendar = Calendar.new(2024, 1, country: :jp)
calendar.holiday?(1)   #=> true (New Year's Day in Japan)
calendar.holiday?(15)  #=> false (regular day)
calendar.holiday?(nil) #=> false

Parameters:

  • day (Integer, nil)

    Day of the month (1-31) or nil

Returns:

  • (Boolean)

    true if the day is a holiday, false otherwise



254
255
256
257
258
259
# File 'lib/fasti/calendar.rb', line 254

def holiday?(day)
  date = to_date(day)
  return false unless date

  holidays_for_month.key?(date)
end

#last_day_of_monthDate

Returns the last day of the calendar month.

Examples:

Calendar.new(2024, 6, country: :jp).last_day_of_month
#=> #<Date: 2024-06-30>

Returns:

  • (Date)

    The last day of the month



102
103
104
105
106
107
108
109
110
111
112
# File 'lib/fasti/calendar.rb', line 102

def last_day_of_month
  # Start from the theoretical last day and work backwards
  max_days = Date.new(year, month, -1).day
  max_days.downto(1).each do |day|
    return @calendar_transition.create_date(year, month, day)
  rescue ArgumentError
    next
  end
  # Fallback to standard Date if all fails
  Date.new(year, month, -1)
end

#leading_empty_daysInteger

Calculates the number of empty cells needed before the first day.

This accounts for the start_of_week preference to properly align the first day of the month in the calendar grid.

Examples:

# If June 1st, 2024 falls on Saturday and we start weeks on Sunday:
calendar = Calendar.new(2024, 6, country: :jp, start_of_week: :sunday)
calendar.leading_empty_days  #=> 6

Returns:

  • (Integer)

    Number of empty cells (0-6)



182
183
184
185
186
187
# File 'lib/fasti/calendar.rb', line 182

def leading_empty_days
  # Calculate offset based on start of week preference
  start_wday = WEEK_DAYS.index(start_of_week) || 0

  (first_day_wday - start_wday) % 7
end

#month_year_headerString

Returns a formatted month and year header string.

Examples:

Calendar.new(2024, 6, country: :jp).month_year_header  #=> "June 2024"
Calendar.new(2024, 12, country: :jp).month_year_header #=> "December 2024"

Returns:

  • (String)

    Formatted month and year (e.g., “June 2024”)



218
219
220
221
# File 'lib/fasti/calendar.rb', line 218

def month_year_header
  date = first_day_of_month
  date.strftime("%B %Y")
end

#to_date(day) ⇒ Date?

Converts a day number to a Date object for this calendar’s month/year.

Examples:

calendar = Calendar.new(2024, 6, country: :jp)
calendar.to_date(15)  #=> #<Date: 2024-06-15>
calendar.to_date(nil) #=> nil

Parameters:

  • day (Integer, nil)

    Day of the month (1-31) or nil

Returns:

  • (Date, nil)

    Date object for the specified day, or nil if day is nil



232
233
234
235
236
237
238
239
240
241
242
# File 'lib/fasti/calendar.rb', line 232

def to_date(day)
  return nil unless day
  return nil unless (1..days_in_month).cover?(day)

  begin
    @calendar_transition.create_date(year, month, day)
  rescue ArgumentError
    # Date falls in calendar transition gap (non-existent)
    nil
  end
end