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
@@ -0,0 +1,4 @@
1
+ module CalendariumRomanum
2
+ # Thrown by {SanctoraleLoader} on attempt to load invalid data
3
+ class InvalidDataError < RuntimeError; end
4
+ end
@@ -0,0 +1,61 @@
1
+ require 'roman-numerals'
2
+
3
+ module CalendariumRomanum
4
+ # Knows how to produce localized ordinals.
5
+ #
6
+ # Used by {Temporale} for building names of Sundays and ferials.
7
+ class Ordinalizer
8
+ class << self
9
+ # @param number [Fixnum] number to build ordinal for
10
+ # @param locale [Symbol,nil]
11
+ # locale; +I18n.locale+ (i.e. the `i18n` gem's current locale)
12
+ # is used if not provided
13
+ # @return [String, Fixnum]
14
+ # ordinal, or unchanged +number+ if +Ordinalizer+ cannot
15
+ # build ordinals for the given locale
16
+ def ordinal(number, locale: nil)
17
+ locale ||= I18n.locale
18
+
19
+ case locale
20
+ when :cs
21
+ "#{number}."
22
+ when :en
23
+ english_ordinal(number)
24
+ when :fr
25
+ french_ordinal(number)
26
+ when :la, :it
27
+ RomanNumerals.to_roman number
28
+ else
29
+ number
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def english_ordinal(number)
36
+ modulo = number % 10
37
+ modulo = 9 if number / 10 == 1
38
+
39
+ case modulo
40
+ when 1
41
+ "#{number}st"
42
+ when 2
43
+ "#{number}nd"
44
+ when 3
45
+ "#{number}rd"
46
+ else
47
+ "#{number}th"
48
+ end
49
+ end
50
+
51
+ def french_ordinal(number)
52
+ case number
53
+ when 1
54
+ '1er'
55
+ else
56
+ "#{number}ème"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,97 @@
1
+ module CalendariumRomanum
2
+ # Has mostly the same public interface as {Calendar},
3
+ # but represents a "perpetual" calendar, not a calendar
4
+ # for a single year, thus allowing the client code
5
+ # to query for liturgical data of any day, without bothering
6
+ # about boundaries of liturgical years.
7
+ #
8
+ # Internally builds {Calendar} instances as needed
9
+ # and delegates method calls to them.
10
+ #
11
+ # @since 0.4.0
12
+ class PerpetualCalendar
13
+ # @param sanctorale [Sanctorale, nil]
14
+ # @param temporale_factory [Proc, nil]
15
+ # +Proc+ receiving a single parameter - year - and returning
16
+ # a {Temporale} instance.
17
+ # @param temporale_options [Hash, nil]
18
+ # +Hash+ of arguments for {Temporale#initialize}.
19
+ # +temporale_factory+ and +temporale_options+ are mutually
20
+ # exclusive - pass either (or none) of them, never both.
21
+ # @param cache [Hash]
22
+ # object to be used as internal cache of {Calendar} instances -
23
+ # anything exposing +#[]=+ and +#[]+ and "behaving mostly like
24
+ # a +Hash+" will work.
25
+ # There's no need to pass it unless you want to have control
26
+ # over the cache. That may be sometimes useful
27
+ # in order to prevent a long-lived
28
+ # +PerpetualCalendar+ instance flooding the memory
29
+ # by huge amount of cached {Calendar} instances.
30
+ # (By default, once a {Calendar} for a certain year is built,
31
+ # it is cached for the +PerpetualCalendar+ instances' lifetime.)
32
+ def initialize(sanctorale: nil, temporale_factory: nil, temporale_options: nil, cache: {})
33
+ if temporale_factory && temporale_options
34
+ raise ArgumentError.new('Specify either temporale_factory or temporale_options, not both')
35
+ end
36
+
37
+ @sanctorale = sanctorale
38
+ @temporale_factory = temporale_factory || build_temporale_factory(temporale_options)
39
+
40
+ @cache = cache
41
+ end
42
+
43
+ # @return [Day]
44
+ # @see Calendar#day
45
+ def day(*args)
46
+ calendar_for(*args).day(*args)
47
+ end
48
+
49
+ # @return [Day, Array<Day>]
50
+ # @see Calendar#[]
51
+ # @since 0.6.0
52
+ def [](arg)
53
+ if arg.is_a? Range
54
+ return arg.collect do |date|
55
+ calendar_for(date).day(date)
56
+ end
57
+ end
58
+
59
+ day(arg)
60
+ end
61
+
62
+ # Returns a {Calendar} instance for the liturgical year containing
63
+ # the specified day
64
+ #
65
+ # Parameters like {Calendar#day}
66
+ #
67
+ # @return [Calendar]
68
+ def calendar_for(*args)
69
+ date = Calendar.mk_date(*args)
70
+ year = Temporale.liturgical_year date
71
+ calendar_instance year
72
+ end
73
+
74
+ # Returns a Calendar instance for the specified liturgical year
75
+ #
76
+ # @param year [Fixnum]
77
+ # @return [Calendar]
78
+ def calendar_for_year(year)
79
+ calendar_instance year
80
+ end
81
+
82
+ private
83
+
84
+ def build_temporale_factory(temporale_options)
85
+ temporale_options ||= {}
86
+ lambda {|year| Temporale.new(year, **temporale_options) }
87
+ end
88
+
89
+ def calendar_instance(year)
90
+ if @cache.has_key? year
91
+ @cache[year]
92
+ else
93
+ @cache[year] = Calendar.new(year, @sanctorale, @temporale_factory.call(year))
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,40 +1,77 @@
1
1
  module CalendariumRomanum
