calendarium-romanum 0.0.1

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,100 @@
1
+ module CalendariumRomanum
2
+
3
+ # understands a plaintext calendar format
4
+ # and knows how to transform it to Celebrations
5
+ # and fill them in a Sanctorale
6
+ #
7
+ # Format of the file:
8
+ # 1/31 m : S. Ioannis Bosco, presbyteri
9
+ #
10
+ # <month>/<day> <rank shortcut> : <title>
11
+ # rank shortcut is optional, default value is optional memorial
12
+ class SanctoraleLoader
13
+
14
+ RANK_CODES = {
15
+ nil => Ranks::MEMORIAL_OPTIONAL,
16
+ 'm' => Ranks::MEMORIAL_GENERAL,
17
+ 'f' => Ranks::FEAST_GENERAL,
18
+ 's' => Ranks::SOLEMNITY_GENERAL
19
+ }
20
+ COLOUR_CODES = {
21
+ nil => Colours::WHITE,
22
+ 'W' => Colours::WHITE,
23
+ 'V' => Colours::VIOLET,
24
+ 'G' => Colours::GREEN,
25
+ 'R' => Colours::RED
26
+ }
27
+
28
+ # dest should be a Sanctorale,
29
+ # src anything with #each_line
30
+ def load(dest, src)
31
+ month_section = nil
32
+ src.each_line.each_with_index do |l, li|
33
+ line_num = li + 1
34
+
35
+ # strip whitespace and comments
36
+ l.sub!(/#.*/, '')
37
+ l.strip!
38
+ next if l.empty?
39
+
40
+ # month section heading
41
+ n = l.match /^=\s*(\d+)\s*$/
42
+ unless n.nil?
43
+ month_section = n[1].to_i
44
+ unless month_section >= 1 && month_section <= 12
45
+ raise error("Invalid month #{month_section}", line_num)
46
+ end
47
+ next
48
+ end
49
+
50
+ # celebration record
51
+ m = l.match /^((\d+)\/)?(\d+)\s*(([mfs])(\d\.\d+)?)?\s*([WVRG])?\s*:(.*)$/
52
+ if m.nil?
53
+ raise error("Syntax error, line skipped '#{l}'", line_num)
54
+ next
55
+ end
56
+
57
+ month, day, rank_char, rank_num, colour, title = m.values_at(2, 3, 5, 6, 7, 8)
58
+ month ||= month_section
59
+ day = day.to_i
60
+ month = month.to_i
61
+
62
+ rank = RANK_CODES[rank_char]
63
+ if rank.nil?
64
+ raise error("Invalid celebration rank code #{rank_char}", line_num)
65
+ end
66
+
67
+ if rank_num
68
+ rank_num = rank_num.to_f
69
+ rank_by_num = Ranks[rank_num]
70
+
71
+ if rank_by_num.nil?
72
+ raise error("Invalid celebration rank code #{rank_num}", line_num)
73
+ elsif rank.priority.to_i != rank_by_num.priority.to_i
74
+ raise error("Invalid combination of rank letter #{rank_char.inspect} and number #{rank_num}.", line_num)
75
+ end
76
+
77
+ rank = rank_by_num
78
+ end
79
+
80
+ dest.add month, day, Celebration.new(
81
+ title.strip,
82
+ rank,
83
+ COLOUR_CODES[colour]
84
+ )
85
+ end
86
+ end
87
+
88
+ alias_method :load_from_string, :load
89
+
90
+ def load_from_file(dest, filename, encoding='utf-8')
91
+ self.load dest, File.open(filename, 'r', encoding: encoding)
92
+ end
93
+
94
+ private
95
+
96
+ def error(message, line_number)
97
+ RuntimeError.new("L#{line_number}: #{message}")
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,382 @@
1
+ require 'date'
2
+
3
+ module CalendariumRomanum
4
+
5
+ # determine seasons and dates of the Temporale feasts of the given year
6
+ class Temporale
7
+
8
+ WEEK = 7
9
+
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)
20
+ @year = year
21
+ prepare_solemnities unless @year.nil?
22
+ end
23
+
24
+ class << self
25
+ # Determines liturgical year for the given date
26
+ def liturgical_year(date)
27
+ year = date.year
28
+ temporale = Temporale.new year
29
+
30
+ if date < temporale.first_advent_sunday
31
+ return year - 1
32
+ end
33
+
34
+ return year
35
+ end
36
+
37
+ # creates a Calendar for the liturgical year including given
38
+ # date
39
+ 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
+ unless date_range.include? date
59
+ raise RangeError.new "Date out of range #{date}"
60
+ end
61
+ end
62
+
63
+ # converts an AbstractDate to a Date in the given
64
+ # liturgical year
65
+ def concretize_abstract_date(abstract_date)
66
+ d = abstract_date.concretize(@year + 1)
67
+ if date_range.include? d
68
+ d
69
+ else
70
+ abstract_date.concretize(@year)
71
+ end
72
+ end
73
+
74
+ def weekday_before(weekday, date)
75
+ if date.wday == weekday then
76
+ return date - WEEK
77
+ elsif weekday < date.wday
78
+ return date - (date.wday - weekday)
79
+ else
80
+ return date - (date.wday + WEEK - weekday)
81
+ end
82
+ end
83
+
84
+ def weekday_after(weekday, date)
85
+ if date.wday == weekday then
86
+ return date + WEEK
87
+ elsif weekday > date.wday
88
+ return date + (weekday - date.wday)
89
+ else
90
+ return date + (WEEK - date.wday + weekday)
91
+ end
92
+ end
93
+
94
+ def octave_of(date)
95
+ date + WEEK
96
+ end
97
+
98
+ WEEKDAYS = %w{sunday monday tuesday wednesday thursday friday saturday}
99
+ WEEKDAYS.each_with_index do |weekday, weekday_i|
100
+ define_method "#{weekday}_before" do |date|
101
+ send('weekday_before', weekday_i, date)
102
+ end
103
+
104
+ define_method "#{weekday}_after" do |date|
105
+ send('weekday_after', weekday_i, date)
106
+ end
107
+ end
108
+
109
+ # first_advent_sunday -> advent_sunday(1)
110
+ %w{first second third fourth}.each_with_index do |word,i|
111
+ define_method "#{word}_advent_sunday" do |year=nil|
112
+ send("advent_sunday", i + 1, year)
113
+ end
114
+ end
115
+
116
+ def advent_sunday(num, year=nil)
117
+ advent_sundays_total = 4
118
+ unless (1..advent_sundays_total).include? num
119
+ raise ArgumentError.new "Invalid Advent Sunday #{num}"
120
+ end
121
+
122
+ year ||= @year
123
+ return sunday_before(nativity(year)) - ((advent_sundays_total - num) * WEEK)
124
+ end
125
+
126
+ def nativity(year=nil)
127
+ year ||= @year
128
+ return Date.new(year, 12, 25)
129
+ end
130
+
131
+ def holy_family(year=nil)
132
+ sunday_after(nativity(year))
133
+ end
134
+
135
+ def mother_of_god(year=nil)
136
+ octave_of(nativity(year))
137
+ end
138
+
139
+ def epiphany(year=nil)
140
+ year ||= @year
141
+ return Date.new(year+1, 1, 6)
142
+ end
143
+
144
+ def baptism_of_lord(year=nil)
145
+ year ||= @year
146
+ return sunday_after epiphany(year)
147
+ end
148
+
149
+ def ash_wednesday(year=nil)
150
+ year ||= @year
151
+ return easter_sunday(year) - (6 * WEEK + 4)
152
+ end
153
+
154
+ def easter_sunday(year=nil)
155
+ year ||= @year
156
+ year += 1
157
+
158
+ # algorithm below taken from the 'easter' gem:
159
+ # https://github.com/jrobertson/easter
160
+
161
+ golden_number = (year % 19) + 1
162
+ if year <= 1752 then
163
+ # Julian calendar
164
+ dominical_number = (year + (year / 4) + 5) % 7
165
+ paschal_full_moon = (3 - (11 * golden_number) - 7) % 30
166
+ else
167
+ # Gregorian calendar
168
+ dominical_number = (year + (year / 4) - (year / 100) + (year / 400)) % 7
169
+ solar_correction = (year - 1600) / 100 - (year - 1600) / 400
170
+ lunar_correction = (((year - 1400) / 100) * 8) / 25
171
+ paschal_full_moon = (3 - 11 * golden_number + solar_correction - lunar_correction) % 30
172
+ end
173
+ dominical_number += 7 until dominical_number > 0
174
+ paschal_full_moon += 30 until paschal_full_moon > 0
175
+ paschal_full_moon -= 1 if paschal_full_moon == 29 or (paschal_full_moon == 28 and golden_number > 11)
176
+ difference = (4 - paschal_full_moon - dominical_number) % 7
177
+ difference += 7 if difference < 0
178
+ day_easter = paschal_full_moon + difference + 1
179
+ if day_easter < 11 then
180
+ # Easter occurs in March.
181
+ return Date.new(y=year, m=3, d=day_easter + 21)
182
+ else
183
+ # Easter occurs in April.
184
+ return Date.new(y=year, m=4, d=day_easter - 10)
185
+ end
186
+ end
187
+
188
+ def good_friday(year=nil)
189
+ return friday_before(easter_sunday(year))
190
+ end
191
+
192
+ def holy_saturday(year=nil)
193
+ return saturday_before(easter_sunday(year))
194
+ end
195
+
196
+ def pentecost(year=nil)
197
+ year ||= @year
198
+ return easter_sunday(year) + 7 * WEEK
199
+ end
200
+
201
+ def holy_trinity(year=nil)
202
+ sunday_after(pentecost(year))
203
+ end
204
+
205
+ def body_blood(year=nil)
206
+ thursday_after(holy_trinity(year))
207
+ end
208
+
209
+ def sacred_heart(year=nil)
210
+ friday_after(sunday_after(body_blood(year)))
211
+ end
212
+
213
+ def christ_king(year=nil)
214
+ year ||= @year
215
+ sunday_before(first_advent_sunday(year + 1))
216
+ end
217
+
218
+ # which liturgical season is it?
219
+ def season(date)
220
+ range_check date
221
+
222
+ if first_advent_sunday <= date and
223
+ nativity > date then
224
+ Seasons::ADVENT
225
+
226
+ elsif nativity <= date and
227
+ baptism_of_lord >= date then
228
+ Seasons::CHRISTMAS
229
+
230
+ elsif ash_wednesday <= date and
231
+ easter_sunday > date then
232
+ Seasons::LENT
233
+
234
+ elsif easter_sunday <= date and
235
+ pentecost >= date then
236
+ Seasons::EASTER
237
+
238
+ else
239
+ Seasons::ORDINARY
240
+ end
241
+ end
242
+
243
+ def season_beginning(s)
244
+ case s
245
+ when Seasons::ADVENT
246
+ first_advent_sunday
247
+ when Seasons::CHRISTMAS
248
+ nativity
249
+ when Seasons::LENT
250
+ ash_wednesday
251
+ when Seasons::EASTER
252
+ easter_sunday
253
+ else # ordinary time
254
+ monday_after(baptism_of_lord)
255
+ end
256
+ end
257
+
258
+ def season_week(seasonn, date)
259
+ week1_beginning = season_beginning = season_beginning(seasonn)
260
+ unless season_beginning.sunday?
261
+ week1_beginning = sunday_after(season_beginning)
262
+ end
263
+
264
+ week = date_difference(date, week1_beginning) / Temporale::WEEK + 1
265
+
266
+ if seasonn == Seasons::ORDINARY
267
+ # ordinary time does not begin with Sunday, but the first week
268
+ # is week 1, not 0
269
+ week += 1
270
+
271
+ if date > pentecost
272
+ # gap made by Lent and Easter time
273
+ week -= 12
274
+ end
275
+ end
276
+
277
+ return week
278
+ end
279
+
280
+ # returns a Celebration
281
+ # scheduled for the given day
282
+ #
283
+ # expected arguments: Date or two Integers (month, day)
284
+ def get(*args)
285
+ if args.size == 1 && args[0].is_a?(Date)
286
+ date = args[0]
287
+ else
288
+ month, day = args
289
+ date = Date.new @year, month, day
290
+ unless date_range.include? date
291
+ date = Date.new @year + 1, month, day
292
+ end
293
+ end
294
+
295
+ return solemnity(date) || sunday(date) || ferial(date)
296
+ end
297
+
298
+ private
299
+
300
+ # the celebration determination split in methods:
301
+
302
+ def solemnity(date)
303
+ if @solemnities.has_key?(date)
304
+ return @solemnities[date]
305
+ end
306
+
307
+ seas = season(date)
308
+ case seas
309
+ when Seasons::EASTER
310
+ if date <= sunday_after(easter_sunday)
311
+ return Celebration.new '', Ranks::PRIMARY, SEASON_COLOUR[seas]
312
+ end
313
+ end
314
+
315
+ return nil
316
+ end
317
+
318
+ def sunday(date)
319
+ return nil unless date.sunday?
320
+
321
+ seas = season date
322
+ rank = Ranks::SUNDAY_UNPRIVILEGED
323
+ if [Seasons::ADVENT, Seasons::LENT, Seasons::EASTER].include?(seas)
324
+ rank = Ranks::PRIMARY
325
+ end
326
+
327
+ return Celebration.new '', rank, SEASON_COLOUR[seas]
328
+ end
329
+
330
+ def ferial(date)
331
+ seas = season date
332
+ rank = Ranks::FERIAL
333
+ case seas
334
+ when Seasons::ADVENT
335
+ if date >= Date.new(@year, 12, 17)
336
+ rank = Ranks::FERIAL_PRIVILEGED
337
+ end
338
+ when Seasons::CHRISTMAS
339
+ if date < mother_of_god
340
+ rank = Ranks::FERIAL_PRIVILEGED
341
+ end
342
+ when Seasons::LENT
343
+ rank = Ranks::FERIAL_PRIVILEGED
344
+ end
345
+
346
+ return Celebration.new '', rank, SEASON_COLOUR[seas]
347
+ end
348
+
349
+ # helper: difference between two Dates in days
350
+ def date_difference(d1, d2)
351
+ return (d1 - d2).numerator
352
+ end
353
+
354
+ # prepare dates of temporale solemnities and their octaves
355
+ def prepare_solemnities
356
+ @solemnities = {}
357
+
358
+ {
359
+ nativity: ['The Nativity of the Lord', nil, nil],
360
+ holy_family: ['The Holy Family of Jesus, Mary and Joseph', Ranks::PRIMARY, nil],
361
+ epiphany: ['The Epiphany of the Lord', nil, nil],
362
+ baptism_of_lord: ['The Baptism of the Lord', Ranks::FEAST_LORD_GENERAL, nil],
363
+ good_friday: ['Friday of the Passion of the Lord', Ranks::TRIDUUM, Colours::RED],
364
+ holy_saturday: ['Holy Saturday', Ranks::TRIDUUM, nil],
365
+ easter_sunday: ['Easter Sunday of the Resurrection of the Lord', Ranks::TRIDUUM, nil],
366
+ pentecost: ['Pentecost Sunday', nil, Colours::RED],
367
+ holy_trinity: ['The Most Holy Trinity', Ranks::SOLEMNITY_GENERAL, Colours::WHITE],
368
+ body_blood: ['The Most Holy Body and Blood of Christ', Ranks::SOLEMNITY_GENERAL, Colours::WHITE],
369
+ sacred_heart: ['The Most Sacred Heart of Jesus', Ranks::SOLEMNITY_GENERAL, Colours::WHITE],
370
+ christ_king: ['Our Lord Jesus Christ, King of the Universe', Ranks::SOLEMNITY_GENERAL, Colours::WHITE],
371
+ }.each_pair do |method_name, data|
372
+ date = send(method_name)
373
+ title, rank, colour = data
374
+ @solemnities[date] = Celebration.new(
375
+ title,
376
+ rank || Ranks::PRIMARY,
377
+ colour || SEASON_COLOUR[season(date)]
378
+ )
379
+ end
380
+ end
381
+ end
382
+ end