Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event-based customization of Calendar behaviour #84

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/calendarium-romanum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module CalendariumRomanum
enum
enums
errors
event_dispatcher
data
day
calendar
Expand All @@ -31,6 +32,7 @@ module CalendariumRomanum
sanctorale_loader
sanctorale_writer
sanctorale_factory
special_cases_handler
transfers
util
ordinalizer
Expand Down
112 changes: 96 additions & 16 deletions lib/calendarium-romanum/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class Calendar
# @raise [RangeError]
# if +year+ is specified for which the implemented calendar
# system wasn't in force
def initialize(year, sanctorale = nil, temporale = nil, vespers: false, transfers: nil)
def initialize(year, sanctorale = nil, temporale = nil, vespers: false, transfers: nil, event_dispatcher: nil)
unless year.is_a? Integer
temporale = year
year = temporale.year
Expand All @@ -61,6 +61,8 @@ def initialize(year, sanctorale = nil, temporale = nil, vespers: false, transfer
@populate_vespers = vespers

@transferred = (transfers || Transfers).call(@temporale, @sanctorale).freeze

@event_dispatcher = event_dispatcher || EventDispatcher.new
end

class << self
Expand Down Expand Up @@ -254,11 +256,11 @@ def freeze
private

def celebrations_for(date)
tr = @transferred[date]
tr = transferred_on_event(date, @transferred[date]) if @transferred[date]
return [tr] if tr

t = @temporale[date]
st = @sanctorale[date]
t = temporale_retrieval_event date, @temporale[date]
st = sanctorale_retrieval_event date, @sanctorale[date]

if date.saturday? &&
@temporale.season(date) == Seasons::ORDINARY &&
Expand All @@ -267,49 +269,127 @@ def celebrations_for(date)
st += [Temporale::CelebrationFactory.saturday_memorial_bvm]
end

unless st.empty?
result =
if st.empty?
[t]
else
if st.first.rank > t.rank
if st.first.rank == Ranks::MEMORIAL_OPTIONAL
return [t] + st
[t] + st
else
return st
st
end
elsif t.rank == Ranks::FERIAL_PRIVILEGED && st.first.rank.memorial?
commemorations = st.collect do |c|
c.change(rank: Ranks::COMMEMORATION, colour: t.colour)
end
return [t] + commemorations

[t] + commemorations
elsif t.symbol == :immaculate_heart &&
[Ranks::MEMORIAL_GENERAL, Ranks::MEMORIAL_PROPER].include?(st.first.rank)
optional_memorials = ([t] + st).collect do |celebration|
celebration.change rank: Ranks::MEMORIAL_OPTIONAL
end
ferial = temporale.send :ferial, date # ugly and evil
return [ferial] + optional_memorials

[ferial] + optional_memorials
else
[t]
end
end

[t]
resolution_event date, result, t, st
end

def first_vespers_on(date, celebrations)
tomorrow = date + 1
tomorrow_celebrations = celebrations_for(tomorrow)

c = tomorrow_celebrations.first

result =
if c.rank >= Ranks::SOLEMNITY_PROPER ||
c.rank == Ranks::SUNDAY_UNPRIVILEGED ||
(c.rank == Ranks::FEAST_LORD_GENERAL && tomorrow.sunday?)
if c.symbol == :ash_wednesday || c.symbol == :good_friday
return nil
end

if c.rank > celebrations.first.rank || c.symbol == :easter_sunday
return c
nil
elsif c.rank > celebrations.first.rank || c.symbol == :easter_sunday
c
else
nil
end
end

nil
vespers_event date, result, celebrations, tomorrow_celebrations
end

# There is a solemnity transferred to the given date.
# Listeners can prevent the transfer from taking effect
# (and thus lose the solemnity for the given year) by setting
# #celebration to nil, or even replace it with a completely
# different celebration.
class TransferredOnEvent < Struct.new(:date, :celebration, :calendar)
EVENT_ID = :calendar__transferred_on
end

# Dispatched whenever {Calendar} retrieves {Celebration} for
# a given date from {Temporale}.
# Listeners can override the {Celebration}.
class TemporaleRetrievalEvent < Struct.new(:date, :result, :calendar)
EVENT_ID = :calendar__temporale_retrieval
end

# Dispatched whenever {Calendar} retrieves {Celebration}s for
# a given date from {Sanctorale}.
# Listeners can override the {Celebration}s.
class SanctoraleRetrievalEvent < Struct.new(:date, :result, :calendar)
EVENT_ID = :calendar__sanctorale_retrieval
end

# Dispatched whenever {Calendar} decides which {Celebration}(s)
# will take place on the given date.
# Listeners can replace the result.
class TemporaleSanctoraleResolutionEvent < Struct.new(:date, :result, :temporale, :sanctorale, :calendar)
EVENT_ID = :calendar__temporale_sanctorale_resolution
end

# Dispatched whenever {Calendar} decides which (if any)
# {Celebration}'s Vespers should be celebrated on the given date.
# Only valid options for +result+ are +nil+ (the day's {Celebration}
# keeps the Vespers) or one of the {Celebration}s from +tomorrow+,
# if it's rank makes it eligible for first Vespers.
class VespersResolutionEvent < Struct.new(:date, :result, :today, :tomorrow, :calendar)
EVENT_ID = :calendar__vespers_resolution
end

def transferred_on_event(*args)
@event_dispatcher
.dispatch(TransferredOnEvent.new(*args, self))
.celebration
end

def temporale_retrieval_event(*args)
@event_dispatcher
.dispatch(TemporaleRetrievalEvent.new(*args, self))
.result
end

def sanctorale_retrieval_event(*args)
@event_dispatcher
.dispatch(SanctoraleRetrievalEvent.new(*args, self))
.result
end

def resolution_event(*args)
@event_dispatcher
.dispatch(TemporaleSanctoraleResolutionEvent.new(*args, self))
.result
end

def vespers_event(*args)
@event_dispatcher
.dispatch(VespersResolutionEvent.new(*args, self))
.result
end

def system_not_effective
Expand Down
26 changes: 26 additions & 0 deletions lib/calendarium-romanum/event_dispatcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module CalendariumRomanum
class EventDispatcher
def initialize
@listeners = {}
end

def add_listener(event_id, listener=nil, &blk)
listener ||= blk
unless listener
raise ArgumentError.new('Either pass a callable as argument or provide a block')
end

@listeners[event_id] ||= []
@listeners[event_id] << listener
end

def dispatch(event, event_id = nil)
event_id ||= event.class::EVENT_ID

listeners = @listeners[event_id]
listeners.each {|l| l.call event, event_id } if listeners

event
end
end
end
104 changes: 104 additions & 0 deletions lib/calendarium-romanum/special_cases_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
module CalendariumRomanum
# Provides {Calendar} event listeners implementing behaviour not fitting
# in the standard calendar rules, but prescribed for given liturgical year
# by the Holy See.
class SpecialCasesHandler
# Returns listeners relevant for the specified liturgical year.
#
# @return [Hash<Symbol=>#call>]
def self.listeners(year)
@listeners ||=
begin
{
# see liturgical_law/2020_dubia_de_calendario_2022.md
2021 => {
# Birth of St. John Baptist celebrated one day earlier
CR::Calendar::SanctoraleRetrievalEvent::EVENT_ID =>
SanctoraleDateChangeListener.new(:baptist_birth, Date.new(2022, 6, 23)),
# Sacred Heart receives first Vespers
CR::Calendar::VespersResolutionEvent::EVENT_ID =>
GrantFirstVespersListener.new(:sacred_heart)
},
}.freeze
end

@listeners[year] || {}
end

# Returns {EventDispatcher} pre-configured with listeners relevant for the
# specified liturgical year.
#
# @return [EventDispatcher]
def self.event_dispatcher(year)
EventDispatcher.new.tap do |el|
listeners(year).each_pair {|event, listener| el.add_listener event, listener }
end
end

# For the given year changes any sanctorale celebration's date.
class SanctoraleDateChangeListener
def initialize(celebration_symbol, date)
@symbol = celebration_symbol
@date = date
end

# TODO: supply test coverage, quite probably it doesn't do everywhere exactly what it should
def call(event, event_id)
unless @celebration
@orig_date, @celebration = event.calendar.sanctorale.by_symbol(@symbol)
end

return unless @celebration

if event.date == @orig_date
event.result = event.result.reject {|c| c.symbol == @symbol }
end

if event.date == @date
event.result =
if @celebration.rank == CR::Ranks::MEMORIAL_OPTIONAL &&
(event.result.empty? || event.result[0].rank == CR::Ranks::MEMORIAL_OPTIONAL)
event.result + [@celebration]
else
[@celebration]
end
end
end
end

# For the given year changes any temporale celebration's date,
# given that the celebration has it's own proper symbol.
class TemporaleDateChangeListener
def initialize(celebration_symbol, date)
@symbol = celebration_symbol
@date = date
end

def call(event, event_id)
@orig_date ||= event.calendar.temporale.public_send @symbol
return if @date == @orig_date

if event.date == @orig_date
# TODO must be handled!
end

if event.date == @date
event.result = event.calendar.temporale[@orig_date]
end
end
end

# Grants first Vespers to a celebration even if it wouldn't get them
# according to the standard logic of celebration precedence.
class GrantFirstVespersListener
def initialize(celebration_symbol)
@symbol = celebration_symbol
end

def call(event, event_id)
found = event.tomorrow.find {|c| c.symbol == @symbol }
event.result = found if found
end
end
end
end
2 changes: 1 addition & 1 deletion lib/calendarium-romanum/transfers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def call
end
@transferred[transfer_to] = loser
# primary celebrations have noone to be beaten by, no need to harden their dates
@transferred[date] = winner unless winner.rank == Ranks::PRIMARY
@transferred[date] = winner unless winner.rank >= Ranks::PRIMARY
end

@transferred
Expand Down
40 changes: 36 additions & 4 deletions liturgical_law/2020_dubia_de_calendario_2022.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ c) Sollemnitas Nativitatis S. Ioanni Baptistae et sollemnitas Sacratissimi Cordi
II Vesperae omittantur. I Vesperae sollemnitatis Sacratissimi Cordis Iesu celebrentur.

