calendarium-romanum 0.2.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +5 -5
  2. data/bin/calendariumrom +4 -1
  3. data/config/locales/cs.yml +54 -2
  4. data/config/locales/en.yml +64 -14
  5. data/config/locales/es.yml +90 -0
  6. data/config/locales/fr.yml +90 -0
  7. data/config/locales/it.yml +54 -4
  8. data/config/locales/la.yml +52 -2
  9. data/data/README.md +105 -5
  10. data/data/czech-brno-cs.txt +11 -5
  11. data/data/czech-budejovice-cs.txt +11 -5
  12. data/data/czech-cechy-cs.txt +11 -5
  13. data/data/czech-cs.txt +243 -234
  14. data/data/czech-hradec-cs.txt +10 -4
  15. data/data/czech-litomerice-cs.txt +12 -6
  16. data/data/czech-morava-cs.txt +11 -5
  17. data/data/czech-olomouc-cs.txt +9 -3
  18. data/data/czech-ostrava-cs.txt +10 -4
  19. data/data/czech-plzen-cs.txt +10 -4
  20. data/data/czech-praha-cs.txt +10 -3
  21. data/data/universal-en.txt +218 -212
  22. data/data/universal-es.txt +243 -0
  23. data/data/universal-fr.txt +243 -0
  24. data/data/universal-it.txt +218 -212
  25. data/data/universal-la.txt +218 -211
  26. data/lib/calendarium-romanum.rb +30 -18
  27. data/lib/calendarium-romanum/abstract_date.rb +12 -0
  28. data/lib/calendarium-romanum/calendar.rb +210 -48
  29. data/lib/calendarium-romanum/cli.rb +101 -52
  30. data/lib/calendarium-romanum/cr.rb +16 -0
  31. data/lib/calendarium-romanum/data.rb +46 -18
  32. data/lib/calendarium-romanum/day.rb +200 -21
  33. data/lib/calendarium-romanum/enum.rb +24 -5
  34. data/lib/calendarium-romanum/enums.rb +123 -37
  35. data/lib/calendarium-romanum/errors.rb +4 -0
  36. data/lib/calendarium-romanum/ordinalizer.rb +61 -0
  37. data/lib/calendarium-romanum/perpetual_calendar.rb +97 -0
  38. data/lib/calendarium-romanum/rank.rb +43 -6
  39. data/lib/calendarium-romanum/sanctorale.rb +142 -22
  40. data/lib/calendarium-romanum/sanctorale_factory.rb +74 -3
  41. data/lib/calendarium-romanum/sanctorale_loader.rb +176 -0
  42. data/lib/calendarium-romanum/temporale.rb +296 -251
  43. data/lib/calendarium-romanum/temporale/celebration_factory.rb +106 -0
  44. data/lib/calendarium-romanum/temporale/dates.rb +232 -0
  45. data/lib/calendarium-romanum/temporale/extensions/christ_eternal_priest.rb +37 -0
  46. data/lib/calendarium-romanum/transfers.rb +43 -6
  47. data/lib/calendarium-romanum/util.rb +36 -3
  48. data/lib/calendarium-romanum/version.rb +5 -1
  49. data/spec/abstract_date_spec.rb +11 -3
  50. data/spec/calendar_spec.rb +645 -188
  51. data/spec/celebration_factory_spec.rb +40 -0
  52. data/spec/celebration_spec.rb +67 -0
  53. data/spec/cli_spec.rb +154 -11
  54. data/spec/colour_spec.rb +22 -0
  55. data/spec/data_spec.rb +26 -3
  56. data/spec/date_parser_spec.rb +68 -0
  57. data/spec/date_spec.rb +8 -8
  58. data/spec/dates_spec.rb +73 -0
  59. data/spec/day_spec.rb +151 -0
  60. data/spec/i18n_spec.rb +11 -2
  61. data/spec/ordinalizer_spec.rb +44 -0
  62. data/spec/perpetual_calendar_spec.rb +125 -0
  63. data/spec/rank_spec.rb +42 -7
  64. data/spec/readme_spec.rb +18 -10
  65. data/spec/sanctorale_factory_spec.rb +113 -9
  66. data/spec/sanctorale_loader_spec.rb +229 -0
  67. data/spec/sanctorale_spec.rb +176 -62
  68. data/spec/season_spec.rb +22 -0
  69. data/spec/spec_helper.rb +27 -1
  70. data/spec/temporale_spec.rb +473 -154
  71. data/spec/year_spec.rb +25 -0
  72. metadata +42 -7
  73. data/lib/calendarium-romanum/sanctoraleloader.rb +0 -104
  74. data/spec/sanctoraleloader_spec.rb +0 -171
@@ -1,15 +1,50 @@
1
1
  module CalendariumRomanum
2
- # conveniently creates sanctorale from several data files
2
+ # Utility loading {Sanctorale} from several sources
3
+ # and building a single {Sanctorale} by layering them
4
+ # over each other.
3
5
  class SanctoraleFactory
4
6
  class << self
