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

Support ad hoc celebration date overrides #81

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
1 change: 1 addition & 0 deletions lib/calendarium-romanum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ module CalendariumRomanum
sanctorale_writer
sanctorale_factory
transfers
date_overrider
util
ordinalizer
).each do |f|
Expand Down
14 changes: 11 additions & 3 deletions lib/calendarium-romanum/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@ class Calendar
# Object with the same public interface as the +Transfers+ class (really class,
# not instance!), responsible for handling transfers of conflicting solemnities.
# Only useful for overriding the default solemnity transfer logic with a custom one.
# @param overrides [DateOverrider, nil]
# List of ad hoc overridden celebration dates.
#
# @overload initialize(temporale, sanctorale=nil, vespers: false, transfers: nil)
# @param temporale [Temporale]
# @param sanctorale [Sanctorale, nil]
# @param vespers [Boolean]
# @param transfers [#call, nil]
# @param overrides [DateOverrider, nil]
# @since 0.8.0
#
# @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, overrides: nil)
unless year.is_a? Integer
temporale = year
year = temporale.year
Expand All @@ -55,9 +58,14 @@ def initialize(year, sanctorale = nil, temporale = nil, vespers: false, transfer
raise ArgumentError.new('Temporale year must be the same as year.')
end

overrides ||= DateOverrider.new

@year = year
@sanctorale = sanctorale || Sanctorale.new
@temporale = temporale || Temporale.new(year)
@temporale, @sanctorale =
overrides.call(
temporale || Temporale.new(year),
sanctorale || Sanctorale.new
)
@populate_vespers = vespers

@transferred = (transfers || Transfers).call(@temporale, @sanctorale).freeze
Expand Down
71 changes: 71 additions & 0 deletions lib/calendarium-romanum/date_overrider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
module CalendariumRomanum
# Church authorities occasionally make ad hoc changes to the liturgical calendar
# by moving a {Celebration} just for a given year to a more convenient date.
# This class is responsible for storing and applying such changes.
class DateOverrider
# @param overrides [Hash<Date=>Symbol>]
def initialize(overrides = {})
@overrides = overrides
end

# Accepts {Temporale} and {Sanctorale}, returns them (if no changes apply)
# or their copies with date changes applied.
#
# @param temporale [Temporale]
# @param sanctorale [Sanctorale]
# @return [Array<Temporale, Sanctorale>]
def call(temporale, sanctorale)
range = temporale.date_range

for_year = @overrides.select {|date, _| range.include? date }

return [temporale, sanctorale] if for_year.empty?

for_temporale = {}
for_sanctorale = {}
for_year.each_pair do |date, symbol|
to = temporale.provides_celebration?(symbol) ? for_temporale : for_sanctorale
to[date] = symbol
end

new_sanctorale =
for_sanctorale.empty? ?
sanctorale :
sanctorale.merge(sanctorale_layer(for_sanctorale, sanctorale), delete_same_symbols: true)

new_temporale =
for_temporale.empty? ?
temporale :
temporale_class(for_temporale, temporale)
.new(
temporale.year,
extensions: temporale.extensions,
transfer_to_sunday: temporale.transfer_to_sunday
)

[new_temporale, new_sanctorale]
end

private

# Returns a new {Sanctorale} applying +overrides+ to +sanctorale+.
def sanctorale_layer(overrides, sanctorale)
r = Sanctorale.new
overrides.each do |date, symbol|
_, celebration = sanctorale.by_symbol symbol
next if celebration.nil?

r.add date.month, date.day, celebration
end
r
end

def temporale_class(overrides, temporale)
Class.new(temporale.class) do
overrides.each_pair do |date, symbol|
define_method(symbol) { date }
end
end
end
end
end
46 changes: 40 additions & 6 deletions lib/calendarium-romanum/sanctorale.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,22 @@ def add(month, day, celebration)
# @param month [Integer]
# @param day [Integer]
# @param celebrations [Array<Celebration>]
# @param symbol_uniqueness [true|false]
# @param delete_same_symbol [Boolean]
# if for any of the newly added celebration symbols is already present,
# delete the celebration(s) in question before the actual replace operation
# @param symbol_uniqueness [Boolean]
# allows disabling symbol uniqueness check.
# Internal feature, not intended for use by client code.
# @return [void]
# @raise [ArgumentError]
# when performing the operation would break the object's invariant
def replace(month, day, celebrations, symbol_uniqueness: true)
def replace(month, day, celebrations, delete_same_symbol: false, symbol_uniqueness: true)
date = AbstractDate.new(month, day)

if delete_same_symbol
celebrations.each {|c| delete_by_symbol c.symbol }
end

symbols_without_day = @symbols
unless @days[date].nil?
old_symbols = @days[date].collect(&:symbol).compact
Expand All @@ -134,27 +141,54 @@ def replace(month, day, celebrations, symbol_uniqueness: true)
@days[date] = celebrations.dup
end

# Deletes {Celebration} specified by it's symbol.
# Returns the {Celebration} or +nil+ if no such {Celebration} is found.
#
# @param symbol [Symbol]
# @return [Celebration, nil]
def delete_by_symbol(symbol)
return nil unless provides_celebration? symbol

date, celebration = by_symbol symbol
if @days[date].size > 1
@days[date].delete celebration
else
@days.delete date
end
@symbols.delete celebration.symbol

celebration
end

# Updates the receiver with {Celebration}s from another instance.
#
# For each date contained in +other+ the content of +self+
# is _replaced_ by that of +other+.
#
# @param other [Sanctorale]
# @param delete_same_symbols [Boolean]
# if any of the celebration symbols from +other+ are already present,
# delete the celebrations in question before adding those from +other+
# @return [void]
# @raise (see #replace)
def update(other)
def update(other, delete_same_symbols: false)
other.each_day do |date, celebrations|
replace date.month, date.day, celebrations, symbol_uniqueness: false
replace(
date.month, date.day, celebrations,
delete_same_symbol: delete_same_symbols, symbol_uniqueness: false
)
end
rebuild_symbols
end

# Returns a new copy containing {Celebration}s from both self and +other+.
#
# @param other [Sanctorale]
# @param delete_same_symbols [Boolean]
# works analogically to the option of the same name for #update
# @return [Sanctorale]
def merge(other)
dup.tap {|dupped| dupped.update other }
def merge(other, delete_same_symbols: false)
dup.tap {|dupped| dupped.update other, delete_same_symbols: delete_same_symbols }
end

# Retrieves {Celebration}s for the given date
Expand Down
10 changes: 6 additions & 4 deletions lib/calendarium-romanum/temporale.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ def initialize(year, extensions: [], transfer_to_sunday: [])
# @return [Integer]
attr_reader :year

# @return [Array<Symbol>]
attr_reader :transfer_to_sunday

# @return [Array<#each_celebration>]
attr_reader :extensions

class << self
# Determines liturgical year for the given date
#
Expand Down Expand Up @@ -361,10 +367,6 @@ def provides_celebration?(symbol)
@all_celebration_symbols.include? symbol
end

protected

attr_reader :transfer_to_sunday, :extensions

private

SeasonWeek = Struct.new(:season, :week)
Expand Down
32 changes: 24 additions & 8 deletions liturgical_law/2020_dubia_de_calendario_2022.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,22 @@ 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

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

expect(i24).to be_friday

overrider = CR::DateOverrider.new({i23 => :baptist_birth})
calendar = CR::Calendar.new 2021, CR::Data::GENERAL_ROMAN_LATIN.load, vespers: true, overrides: overrider

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

day = calendar[i23]
expect(day.celebrations[0].symbol).to be :baptist_birth
expect(day.vespers.symbol).to be :sacred_heart
# TODO: Currently we cannot do this as expected, as for calendarium-romanum
# the two solemnities have exactly the same rank
#
# day = calendar[i23]
# expect(day.celebrations[0].symbol).to be :baptist_birth
# expect(day.vespers.symbol).to be :sacred_heart
```

Ubi vero S. Ioannes Baptista patronus sit nationis vel dioecesis vel civitatis aut
Expand All @@ -77,9 +81,21 @@ 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'
i23 = Date.new(2022, 6, 23)
i24 = i23 + 1

overrider = CR::DateOverrider.new({i23 => :sacred_heart})
calendar = CR::Calendar.new 2021, CR::Data::GENERAL_ROMAN_LATIN.load, vespers: true, overrides: overrider

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

# TODO: Currently we cannot do this as expected, as for calendarium-romanum
# the two solemnities have exactly the same rank
#
# expect(day.vespers.symbol).to be :baptist_birth

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

d) Dominica XX Temporis "per annum", die 14 augusti.
Expand Down
80 changes: 80 additions & 0 deletions spec/date_overrider_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
require_relative 'spec_helper'

describe CR::DateOverrider do
let(:subject) { described_class.new overrides }

let(:year) { 2000 }
let(:temporale) { CR::Temporale.new year }
let(:sanctorale) { CR::Sanctorale.new }

let(:nullus) { CR::Celebration.new('S. Nullius', CR::Ranks::MEMORIAL_OPTIONAL, CR::Colours::WHITE, :nullus) }
let(:ignotus) { CR::Celebration.new('S. Ignoti', CR::Ranks::MEMORIAL_OPTIONAL, CR::Colours::WHITE, :ignotus) }

describe 'no overrides' do
let(:overrides) { {} }

it 'returns arguments unchanged' do
result = subject.call temporale, sanctorale

expect(result[0]).to be temporale
expect(result[1]).to be sanctorale
end
end

describe 'overrides for unknown celebrations' do
# the overridden celebration is not known to either temporale or sanctorale
let(:overrides) { {Date.new(year - 10, 7, 5) => :nullus} }

it 'returns arguments unchanged' do
result = subject.call temporale, sanctorale

expect(result[0]).to be temporale
expect(result[1]).to be sanctorale
end
end

describe 'no overrides for the given year' do
let(:overrides) { {Date.new(1990, 7, 5) => :nullus} }

it 'returns arguments unchanged' do
sanctorale.add 1, 14, nullus

result = subject.call temporale, sanctorale

expect(result[0]).to be temporale
expect(result[1]).to be sanctorale
end
end

describe 'sanctorale celebration overridden' do
let(:overrides) { {Date.new(year + 1, 7, 5) => :nullus} }

it 'returns sanctorale with the override applied' do
sanctorale.add 1, 14, nullus

new_temporale, new_sanctorale = subject.call temporale, sanctorale

expect(new_sanctorale).to be_a CR::Sanctorale
expect(new_sanctorale).not_to be sanctorale
expect(new_sanctorale.get(7, 5)).to eq [nullus]
expect(new_sanctorale.get(1, 17)).to eq []

expect(new_temporale).to be temporale
end
end

describe 'temporale celebration overridden' do
let(:overrides) { {Date.new(year + 1, 7, 5) => :corpus_christi} }

it 'returns temporale with the override applied' do
new_temporale, new_sanctorale = subject.call temporale, sanctorale

expect(new_temporale).to be_a CR::Temporale
expect(new_temporale).not_to be temporale
expect(new_temporale[Date.new(2001, 7, 5)]).to eq CR::Temporale::CelebrationFactory.corpus_christi
expect(new_temporale[CR::Temporale::Dates.corpus_christi(year)]).to be_ferial

expect(new_sanctorale).to be sanctorale
end
end
end
Loading