summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/helpers/nodes_helper.rb16
-rw-r--r--app/models/concerns/rrule_humanizer.rb82
-rw-r--r--app/models/event.rb1
-rw-r--r--app/views/nodes/show.html.erb14
-rw-r--r--config/locales/de.yml3
-rw-r--r--config/locales/en.yml3
-rw-r--r--test/models/concerns/rrule_humanizer_test.rb84
7 files changed, 202 insertions, 1 deletions
diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb
index 4293628..329bcc5 100644
--- a/app/helpers/nodes_helper.rb
+++ b/app/helpers/nodes_helper.rb
@@ -43,4 +43,20 @@ module NodesHelper
43 link_to('add event', new_event_path(:node_id => @node.id)) 43 link_to('add event', new_event_path(:node_id => @node.id))
44 ]) 44 ])
45 end 45 end
46
47 def event_schedule_text(event)
48 if event.rrule.present?
49 recurrence = event.humanize_rrule(I18n.locale)
50 if recurrence
51 time = event.start_time&.strftime("%H:%M")
52 time ? "#{recurrence} #{t(:event_schedule_time, time: time)}" : recurrence
53 else
54 "#{event.rrule} (#{t(:event_schedule_unrecognized)})"
55 end
56 elsif event.start_time
57 I18n.l(event.start_time, format: :long)
58 else
59 t(:event_schedule_none)
60 end
61 end
46end 62end
diff --git a/app/models/concerns/rrule_humanizer.rb b/app/models/concerns/rrule_humanizer.rb
new file mode 100644
index 0000000..6cee711
--- /dev/null
+++ b/app/models/concerns/rrule_humanizer.rb
@@ -0,0 +1,82 @@
1module RruleHumanizer
2 extend ActiveSupport::Concern
3
4 WEEKDAY_NAMES = {
5 de: { "MO"=>"Montag","TU"=>"Dienstag","WE"=>"Mittwoch","TH"=>"Donnerstag","FR"=>"Freitag","SA"=>"Samstag","SU"=>"Sonntag" },
6 en: { "MO"=>"Monday","TU"=>"Tuesday","WE"=>"Wednesday","TH"=>"Thursday","FR"=>"Friday","SA"=>"Saturday","SU"=>"Sunday" }
7 }.freeze
8
9 WEEKDAY_NAMES_ADVERBIAL = {
10 de: { "MO"=>"montags","TU"=>"dienstags","WE"=>"mittwochs","TH"=>"donnerstags","FR"=>"freitags","SA"=>"samstags","SU"=>"sonntags" }
11 }.freeze
12
13 ORDINAL_NAMES = {
14 de: { 1=>"ersten", 2=>"zweiten", 3=>"dritten", 4=>"vierten", -1=>"letzten", -2=>"vorletzten" },
15 en: { 1=>"first", 2=>"second", 3=>"third", 4=>"fourth", -1=>"last", -2=>"second-to-last" }
16 }.freeze
17
18 MONTH_NAMES = {
19 de: %w[Januar Februar März April Mai Juni Juli August September Oktober November Dezember],
20 en: %w[January February March April May June July August September October November December]
21 }.freeze
22
23 def humanize_rrule(locale = I18n.locale)
24 return nil if rrule.blank?
25 parts = Hash[rrule.split(";").map { |p| p.split("=", 2) }]
26 return nil if parts["COUNT"] || parts["UNTIL"] # old one-off data, don't guess
27
28 freq, interval, byday, bymonth = parts["FREQ"], parts["INTERVAL"].to_i, parts["BYDAY"], parts["BYMONTH"]
29 loc = locale.to_sym
30 weekdays = WEEKDAY_NAMES[loc] || WEEKDAY_NAMES[:en]
31 ordinals = ORDINAL_NAMES[loc] || ORDINAL_NAMES[:en]
32 months = MONTH_NAMES[loc] || MONTH_NAMES[:en]
33
34 days = byday&.split(",")&.map do |d|
35 if d =~ /^(-?\d+)([A-Z]{2})$/
36 "#{ordinals[$1.to_i]} #{weekdays[$2]}"
37 else
38 weekdays[d]
39 end
40 end
41
42 base =
43 case loc
44 when :de
45 case freq
46 when "WEEKLY"
47 if days
48 if interval == 2
49 adverbial = byday.split(",").map { |d| WEEKDAY_NAMES_ADVERBIAL[:de][d] }
50 "Alle zwei Wochen #{adverbial.join(' und ')}"
51 else
52 "Jeden #{days.join(' und ')}"
53 end
54 else
55 interval == 2 ? "Alle zwei Wochen" : "Wöchentlich"
56 end
57 when "MONTHLY"
58 days ? "Jeden #{days.join(' und ')} im Monat" : "Monatlich"
59 end
60 else
61 case freq
62 when "WEEKLY"
63 days ? "#{interval == 2 ? 'Every other' : 'Every'} #{days.join(' and ')}" : (interval == 2 ? "Every other week" : "Weekly")
64 when "MONTHLY"
65 days ? "Every #{days.join(' and ')} of the month" : "Monthly"
66 end
67 end
68 return nil unless base
69
70 if bymonth
71 included = bymonth.split(",").map(&:to_i)
72 missing = ((1..12).to_a - included)
73 if missing.size == 1
74 excluded_name = months[missing.first - 1]
75 base += (loc == :de ? ", außer im #{excluded_name}" : ", except in #{excluded_name}")
76 end
77 # more than one missing month: bymonth pattern more complex than we handle, leave base as-is silently
78 end
79
80 base
81 end
82end
diff --git a/app/models/event.rb b/app/models/event.rb
index 26c79e4..de82674 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -1,4 +1,5 @@
1class Event < ApplicationRecord 1class Event < ApplicationRecord
2 include RruleHumanizer
2 3
3 belongs_to :node, optional: true 4 belongs_to :node, optional: true
4 has_many :occurrences 5 has_many :occurrences
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index de4d8a2..7223219 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -42,6 +42,18 @@
42 <td class="description">Tagged with:</td> 42 <td class="description">Tagged with:</td>
43 <td><%= @page.tag_list %></td> 43 <td><%= @page.tag_list %></td>
44 </tr> 44 </tr>
45 <% if @node.events.any? %>
46 <tr>
47 <td class="description">Events</td>
48 <td>
49 <ul>
50 <% @node.events.order(:start_time).each do |event| %>
51 <li><%= event_schedule_text(event) %><%= " (primary)" if event.is_primary? %></li>
52 <% end %>
53 </ul>
54 </td>
55 </tr>
56 <% end %>
45 <tr> 57 <tr>
46 <td class="description"><strong>Title</strong></td> 58 <td class="description"><strong>Title</strong></td>
47 <td><%= sanitize( @page.title ) %></td> 59 <td><%= sanitize( @page.title ) %></td>
@@ -59,4 +71,4 @@
59 <td class="right"></td> 71 <td class="right"></td>
60 </tr> 72 </tr>
61 </table> 73 </table>
62</div> \ No newline at end of file 74</div>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 5f77d79..0b42dd3 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -7,6 +7,9 @@ de:
7 sponsors: Sponsoren 7 sponsors: Sponsoren
8 show_tag_headline: "Seiten mit dem Tag:" 8 show_tag_headline: "Seiten mit dem Tag:"
9 old_ccc_de: das alte ccc.de 9 old_ccc_de: das alte ccc.de
10 event_schedule_time: "um %{time} Uhr"
11 event_schedule_unrecognized: "Termin-Regel nicht automatisch lesbar"
12 event_schedule_none: "Kein Termin"
10 date: 13 date:
11 formats: 14 formats:
12 default: "%d.%m.%Y" 15 default: "%d.%m.%Y"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 2458d4d..93a0d55 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -8,6 +8,9 @@ en:
8 hello: "Hello world" 8 hello: "Hello world"
9 show_tag_headline: "Pages tagged with:" 9 show_tag_headline: "Pages tagged with:"
10 old_ccc_de: the old ccc.de 10 old_ccc_de: the old ccc.de
11 event_schedule_time: "at %{time}"
12 event_schedule_unrecognized: "Schedule not automatically readable"
13 event_schedule_none: "No schedule"
11 14
12 time: 15 time:
13 formats: 16 formats:
diff --git a/test/models/concerns/rrule_humanizer_test.rb b/test/models/concerns/rrule_humanizer_test.rb
new file mode 100644
index 0000000..500dbc7
--- /dev/null
+++ b/test/models/concerns/rrule_humanizer_test.rb
@@ -0,0 +1,84 @@
1require 'test_helper'
2
3class RruleHumanizerTest < ActiveSupport::TestCase
4 def humanize(rrule, locale = :de)
5 Event.new(rrule: rrule).humanize_rrule(locale)
6 end
7
8 test "weekly single day" do
9 assert_equal "Jeden Dienstag", humanize("FREQ=WEEKLY;BYDAY=TU")
10 assert_equal "Every Tuesday", humanize("FREQ=WEEKLY;BYDAY=TU", :en)
11 end
12
13 test "weekly two days" do
14 assert_equal "Jeden Mittwoch und Freitag", humanize("FREQ=WEEKLY;BYDAY=WE,FR")
15 assert_equal "Every Wednesday and Friday", humanize("FREQ=WEEKLY;BYDAY=WE,FR", :en)
16 end
17
18 test "weekly no byday" do
19 assert_equal "Wöchentlich", humanize("FREQ=WEEKLY")
20 assert_equal "Weekly", humanize("FREQ=WEEKLY", :en)
21 end
22
23 test "biweekly with day" do
24 assert_equal "Alle zwei Wochen donnerstags", humanize("FREQ=WEEKLY;INTERVAL=2;BYDAY=TH")
25 assert_equal "Every other Thursday", humanize("FREQ=WEEKLY;INTERVAL=2;BYDAY=TH", :en)
26 end
27
28 test "biweekly no day" do
29 assert_equal "Alle zwei Wochen", humanize("FREQ=WEEKLY;INTERVAL=2")
30 assert_equal "Every other week", humanize("FREQ=WEEKLY;INTERVAL=2", :en)
31 end
32
33 test "monthly nth weekday" do
34 assert_equal "Jeden ersten Dienstag im Monat", humanize("FREQ=MONTHLY;BYDAY=1TU")
35 assert_equal "Jeden zweiten Freitag im Monat", humanize("FREQ=MONTHLY;BYDAY=2FR")
36 assert_equal "Jeden dritten Sonntag im Monat", humanize("FREQ=MONTHLY;BYDAY=3SU")
37 assert_equal "Jeden letzten Mittwoch im Monat", humanize("FREQ=MONTHLY;BYDAY=-1WE")
38 end
39
40 test "monthly nth weekday english" do
41 assert_equal "Every first Tuesday of the month", humanize("FREQ=MONTHLY;BYDAY=1TU", :en)
42 assert_equal "Every last Wednesday of the month", humanize("FREQ=MONTHLY;BYDAY=-1WE", :en)
43 end
44
45 test "monthly second-to-last" do
46 assert_equal "Jeden vorletzten Donnerstag im Monat", humanize("FREQ=MONTHLY;BYDAY=-2TH")
47 assert_equal "Every second-to-last Thursday of the month", humanize("FREQ=MONTHLY;BYDAY=-2TH", :en)
48 end
49
50 test "monthly no byday" do
51 assert_equal "Monatlich", humanize("FREQ=MONTHLY")
52 assert_equal "Monthly", humanize("FREQ=MONTHLY", :en)
53 end
54
55 test "monthly with single excluded month" do
56 assert_equal "Jeden letzten Donnerstag im Monat, außer im Dezember",
57 humanize("FREQ=MONTHLY;BYDAY=-1TH;BYMONTH=1,2,3,4,5,6,7,8,9,10,11")
58 assert_equal "Every last Thursday of the month, except in December",
59 humanize("FREQ=MONTHLY;BYDAY=-1TH;BYMONTH=1,2,3,4,5,6,7,8,9,10,11", :en)
60 end
61
62 test "monthly excluding january" do
63 assert_equal "Jeden zweiten Mittwoch im Monat, außer im Januar",
64 humanize("FREQ=MONTHLY;BYMONTH=2,3,4,5,6,7,8,9,10,11,12;BYDAY=2WE")
65 end
66
67 test "blank rrule returns nil" do
68 assert_nil humanize(nil)
69 assert_nil humanize("")
70 end
71
72 test "count and until are not guessed at" do
73 assert_nil humanize("FREQ=MONTHLY;BYDAY=1WE;COUNT=36")
74 assert_nil humanize("FREQ=MONTHLY;BYDAY=1WE;UNTIL=20050105T222222Z")
75 end
76
77 test "unrecognized freq returns nil" do
78 assert_nil humanize("FREQ=YEARLY;BYMONTH=12")
79 end
80
81 test "falls back to english for unknown locale" do
82 assert_equal "Every Tuesday", humanize("FREQ=WEEKLY;BYDAY=TU", :fr)
83 end
84end