calendarium-romanum 0.4.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.rubocop.yml +47 -0
- data/.travis.yml +22 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +431 -0
- data/Gemfile +25 -0
- data/Gemfile.lock +86 -0
- data/README.md +598 -0
- data/Rakefile +16 -0
- data/bin/calendariumrom +4 -1
- data/calendarium-romanum.gemspec +31 -0
- data/config/locales/cs.yml +5 -0
- data/config/locales/en.yml +21 -14
- data/config/locales/es.yml +94 -0
- data/config/locales/fr.yml +7 -0
- data/config/locales/it.yml +7 -0
- data/config/locales/la.yml +7 -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 +236 -234
- 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 +214 -211
- data/data/universal-es.txt +243 -0
- data/data/universal-fr.txt +214 -210
- data/data/universal-it.txt +214 -211
- data/data/universal-la.txt +214 -210
- 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 +35 -22
- data/lib/calendarium-romanum/abstract_date.rb +15 -0
- data/lib/calendarium-romanum/calendar.rb +207 -42
- data/lib/calendarium-romanum/cli.rb +63 -80
- data/lib/calendarium-romanum/cli/comparator.rb +63 -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 +50 -20
- data/lib/calendarium-romanum/day.rb +208 -32
- data/lib/calendarium-romanum/enum.rb +42 -25
- data/lib/calendarium-romanum/enums.rb +124 -44
- data/lib/calendarium-romanum/errors.rb +4 -0
- data/lib/calendarium-romanum/ordinalizer.rb +23 -2
- data/lib/calendarium-romanum/perpetual_calendar.rb +58 -7
- data/lib/calendarium-romanum/rank.rb +43 -12
- data/lib/calendarium-romanum/rank_predicates.rb +43 -0
- data/lib/calendarium-romanum/sanctorale.rb +164 -24
- 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 +119 -0
- data/lib/calendarium-romanum/temporale.rb +226 -94
- data/lib/calendarium-romanum/temporale/celebration_factory.rb +107 -0
- data/lib/calendarium-romanum/temporale/dates.rb +84 -16
- 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 +60 -15
- data/lib/calendarium-romanum/util.rb +22 -3
- data/lib/calendarium-romanum/version.rb +5 -1
- 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/README.md +74 -0
- metadata +50 -28
- data/lib/calendarium-romanum/sanctoraleloader.rb +0 -115
- data/spec/abstract_date_spec.rb +0 -62
- data/spec/calendar_spec.rb +0 -330
- data/spec/celebration_spec.rb +0 -23
- data/spec/cli_spec.rb +0 -26
- data/spec/colour_spec.rb +0 -17
- data/spec/data_spec.rb +0 -23
- data/spec/date_spec.rb +0 -61
- data/spec/dates_spec.rb +0 -45
- data/spec/day_spec.rb +0 -59
- data/spec/enum_spec.rb +0 -51
- data/spec/i18n_spec.rb +0 -59
- data/spec/ordinalizer_spec.rb +0 -22
- data/spec/perpetual_calendar_spec.rb +0 -91
- data/spec/rank_spec.rb +0 -57
- data/spec/readme_spec.rb +0 -52
- data/spec/sanctorale_factory_spec.rb +0 -42
- data/spec/sanctorale_spec.rb +0 -191
- data/spec/sanctoraleloader_spec.rb +0 -171
- data/spec/season_spec.rb +0 -17
- data/spec/spec_helper.rb +0 -35
- data/spec/temporale_spec.rb +0 -519
@@ -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,60 @@
|
|
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
|
10
23
|
end
|
11
24
|
|
25
|
+
# Content subset - only {Celebration}s in the rank(s) of solemnity.
|
26
|
+
#
|
27
|
+
# @return [Hash<AbstractDate=>Celebration>]
|
12
28
|
attr_reader :solemnities
|
13
29
|
|
30
|
+
# Sanctorale metadata.
|
31
|
+
#
|
32
|
+
# Data files may contain YAML front matter.
|
33
|
+
# If provided, it's loaded by {SanctoraleLoader} and
|
34
|
+
# stored in this property.
|
35
|
+
# All data files bundled in the gem (see {Data}) have YAML
|
36
|
+
# front matter which is a Hash with a few standardized keys.
|
37
|
+
# While YAML also supports top-level content of other types,
|
38
|
+
# sanctorale data authors should stick to the convention
|
39
|
+
# of using Hash as the top-level data structure of their
|
40
|
+
# front matters.
|
41
|
+
#
|
42
|
+
# @return [Hash, nil]
|
43
|
+
# @since 0.7.0
|
44
|
+
attr_accessor :metadata
|
45
|
+
|
46
|
+
# Adds a new {Celebration}
|
47
|
+
#
|
48
|
+
# @param month [Integer]
|
49
|
+
# @param day [Integer]
|
50
|
+
# @param celebration [Celebration]
|
51
|
+
# @return [void]
|
52
|
+
# @raise [ArgumentError]
|
53
|
+
# when performing the operation would break the object's invariant
|
14
54
|
def add(month, day, celebration)
|
15
55
|
date = AbstractDate.new(month, day)
|
16
|
-
unless @days.has_key? date
|
17
|
-
@days[date] = []
|
18
|
-
end
|
19
|
-
|
20
|
-
if celebration.solemnity?
|
21
|
-
@solemnities[date] = celebration
|
22
|
-
end
|
23
56
|
|
24
|
-
unless @days[date].empty?
|
57
|
+
unless @days[date].nil? || @days[date].empty?
|
25
58
|
present = @days[date][0]
|
26
59
|
if present.rank != Ranks::MEMORIAL_OPTIONAL
|
27
60
|
raise ArgumentError.new("On #{date} there is already a #{present.rank}. No more celebrations can be added.")
|
@@ -30,13 +63,54 @@ module CalendariumRomanum
|
|
30
63
|
end
|
31
64
|
end
|
32
65
|
|
66
|
+
unless celebration.symbol.nil?
|
67
|
+
if @symbols.include? celebration.symbol
|
68
|
+
raise ArgumentError.new("Attempted to add Celebration with duplicate symbol #{celebration.symbol.inspect}")
|
69
|
+
end
|
70
|
+
|
71
|
+
@symbols << celebration.symbol
|
72
|
+
end
|
73
|
+
|
74
|
+
unless @days.has_key? date
|
75
|
+
@days[date] = []
|
76
|
+
end
|
77
|
+
|
78
|
+
if celebration.solemnity?
|
79
|
+
@solemnities[date] = celebration
|
80
|
+
end
|
81
|
+
|
33
82
|
@days[date] << celebration
|
34
83
|
end
|
35
84
|
|
36
|
-
#
|
37
|
-
|
85
|
+
# Replaces content of the given day by given {Celebration}s
|
86
|
+
#
|
87
|
+
# @param month [Integer]
|
88
|
+
# @param day [Integer]
|
89
|
+
# @param celebrations [Array<Celebration>]
|
90
|
+
# @param symbol_uniqueness [true|false]
|
91
|
+
# allows disabling symbol uniqueness check.
|
92
|
+
# Internal feature, not intended for use by client code.
|
93
|
+
# @return [void]
|
94
|
+
# @raise [ArgumentError]
|
95
|
+
# when performing the operation would break the object's invariant
|
96
|
+
def replace(month, day, celebrations, symbol_uniqueness: true)
|
38
97
|
date = AbstractDate.new(month, day)
|
39
98
|
|
99
|
+
symbols_without_day = @symbols
|
100
|
+
unless @days[date].nil?
|
101
|
+
old_symbols = @days[date].collect(&:symbol).compact
|
102
|
+
symbols_without_day = @symbols - old_symbols
|
103
|
+
end
|
104
|
+
|
105
|
+
new_symbols = celebrations.collect(&:symbol).compact
|
106
|
+
duplicate = symbols_without_day.intersection new_symbols
|
107
|
+
if symbol_uniqueness && !duplicate.empty?
|
108
|
+
raise ArgumentError.new("Attempted to add Celebrations with duplicate symbols #{duplicate.to_a.inspect}")
|
109
|
+
end
|
110
|
+
|
111
|
+
@symbols = symbols_without_day
|
112
|
+
@symbols.merge new_symbols
|
113
|
+
|
40
114
|
if celebrations.first.solemnity?
|
41
115
|
@solemnities[date] = celebrations.first
|
42
116
|
elsif @solemnities.has_key? date
|
@@ -46,17 +120,39 @@ module CalendariumRomanum
|
|
46
120
|
@days[date] = celebrations.dup
|
47
121
|
end
|
48
122
|
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
123
|
+
# Updates the receiver with {Celebration}s from another instance.
|
124
|
+
#
|
125
|
+
# For each date contained in +other+ the content of +self+
|
126
|
+
# is _replaced_ by that of +other+.
|
127
|
+
#
|
128
|
+
# @param other [Sanctorale]
|
129
|
+
# @return [void]
|
130
|
+
# @raise (see #replace)
|
131
|
+
def update(other)
|
132
|
+
other.each_day do |date, celebrations|
|
133
|
+
replace date.month, date.day, celebrations, symbol_uniqueness: false
|
53
134
|
end
|
135
|
+
rebuild_symbols
|
54
136
|
end
|
55
137
|
|
56
|
-
#
|
57
|
-
# scheduled for the given day
|
138
|
+
# Retrieves {Celebration}s for the given date
|
58
139
|
#
|
59
|
-
#
|
140
|
+
# @param date [AbstractDate, Date]
|
141
|
+
# @return [Array<Celebration>] (may be empty)
|
142
|
+
# @since 0.6.0
|
143
|
+
def [](date)
|
144
|
+
adate = date.is_a?(AbstractDate) ? date : AbstractDate.from_date(date)
|
145
|
+
@days[adate] || []
|
146
|
+
end
|
147
|
+
|
148
|
+
# Retrieves {Celebration}s for the given date
|
149
|
+
#
|
150
|
+
# @overload get(date)
|
151
|
+
# @param date[AbstractDate, Date]
|
152
|
+
# @overload get(month, day)
|
153
|
+
# @param month [Integer]
|
154
|
+
# @param day [Integer]
|
155
|
+
# @return (see #[])
|
60
156
|
def get(*args)
|
61
157
|
if args.size == 1 && args[0].is_a?(Date)
|
62
158
|
month = args[0].month
|
@@ -66,31 +162,75 @@ module CalendariumRomanum
|
|
66
162
|
end
|
67
163
|
|
68
164
|
date = AbstractDate.new(month, day)
|
69
|
-
|
165
|
+
self[date]
|
70
166
|
end
|
71
167
|
|
72
|
-
#
|
73
|
-
#
|
168
|
+
# Enumerates dates for which any {Celebration}s are available
|
169
|
+
#
|
170
|
+
# @yield [AbstractDate, Array<Celebration>] the array is never empty
|
171
|
+
# @return [void, Enumerator] if called without a block, returns +Enumerator+
|
74
172
|
def each_day
|
173
|
+
return to_enum(__method__) unless block_given?
|
174
|
+
|
75
175
|
@days.each_pair do |date, celebrations|
|
76
176
|
yield date, celebrations
|
77
177
|
end
|
78
178
|
end
|
79
179
|
|
80
|
-
#
|
180
|
+
# Returns count of _days_ with {Celebration}s filled
|
181
|
+
#
|
182
|
+
# @return [Integer]
|
81
183
|
def size
|
82
184
|
@days.size
|
83
185
|
end
|
84
186
|
|
187
|
+
# It is empty if it doesn't contain any {Celebration}
|
188
|
+
#
|
189
|
+
# @return [Boolean]
|
85
190
|
def empty?
|
86
191
|
@days.empty?
|
87
192
|
end
|
88
193
|
|
194
|
+
# Freezes the instance
|
89
195
|
def freeze
|
90
196
|
@days.freeze
|
91
|
-
@days.values.each
|
197
|
+
@days.values.each(&:freeze)
|
92
198
|
@solemnities.freeze
|
93
199
|
super
|
94
200
|
end
|
201
|
+
|
202
|
+
# @return [Boolean]
|
203
|
+
# @since 0.6.0
|
204
|
+
def ==(b)
|
205
|
+
self.class == b.class &&
|
206
|
+
days == b.days
|
207
|
+
end
|
208
|
+
|
209
|
+
protected
|
210
|
+
|
211
|
+
attr_reader :days
|
212
|
+
|
213
|
+
# Builds the registry of celebration symbols anew,
|
214
|
+
# raises error if any duplicates are found.
|
215
|
+
def rebuild_symbols
|
216
|
+
@symbols = Set.new
|
217
|
+
duplicates = []
|
218
|
+
|
219
|
+
@days.each_pair do |date,celebrations|
|
220
|
+
celebrations.each do |celebration|
|
221
|
+
if @symbols.include?(celebration.symbol) &&
|
222
|
+
!duplicates.include?(celebration.symbol) &&
|
223
|
+
!celebration.symbol.nil?
|
224
|
+
duplicates << celebration.symbol
|
225
|
+
end
|
226
|
+
|
227
|
+
@symbols << celebration.symbol
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
unless duplicates.empty?
|
232
|
+
raise ArgumentError.new("Duplicate celebration symbols: #{duplicates.inspect}")
|
233
|
+
end
|
234
|
+
end
|
95
235
|
end
|
96
236
|
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
|
+
# '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.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
|