2
+ # Celebration rank
2
3
  class Rank
3
4
  include Comparable
4
5
 
5
- def initialize(priority=nil, desc=nil, short_desc=nil)
6
+ # @param priority [Float, nil] number in the Table of Liturgical Days
7
+ # @param desc [String, nil]
8
+ # full description (translation string identifier)
9
+ # @param short_desc [String, nil]
10
+ # short rank name (translation string identifier)
11
+ def initialize(priority = nil, desc = nil, short_desc = nil)
6
12
  @priority = priority
7
13
  @desc = desc
8
14
  @short_desc = short_desc
9
15
  end
10
16
 
17
+ # @return [Float, nil]
11
18
  attr_reader :priority
12
- alias_method :to_f, :priority
19
+ alias to_f priority
13
20
 
21
+ # Full description - internationalized human-readable string.
22
+ #
23
+ # @return [String, nil]
14
24
  def desc
15
25
  @desc && I18n.t(@desc)
16
26
  end
17
27
 
18
- alias_method :to_s, :desc
28
+ # String representation mostly for debugging purposes.
29
+ #
30
+ # @return [String]
31
+ def to_s
32
+ # 'desc' instead of '@desc' is intentional -
33
+ # for a good reason we don't present contents of an instance
34
+ # variable but result of an instance method
35
+ "#<#{self.class.name} @priority=#{priority} desc=#{desc.inspect}>"
36
+ end
19
37
 
38
+ # Short name - internationalized human-readable string.
39
+ #
40
+ # @return [String, nil]
20
41
  def short_desc
21
42
  @short_desc && I18n.t(@short_desc)
22
43
  end
23
44
 
24
- def <=>(b)
25
- b.priority <=> self.priority
45
+ def <=>(other)
46
+ other.priority <=> priority
26
47
  end
27
48
 
49
+ # @return [Boolean]
28
50
  def solemnity?
29
51
  priority.to_i == 1
30
52
  end
31
53
 
54
+ # @return [Boolean]
55
+ # @since 0.6.0
56
+ def sunday?
57
+ self == Ranks::SUNDAY_UNPRIVILEGED
58
+ end
59
+
60
+ # @return [Boolean]
32
61
  def feast?
33
62
  priority.to_i == 2
34
63
  end
35
64
 
65
+ # @return [Boolean]
36
66
  def memorial?
37
- priority.to_i == 3
67
+ priority.to_i == 3 && priority <= 3.12
68
+ end
69
+
70
+ # @return [Boolean]
71
+ # @since 0.6.0
72
+ def ferial?
73
+ self == Ranks::FERIAL ||
74
+ self == Ranks::FERIAL_PRIVILEGED
38
75
  end
39
76
  end
40
77
  end
@@ -1,27 +1,60 @@
1
+ require 'set'
2
+
1
3
  module CalendariumRomanum
2
4
 
3
- # knows the fixed-date celebrations
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 [Fixnum]
49
+ # @param day [Fixnum]
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,33 +63,92 @@ 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
- # replaces content of the given day by given celebrations
85
+ # Replaces content of the given day by given {Celebration}s
86
+ #
87
+ # @param month [Fixnum]
88
+ # @param day [Fixnum]
89
+ # @param celebrations [Array<Celebration>]
90
+ # @return [void]
91
+ # @raise [ArgumentError]
92
+ # when performing the operation would break the object's invariant
37
93
  def replace(month, day, celebrations)
