summaryrefslogtreecommitdiff
path: root/app/models/concerns/rrule_humanizer.rb
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-01 00:24:10 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-01 00:24:10 +0200
commit95955abaa339098755a214cfcadf87c90211fe64 (patch)
treea3ad7a789e71b20c7c760dc1b09f3efcea3f0331 /app/models/concerns/rrule_humanizer.rb
parent51629c5c42270a346885057a441095c964101cc1 (diff)
Add RRULE humanizer and wire events into nodes#showerdgeist-revive-events
- app/models/concerns/rrule_humanizer.rb: new concern included into Event, renders recurring schedule as natural-language German or English from RRULE string; handles WEEKLY/MONTHLY, biweekly (INTERVAL=2), ordinal weekday positions (1TU, -1TH, -2WE), BYMONTH single-month exclusions (December pause convention); gracefully returns nil for COUNT/UNTIL/unrecognized shapes - test/models/concerns/rrule_humanizer_test.rb: 15 tests covering all distinct RRULE shapes found in production data - app/helpers/nodes_helper.rb: add event_schedule_text helper combining humanize_rrule with start_time formatting - app/views/nodes/show.html.erb: add events row, conditionally rendered when node has associated events - config/locales/de.yml, en.yml: add event_schedule_time, event_schedule_unrecognized, event_schedule_none keys
Diffstat (limited to 'app/models/concerns/rrule_humanizer.rb')
-rw-r--r--app/models/concerns/rrule_humanizer.rb82
1 files changed, 82 insertions, 0 deletions
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