calrom 0.1.0 → 0.4.0

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.
@@ -1,5 +1,32 @@
1
1
  module Calrom
2
2
  class DateRange < Range
3
+ def each_month
4
+ return to_enum(:each_month) unless block_given?
5
+
6
+ if first.year == last.year && first.month == last.month
7
+ # a single month or it's part
8
+ yield self
9
+ return
10
+ end
11
+
12
+ (Month.new(first.year, first.month) .. Month.new(last.year, last.month))
13
+ .each_with_index do |m,i|
14
+ if i == 0 && first.day > 1
15
+ # first month, incomplete
16
+ yield self.class.new(first, m.last)
17
+ elsif m.first.year == last.year && m.first.month == last.month && last != m.last
18
+ # last month, incomplete
19
+ yield self.class.new(m.first, last)
20
+ else
21
+ yield m
22
+ end
23
+ end
24
+ end
25
+
26
+ def spans_multiple_months?
27
+ first.month != last.month ||
28
+ first.year != last.year
29
+ end
3
30
  end
4
31
 
5
32
  class Year < DateRange
@@ -10,16 +37,68 @@ module Calrom
10
37
  def to_s
11
38
  first.year.to_s
12
39
  end
40
+
41
+ def each_month
42
+ return to_enum(:each_month) unless block_given?
43
+
44
+ 1.upto(12) {|month| yield Month.new(first.year, month) }
45
+ end
46
+ end
47
+
48
+ class ThreeMonths < DateRange
49
+ def initialize(year, month)
50
+ super first_day_of_last_month(year, month), last_day_of_next_month(year, month)
51
+ end
52
+
53
+ private
54
+
55
+ def first_day_of_last_month(year, month)
56
+ Date.new(year, month, 1).prev_month
57
+ end
58
+
59
+ def last_day_of_next_month(year, month)
60
+ n = Date.new(year, month).next_month
61
+
62
+ Date.new(n.year, n.month, -1)
63
+ end
13
64
  end
14
65
 
15
66
  class Month < DateRange
16
67
  def initialize(year, month)
17
- super Date.new(year, month, 1), Date.new(year, month + 1, 1) - 1
68
+ @year = year
69
+ @month = month
70
+
71
+ super Date.new(year, month, 1), Date.new(year, month, -1)
18
72
  end
19
73
 
20
74
  def to_s
21
- "#{first.month}/#{first.year}"
75
+ first.strftime '%B %Y'
76
+ end
77
+
78
+ def each_month
79
+ return to_enum(:each_month) unless block_given?
80
+
81
+ yield self
82
+ end
83
+
84
+ def succ
85
+ n = Date.new(@year, @month, 1).next_month
86
+ self.class.new(n.year, n.month)
22
87
  end
88
+
89
+ def <=>(other)
90
+ years_cmp = year <=> other.year
91
+
92
+ if years_cmp != 0
93
+ years_cmp
94
+ else
95
+ month <=> other.month
96
+ end
97
+ end
98
+
99
+ protected
100
+
101
+ attr_reader :year, :month
23
102
  end
24
103
 
25
104
  class Day < DateRange
@@ -30,5 +109,9 @@ module Calrom
30
109
  def to_s
31
110
  first.to_s
32
111
  end
112
+
113
+ def each_month
114
+ yield self
115
+ end
33
116
  end
34
117
  end