38
94
  date = AbstractDate.new(month, day)
39
95
 
96
+ symbols_without_day = @symbols
97
+ unless @days[date].nil?
98
+ old_symbols = @days[date].collect(&:symbol).compact
99
+ symbols_without_day = @symbols - old_symbols
100
+ end
101
+
102
+ new_symbols = celebrations.collect(&:symbol).compact
103
+ duplicate = symbols_without_day.intersection new_symbols
104
+ unless duplicate.empty?
105
+ raise ArgumentError.new("Attempted to add Celebrations with duplicate symbols #{duplicate.to_a.inspect}")
106
+ end
107
+
108
+ @symbols = symbols_without_day
109
+ @symbols.merge new_symbols
110
+
40
111
  if celebrations.first.solemnity?
41
112
  @solemnities[date] = celebrations.first
42
113
  elsif @solemnities.has_key? date
43
114
  @solemnities.delete date
44
115
  end
45
116
 
46
- @days[date] = celebrations
117
+ @days[date] = celebrations.dup
47
118
  end
48
119
 
49
- # adds all Celebrations from another instance
50
- def update(sanctorale)
51
- sanctorale.each_day do |date, celebrations|
120
+ # Updates the receiver with {Celebration}s from another instance.
121
+ #
122
+ # For each date contained in +other+ the content of +self+
123
+ # is _replaced_ by that of +other+.
124
+ #
125
+ # @param other [Sanctorale]
126
+ # @return [void]
127
+ # @raise (see #replace)
128
+ def update(other)
129
+ other.each_day do |date, celebrations|
52
130
  replace date.month, date.day, celebrations
53
131
  end
54
132
  end
55
133
 
56
- # returns an Array with one or more Celebrations
57
- # scheduled for the given day
134
+ # Retrieves {Celebration}s for the given date
58
135
  #
59
- # expected arguments: Date or two Integers (month, day)
136
+ # @param date [AbstractDate, Date]
137
+ # @return [Array<Celebration>] (may be empty)
138
+ # @since 0.6.0
139
+ def [](date)
140
+ adate = date.is_a?(AbstractDate) ? date : AbstractDate.from_date(date)
141
+ @days[adate] || []
142
+ end
143
+
144
+ # Retrieves {Celebration}s for the given date
145
+ #
146
+ # @overload get(date)
147
+ # @param date[AbstractDate, Date]
148
+ # @overload get(month, day)
149
+ # @param month [Fixnum]
150
+ # @param day [Fixnum]
151
+ # @return (see #[])
60
152
  def get(*args)
61
153
  if args.size == 1 && args[0].is_a?(Date)
62
154
  month = args[0].month
@@ -66,24 +158,52 @@ module CalendariumRomanum
66
158
  end
67
159
 
68
160
  date = AbstractDate.new(month, day)
69
- return @days[date] || []
161
+ self[date]
70
162
  end
71
163
 
72
- # for each day for which an entry is available
73
- # yields an AbstractDate and an Array of Celebrations
164
+ # Enumerates dates for which any {Celebration}s are available
165
+ #
166
+ # @yield [AbstractDate, Array<Celebration>] the array is never empty
167
+ # @return [void, Enumerator] if called without a block, returns +Enumerator+
74
168
  def each_day
169
+ return to_enum(__method__) unless block_given?
170
+
75
171
  @days.each_pair do |date, celebrations|
76
172
  yield date, celebrations
77
173
  end
78
174
  end
79
175
 
80
- # returns count of the _days_ with celebrations filled
176
+ # Returns count of _days_ with {Celebration}s filled
177
+ #
178
+ # @return [Fixnum]
81
179
  def size
82
180
  @days.size
83
181
  end
84
182
 
183
+ # It is empty if it doesn't contain any {Celebration}
184
+ #
185
+ # @return [Boolean]
85
186
  def empty?
86
187
  @days.empty?
87
188
  end
189
+
190
+ # Freezes the instance
191
+ def freeze
192
+ @days.freeze
193
+ @days.values.each(&:freeze)
194
+ @solemnities.freeze
195
+ super
196
+ end
197
+
198
+ # @return [Boolean]
199
+ # @since 0.6.0
200
+ def ==(b)
201
+ self.class == b.class &&
202
+ days == b.days
203
+ end
204
+
205
+ protected
206
+
207
+ attr_reader :days
88
208
  end
89
209
  end