hiccup 0.0.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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