calendarium-romanum 0.5.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.rubocop.yml +50 -0
- data/.travis.yml +23 -0
- data/.yardopts +3 -0
- data/Appraisals +67 -0
- data/CHANGELOG.md +488 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +95 -0
- data/README.md +601 -0
- data/Rakefile +27 -0
- data/bin/calendariumrom +3 -0
- data/calendarium-romanum.gemspec +31 -0
- data/config/locales/cs.yml +4 -0
- data/config/locales/en.yml +20 -14
- data/config/locales/es.yml +94 -0
- data/config/locales/fr.yml +6 -0
- data/config/locales/it.yml +6 -0
- data/config/locales/la.yml +6 -0
- data/config/locales/pt.yml +94 -0
- data/data/README.md +70 -24
- data/data/czech-brno-cs.txt +4 -6
- data/data/czech-budejovice-cs.txt +4 -6
- data/data/czech-cechy-cs.txt +4 -5
- data/data/czech-cs.txt +239 -235
- data/data/czech-hradec-cs.txt +3 -5
- data/data/czech-litomerice-cs.txt +5 -7
- data/data/czech-morava-cs.txt +4 -5
- data/data/czech-olomouc-cs.txt +2 -4
- data/data/czech-ostrava-cs.txt +3 -5
- data/data/czech-plzen-cs.txt +3 -5
- data/data/czech-praha-cs.txt +3 -4
- data/data/easter_dates.txt +67 -0
- data/data/universal-1969-la.txt +234 -0
- data/data/universal-en.txt +217 -211
- data/data/universal-es.txt +246 -0
- data/data/universal-fr.txt +217 -210
- data/data/universal-it.txt +217 -211
- data/data/universal-la.txt +217 -212
- data/data/universal-pt.txt +248 -0
- data/doc/data_readme.md +2 -0
- data/doc/images/class_diagram.png +0 -0
- data/doc/images/class_diagram.puml +44 -0
- data/doc/yard_readme.rdoc +76 -0
- data/lib/calendarium-romanum.rb +16 -2
- data/lib/calendarium-romanum/abstract_date.rb +15 -0
- data/lib/calendarium-romanum/calendar.rb +150 -33
- data/lib/calendarium-romanum/cli.rb +80 -100
- data/lib/calendarium-romanum/cli/comparator.rb +83 -0
- data/lib/calendarium-romanum/cli/date_parser.rb +30 -0
- data/lib/calendarium-romanum/cli/dumper.rb +68 -0
- data/lib/calendarium-romanum/cli/helper.rb +23 -0
- data/lib/calendarium-romanum/cli/querier.rb +73 -0
- data/lib/calendarium-romanum/cr.rb +16 -0
- data/lib/calendarium-romanum/data.rb +40 -8
- data/lib/calendarium-romanum/day.rb +187 -32
- data/lib/calendarium-romanum/enum.rb +41 -24
- data/lib/calendarium-romanum/enums.rb +127 -43
- data/lib/calendarium-romanum/errors.rb +1 -1
- data/lib/calendarium-romanum/ordinalizer.rb +10 -1
- data/lib/calendarium-romanum/perpetual_calendar.rb +58 -7
- data/lib/calendarium-romanum/rank.rb +39 -8
- data/lib/calendarium-romanum/rank_predicates.rb +43 -0
- data/lib/calendarium-romanum/sanctorale.rb +213 -23
- data/lib/calendarium-romanum/sanctorale_factory.rb +74 -3
- data/lib/calendarium-romanum/sanctorale_loader.rb +180 -0
- data/lib/calendarium-romanum/sanctorale_writer.rb +124 -0
- data/lib/calendarium-romanum/temporale.rb +222 -42
- data/lib/calendarium-romanum/temporale/celebration_factory.rb +68 -9
- data/lib/calendarium-romanum/temporale/date_helper.rb +85 -0
- data/lib/calendarium-romanum/temporale/dates.rb +52 -59
- data/lib/calendarium-romanum/temporale/easter_table.rb +27 -0
- data/lib/calendarium-romanum/temporale/extensions.rb +15 -0
- data/lib/calendarium-romanum/temporale/extensions/christ_eternal_priest.rb +16 -3
- data/lib/calendarium-romanum/temporale/extensions/dedication_before_all_saints.rb +73 -0
- data/lib/calendarium-romanum/transfers.rb +84 -24
- data/lib/calendarium-romanum/util.rb +21 -23
- data/lib/calendarium-romanum/version.rb +3 -2
- data/liturgical_law/1969_normae_universales.md +568 -0
- data/liturgical_law/1977_decretum_de_celebratione_baptismatis_domini.md +58 -0
- data/liturgical_law/1990_decretum_de_variatione_inducenda.md +67 -0
- data/liturgical_law/1998_notificatio_de_occurrentia.md +57 -0
- data/liturgical_law/2002_normae_universales.md +946 -0
- data/liturgical_law/2006_notification.md +37 -0
- data/liturgical_law/2012_declarationes.md +38 -0
- data/liturgical_law/2020_dubia_de_calendario_2022.md +100 -0
- data/liturgical_law/README.md +74 -0
- metadata +61 -38
- data/lib/calendarium-romanum/sanctoraleloader.rb +0 -122
- data/spec/abstract_date_spec.rb +0 -62
- data/spec/calendar_spec.rb +0 -559
- data/spec/celebration_factory_spec.rb +0 -16
- data/spec/celebration_spec.rb +0 -43
- data/spec/cli_spec.rb +0 -155
- data/spec/colour_spec.rb +0 -17
- data/spec/data_spec.rb +0 -23
- data/spec/date_parser_spec.rb +0 -68
- data/spec/date_spec.rb +0 -61
- data/spec/dates_spec.rb +0 -45
- data/spec/day_spec.rb +0 -108
- data/spec/enum_spec.rb +0 -51
- data/spec/i18n_spec.rb +0 -58
- data/spec/ordinalizer_spec.rb +0 -36
- data/spec/perpetual_calendar_spec.rb +0 -91
- data/spec/rank_spec.rb +0 -57
- data/spec/readme_spec.rb +0 -56
- data/spec/sanctorale_factory_spec.rb +0 -42
- data/spec/sanctorale_spec.rb +0 -191
- data/spec/sanctoraleloader_spec.rb +0 -176
- data/spec/season_spec.rb +0 -17
- data/spec/spec_helper.rb +0 -46
- data/spec/temporale_spec.rb +0 -572
@@ -0,0 +1,43 @@
|
|
1
|
+
module CalendariumRomanum
|
2
|
+
# Mixin providing rank-describing predicates.
|
3
|
+
# Expects the including class to have instance method +#rank+ returning a {Rank}.
|
4
|
+
module RankPredicates
|
5
|
+
# @return [Boolean]
|
6
|
+
def solemnity?
|
7
|
+
rank.priority.to_i == 1
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return [Boolean]
|
11
|
+
# @since 0.6.0
|
12
|
+
def sunday?
|
13
|
+
rank == Ranks::SUNDAY_UNPRIVILEGED
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Boolean]
|
17
|
+
def feast?
|
18
|
+
rank.priority.to_i == 2
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Boolean]
|
22
|
+
def memorial?
|
23
|
+
rank.priority.to_i == 3 && rank.priority <= 3.12
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Boolean]
|
27
|
+
def optional_memorial?
|
28
|
+
rank == Ranks::MEMORIAL_OPTIONAL
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Boolean]
|
32
|
+
def obligatory_memorial?
|
33
|
+
memorial? && !optional_memorial?
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean]
|
37
|
+
# @since 0.6.0
|
38
|
+
def ferial?
|
39
|
+
rank == Ranks::FERIAL ||
|
40
|
+
rank == Ranks::FERIAL_PRIVILEGED
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -1,27 +1,74 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module CalendariumRomanum
|
2
4
|
|
3
|
-
#
|
5
|
+
# One of the two main {Calendar} components.
|
6
|
+
# Contains celebrations with fixed date, mostly feasts of saints.
|
7
|
+
#
|
8
|
+
# Basically a mapping {AbstractDate} => Array<{Celebration}>
|
9
|
+
# additionally enforcing some constraints:
|
10
|
+
#
|
11
|
+
# - for a given {AbstractDate} there may be multiple {Celebration}s,
|
12
|
+
# but only if all of them are in the rank of an optional
|
13
|
+
# memorial
|
14
|
+
# - {Celebration#symbol} must be unique in the whole set of
|
15
|
+
# contained celebrations
|
4
16
|
class Sanctorale
|
5
17
|
|
6
18
|
def initialize
|
7
19
|
@days = {}
|
8
|
-
|
9
20
|
@solemnities = {}
|
21
|
+
@symbols = Set.new
|
22
|
+
@metadata = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# @!method dup
|
26
|
+
# Returns a copy of the receiver with properly copied internal
|
27
|
+
# data structures, i.e. a copy which can be safely modified without
|
28
|
+
# danger of unintentionally modifying the original instance.
|
29
|
+
# @return [Sanctorale]
|
30
|
+
|
31
|
+
# @api private
|
32
|
+
def initialize_dup(other)
|
33
|
+
@days = other.days.dup
|
34
|
+
@solemnities = other.solemnities.dup
|
35
|
+
@symbols = other.symbols.dup
|
36
|
+
@metadata = Marshal.load Marshal.dump other.metadata # deep copy
|
10
37
|
end
|
11
38
|
|
39
|
+
# Content subset - only {Celebration}s in the rank(s) of solemnity.
|
40
|
+
#
|
41
|
+
# @return [Hash<AbstractDate=>Celebration>]
|
12
42
|
attr_reader :solemnities
|
13
43
|
|
44
|
+
# Sanctorale metadata.
|
45
|
+
#
|
46
|
+
# Data files may contain YAML front matter.
|
47
|
+
# If provided, it's loaded by {SanctoraleLoader} and
|
48
|
+
# stored in this property.
|
49
|
+
# All data files bundled in the gem (see {Data}) have YAML
|
50
|
+
# front matter which is a Hash with a few standardized keys.
|
51
|
+
# While YAML also supports top-level content of other types,
|
52
|
+
# sanctorale data authors should stick to the convention
|
53
|
+
# of using Hash as the top-level data structure of their
|
54
|
+
# front matters.
|
55
|
+
#
|
56
|
+
# @return [Hash, nil]
|
57
|
+
# @since 0.7.0
|
58
|
+
attr_accessor :metadata
|
59
|
+
|
60
|
+
# Adds a new {Celebration}
|
61
|
+
#
|
62
|
+
# @param month [Integer]
|
63
|
+
# @param day [Integer]
|
64
|
+
# @param celebration [Celebration]
|
65
|
+
# @return [void]
|
66
|
+
# @raise [ArgumentError]
|
67
|
+
# when performing the operation would break the object's invariant
|
14
68
|
def add(month, day, celebration)
|
15
69
|
date = AbstractDate.new(month, day)
|
16
|
-
unless @days.has_key? date
|
17
|
-
@days[date] = []
|
18
|
-
end
|
19
70
|
|
20
|
-
|
21
|
-
@solemnities[date] = celebration
|
22
|
-
end
|
23
|
-
|
24
|
-
unless @days[date].empty?
|
71
|
+
unless @days[date].nil? || @days[date].empty?
|
25
72
|
present = @days[date][0]
|
26
73
|
if present.rank != Ranks::MEMORIAL_OPTIONAL
|
27
74
|
raise ArgumentError.new("On #{date} there is already a #{present.rank}. No more celebrations can be added.")
|
@@ -30,13 +77,54 @@ module CalendariumRomanum
|
|
30
77
|
end
|
31
78
|
end
|
32
79
|
|
80
|
+
unless celebration.symbol.nil?
|
81
|
+
if @symbols.include? celebration.symbol
|
82
|
+
raise ArgumentError.new("Attempted to add Celebration with duplicate symbol #{celebration.symbol.inspect}")
|
83
|
+
end
|
84
|
+
|
85
|
+
@symbols << celebration.symbol
|
86
|
+
end
|
87
|
+
|
88
|
+
unless @days.has_key? date
|
89
|
+
@days[date] = []
|
90
|
+
end
|
91
|
+
|
92
|
+
if celebration.solemnity?
|
93
|
+
@solemnities[date] = celebration
|
94
|
+
end
|
95
|
+
|
33
96
|
@days[date] << celebration
|
34
97
|
end
|
35
98
|
|
36
|
-
#
|
37
|
-
|
99
|
+
# Replaces content of the given day by given {Celebration}s
|
100
|
+
#
|
101
|
+
# @param month [Integer]
|
102
|
+
# @param day [Integer]
|
103
|
+
# @param celebrations [Array<Celebration>]
|
104
|
+
# @param symbol_uniqueness [true|false]
|
105
|
+
# allows disabling symbol uniqueness check.
|
106
|
+
# Internal feature, not intended for use by client code.
|
107
|
+
# @return [void]
|
108
|
+
# @raise [ArgumentError]
|
109
|
+
# when performing the operation would break the object's invariant
|
110
|
+
def replace(month, day, celebrations, symbol_uniqueness: true)
|
38
111
|
date = AbstractDate.new(month, day)
|
39
112
|
|
113
|
+
symbols_without_day = @symbols
|
114
|
+
unless @days[date].nil?
|
115
|
+
old_symbols = @days[date].collect(&:symbol).compact
|
116
|
+
symbols_without_day = @symbols - old_symbols
|
117
|
+
end
|
118
|
+
|
119
|
+
new_symbols = celebrations.collect(&:symbol).compact
|
120
|
+
duplicate = symbols_without_day.intersection new_symbols
|
121
|
+
if symbol_uniqueness && !duplicate.empty?
|
122
|
+
raise ArgumentError.new("Attempted to add Celebrations with duplicate symbols #{duplicate.to_a.inspect}")
|
123
|
+
end
|
124
|
+
|
125
|
+
@symbols = symbols_without_day
|
126
|
+
@symbols.merge new_symbols
|
127
|
+
|
40
128
|
if celebrations.first.solemnity?
|
41
129
|
@solemnities[date] = celebrations.first
|
42
130
|
elsif @solemnities.has_key? date
|
@@ -46,17 +134,48 @@ module CalendariumRomanum
|
|
46
134
|
@days[date] = celebrations.dup
|
47
135
|
end
|
48
136
|
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
137
|
+
# Updates the receiver with {Celebration}s from another instance.
|
138
|
+
#
|
139
|
+
# For each date contained in +other+ the content of +self+
|
140
|
+
# is _replaced_ by that of +other+.
|
141
|
+
#
|
142
|
+
# @param other [Sanctorale]
|
143
|
+
# @return [void]
|
144
|
+
# @raise (see #replace)
|
145
|
+
def update(other)
|
146
|
+
other.each_day do |date, celebrations|
|
147
|
+
replace date.month, date.day, celebrations, symbol_uniqueness: false
|
53
148
|
end
|
149
|
+
rebuild_symbols
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns a new copy containing {Celebration}s from both self and +other+.
|
153
|
+
#
|
154
|
+
# @param other [Sanctorale]
|
155
|
+
# @return [Sanctorale]
|
156
|
+
# @since 0.9.0
|
157
|
+
def merge(other)
|
158
|
+
dup.tap {|dupped| dupped.update other }
|
54
159
|
end
|
55
160
|
|
56
|
-
#
|
57
|
-
# scheduled for the given day
|
161
|
+
# Retrieves {Celebration}s for the given date
|
58
162
|
#
|
59
|
-
#
|
163
|
+
# @param date [AbstractDate, Date]
|
164
|
+
# @return [Array<Celebration>] (may be empty)
|
165
|
+
# @since 0.6.0
|
166
|
+
def [](date)
|
167
|
+
adate = date.is_a?(AbstractDate) ? date : AbstractDate.from_date(date)
|
168
|
+
@days[adate] || []
|
169
|
+
end
|
170
|
+
|
171
|
+
# Retrieves {Celebration}s for the given date
|
172
|
+
#
|
173
|
+
# @overload get(date)
|
174
|
+
# @param date[AbstractDate, Date]
|
175
|
+
# @overload get(month, day)
|
176
|
+
# @param month [Integer]
|
177
|
+
# @param day [Integer]
|
178
|
+
# @return (see #[])
|
60
179
|
def get(*args)
|
61
180
|
if args.size == 1 && args[0].is_a?(Date)
|
62
181
|
month = args[0].month
|
@@ -66,31 +185,102 @@ module CalendariumRomanum
|
|
66
185
|
end
|
67
186
|
|
68
187
|
date = AbstractDate.new(month, day)
|
69
|
-
|
188
|
+
self[date]
|
70
189
|
end
|
71
190
|
|
72
|
-
#
|
73
|
-
#
|
191
|
+
# Enumerates dates for which any {Celebration}s are available
|
192
|
+
#
|
193
|
+
# @yield [AbstractDate, Array<Celebration>] the array is never empty
|
194
|
+
# @return [void, Enumerator] if called without a block, returns +Enumerator+
|
74
195
|
def each_day
|
196
|
+
return to_enum(__method__) unless block_given?
|
197
|
+
|
75
198
|
@days.each_pair do |date, celebrations|
|
76
199
|
yield date, celebrations
|
77
200
|
end
|
78
201
|
end
|
79
202
|
|
80
|
-
#
|
203
|
+
# Returns count of _days_ with {Celebration}s filled
|
204
|
+
#
|
205
|
+
# @return [Integer]
|
81
206
|
def size
|
82
207
|
@days.size
|
83
208
|
end
|
84
209
|
|
210
|
+
# It is empty if it doesn't contain any {Celebration}
|
211
|
+
#
|
212
|
+
# @return [Boolean]
|
85
213
|
def empty?
|
86
214
|
@days.empty?
|
87
215
|
end
|
88
216
|
|
217
|
+
# Freezes the instance
|
89
218
|
def freeze
|
90
219
|
@days.freeze
|
91
220
|
@days.values.each(&:freeze)
|
92
221
|
@solemnities.freeze
|
93
222
|
super
|
94
223
|
end
|
224
|
+
|
225
|
+
# @return [Boolean]
|
226
|
+
# @since 0.6.0
|
227
|
+
def ==(b)
|
228
|
+
self.class == b.class &&
|
229
|
+
days == b.days
|
230
|
+
end
|
231
|
+
|
232
|
+
# Does this instance provide celebration identified by symbol +symbol+?
|
233
|
+
#
|
234
|
+
# @param symbol [Symbol]
|
235
|
+
# @return [Boolean]
|
236
|
+
# @since 0.9.0
|
237
|
+
def provides_celebration?(symbol)
|
238
|
+
@symbols.include? symbol
|
239
|
+
end
|
240
|
+
|
241
|
+
# If the instance contains a {Celebration} identified by the specified symbol,
|
242
|
+
# returns it's date and the {Celebration} itself, nil otherwise.
|
243
|
+
#
|
244
|
+
# @param symbol [Symbol]
|
245
|
+
# @return [Array<AbstractDate, Celebration>, nil]
|
246
|
+
# @since 0.9.0
|
247
|
+
def by_symbol(symbol)
|
248
|
+
return nil unless provides_celebration? symbol
|
249
|
+
|
250
|
+
@days.each_pair do |date, celebrations|
|
251
|
+
found = celebrations.find {|c| c.symbol == symbol }
|
252
|
+
return [date, found] if found
|
253
|
+
end
|
254
|
+
|
255
|
+
# reaching this point would mean that contents of @symbols and @days are not consistent
|
256
|
+
raise 'this point should never be reached'
|
257
|
+
end
|
258
|
+
|
259
|
+
protected
|
260
|
+
|
261
|
+
attr_reader :days, :symbols
|
262
|
+
|
263
|
+
# Builds the registry of celebration symbols anew,
|
264
|
+
# raises error if any duplicates are found.
|
265
|
+
def rebuild_symbols
|
266
|
+
@symbols = Set.new
|
267
|
+
duplicates = []
|
268
|
+
|
269
|
+
@days.each_pair do |date,celebrations|
|
270
|
+
celebrations.each do |celebration|
|
271
|
+
if @symbols.include?(celebration.symbol) &&
|
272
|
+
!duplicates.include?(celebration.symbol) &&
|
273
|
+
!celebration.symbol.nil?
|
274
|
+
duplicates << celebration.symbol
|
275
|
+
end
|
276
|
+
|
277
|
+
@symbols << celebration.symbol
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
unless duplicates.empty?
|
282
|
+
raise ArgumentError.new("Duplicate celebration symbols: #{duplicates.inspect}")
|
283
|
+
end
|
284
|
+
end
|
95
285
|
end
|
96
286
|
end
|
@@ -1,15 +1,50 @@
|
|
1
1
|
module CalendariumRomanum
|
2
|
-
#
|
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
|
-
#
|
7
|
+
# Takes several {Sanctorale} instances, returns a new one,
|
8
|
+
# created 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
|
-
#
|
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
|
+
# 'data/czech-cs.txt',
|
46
|
+
# 'data/czech-cechy-cs.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.is_a?(Hash) && 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,180 @@
|
|
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
|
+
# of the data directory, For a complete example see e.g.
|
11
|
+
# {file:universal-en.txt the file describing General Roman Calendar}.
|
12
|
+
class SanctoraleLoader
|
13
|
+
|
14
|
+
# @api private
|
15
|
+
RANK_CODES = {
|
16
|
+
nil => Ranks::MEMORIAL_OPTIONAL, # default
|
17
|
+
'm' => Ranks::MEMORIAL_GENERAL,
|
18
|
+
'mp' => Ranks::MEMORIAL_PROPER,
|
19
|
+
'f' => Ranks::FEAST_GENERAL,
|
20
|
+
'fl' => Ranks::FEAST_LORD_GENERAL,
|
21
|
+
'fp' => Ranks::FEAST_PROPER,
|
22
|
+
's' => Ranks::SOLEMNITY_GENERAL,
|
23
|
+
'sp' => Ranks::SOLEMNITY_PROPER,
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
COLOUR_CODES = {
|
28
|
+
nil => Colours::WHITE, # default
|
29
|
+
'w' => Colours::WHITE,
|
30
|
+
'v' => Colours::VIOLET,
|
31
|
+
'g' => Colours::GREEN,
|
32
|
+
'r' => Colours::RED
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
# Load from an object which understands +#each_line+
|
36
|
+
#
|
37
|
+
# @param src [String, File, #each_line]
|
38
|
+
# source of the loaded data
|
39
|
+
# @param dest [Sanctorale, nil]
|
40
|
+
# objects to populate. If not provided, a new {Sanctorale}
|
41
|
+
# instance will be created
|
42
|
+
# @return [Sanctorale]
|
43
|
+
# @raise [InvalidDataError]
|
44
|
+
def load(src, dest = nil)
|
45
|
+
dest ||= Sanctorale.new
|
46
|
+
|
47
|
+
in_front_matter = false
|
48
|
+
front_matter = ''
|
49
|
+
month_section = nil
|
50
|
+
src.each_line.with_index(1) do |l, line_num|
|
51
|
+
# skip YAML front matter
|
52
|
+
if line_num == 1 && l.start_with?('---')
|
53
|
+
in_front_matter = true
|
54
|
+
front_matter += l
|
55
|
+
next
|
56
|
+
elsif in_front_matter
|
57
|
+
if l.start_with?('---')
|
58
|
+
in_front_matter = false
|
59
|
+
dest.metadata = YAML.load(front_matter).freeze
|
60
|
+
end
|
61
|
+
|
62
|
+
front_matter += l
|
63
|
+
|
64
|
+
next
|
65
|
+
end
|
66
|
+
|
67
|
+
# strip whitespace and comments
|
68
|
+
l.sub!(/#.*/, '')
|
69
|
+
l.strip!
|
70
|
+
next if l.empty?
|
71
|
+
|
72
|
+
# month section heading
|
73
|
+
n = l.match(/^=\s*(\d+)\s*$/)
|
74
|
+
unless n.nil?
|
75
|
+
month_section = n[1].to_i
|
76
|
+
unless month_section >= 1 && month_section <= 12
|
77
|
+
raise error("Invalid month #{month_section}", line_num)
|
78
|
+
end
|
79
|
+
next
|
80
|
+
end
|
81
|
+
|
82
|
+
begin
|
83
|
+
celebration = load_line l, month_section
|
84
|
+
rescue RangeError, RuntimeError => err
|
85
|
+
raise error(err.message, line_num)
|
86
|
+
end
|
87
|
+
|
88
|
+
dest.add(
|
89
|
+
celebration.date.month,
|
90
|
+
celebration.date.day,
|
91
|
+
celebration
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
dest
|
96
|
+
end
|
97
|
+
|
98
|
+
alias load_from_string load
|
99
|
+
|
100
|
+
# Load from a filesystem path
|
101
|
+
#
|
102
|
+
# @param filename [String]
|
103
|
+
# @param dest [Sanctorale, nil]
|
104
|
+
# @param encoding [String]
|
105
|
+
# @return (see #load)
|
106
|
+
# @raise (see #load)
|
107
|
+
def load_from_file(filename, dest = nil, encoding = 'utf-8')
|
108
|
+
load File.open(filename, 'r', encoding: encoding), dest
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def line_regexp
|
114
|
+
@line_regexp ||=
|
115
|
+
begin
|
116
|
+
rank_letters = RANK_CODES.keys.compact.join('|')
|
117
|
+
colour_letters = COLOUR_CODES.keys.compact.join('')
|
118
|
+
|
119
|
+
Regexp.new(
|
120
|
+
'^((?<month>\d+)\/)?(?<day>\d+)' + # date
|
121
|
+
'(\s+(?<rank_char>' + rank_letters + ')?(?<rank_num>\d\.\d{1,2})?)?' + # rank (optional)
|
122
|
+
'(\s+(?<colour>[' + colour_letters + ']))?' + # colour (optional)
|
123
|
+
'(\s+(?<symbol>[\w]{2,}))?' + # symbol (optional)
|
124
|
+
'\s*:(?<title>.*)$', # title
|
125
|
+
Regexp::IGNORECASE
|
126
|
+
)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# parses a line containing celebration record,
|
131
|
+
# returns a single Celebration
|
132
|
+
def load_line(line, month_section = nil)
|
133
|
+
# celebration record
|
134
|
+
m = line.match(line_regexp)
|
135
|
+
if m.nil?
|
136
|
+
raise RuntimeError.new("Syntax error, line skipped '#{line}'")
|
137
|
+
end
|
138
|
+
|
139
|
+
month = (m[:month] || month_section).to_i
|
140
|
+
day = m[:day].to_i
|
141
|
+
rank_char = m[:rank_char]
|
142
|
+
rank_num = m[:rank_num]
|
143
|
+
colour = m[:colour]
|
144
|
+
symbol_str = m[:symbol]
|
145
|
+
title = m[:title]
|
146
|
+
|
147
|
+
rank = RANK_CODES[rank_char && rank_char.downcase]
|
148
|
+
|
149
|
+
if rank_num
|
150
|
+
rank_num = rank_num.to_f
|
151
|
+
rank_by_num = Ranks[rank_num]
|
152
|
+
|
153
|
+
if rank_by_num.nil?
|
154
|
+
raise RuntimeError.new("Invalid celebration rank code #{rank_num}")
|
155
|
+
elsif rank_char && (rank.priority.to_i != rank_by_num.priority.to_i)
|
156
|
+
raise RuntimeError.new("Invalid combination of rank letter #{rank_char.inspect} and number #{rank_num}.")
|
157
|
+
end
|
158
|
+
|
159
|
+
rank = rank_by_num
|
160
|
+
end
|
161
|
+
|
162
|
+
symbol = nil
|
163
|
+
if symbol_str
|
164
|
+
symbol = symbol_str.to_sym
|
165
|
+
end
|
166
|
+
|
167
|
+
Celebration.new(
|
168
|
+
title.strip,
|
169
|
+
rank,
|
170
|
+
COLOUR_CODES[colour && colour.downcase],
|
171
|
+
symbol,
|
172
|
+
AbstractDate.new(month, day)
|
173
|
+
)
|
174
|
+
end
|
175
|
+
|
176
|
+
def error(message, line_number)
|
177
|
+
InvalidDataError.new("L#{line_number}: #{message}")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|