```ruby
calendar = CR::Calendar.new 2021, CR::Data::GENERAL_ROMAN_LATIN.load, vespers: true
year = 2021
calendar = CR::Calendar.new year, CR::Data::GENERAL_ROMAN_LATIN.load, vespers: true, event_dispatcher: CR::SpecialCasesHandler.event_dispatcher(year)

i23 = Date.new(2022, 6, 23)
i24 = i23 + 1
Expand All @@ -77,9 +78,40 @@ feria VI, celebretur; sollemnitas autem Sacratissimi Cordis Iesu ad diem 23 iuni
feriam V transferatur, usque ad horam Nonam inclusive.

```ruby
skip 'there is currently no pretty way how to model this scenario using calendarium-romanum -
a custom Temporale is required, either with a changed date of Sacred Heart or with
customized solemnity transfer logic'
year = 2021

i23 = Date.new(2022, 6, 23)
i24 = i23 + 1

dispatcher = CR::EventDispatcher.new

# TODO these three listeners should be replaced by a single listener customizing
# the logic of solemnity transfer (once that is possible)
dispatcher.add_listener(
CR::Calendar::TemporaleRetrievalEvent::EVENT_ID,
CR::SpecialCasesHandler::TemporaleDateChangeListener.new(:sacred_heart, i23)
)
dispatcher.add_listener(CR::Calendar::TransferredOnEvent::EVENT_ID) do |event|
if event.celebration && [:baptist_birth, :sacred_heart].include?(event.celebration.symbol)
event.celebration = nil
end
end
dispatcher.add_listener(CR::Calendar::TemporaleSanctoraleResolutionEvent::EVENT_ID) do |event|
event.result = event.sanctorale if event.date == i24
end

dispatcher.add_listener(
CR::Calendar::VespersResolutionEvent::EVENT_ID,
CR::SpecialCasesHandler::GrantFirstVespersListener.new(:baptist_birth)
)

calendar = CR::Calendar.new year, CR::Data::GENERAL_ROMAN_LATIN.load, vespers: true, event_dispatcher: dispatcher

expect(calendar[i24].celebrations[0].symbol).to be :baptist_birth

day = calendar[i23]
expect(day.celebrations[0].symbol).to be :sacred_heart
expect(day.vespers.symbol).to be :baptist_birth
```

d) Dominica XX Temporis "per annum", die 14 augusti.
Expand Down
Loading