@@ -0,0 +1,36 @@
1
+ module Calrom
2
+ # Reads configuration from environment variables
3
+ class EnvironmentReader
4
+ def self.call(config = nil)
5
+ new(config || Config.new).call
6
+ end
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def call
13
+ today
14
+
15
+ @config
16
+ end
17
+
18
+ private
19
+
20
+ def today
21
+ with_envvar 'CALROM_CURRENT_DATE' do |value, name|
22
+ begin
23
+ @config.today = Date.parse value
24
+ rescue ArgumentError
25
+ raise InputError.new "value of environment variable #{name} is not a valid date"
26
+ end
27
+ end
28
+ end
29
+
30
+ def with_envvar(name)
31
+ value = ENV[name]
32
+
33
+ yield value, name if value
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,4 @@
1
+ module Calrom
2
+ # Invalid user input. Message should be human-readable and helpful, intended for the user.
3
+ class InputError< RuntimeError; end
4
+ end
@@ -0,0 +1,71 @@
1
+ require 'delegate'
2
+
3
+ module Calrom
4
+ # decorates /(Perpetual)?Calendar/, returns data filtered
5
+ class FilteringCalendar < SimpleDelegator
6
+ using Refinement::CalendariumRomanum::TriduumNameClashWorkaround
7
+
8
+ def initialize(calendar, days_filter_expressions=[], celebrations_filter_expressions=[])
9
+ super(calendar)
10
+
11
+ @days_filter = proc do |day|
12
+ days_filter_expressions.all? do |expr|
13
+ eval_filtering_expression(day, expr)
14
+ end
15
+ end
16
+
17
+ @celebrations_filter = proc do |celebration|
18
+ celebrations_filter_expressions.all? do |expr|
19
+ eval_filtering_expression(celebration, expr)
20
+ end
21
+ end
22
+ end
23
+
24
+ def [](arg)
25
+ raw = super(arg)
26
+
27
+ unless @days_filter.(raw)
28
+ return FilteredDay.build_skipped raw
29
+ end
30
+
31
+ FilteredDay.new raw, raw.celebrations.select(&@celebrations_filter)
32
+ end
33
+
34
+ def each_day_in_range(range, include_skipped: false)
35
+ return to_enum(__method__, range, include_skipped: include_skipped) unless block_given?
36
+
37
+ range.each do |date|
38
+ day = self[date]
39
+ yield day if (include_skipped || !day.skipped?)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def eval_filtering_expression(object, expression)
46
+ object.instance_eval expression
47
+ rescue StandardError, SyntaxError => exception
48
+ raise InputError.new "Filter expression '#{expression}' raised #{exception.class}: #{exception.message}"
49
+ end
50
+
51
+ class FilteredDay < SimpleDelegator
52
+ def initialize(day, filtered_celebrations)
53
+ super(day)
54
+
55
+ @filtered_celebrations = filtered_celebrations
56
+ end
57
+
58
+ def self.build_skipped(day)
59
+ new day, []
60
+ end
61
+
62
+ def celebrations
63
+ @filtered_celebrations
64
+ end
65
+
66
+ def skipped?
67
+ celebrations.empty?
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,29 @@
1
+ module Calrom
2
+ module Formatter
3
+ # Prints list of available bundled calendars
4
+ class Calendars < Formatter
5
+ def call(calendar, date_range)
6
+ last_locale = nil
7
+ CR::Data.each do |d|
8
+ sanctorale = d.load_with_parents
9
+ meta = sanctorale.metadata
10
+ puts if last_locale && last_locale != meta['locale']
11
+ default = d == Config::DEFAULT_DATA ? ' [default]' : ''
12
+ puts "%-20s: %s [%s]%s" % [d.siglum, meta['title'], meta['locale'], default]
13
+
14
+ next unless meta['components']
15
+
16
+ parents =
17
+ meta['components']
18
+ .collect {|c| c['extends'] }
19
+ .compact
20
+ .map {|e| e.is_a?(Array) ? e : [e] } # 'extends' is String or Array
21
+ .flatten
22
+ .map {|p| p.sub(/\.\w{3,4}$/, '') } # file name to "siglum"
23
+ parents.each {|p| puts " < #{p}" }
24
+ last_locale = meta['locale']
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,51 @@
1
+ module Calrom
2
+ module Formatter
3
+ class Condensed < Formatter
4
+ def call(calendar, date_range)
5
+ calendar.each_day_in_range(date_range) {|d| day d }
6
+ end
7
+
8
+ private
9
+
10
+ def day(liturgical_day)
11
+ c = liturgical_day.celebrations.first
12
+
13
+ colour = highlighter.colour(c.colour.name[0].upcase, c.colour)
14
+ rank = highlighter.rank(rank(c.rank), c.rank)
15
+ title = short_title c
16
+ more = additional_celebrations(liturgical_day) + vespers(liturgical_day)
17
+
18
+ puts "#{title} #{rank}#{colour}#{more}"
19
+ end
20
+
21
+ def rank(rank)
22
+ if rank.solemnity?
23
+ '*'
24
+ elsif rank.feast?
25
+ '+'
26
+ else
27
+ ''
28
+ end
29
+ end
30
+
31
+ def short_title(celebration)
32
+ if celebration.cycle == :sanctorale
33
+ # naive attempt to strip feast titles
34
+ celebration.title.sub /,[^,]*$/, ''
35
+ else
36
+ celebration.title
37
+ end
38
+ end
39
+
40
+ def additional_celebrations(day)
41
+ size = day.celebrations.size
42
+
43
+ size > 1 ? " +#{size-1}" : ''
44
+ end
45
+
46
+ def vespers(day)
47
+ day.vespers_from_following? ? '>' : ''
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,27 @@
1
+ require 'csv'
2
+
3
+ module Calrom
4
+ module Formatter
5
+ class Csv < Formatter
6
+ def call(calendar, date_range)
7
+ CSV do |out|
8
+ out << %w(date title symbol rank rank_num colour season)
9
+
10
+ calendar.each_day_in_range(date_range) do |day|
11
+ day.celebrations.each do |c|
12
+ out << [
13
+ day.date,
14
+ c.title,
15
+ c.symbol,
16
+ c.rank.short_desc,
17
+ c.rank.priority,
18
+ c.colour.symbol,
19
+ day.season.symbol
20
+ ]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ module Calrom
2
+ module Formatter
3
+ # Prints (only) date of Easter for the specified year.
4
+ class Easter < Formatter
5
+ def call(calendar, date_range)
6
+ unless date_range.is_a?(Year) || date_range.is_a?(Month)
7
+ raise 'unexpected date range, expected a year'
8
+ end
9
+
10
+ puts CR::Temporale::Dates
11
+ .easter_sunday(date_range.first.year - 1)
12
+ .strftime('%D')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,14 +1,25 @@
1
1
  module Calrom
