hiccup 0.0.0 → 0.2.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.
@@ -0,0 +1,68 @@
1
+ require "hiccup/convenience"
2
+ require "hiccup/core_ext/fixnum"
3
+
4
+
5
+ module Hiccup
6
+ module Humanizable
7
+ include Convenience
8
+
9
+
10
+
11
+ def humanize
12
+ case kind
13
+ when :never; ""
14
+ when :weekly; weekly_humanize
15
+ when :monthly; monthly_humanize
16
+ when :annually; yearly_humanize
17
+ else; "Invalid"
18
+ end
19
+ end
20
+
21
+
22
+
23
+ private
24
+
25
+
26
+
27
+ def weekly_humanize
28
+ weekdays = pattern.map(&:humanize).to_sentence
29
+ if skip == 1 || pattern.length == 1
30
+ sentence("Every", ordinal, weekdays)
31
+ else
32
+ sentence(weekdays, "of every", ordinal, "week")
33
+ end
34
+ end
35
+
36
+ def monthly_humanize
37
+ monthly_occurrences = pattern.map(&method(:monthly_occurrence_to_s)).to_sentence
38
+ sentence("The", monthly_occurrences, "of every", ordinal, "month")
39
+ end
40
+
41
+ def yearly_humanize
42
+ sentence("Every", ordinal, "year on", self.start_date.strftime('%b %d'))
43
+ end
44
+
45
+
46
+
47
+ def monthly_occurrence_to_s(monthly_occurrence)
48
+ if monthly_occurrence.is_a?(Array)
49
+ _skip, weekday = monthly_occurrence
50
+ ordinal = _skip.human_ordinalize
51
+ sentence(ordinal, weekday.humanize)
52
+ else
53
+ monthly_occurrence.ordinalize
54
+ end
55
+ end
56
+
57
+ def ordinal
58
+ skip && skip.human_ordinalize(1 => nil, 2 => "other")
59
+ end
60
+
61
+ def sentence(*array)
62
+ array.compact.join(" ")
63
+ end
64
+
65
+
66
+
67
+ end
68
+ end
@@ -0,0 +1,42 @@
1
+ require "hiccup"
2
+ require "active_model/validations"
3
+
4
+
5
+ module Hiccup
6
+ class Schedule
7
+ extend Hiccup
8
+ include ActiveModel::Validations
9
+
10
+
11
+ hiccup :enumerable,
12
+ :validatable,
13
+ :humanizable,
14
+ :serializable => [:ical]
15
+
16
+
17
+ def initialize(options={})
18
+ @kind = options[:kind] || :never
19
+ @start_date = options[:start_date] || Date.today
20
+ @ends = options.key?(:ends) ? options[:ends] : false
21
+ @end_date = options[:end_date]
22
+ @skip = options[:skip] || options[:interval] || 1
23
+ @pattern = options[:pattern] || []
24
+ end
25
+
26
+
27
+ attr_accessor :kind, :start_date, :ends, :end_date, :skip, :pattern
28
+
29
+
30
+ def to_hash
31
+ {
32
+ :kind => kind,
33
+ :start_date => start_date,
34
+ :ends => ends,
35
+ :end_date => end_date,
36
+ :pattern => pattern
37
+ }
38
+ end
39
+
40
+
41
+ end
42
+ end
@@ -0,0 +1,39 @@
1
+ require "hiccup/serializers/ical"
2
+ require "active_support/concern"
3
+
4
+
5
+ module Hiccup
6
+ module Serializable
7
+ module Ical
8
+ extend ActiveSupport::Concern
9
+
10
+
11
+ def to_ical
12
+ ical_serializer.dump(self)
13
+ end
14
+
15
+
16
+ module ClassMethods
17
+
18
+ def from_ical(ics)
19
+ ical_serializer.load(ics)
20
+ end
21
+
22
+ def ical_serializer
23
+ @ical_serializer ||= Serializers::Ical.new(self)
24
+ end
25
+
26
+ end
27
+
28
+
29
+ private
30
+
31
+
32
+ def ical_serializer
33
+ self.class.ical_serializer
34
+ end
35
+
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,189 @@
1
+ require "ri_cal"
2
+
3
+
4
+ module Hiccup
5
+ module Serializers
6
+ class Ical
7
+
8
+ def initialize(klass)
9
+ @klass = klass
10
+ end
11
+
12
+
13
+
14
+ def dump(obj)
15
+ @component = RiCal::Component::Event.new
16
+ @component.dtstart = obj.start_date.to_time
17
+ @obj = obj
18
+
19
+ case obj.kind
20
+ when :weekly; add_weekly_rule
21
+ when :monthly; add_monthly_rule
22
+ when :annually; add_yearly_rule
23
+ end
24
+
25
+ StringIO.new.tap {|io|
26
+ @component.export_properties_to(io)
27
+ @component.export_x_properties_to(io)
28
+ }.string
29
+ end
30
+
31
+
32
+
33
+ def load(ics)
34
+ return unless ics # Required for now, for ActiveRecord
35
+
36
+ component_ics = "BEGIN:VEVENT\n#{ics}\nEND:VEVENT"
37
+ component = RiCal.parse_string(component_ics).first
38
+
39
+ @obj = @klass.new
40
+ @obj.start_date = component.dtstart_property.to_datetime
41
+ component.rrule_property.each do |rule|
42
+ case rule.freq
43
+ when "WEEKLY"; parse_weekly_rule(rule)
44
+ when "MONTHLY"; parse_monthly_rule(rule)
45
+ when "YEARLY"; parse_yearly_rule(rule)
46
+ end
47
+ end
48
+
49
+ @obj
50
+ end
51
+
52
+
53
+
54
+ private
55
+
56
+
57
+
58
+ def add_weekly_rule
59
+ add_rule("WEEKLY", :byday => abbreviate_weekdays(@obj.pattern))
60
+ end
61
+
62
+
63
+
64
+ def add_monthly_rule
65
+ byday = []
66
+ bymonthday = []
67
+ @obj.pattern.each do |occurrence|
68
+ if occurrence.is_a?(Array)
69
+ i, weekday = occurrence
70
+ byday << "#{i}#{abbreviate_weekday(weekday)}"
71
+ else
72
+ bymonthday << occurrence
73
+ end
74
+ end
75
+
76
+ add_rule("MONTHLY", :bymonthday => bymonthday) if bymonthday.any?
77
+ add_rule("MONTHLY", :byday => byday) if byday.any?
78
+ end
79
+
80
+
81
+
82
+ def add_yearly_rule
83
+ add_rule("YEARLY")
84
+ end
85
+
86
+
87
+
88
+ def add_rule(freq, options={})
89
+ merge_default_options_for_new_rule!(freq, options)
90
+ parent = options.delete(:parent)
91
+ # puts "[add_rule] (#{parent.inspect}, #{options.inspect})"
92
+ rrule = RiCal::PropertyValue::RecurrenceRule.new(parent, options)
93
+ @component.rrule_property.push(rrule)
94
+ end
95
+
96
+ def merge_default_options_for_new_rule!(freq, options)
97
+ options.merge!({
98
+ :freq => freq,
99
+ :interval => @obj.skip,
100
+ :until => @obj.ends? && @obj.end_date && @obj.end_date.to_time
101
+ })
102
+ end
103
+
104
+
105
+
106
+
107
+
108
+ def parse_weekly_rule(rule)
109
+ @obj.kind = :weekly
110
+ @obj.pattern = backmap_weekdays(rule.by_list[:byday])
111
+ parse_rule(rule)
112
+ end
113
+
114
+
115
+
116
+ def parse_monthly_rule(rule)
117
+ @obj.kind = :monthly
118
+ parse_monthly_bymonthyday(rule.by_list[:bymonthday])
119
+ parse_monthly_byday(rule.by_list[:byday])
120
+ parse_rule(rule)
121
+ end
122
+
123
+ def parse_monthly_bymonthyday(bymonthday)
124
+ (bymonthday || []).each do |bymonthday|
125
+ @obj.pattern = @obj.pattern + [bymonthday.ordinal]
126
+ end
127
+ end
128
+
129
+ def parse_monthly_byday(byday)
130
+ (byday || []).each do |byday|
131
+ @obj.pattern = @obj.pattern + [[byday.index, backmap_weekday(byday)]]
132
+ end
133
+ end
134
+
135
+
136
+
137
+ def parse_yearly_rule(rule)
138
+ @obj.kind = :annually
139
+ parse_rule(rule)
140
+ end
141
+
142
+
143
+
144
+ def parse_rule(rule)
145
+ @obj.skip = rule.interval
146
+ if rule.until
147
+ @obj.ends = true
148
+ @obj.end_date = rule.until.to_datetime
149
+ end
150
+ end
151
+
152
+
153
+
154
+
155
+
156
+ def abbreviate_weekdays(weekdays)
157
+ weekdays.map(&method(:abbreviate_weekday)).compact
158
+ end
159
+
160
+ def abbreviate_weekday(weekday)
161
+ WEEKDAY_MAP[weekday.to_s.downcase]
162
+ end
163
+
164
+ def backmap_weekdays(byday)
165
+ byday ||= []
166
+ byday.map(&method(:backmap_weekday)).compact
167
+ end
168
+
169
+ def backmap_weekday(byday)
170
+ Date::DAYNAMES[byday.wday]
171
+ end
172
+
173
+
174
+
175
+ WEEKDAY_MAP = {
176
+ "sunday" => "SU",
177
+ "monday" => "MO",
178
+ "tuesday" => "TU",
179
+ "wednesday" => "WE",
180
+ "thursday" => "TH",
181
+ "friday" => "FR",
182
+ "saturday" => "SA"
183
+ }
184
+
185
+
186
+
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,110 @@
1
+ require "hiccup/convenience"
2
+ require "active_support/concern"
3
+
4
+
5
+ module Hiccup
6
+ module Validatable
7
+ include Convenience
8
+ extend ActiveSupport::Concern
9
+
10
+
11
+ Kinds = [:never, :weekly, :monthly, :annually]
12
+
13
+
14
+ # !todo: use ActiveModel:Validation rather than a custom method
15
+ included do
16
+ validate :validate_recurrence
17
+ end
18
+
19
+
20
+ private
21
+
22
+
23
+ # !todo: use i18n to let clients of this library supply their own wording
24
+ def validate_recurrence
25
+ case kind
26
+ when :never;
27
+ when :weekly; validate_weekly_recurrence
28
+ when :monthly; validate_monthly_recurrence
29
+ when :annually;
30
+ else; invalid_kind!
31
+ end
32
+
33
+ errors.add(:start_date, "is a #{self.start_date.class} not a Date") unless self.start_date.is_a?(Date)
34
+ errors.add(:skip, "is not a positive integer") unless (skip.is_a? Fixnum) and (skip > 0)
35
+ if ends?
36
+ if self.end_date.is_a? Date
37
+ errors.add(:end_date, "cannot be before start") if (self.end_date < self.start_date)
38
+ else
39
+ errors.add(:end_date, "is a #{self.end_date.class} not a Date")
40
+ end
41
+ end
42
+ end
43
+
44
+
45
+ def validate_weekly_recurrence
46
+ if !pattern.is_a?(Array)
47
+ errors.add(:pattern, "is a #{pattern.class}. It should be an array")
48
+ elsif pattern.empty?
49
+ errors.add(:pattern, "is empty. It should contain a list of weekdays")
50
+ elsif (invalid_names = pattern - Date::DAYNAMES).any?
51
+ errors.add(:pattern, "should contain only weekdays. (#{invalid_names.to_sentence} are invalid)")
52
+ end
53
+ end
54
+
55
+
56
+ def validate_monthly_recurrence
57
+ if !pattern.is_a?(Array)
58
+ errors.add(:pattern, "is a #{pattern.class}. It should be an array")
59
+ elsif pattern.empty?
60
+ errors.add(:pattern, "is empty. It should contain a list of monthly occurrences")
61
+ elsif pattern.select(&method(:invalid_occurrence?)).any?
62
+ errors.add(:pattern, "contains invalid monthly occurrences")
63
+ end
64
+ end
65
+
66
+
67
+ def invalid_occurrence?(occurrence)
68
+ !valid_occurrence?(occurrence)
69
+ end
70
+
71
+ def valid_occurrence?(occurrence)
72
+ if occurrence.is_a?(Array)
73
+ i, wd = occurrence
74
+ Date::DAYNAMES.member?(wd) && i.is_a?(Fixnum) && ((i == -1) || (1..6).include?(i))
75
+ else
76
+ i = occurrence
77
+ i.is_a?(Fixnum) && (1..31).include?(i)
78
+ end
79
+ end
80
+
81
+
82
+ def invalid_kind!
83
+ errors.add(:kind, "is not recognized. It must be one of #{Kinds.collect{|kind| "'#{kind}'"}.to_sentence(:two_words_connector => " or ", :last_word_connector => ", or ")}.")
84
+ end
85
+
86
+
87
+ # def valid_occurrence?(occurrence)
88
+ # if occurrence.is_a?(Array)
89
+ # ordinal, kind = occurrence
90
+ #
91
+ # errors.add(:kind, "is not a valid monthly occurrence kind") unless Date::DAYNAMES.member?(kind)
92
+ # if ordinal.is_a?(Fixnum)
93
+ # errors.add(:ordinal, "is not a valid integer") unless (ordinal==-1) or (1..6).include?(ordinal)
94
+ # else
95
+ # errors.add(:ordinal, "is not an integer")
96
+ # end
97
+ # else
98
+ # ordinal = occurrence
99
+ #
100
+ # if ordinal.is_a?(Fixnum)
101
+ # errors.add(:ordinal, "is not an integer between 1 and 31") unless (1..31).include?(ordinal)
102
+ # else
103
+ # errors.add(:ordinal, "is not an integer")
104
+ # end
105
+ # end
106
+ # end
107
+
108
+
109
+ end
110
+ end