calendarium-romanum 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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