2
2
  module Formatter
3
3
  class Formatter
4
- def initialize(highlighter, today)
4
+ def initialize(highlighter, today, io = STDOUT)
5
5
  @highlighter = highlighter
6
6
  @today = today
7
+ @io = io
7
8
  end
8
9
 
9
10
  attr_reader :highlighter, :today
10
11
 
11
- def call(days, date_range)
12
+ def call(calendar, date_range)
13
+ end
14
+
15
+ private
16
+
17
+ def puts(s = '')
18
+ @io.puts s
19
+ end
20
+
21
+ def print(s)
22
+ @io.print s
12
23
  end
13
24
  end
14
25
  end
@@ -0,0 +1,38 @@
1
+ require 'json'
2
+
3
+ module Calrom
4
+ module Formatter
5
+ # JSON format mimicking Church Calendar API v0 (https://github.com/igneus/church-calendar-api)
6
+ class Json < Formatter
7
+ def call(calendar, date_range)
8
+ # We build the outer JSON Array manually in order to be able to print
9
+ # vast amounts of calendar data without risking RAM exhaustion.
10
+ print "["
11
+
12
+ calendar.each_day_in_range(date_range).each_with_index do |day,i|
13
+ date = day.date
14
+ hash = {
15
+ date: date,
16
+ season: day.season.symbol,
17
+ season_week: day.season_week,
18
+ celebrations: day.celebrations.collect do |c|
19
+ {
20
+ title: c.title,
21
+ symbol: c.symbol,
22
+ colour: c.colour.symbol,
23
+ rank: c.rank.short_desc,
24
+ rank_num: c.rank.priority
25
+ }
26
+ end,
27
+ weekday: date.strftime('%A'),
28
+ }
29
+
30
+ puts "," if i > 0
31
+ print JSON.generate hash
32
+ end
33
+
34
+ puts "]"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,21 +1,21 @@
1
1
  module Calrom
2
2
  module Formatter
3
3
  class List < Formatter
4
- def call(days, date_range)
5
- print_months = date_range.first.month != date_range.last.month
4
+ def call(calendar, date_range)
5
+ print_months = date_range.spans_multiple_months?
6
6
 
7
7
  puts date_range.to_s
8
8
  puts
9
9
 
10
10
  current_month = nil
11
11
 
12
- days.each do |liturgical_day|
12
+ calendar.each_day_in_range(date_range) do |liturgical_day|
13
13
  if print_months && liturgical_day.date.month != current_month
14
- current_month = liturgical_day.date.month
15
-
16
- puts
17
- puts current_month
14
+ puts unless current_month == nil
15
+ puts liturgical_day.date.strftime('%B') #current_month
18
16
  puts
17
+
18
+ current_month = liturgical_day.date.month
19
19
  end
20
20
 
21
21
  day liturgical_day
@@ -28,9 +28,10 @@ module Calrom
28
28
  liturgical_day.celebrations.each_with_index do |celebration, i|
29
29
  s =
30
30
  if i > 0
31
- ' ' * 3
31
+ ' ' * 6
32
32
  else
33
- liturgical_day.date.day.to_s.rjust(3)
33
+ liturgical_day.date.strftime('%a') +
34
+ liturgical_day.date.day.to_s.rjust(3)
34
35
  end
35
36
  s += ' '
36
37
 
@@ -39,7 +40,7 @@ module Calrom
39
40
  s += highlighter.colour(colour.name[0].upcase, colour) +
40
41
  ' ' +
41
42
  highlighter.rank(celebration.title, rank) +
42
- (rank.short_desc.nil? ? '' : ', ' + rank.short_desc)
43
+ ((rank.short_desc.nil? || rank.sunday? || rank.ferial?) ? '' : ', ' + rank.short_desc)
43
44
 
44
45
  if liturgical_day.date == today
45
46
  s = highlighter.today s