5
- # layers several sanctorale instances.
7
+ # Takes several {Sanctorale} instances, returns a new one,
8
+ # resulting by merging them all together
9
+ # (using {Sanctorale#update})
10
+ #
11
+ # @return [Sanctorale]
12
+ #
13
+ # @example
14
+ # include CalendariumRomanum
15
+ #
16
+ # prague_sanctorale = SanctoraleFactory.create_layered(
17
+ # Data['czech-cs'].load, # Czech Republic
18
+ # Data['czech-cechy-cs'].load, # Province of Bohemia
19
+ # Data['czech-praha-cs'].load, # Archdiocese of Prague
20
+ # )
6
21
  def create_layered(*instances)
7
22
  r = Sanctorale.new
8
23
  instances.each {|i| r.update i }
24
+
25
+ metadata = instances
26
+ .collect(&:metadata)
27
+ .select {|i| i.is_a? Hash }
28
+ r.metadata = metadata.inject((metadata.first || {}).dup) {|merged,i| merged.update i }
29
+ r.metadata.delete 'extends'
30
+ r.metadata['components'] = instances.collect(&:metadata)
31
+
9
32
  r
10
33
  end
11
34
 
12
- # loads and layers several sanctorale instances.
35
+ # Takes several filesystem paths, loads a {Sanctorale}
36
+ # from each of them (using {SanctoraleLoader})
37
+ # and then merges them (using {.create_layered})
38
+ #
39
+ # @return [Sanctorale]
40
+ #
41
+ # @example
42
+ # include CalendariumRomanum
43
+ #
44
+ # my_sanctorale = SanctoraleFactory.load_layered_from_files(
45
+ # 'my_data/general_calendar.txt',
46
+ # 'my_data/particular_calendar.txt'
47
+ # )
13
48
  def load_layered_from_files(*paths)
14
49
  loader = SanctoraleLoader.new
15
50
  instances = paths.collect do |p|
@@ -17,6 +52,42 @@ module CalendariumRomanum
17
52
  end
18
53
  create_layered(*instances)
19
54
  end
55
+
56
+ # Takes a single filesystem path. If the file's YAML front
57
+ # matter references any parent data files using the
58
+ # 'extends' key, it loads all the parents and assembles
59
+ # the resulting {Sanctorale}.
60
+ # If the data file doesn't reference any parents,
61
+ # result is the same as {SanctoraleLoader#load_from_file}.
62
+ #
63
+ # @return [Sanctorale]
64
+ # @since 0.7.0
65
+ def load_with_parents(path)
66
+ loader = SanctoraleLoader.new
67
+
68
+ hierarchy = load_parent_hierarchy(path, loader)
69
+ return hierarchy.first if hierarchy.size == 1
70
+
71
+ create_layered *hierarchy
72
+ end
73
+
74
+ private
75
+
76
+ def load_parent_hierarchy(path, loader)
77
+ main = loader.load_from_file path
78
+ return [main] unless main.metadata.has_key? 'extends'
79
+
80
+ to_merge = [main]
81
+ parents = main.metadata['extends']
82
+ parents = [parents] unless parents.is_a? Array
83
+ parents.reverse.each do |parent_path|
84
+ expanded_path = File.expand_path parent_path, File.dirname(path)
85
+ subtree = load_parent_hierarchy(expanded_path, loader)
86
+ to_merge = subtree + to_merge
87
+ end
88
+
89
+ to_merge
90
+ end
20
91
  end
21
92
  end
22
93
  end