@@ -0,0 +1,104 @@
1
+ require 'set'
2
+ require 'stringio'
3
+
4
+ module Calrom
5
+ module Formatter
6
+ class Overview < Formatter
7
+ def call(calendar, date_range)
8
+ colnum = 3 # TODO: expose configuration
9
+ if date_range.is_a? Year
10
+ puts center_on(weekdays.size * colnum + 2 * (colnum - 1), date_range.to_s)
11
+ end
12
+
13
+ date_range.each_month.each_slice(colnum) do |months|
14
+ columns = months.collect do |month|
15
+ StringIO.new.tap do |io|
16
+ print_month io, calendar, month, date_range.is_a?(Year)
17
+ end
18
+ end
19
+ print_columns columns, @io
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def print_month(io, calendar, month, year_in_heading)
26
+ heading = month.first.strftime(year_in_heading ? '%B' : '%B %Y')
27
+ io.puts center_on weekdays.size, heading
28
+
29
+ io.puts weekdays
30
+
31
+ io.print ' ' * month.first.wday
32
+ calendar.each_day_in_range(month, include_skipped: true) do |liturgical_day|
33
+ date = liturgical_day.date
34
+
35
+ if liturgical_day.skipped?
36
+ datestr = ' '
37
+ else
38
+ celebration = liturgical_day.celebrations.first
39
+
40
+ datestr = date.day.to_s.rjust(2)
41
+ datestr = highlighter.colour(datestr, celebration.colour)
42
+ datestr = highlighter.rank(datestr, celebration.rank)
43
+ end
44
+
45
+ if date == today
46
+ datestr = highlighter.today datestr
47
+ end
48
+ io.print datestr
49
+ if date.wday == 6
50
+ io.puts
51
+ else
52
+ io.print ' '
53
+ end
54
+ end
55
+ end
56
+
57
+ # localizable 2-character weekday shortcuts
58
+ def weekdays
59
+ @weekdays ||=
60
+ begin
61
+ sunday = Date.new 1987, 10, 25
62
+ sunday
63
+ .upto(sunday + 6)
64
+ .collect {|d| d.strftime('%a')[0..1] }
65
+ .join(' ')
66
+ end
67
+ end
68
+
69
+ # centers given string on a given line length
70
+ def center_on(line_length, content)
71
+ (' ' * (line_length / 2 - content.size / 2)) +
72
+ content
73
+ end
74
+
75
+ def print_columns(columns, io)
76
+ line_enumerators = columns.collect {|c| c.string.each_line }
77
+ not_yet_exhausted = Set.new line_enumerators
78
+ column_width = weekdays.size
79
+
80
+ loop do
81
+ break if not_yet_exhausted.empty?
82
+
83
+ line_enumerators.each do |l|
84
+ begin
85
+ line = l.next.chop
86
+ io.print line
87
+ io.print ' ' * (column_width - colour_aware_size(line))
88
+ rescue StopIteration
89
+ io.print ' ' * column_width
90
+ not_yet_exhausted.delete l
91
+ end
92
+ io.print ' '
93
+ end
94
+ io.puts
95
+ end
96
+ end
97
+
98
+ # String length ignoring colour codes
99
+ def colour_aware_size(str)
100
+ ColorizedString.new(str).uncolorize.size
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,8 +1,13 @@
1
1
  module Calrom
2
2
  module Highlighter
3
3
  class List
4
+ COLOUR_OVERRIDE = {
5
+ # 'colorize' does not know colour :violet
6
+ CR::Colours::VIOLET => :magenta,
7
+ }
8
+
4
9
  def colour(text, colour)
5
- ColorizedString.new(text).colorize(colour.symbol)
10
+ ColorizedString.new(text).colorize(COLOUR_OVERRIDE[colour] || colour.symbol)
6
11
  end
7
12
 
8
13
  def rank(text, rank)
@@ -0,0 +1,17 @@
1
+ module Calrom
2
+ module Highlighter
3
+ class No
4
+ def colour(text, colour)
5
+ text
6
+ end
7
+
8
+ def rank(text, rank)
9
+ text
10
+ end
11
+
12
+ def today(text)
13
+ text
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module Calrom
2
+ module Highlighter
3
+ class Overview < List
4
+ def rank(text, rank)
5
+ if rank.solemnity?
6
+ ColorizedString.new(text).colorize(mode: :bold)
7
+ elsif rank.feast?
8
+ ColorizedString.new(text).colorize(mode: :underline)
9
+ else
10
+ text
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ module Calrom
2
+ module Highlighter
3
+ class Selective
4
+ def initialize(selected, highlighter)
5
+ @selected = selected
6
+ @highlighter = highlighter
7
+ end
8
+
9
+ def colour(text, colour)
10
+ if @selected.include? __method__
11
+ @highlighter.public_send __method__, text, colour
12
+ else
13
+ text
14
+ end
15
+ end
16
+
17
+ def rank(text, rank)
18
+ if @selected.include? __method__
19
+ @highlighter.public_send __method__, text, rank
20
+ else
21
+ text
22
+ end
23
+ end
24
+
25
+ def today(text)
26
+ if @selected.include? __method__
27
+ @highlighter.public_send __method__, text
28
+ else
29
+ text
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end