@@ -0,0 +1,176 @@
1
+ require 'yaml'
2
+
3
+ module CalendariumRomanum
4
+
5
+ # Understands a custom plaintext calendar format
6
+ # and knows how to transform it to {Celebration}s
7
+ # and fill them in a {Sanctorale}.
8
+ #
9
+ # For specification of the data format see {file:data/README.md},
10
+ # for a complete example see the file describing General Roman Calendar:
11
+ # {file:data/universal-en.txt}
12
+ class SanctoraleLoader
13
+
14
+ # @api private
15
+ RANK_CODES = {
16
+ nil => Ranks::MEMORIAL_OPTIONAL, # default
17
+ 'm' => Ranks::MEMORIAL_GENERAL,
18
+ 'f' => Ranks::FEAST_GENERAL,
19
+ 's' => Ranks::SOLEMNITY_GENERAL
20
+ }.freeze
21
+
22
+ # @api private
23
+ COLOUR_CODES = {
24
+ nil => Colours::WHITE, # default
25
+ 'w' => Colours::WHITE,
26
+ 'v' => Colours::VIOLET,
27
+ 'g' => Colours::GREEN,
28
+ 'r' => Colours::RED
29
+ }.freeze
30
+
31
+ # Load from an object which understands +#each_line+
32
+ #
33
+ # @param src [String, File, #each_line]
34
+ # source of the loaded data
35
+ # @param dest [Sanctorale, nil]
36
+ # objects to populate. If not provided, a new {Sanctorale}
37
+ # instance will be created
38
+ # @return [Sanctorale]
39
+ # @raise [InvalidDataError]
40
+ def load(src, dest = nil)
41
+ dest ||= Sanctorale.new
42
+
43
+ in_front_matter = false
44
+ front_matter = ''
45
+ month_section = nil
46
+ src.each_line.with_index(1) do |l, line_num|
47
+ # skip YAML front matter
48
+ if line_num == 1 && l.start_with?('---')
49
+ in_front_matter = true
50
+ front_matter += l
51
+ next
52
+ elsif in_front_matter
53
+ if l.start_with?('---')
54
+ in_front_matter = false
55
+ dest.metadata = YAML.load(front_matter).freeze
56
+ end
57
+
58
+ front_matter += l
59
+
60
+ next
61
+ end
62
+
63
+ # strip whitespace and comments
64
+ l.sub!(/#.*/, '')
65
+ l.strip!
66
+ next if l.empty?
67
+
68
+ # month section heading
69
+ n = l.match(/^=\s*(\d+)\s*$/)
70
+ unless n.nil?
71
+ month_section = n[1].to_i
72
+ unless month_section >= 1 && month_section <= 12
73
+ raise error("Invalid month #{month_section}", line_num)
74
+ end
75
+ next
76
+ end
77
+
78
+ begin
79
+ celebration = load_line l, month_section
80
+ rescue RangeError, RuntimeError => err
81
+ raise error(err.message, line_num)
82
+ end
83
+
84
+ dest.add(
85
+ celebration.date.month,
86
+ celebration.date.day,
87
+ celebration
88
+ )
89
+ end
90
+
91
+ dest
92
+ end
93
+
94
+ alias load_from_string load
95
+
96
+ # Load from a filesystem path
97
+ #
98
+ # @param filename [String]
99
+ # @param dest [Sanctorale, nil]
100
+ # @param encoding [String]
101
+ # @return (see #load)
102
+ # @raise (see #load)
103
+ def load_from_file(filename, dest = nil, encoding = 'utf-8')
104
+ load File.open(filename, 'r', encoding: encoding), dest
105
+ end
106
+
107
+ private
108
+
109
+ def line_regexp
110
+ @line_regexp ||=
111
+ begin
112
+ rank_letters = RANK_CODES.keys.compact.join('')
113
+ colour_letters = COLOUR_CODES.keys.compact.join('')
114
+
115
+ Regexp.new(
116
+ '^((?<month>\d+)\/)?(?<day>\d+)' + # date
117
+ '(\s+(?<rank_char>[' + rank_letters + '])?(?<rank_num>\d\.\d{1,2})?)?' + # rank (optional)
118
+ '(\s+(?<colour>[' + colour_letters + ']))?' + # colour (optional)
119
+ '(\s+(?<symbol>[\w]{2,}))?' + # symbol (optional)
120
+ '\s*:(?<title>.*)$', # title
121
+ Regexp::IGNORECASE
122
+ )
123
+ end
124
+ end
125
+
126
+ # parses a line containing celebration record,
127
+ # returns a single Celebration
128
+ def load_line(line, month_section = nil)
129
+ # celebration record
130
+ m = line.match(line_regexp)
131
+ if m.nil?
132
+ raise RuntimeError.new("Syntax error, line skipped '#{line}'")
133
+ end
134
+
135
+ month = (m[:month] || month_section).to_i
136
+ day = m[:day].to_i
137
+ rank_char = m[:rank_char]
138
+ rank_num = m[:rank_num]
139
+ colour = m[:colour]
140
+ symbol_str = m[:symbol]
141
+ title = m[:title]
142
+
143
+ rank = RANK_CODES[rank_char && rank_char.downcase]
144
+
145
+ if rank_num
146
+ rank_num = rank_num.to_f
147
+ rank_by_num = Ranks[rank_num]
148
+
149
+ if rank_by_num.nil?
150
+ raise RuntimeError.new("Invalid celebration rank code #{rank_num}")
151
+ elsif rank_char && (rank.priority.to_i != rank_by_num.priority.to_i)
152
+ raise RuntimeError.new("Invalid combination of rank letter #{rank_char.inspect} and number #{rank_num}.")
153
+ end
154
+
155
+ rank = rank_by_num
156
+ end
157
+
158
+ symbol = nil
159
+ if symbol_str
160
+ symbol = symbol_str.to_sym
161
+ end
162
+
163
+ Celebration.new(
164
+ title.strip,
165
+ rank,
166
+ COLOUR_CODES[colour && colour.downcase],
167
+ symbol,
168
+ AbstractDate.new(month, day)
169
+ )
170
+ end
171
+
172
+ def error(message, line_number)
173
+ InvalidDataError.new("L#{line_number}: #{message}")
174
+ end
175
+ end
176
+ end
@@ -2,27 +2,44 @@ require 'date'
2
2
 
3
3
  module CalendariumRomanum
4
4
 
5
- # determine seasons and dates of the Temporale feasts of the given year
5
+ # One of the two main {Calendar} components.
6
+ # Handles seasons and celebrations of the temporale cycle
7
+ # for a given liturgical year.
6
8
  class Temporale
7
9
 
10
+ # How many days in a week
8
11
  WEEK = 7
9
12
 
10
- SEASON_COLOUR = {
11
- Seasons::ADVENT => Colours::VIOLET,
12
- Seasons::CHRISTMAS => Colours::WHITE,
13
- Seasons::ORDINARY => Colours::GREEN,
14
- Seasons::LENT => Colours::VIOLET,
15
- Seasons::EASTER => Colours::WHITE,
16
- }
17
-
18
- # year is Integer - the civil year when the liturgical year begins
19
- def initialize(year=nil)
13
+ # Which solemnities can be transferred to Sunday
14
+ SUNDAY_TRANSFERABLE_SOLEMNITIES =
15
+ %i(epiphany ascension corpus_christi).freeze
16
+
17
+ # @param year [Fixnum]
18
+ # the civil year when the liturgical year _begins_
19
+ # @param extensions [Array<#each_celebration>]
20
+ # extensions implementing custom temporale celebrations
21
+ # @param transfer_to_sunday [Array<Symbol>]
22
+ # which solemnities should be transferred to a nearby
23
+ # Sunday - see {SUNDAY_TRANSFERABLE_SOLEMNITIES}
24
+ # for possible values
25
+ def initialize(year, extensions: [], transfer_to_sunday: [])
20
26
  @year = year
21
- prepare_solemnities unless @year.nil?
27
+
28
+ @extensions = extensions
29
+ @transfer_to_sunday = transfer_to_sunday.sort
30
+ validate_sunday_transfer!
31
+
32
+ prepare_solemnities
22
33
  end
23
34
 
35
+ # @return [Fixnum]
36
+ attr_reader :year
37
+
24
38
  class << self
25
39
  # Determines liturgical year for the given date
40
+ #
41
+ # @param date [Date]
42
+ # @return [Fixnum]
26
43
  def liturgical_year(date)
27
44
  year = date.year
28
45
  temporale = Temporale.new year
@@ -31,225 +48,190 @@ module CalendariumRomanum
31
48
  return year - 1
32
49
  end
33
50
 
34
- return year
51
+ year
35
52
  end
36
53
 
37
- # creates a Calendar for the liturgical year including given
54
+ # Creates an instance for the liturgical year including given
38
55
  # date
56
+ #
57
+ # @param date [Date]
58
+ # @return [Temporale]
39
59
  def for_day(date)
40
- return new(liturgical_year(date))
41
- end
42
- end
43
-
44
- def start_date(year=nil)
45
- first_advent_sunday(year)
46
- end
47
-
48
- def end_date(year=nil)
49
- year ||= @year
50
- first_advent_sunday(year+1) - 1
51
- end
52
-
53
- def date_range(year=nil)
54
- start_date(year) .. end_date(year)
55
- end
56
-
57
- def range_check(date)
58
- # necessary in order to handle Date correctly
59
- date = date.to_date if date.class != Date
60
-
61
- unless date_range.include? date
62
- raise RangeError.new "Date out of range #{date}"
63
- end
64
- end
65
-
66
- # converts an AbstractDate to a Date in the given
67
- # liturgical year
68
- def concretize_abstract_date(abstract_date)
69
- d = abstract_date.concretize(@year + 1)
70
- if date_range.include? d
71
- d
72
- else
73
- abstract_date.concretize(@year)
60
+ new(liturgical_year(date))
74
61
  end
75
- end
76
62
 
77
- def weekday_before(weekday, date)
78
- if date.wday == weekday then
79
- return date - WEEK
80
- elsif weekday < date.wday
81
- return date - (date.wday - weekday)
82
- else
83
- return date - (date.wday + WEEK - weekday)
63
+ # Factory method creating temporale {Celebration}s
64
+ # with sensible defaults
65
+ #
66
+ # See {Celebration#initialize} for argument description.
67
+ def create_celebration(title, rank, colour, symbol: nil, date: nil)
68
+ Celebration.new(title, rank, colour, symbol, date, :temporale)
84
69
  end
85
- end
86
70
 
87
- def weekday_after(weekday, date)
88
- if date.wday == weekday then
89
- return date + WEEK
90
- elsif weekday > date.wday
91
- return date + (weekday - date.wday)
92
- else
93
- return date + (WEEK - date.wday + weekday)
71
+ C = Struct.new(:date_method, :celebration)
72
+ private_constant :C
73
+
74
+ # @api private
75
+ def celebrations
76
+ @celebrations ||=
77
+ begin
78
+ %i(
79
+ nativity
80
+ holy_family
81
+ mother_of_god
82
+ epiphany
83
+ baptism_of_lord
84
+ ash_wednesday
85
+ good_friday
86
+ holy_saturday
87
+ palm_sunday
88
+ easter_sunday
89
+ ascension
90
+ pentecost
91
+ holy_trinity
92
+ corpus_christi
93
+ mother_of_church
94
+ sacred_heart
95
+ christ_king
96
+ immaculate_heart
97
+ ).collect do |symbol|
98
+ date_method = symbol
99
+ C.new(
100
+ date_method,
101
+ CelebrationFactory.public_send(symbol)
102
+ )
103
+ end
104
+ # Immaculate Heart of Mary and Mary, Mother of the Church
105
+ # are actually movable *sanctorale* feasts,
106
+ # but as it would make little sense
107
+ # to add support for movable sanctorale feasts because of
108
+ # two, we cheat a bit and handle them in temporale.
109
+ end
94
110
  end
95
111
  end
96
112
 
97
- def octave_of(date)
98
- date + WEEK
113
+ # Does this instance transfer the specified solemnity to Sunday?
114
+ #
115
+ # @param solemnity [Symbol]
116
+ # @return [Boolean]
117
+ def transferred_to_sunday?(solemnity)
118
+ @transfer_to_sunday.include?(solemnity)
99
119
  end
100
120
 
101
- WEEKDAYS = %w{sunday monday tuesday wednesday thursday friday saturday}
102
- WEEKDAYS.each_with_index do |weekday, weekday_i|
103
- define_method "#{weekday}_before" do |date|
104
- send('weekday_before', weekday_i, date)
105
- end
106
-
107
- define_method "#{weekday}_after" do |date|
108
- send('weekday_after', weekday_i, date)
109
- end
121
+ # First day of the liturgical year
122
+ #
123
+ # @return [Date]
124
+ def start_date
125
+ first_advent_sunday
110
126
  end
111
127
 
112
- # first_advent_sunday -> advent_sunday(1)
113
- %w{first second third fourth}.each_with_index do |word,i|
114
- define_method "#{word}_advent_sunday" do |year=nil|
115
- send("advent_sunday", i + 1, year)
116
- end
128
+ # Last day of the liturgical year
129
+ #
130
+ # @return [Date]
131
+ def end_date
132
+ Dates.first_advent_sunday(year + 1) - 1
117
133
  end
118
134
 
119
- def advent_sunday(num, year=nil)
120
- advent_sundays_total = 4
121
- unless (1..advent_sundays_total).include? num
122
- raise ArgumentError.new "Invalid Advent Sunday #{num}"
123
- end
124
-
125
- year ||= @year
126
- return sunday_before(nativity(year)) - ((advent_sundays_total - num) * WEEK)
135
+ # Date range of the liturgical year
136
+ #
137
+ # @return [Range<Date>]
138
+ def date_range
139
+ start_date .. end_date
127
140
  end
128
141
 
129
- def nativity(year=nil)
130
- year ||= @year
131
- return Date.new(year, 12, 25)
132
- end
142
+ # Check that the date belongs to the liturgical year.
143
+ # If it does not, throw exception.
144
+ #
145
+ # @param date [Date]
146
+ # @return [void]
147
+ # @raise [RangeError]
148
+ def range_check(date)
149
+ # necessary in order to handle Date correctly
150
+ date = date.to_date if date.class != Date
133
151
 
134
- def holy_family(year=nil)
135
- year ||= @year
136
- xmas = nativity(year)
137
- if xmas.sunday?
138
- return Date.new(year, 12, 30)
139
- else
140
- sunday_after(xmas)
152
+ unless date_range.include? date
153
+ raise RangeError.new "Date out of range #{date}"
141
154
  end
142
155
  end
143
156
 
144
- def mother_of_god(year=nil)
145
- octave_of(nativity(year))
146
- end
147
-
148
- def epiphany(year=nil)
149
- year ||= @year
150
- return Date.new(year+1, 1, 6)
151
- end
152
-
153
- def baptism_of_lord(year=nil)
154
- year ||= @year
155
- return sunday_after epiphany(year)
156
- end
157
-
158
- def ash_wednesday(year=nil)
159
- year ||= @year
160
- return easter_sunday(year) - (6 * WEEK + 4)
161
- end
162
-
163
- def easter_sunday(year=nil)
164
- year ||= @year
165
- year += 1
166
-
167
- # algorithm below taken from the 'easter' gem:
168
- # https://github.com/jrobertson/easter
169
-
170
- golden_number = (year % 19) + 1
171
- if year <= 1752 then
172
- # Julian calendar
173
- dominical_number = (year + (year / 4) + 5) % 7
174
- paschal_full_moon = (3 - (11 * golden_number) - 7) % 30
175
- else
176
- # Gregorian calendar
177
- dominical_number = (year + (year / 4) - (year / 100) + (year / 400)) % 7
178
- solar_correction = (year - 1600) / 100 - (year - 1600) / 400
179
- lunar_correction = (((year - 1400) / 100) * 8) / 25
180
- paschal_full_moon = (3 - 11 * golden_number + solar_correction - lunar_correction) % 30
181
- end
182
- dominical_number += 7 until dominical_number > 0
183
- paschal_full_moon += 30 until paschal_full_moon > 0
184
- paschal_full_moon -= 1 if paschal_full_moon == 29 or (paschal_full_moon == 28 and golden_number > 11)
185
- difference = (4 - paschal_full_moon - dominical_number) % 7
186
- difference += 7 if difference < 0
187
- day_easter = paschal_full_moon + difference + 1
188
- if day_easter < 11 then
189
- # Easter occurs in March.
190
- return Date.new(y=year, m=3, d=day_easter + 21)
157
+ # @!method nativity
158
+ # @return [Date]
159
+ # @!method holy_family
160
+ # @return [Date]
161
+ # @!method mother_of_god
162
+ # @return [Date]
163
+ # @!method epiphany
164
+ # @return [Date]
165
+ # @!method baptism_of_lord
166
+ # @return [Date]
167
+ # @!method ash_wednesday
168
+ # @return [Date]
169
+ # @!method good_friday
170
+ # @return [Date]
171
+ # @!method holy_saturday
172
+ # @return [Date]
173
+ # @!method palm_sunday
174
+ # @return [Date]
175
+ # @!method easter_sunday
176
+ # @return [Date]
177
+ # @!method ascension
178
+ # @return [Date]
179
+ # @!method pentecost
180
+ # @return [Date]
181
+ # @!method holy_trinity
182
+ # @return [Date]
183
+ # @!method corpus_christi
184
+ # @return [Date]
185
+ # @!method mother_of_church
186
+ # @return [Date]
187
+ # @!method sacred_heart
188
+ # @return [Date]
189
+ # @!method christ_king
190
+ # @return [Date]
191
+ # @!method immaculate_heart
192
+ # @return [Date]
193
+ # @!method first_advent_sunday
194
+ # @return [Date]
195
+ (celebrations.collect(&:date_method) + [:first_advent_sunday])
196
+ .each do |feast|
197
+ if SUNDAY_TRANSFERABLE_SOLEMNITIES.include? feast
198
+ define_method feast do
199
+ Dates.public_send feast, year, sunday: transferred_to_sunday?(feast)
200
+ end
201
+ elsif feast == :baptism_of_lord
202
+ define_method feast do
203
+ Dates.public_send feast, year, epiphany_on_sunday: transferred_to_sunday?(:epiphany)
204
+ end
191
205
  else
192
- # Easter occurs in April.
193
- return Date.new(y=year, m=4, d=day_easter - 10)
206
+ define_method feast do
207
+ Dates.public_send feast, year
208
+ end
194
209
  end
195
210
  end
196
211
 
197
- def palm_sunday(year=nil)
198
- return easter_sunday(year) - 7
199
- end
200
-
201
- def good_friday(year=nil)
202
- return easter_sunday(year) - 2
203
- end
204
-
205
- def holy_saturday(year=nil)
206
- return easter_sunday(year) - 1
207
- end
208
-
209
- def ascension(year=nil)
210
- return pentecost(year) - 10
211
- end
212
-
213
- def pentecost(year=nil)
214
- year ||= @year
215
- return easter_sunday(year) + 7 * WEEK
216
- end
217
-
218
- def holy_trinity(year=nil)
219
- sunday_after(pentecost(year))
220
- end
221
-
222
- def body_blood(year=nil)
223
- thursday_after(holy_trinity(year))
224
- end
225
-
226
- def sacred_heart(year=nil)
227
- friday_after(sunday_after(body_blood(year)))
228
- end
229
-
230
- def christ_king(year=nil)
231
- year ||= @year
232
- sunday_before(first_advent_sunday(year + 1))
233
- end
234
-
235
- # which liturgical season is it?
212
+ # Determine liturgical season for a given date
213
+ #
214
+ # @param date [Date]
215
+ # @return [Season]
216
+ # @raise [RangeError]
217
+ # if the given date doesn't belong to the liturgical year
236
218
  def season(date)
237
219
  range_check date
238
220
 
239
- if first_advent_sunday <= date and
240
- nativity > date then
221
+ if (first_advent_sunday <= date) &&
222
+ nativity > date
241
223
  Seasons::ADVENT
242
224
 
243
- elsif nativity <= date and
244
- baptism_of_lord >= date then
225
+ elsif (nativity <= date) &&
226
+ (baptism_of_lord >= date)
245
227
  Seasons::CHRISTMAS
246
228
 
247
- elsif ash_wednesday <= date and
248
- easter_sunday > date then
229
+ elsif (ash_wednesday <= date) &&
230
+ easter_sunday > date
249
231
  Seasons::LENT
250
232
 
251
- elsif easter_sunday <= date and
252
- pentecost >= date then
233
+ elsif (easter_sunday <= date) &&
234
+ (pentecost >= date)
253
235
  Seasons::EASTER
254
236
 
255
237
  else
@@ -257,6 +239,10 @@ module CalendariumRomanum
257
239
  end
258
240
  end
259
241
 
242
+ # When the specified liturgical season begins
243
+ #
244
+ # @param s [Season]
245
+ # @return [Date]
260
246
  def season_beginning(s)
261
247
  case s
262
248
  when Seasons::ADVENT
@@ -267,18 +253,24 @@ module CalendariumRomanum
267
253
  ash_wednesday
268
254
  when Seasons::EASTER
269
255
  easter_sunday
270
- else # ordinary time
271
- monday_after(baptism_of_lord)
256
+ when Seasons::ORDINARY # ordinary time
257
+ baptism_of_lord + 1
258
+ else
259
+ raise ArgumentError.new('unsupported season')
272
260
  end
273
261
  end
274
262
 
263
+ # Determine week of a season for a given date
264
+ #
265
+ # @param seasonn [Season]
266
+ # @param date [Date]
275
267
  def season_week(seasonn, date)
276
268
  week1_beginning = season_beginning = season_beginning(seasonn)
277
269
  unless season_beginning.sunday?
278
- week1_beginning = sunday_after(season_beginning)
270
+ week1_beginning = Dates.sunday_after(season_beginning)
279
271
  end
280
272
 
281
- week = date_difference(date, week1_beginning) / Temporale::WEEK + 1
273
+ week = date_difference(date, week1_beginning) / WEEK + 1
282
274
 
283
275
  if seasonn == Seasons::ORDINARY
284
276
  # ordinary time does not begin with Sunday, but the first week
@@ -286,19 +278,32 @@ module CalendariumRomanum
286
278
  week += 1
287
279
 
288
280
  if date > pentecost
289
- weeks_after_date = date_difference(first_advent_sunday(@year + 1), date) / 7
281
+ weeks_after_date = date_difference(Dates.first_advent_sunday(@year + 1), date) / WEEK
290
282
  week = 34 - weeks_after_date
291
283
  week += 1 if date.sunday?
292
284
  end
293
285
  end
294
286
 
295
- return week
287
+ week
288
+ end
289
+
290
+ # Retrieve temporale celebration for the given day
291
+ #
292
+ # @param date [Date]
293
+ # @return [Celebration]
294
+ # @since 0.6.0
295
+ def [](date)
296
+ @solemnities[date] || @feasts[date] || sunday(date) || @memorials[date] || ferial(date)
296
297
  end
297
298
 
298
- # returns a Celebration
299
- # scheduled for the given day
299
+ # Retrieve temporale celebration for the given day
300
300
  #
301
- # expected arguments: Date or two Integers (month, day)
301
+ # @overload get(date)
302
+ # @param date [Date]
303
+ # @overload get(month, day)
304
+ # @param month [Fixnum]
305
+ # @param day [Fixnum]
306
+ # @return (see #[])
302
307
  def get(*args)
303
308
  if args.size == 1 && args[0].is_a?(Date)
304
309
  date = args[0]
@@ -310,94 +315,134 @@ module CalendariumRomanum
310
315
  end
311
316
  end
312
317
 
313
- return solemnity(date) || sunday(date) || ferial(date)
318
+ self[date]
314
319
  end
315
320
 
316
- private
321
+ # @return [Boolean]
322
+ # @since 0.6.0
323
+ def ==(b)
324
+ self.class == b.class &&
325
+ year == b.year &&
326
+ transfer_to_sunday == b.transfer_to_sunday &&
327
+ Set.new(extensions) == Set.new(b.extensions)
328
+ end
317
329
 
318
- # the celebration determination split in methods:
330
+ protected
319
331
 
320
- def solemnity(date)
321
- if @solemnities.has_key?(date)
322
- return @solemnities[date]
323
- end
332
+ attr_reader :transfer_to_sunday, :extensions
324
333
 
325
- seas = season(date)
326
- case seas
327
- when Seasons::EASTER
328
- if date <= sunday_after(easter_sunday)
329
- return Celebration.new '', Ranks::PRIMARY, SEASON_COLOUR[seas]
330
- end
331
- end
334
+ private
332
335
 
333
- return nil
334
- end
336
+ # seasons when Sundays have higher rank
337
+ SEASONS_SUNDAY_PRIMARY = [Seasons::ADVENT, Seasons::LENT, Seasons::EASTER].freeze
335
338
 
336
339
  def sunday(date)
337
340
  return nil unless date.sunday?
338
341
 
339
342
  seas = season date
340
343
  rank = Ranks::SUNDAY_UNPRIVILEGED
341
- if [Seasons::ADVENT, Seasons::LENT, Seasons::EASTER].include?(seas)
344
+ if SEASONS_SUNDAY_PRIMARY.include?(seas)
342
345
  rank = Ranks::PRIMARY
343
346
  end
344
347
 
345
- return Celebration.new '', rank, SEASON_COLOUR[seas]
348
+ week = Ordinalizer.ordinal season_week(seas, date)
349
+ title = I18n.t "temporale.#{seas.to_sym}.sunday", week: week
350
+
351
+ self.class.create_celebration title, rank, seas.colour
346
352
  end
347
353
 
348
354
  def ferial(date)
349
355
  seas = season date
356
+ week = season_week(seas, date)
350
357
  rank = Ranks::FERIAL
358
+ title = nil
351
359
  case seas
352
360
  when Seasons::ADVENT
353
361
  if date >= Date.new(@year, 12, 17)
354
362
  rank = Ranks::FERIAL_PRIVILEGED
363
+ nth = Ordinalizer.ordinal(date.day)
364
+ title = I18n.t 'temporale.advent.before_christmas', day: nth
355
365
  end
356
366
  when Seasons::CHRISTMAS
357
367
  if date < mother_of_god
358
368
  rank = Ranks::FERIAL_PRIVILEGED
369
+
370
+ nth = Ordinalizer.ordinal(date.day - nativity.day + 1) # 1-based counting
371
+ title = I18n.t 'temporale.christmas.nativity_octave.ferial', day: nth
372
+ elsif date > epiphany
373
+ title = I18n.t 'temporale.christmas.after_epiphany.ferial', weekday: I18n.t("weekday.#{date.wday}")
359
374
  end
360
375
  when Seasons::LENT
361
- rank = Ranks::FERIAL_PRIVILEGED
376
+ if week == 0
377
+ title = I18n.t 'temporale.lent.after_ashes.ferial', weekday: I18n.t("weekday.#{date.wday}")
378
+ elsif date > palm_sunday
379
+ rank = Ranks::PRIMARY
380
+ title = I18n.t 'temporale.lent.holy_week.ferial', weekday: I18n.t("weekday.#{date.wday}")
381
+ end
382
+ rank = Ranks::FERIAL_PRIVILEGED unless rank > Ranks::FERIAL_PRIVILEGED
383
+ when Seasons::EASTER
384
+ if week == 1
385
+ rank = Ranks::PRIMARY
386
+ title = I18n.t 'temporale.easter.octave.ferial', weekday: I18n.t("weekday.#{date.wday}")
387
+ end
362
388
  end
363
389
 
364
- return Celebration.new '', rank, SEASON_COLOUR[seas]
390
+ week_ord = Ordinalizer.ordinal week
391
+ title ||= I18n.t "temporale.#{seas.to_sym}.ferial", week: week_ord, weekday: I18n.t("weekday.#{date.wday}")
392
+
393
+ self.class.create_celebration title, rank, seas.colour
365
394
  end
366
395
 
367
396
  # helper: difference between two Dates in days
368
397
  def date_difference(d1, d2)
369
- return (d1 - d2).numerator
398
+ (d1 - d2).numerator
370
399
  end
371
400
 
372
- # prepare dates of temporale solemnities and their octaves
401
+ # prepare dates of temporale solemnities
373
402
  def prepare_solemnities
374
403
  @solemnities = {}
404
+ @feasts = {}
405
+ @memorials = {}
406
+
407
+ self.class.celebrations.each do |c|
408
+ prepare_celebration_date c.date_method, c.celebration
409
+ end
410
+
411
+ @extensions.each do |extension|
412
+ extension.each_celebration do |date_method, celebration|
413
+ date_proc = date_method
414
+ if date_method.is_a? Symbol
415
+ date_proc = extension.method(date_method)
416
+ end
417
+
418
+ prepare_celebration_date date_proc, celebration
419
+ end
420
+ end
421
+ end
422
+
423
+ def prepare_celebration_date(date_method, celebration)
424
+ date =
425
+ if date_method.respond_to? :call
426
+ date_method.call(year)
427
+ else
428
+ public_send(date_method)
429
+ end
430
+
431
+ add_to =
432
+ if celebration.feast?
433
+ @feasts
434
+ elsif celebration.memorial?
435
+ @memorials
436
+ else
437
+ @solemnities
438
+ end
439
+ add_to[date] = celebration
440
+ end
375
441
 
376
- {
377
- nativity: [Ranks::PRIMARY, nil],
378
- holy_family: [Ranks::FEAST_LORD_GENERAL, nil],
379
- mother_of_god: [Ranks::SOLEMNITY_GENERAL],
380
- epiphany: [Ranks::PRIMARY, nil],
381
- baptism_of_lord: [Ranks::FEAST_LORD_GENERAL, nil],
382
- ash_wednesday: [Ranks::PRIMARY, nil],
383
- good_friday: [Ranks::TRIDUUM, Colours::RED],
384
- holy_saturday: [Ranks::TRIDUUM, nil],
385
- palm_sunday: [Ranks::PRIMARY, Colours::RED],
386
- easter_sunday: [Ranks::TRIDUUM, nil],
387
- ascension: [Ranks::PRIMARY, Colours::WHITE],
388
- pentecost: [Ranks::PRIMARY, Colours::RED],
389
- holy_trinity: [Ranks::SOLEMNITY_GENERAL, Colours::WHITE],
390
- body_blood: [Ranks::SOLEMNITY_GENERAL, Colours::WHITE],
391
- sacred_heart: [Ranks::SOLEMNITY_GENERAL, Colours::WHITE],
392
- christ_king: [Ranks::SOLEMNITY_GENERAL, Colours::WHITE],
393
- }.each_pair do |method_name, data|
394
- date = send(method_name)
395
- rank, colour = data
396
- @solemnities[date] = Celebration.new(
397
- proc { I18n.t("temporale.solemnity.#{method_name}") },
398
- rank,
399
- colour || SEASON_COLOUR[season(date)]
400
- )
442
+ def validate_sunday_transfer!
443
+ unsupported = @transfer_to_sunday - SUNDAY_TRANSFERABLE_SOLEMNITIES
444
+ unless unsupported.empty?
445
+ raise RuntimeError.new("Transfer of #{unsupported.inspect} to a Sunday not supported. Only #{SUNDAY_TRANSFERABLE_SOLEMNITIES} are allowed.")
401
446
  end
402
447
  end
403
448
  end