tickle 1.0.2 → 2.0.0rc3

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,58 @@
1
+ module Tickle
2
+
3
+ require 'texttube/filterable'
4
+ module Filters
5
+
6
+ extend TextTube::Filterable
7
+ # Normalize natural string removing prefix language
8
+ filter_with :remove_prefix do |text|
9
+ text.gsub(/every(\s)?/, '')
10
+ .gsub(/each(\s)?/, '')
11
+ .gsub(/repeat(s|ing)?(\s)?/, '')
12
+ .gsub(/on the(\s)?/, '')
13
+ .gsub(/([^\w\d\s])+/, '')
14
+ .downcase.strip
15
+ text
16
+ end
17
+
18
+
19
+ # Converts natural language US Holidays into a date expression to be
20
+ # parsed.
21
+ filter_with :normalize_us_holidays do |text|
22
+ normalized_text = text.to_s.downcase
23
+ normalized_text.gsub(/\bnew\syear'?s?(\s)?(day)?\b/){|md| $1 }
24
+ .gsub(/\bnew\syear'?s?(\s)?(eve)?\b/){|md| $1 }
25
+ .gsub(/\bm(artin\s)?l(uther\s)?k(ing)?(\sday)?\b/){|md| $1 }
26
+ .gsub(/\binauguration(\sday)?\b/){|md| $1 }
27
+ .gsub(/\bpresident'?s?(\sday)?\b/){|md| $1 }
28
+ .gsub(/\bmemorial\sday\b/){|md| $1 }
29
+ .gsub(/\bindepend(e|a)nce\sday\b/){|md| $1 }
30
+ .gsub(/\blabor\sday\b/){|md| $1 }
31
+ .gsub(/\bcolumbus\sday\b/){|md| $1 }
32
+ .gsub(/\bveterans?\sday\b/){|md| $1 }
33
+ .gsub(/\bthanksgiving(\sday)?\b/){|md| $1 }
34
+ .gsub(/\bchristmas\seve\b/){|md| $1 }
35
+ .gsub(/\bchristmas(\sday)?\b/){|md| $1 }
36
+ .gsub(/\bsuper\sbowl(\ssunday)?\b/){|md| $1 }
37
+ .gsub(/\bgroundhog(\sday)?\b/){|md| $1 }
38
+ .gsub(/\bvalentine'?s?(\sday)?\b/){|md| $1 }
39
+ .gsub(/\bs(ain)?t\spatrick'?s?(\sday)?\b/){|md| $1 }
40
+ .gsub(/\bapril\sfool'?s?(\sday)?\b/){|md| $1 }
41
+ .gsub(/\bearth\sday\b/){|md| $1 }
42
+ .gsub(/\barbor\sday\b/){|md| $1 }
43
+ .gsub(/\bcinco\sde\smayo\b/){|md| $1 }
44
+ .gsub(/\bmother'?s?\sday\b/){|md| $1 }
45
+ .gsub(/\bflag\sday\b/){|md| $1 }
46
+ .gsub(/\bfather'?s?\sday\b/){|md| $1 }
47
+ .gsub(/\bhalloween\b/){|md| $1 }
48
+ .gsub(/\belection\sday\b/){|md| $1 }
49
+ .gsub(/\bkwanzaa\b/){|md| $1 }
50
+ normalized_text
51
+ end
52
+
53
+ # filter_with :strip do |text|
54
+ # text.strip
55
+ # end
56
+
57
+ end
58
+ end
@@ -1,129 +1,272 @@
1
- module Tickle #:nodoc:
2
- class << self #:nodoc:
1
+ module Tickle
2
+
3
+ require_relative "helpers.rb"
4
+ require_relative "token.rb"
5
+
3
6
 
4
7
  # The heavy lifting. Goes through each token groupings to determine what natural language should either by
5
8
  # parsed by Chronic or returned. This methodology makes extension fairly simple, as new token types can be
6
9
  # easily added in repeater and then processed by the guess method
7
10
  #
8
- def guess()
9
- return nil if @tokens.empty?
11
+ def self.guess(tokens, start)
12
+ return nil if tokens.empty?
10
13
 
11
- guess_unit_types
12
- guess_weekday unless @next
13
- guess_month_names unless @next
14
- guess_number_and_unit unless @next
15
- guess_ordinal unless @next
16
- guess_ordinal_and_unit unless @next
17
- guess_special unless @next
14
+ _next = catch(:guessed) {
15
+ %w{guess_unit_types guess_weekday guess_month_names guess_number_and_unit guess_ordinal guess_ordinal_and_unit guess_special}.each do |meth| # TODO pick better enumerator
16
+ send meth, tokens, start
17
+ end
18
+ nil # stop each sending the array to _next
19
+ }
18
20
 
19
21
  # check to see if next is less than now and, if so, set it to next year
20
- @next = Time.local(@next.year + 1, @next.month, @next.day, @next.hour, @next.min, @next.sec) if @next && @next.to_date < @start.to_date
21
-
22
+ if _next &&
23
+ _next.to_date < start.to_date
24
+ _next = Time.local(_next.year + 1, _next.month, _next.day, _next.hour, _next.min, _next.sec)
25
+ end
22
26
  # return the next occurrence
23
- return @next.to_time if @next
27
+ _next.to_time if _next
24
28
  end
25
29
 
26
- def guess_unit_types
27
- @next = @start.bump(:day) if token_types.same?([:day])
28
- @next = @start.bump(:week) if token_types.same?([:week])
29
- @next = @start.bump(:month) if token_types.same?([:month])
30
- @next = @start.bump(:year) if token_types.same?([:year])
30
+
31
+ def self.guess_unit_types( tokens, start)
32
+ [:sec,:day,:week,:month,:year].each {|unit|
33
+ if Token.types(tokens).same?([unit])
34
+ throw :guessed, start.bump(unit)
35
+ end
36
+ }
37
+ nil
31
38
  end
32
39
 
33
- def guess_weekday
34
- @next = chronic_parse_with_start("#{token_of_type(:weekday).start.to_s}") if token_types.same?([:weekday])
40
+
41
+ def self.guess_weekday( tokens, start)
42
+ if Token.types(tokens).same? [:weekday]
43
+ throw :guessed, chronic_parse_with_start(
44
+ "#{Token.token_of_type(:weekday,tokens).start.to_s}", start
45
+ )
46
+ end
47
+ nil
35
48
  end
36
49
 
37
- def guess_month_names
38
- @next = chronic_parse_with_start("#{Date::MONTHNAMES[token_of_type(:month_name).start]} 1") if token_types.same?([:month_name])
50
+
51
+ def self.guess_month_names( tokens, start)
52
+ if Token.types(tokens).same? [:month_name]
53
+ throw :guessed, chronic_parse_with_start(
54
+ "#{Date::MONTHNAMES[Token.token_of_type(:month_name,tokens).start]} 1", start
55
+ )
56
+ end
57
+ nil
39
58
  end
40
59
 
41
- def guess_number_and_unit
42
- @next = @start.bump(:day, token_of_type(:number).interval) if token_types.same?([:number, :day])
43
- @next = @start.bump(:week, token_of_type(:number).interval) if token_types.same?([:number, :week])
44
- @next = @start.bump(:month, token_of_type(:number).interval) if token_types.same?([:number, :month])
45
- @next = @start.bump(:year, token_of_type(:number).interval) if token_types.same?([:number, :year])
46
- @next = chronic_parse_with_start("#{token_of_type(:month_name).word} #{token_of_type(:number).start}") if token_types.same?([:number, :month_name])
47
- @next = chronic_parse_with_start("#{token_of_type(:specific_year).word}-#{token_of_type(:month_name).start}-#{token_of_type(:number).start}") if token_types.same?([:number, :month_name, :specific_year])
60
+
61
+ def self.guess_number_and_unit( tokens, start)
62
+ _next =
63
+ [:sec,:day,:week,:month,:year].each {|unit|
64
+ if Token.types(tokens).same?([:number, unit])
65
+ throw :guessed, start.bump( unit, Token.token_of_type(:number,tokens).interval )
66
+ end
67
+ }
68
+
69
+ if Token.types(tokens).same?([:number, :month_name])
70
+ throw :guessed, chronic_parse_with_start(
71
+ "#{Token.token_of_type(:month_name,tokens, start).word} #{Token.token_of_type(:number,tokens).start}", start
72
+ )
73
+ end
74
+
75
+ if Token.types(tokens).same?([:number, :month_name, :specific_year])
76
+ throw :guessed, chronic_parse_with_start(
77
+ [
78
+ Token.token_of_type(:specific_year,tokens, start).word,
79
+ Token.token_of_type(:month_name,tokens).start,
80
+ Token.token_of_type(:number,tokens).start
81
+ ].join("_"), start
82
+ )
83
+ end
84
+ nil
48
85
  end
49
86
 
50
- def guess_ordinal
51
- @next = handle_same_day_chronic_issue(@start.year, @start.month, token_of_type(:ordinal).start) if token_types.same?([:ordinal])
87
+
88
+ def self.guess_ordinal( tokens, start)
89
+ if Token.types(tokens).same?([:ordinal])
90
+ throw :guessed, handle_same_day_chronic_issue(
91
+ start.year, start.month, Token.token_of_type(:ordinal,tokens).start, start
92
+ )
93
+ end
94
+ nil
52
95
  end
53
96
 
54
- def guess_ordinal_and_unit
55
- @next = handle_same_day_chronic_issue(@start.year, token_of_type(:month_name).start, token_of_type(:ordinal).start) if token_types.same?([:ordinal, :month_name])
56
- @next = handle_same_day_chronic_issue(@start.year, @start.month, token_of_type(:ordinal).start) if token_types.same?([:ordinal, :month])
57
- @next = handle_same_day_chronic_issue(token_of_type(:specific_year).word, token_of_type(:month_name).start, token_of_type(:ordinal).start) if token_types.same?([:ordinal, :month_name, :specific_year])
58
97
 
59
- if token_types.same?([:ordinal, :weekday, :month_name])
60
- @next = chronic_parse_with_start("#{token_of_type(:ordinal).word} #{token_of_type(:weekday).start.to_s} in #{Date::MONTHNAMES[token_of_type(:month_name).start]}")
61
- @next = handle_same_day_chronic_issue(@start.year, token_of_type(:month_name).start, token_of_type(:ordinal).start) if @next.to_date == @start.to_date
98
+ def self.guess_ordinal_and_unit( tokens, start)
99
+ if Token.types(tokens).same?([:ordinal, :month_name])
100
+ throw :guessed, handle_same_day_chronic_issue(
101
+ start.year, Token.token_of_type(:month_name,tokens).start, Token.token_of_type(:ordinal,tokens).start, start
102
+ )
103
+ nil
104
+ end
105
+
106
+ if Token.types(tokens).same?([:ordinal, :month])
107
+ throw :guessed, handle_same_day_chronic_issue(
108
+ start.year,
109
+ start.month,
110
+ Token.token_of_type(:ordinal,tokens).start,
111
+ start
112
+ )
113
+ nil
62
114
  end
63
115
 
64
- if token_types.same?([:ordinal, :weekday, :month])
65
- @next = chronic_parse_with_start("#{token_of_type(:ordinal).word} #{token_of_type(:weekday).start.to_s} in #{Date::MONTHNAMES[get_next_month(token_of_type(:ordinal).start)]}")
66
- @next = handle_same_day_chronic_issue(@start.year, @start.month, token_of_type(:ordinal).start) if @next.to_date == @start.to_date
116
+ if Token.types(tokens).same?([:ordinal, :month_name, :specific_year])
117
+ throw :guessed, handle_same_day_chronic_issue(
118
+ Token.token_of_type(:specific_year,tokens).word, Token.token_of_type(:month_name,tokens).start, Token.token_of_type(:ordinal,tokens).start, start
119
+ )
120
+ nil
67
121
  end
122
+
123
+ if Token.types(tokens).same?([:ordinal, :weekday, :month_name])
124
+ _next = chronic_parse_with_start(
125
+ "#{Token.token_of_type(:ordinal,tokens).word} #{Token.token_of_type(:weekday,tokens).start.to_s} in #{Date::MONTHNAMES[Token.token_of_type(:month_name,tokens).start]}", start
126
+ )
127
+ if _next.to_date == start.to_date
128
+ throw :guessed, handle_same_day_chronic_issue(start.year, Token.token_of_type(:month_name,tokens).start, Token.token_of_type(:ordinal,tokens).start, start)
129
+ end
130
+ throw :guessed, _next
131
+ nil
132
+ end
133
+
134
+ if Token.types(tokens).same?([:ordinal, :weekday, :month])
135
+ _next = chronic_parse_with_start(
136
+ "#{Token.token_of_type(:ordinal,tokens).word} #{Token.token_of_type(:weekday,tokens).start.to_s} in #{Date::MONTHNAMES[get_next_month(Token.token_of_type(:ordinal,tokens).start)]}", start
137
+ )
138
+ _next =
139
+ if _next.to_date == start.to_date
140
+ handle_same_day_chronic_issue(
141
+ start.year, start.month, Token.token_of_type(:ordinal,tokens).start, start
142
+ )
143
+ else
144
+ _next
145
+ end
146
+ throw :guessed, _next
147
+ nil
148
+ end
149
+ nil
68
150
  end
69
151
 
70
- def guess_special
71
- guess_special_other
72
- guess_special_beginning unless @next
73
- guess_special_middle unless @next
74
- guess_special_end unless @next
152
+
153
+ def self.guess_special( tokens, start)
154
+ guess_special_other tokens, start
155
+ guess_special_beginning tokens, start
156
+ guess_special_middle tokens, start
157
+ guess_special_end tokens, start
158
+ nil
75
159
  end
76
160
 
77
161
  private
78
162
 
79
- def guess_special_other
80
- @next = @start.bump(:day, 2) if token_types.same?([:special, :day]) && token_of_type(:special).start == :other
81
- @next = @start.bump(:week, 2) if token_types.same?([:special, :week]) && token_of_type(:special).start == :other
82
- @next = chronic_parse_with_start('2 months from now') if token_types.same?([:special, :month]) && token_of_type(:special).start == :other
83
- @next = chronic_parse_with_start('2 years from now') if token_types.same?([:special, :year]) && token_of_type(:special).start == :other
84
- end
163
+ def self.guess_special_other( tokens, start)
164
+ if Token.types(tokens).same?([:special, :day]) &&
165
+ Token.token_of_type(:special, tokens).start == :other
166
+ throw :guessed, start.bump(:day, 2)
167
+ nil
168
+ end
169
+
170
+ if Token.types(tokens).same?([:special, :week]) &&
171
+ Token.token_of_type(:special, tokens).start == :other
172
+ throw :guessed, start.bump(:week, 2)
173
+ nil
174
+ end
175
+
176
+ if Token.types(tokens).same?([:special, :month]) &&
177
+ Token.token_of_type(:special, tokens).start == :other
178
+ throw :guessed, chronic_parse_with_start('2 months from now', start)
179
+ nil
180
+ end
85
181
 
86
- def guess_special_beginning
87
- if token_types.same?([:special, :week]) && token_of_type(:special).start == :beginning then @next = chronic_parse_with_start('Sunday'); end
88
- if token_types.same?([:special, :month]) && token_of_type(:special).start == :beginning then @next = Date.civil(@start.year, @start.month + 1, 1); end
89
- if token_types.same?([:special, :year]) && token_of_type(:special).start == :beginning then @next = Date.civil(@start.year+1, 1, 1); end
182
+ if Token.types(tokens).same?([:special, :year]) &&
183
+ Token.token_of_type(:special, tokens).start == :other
184
+ throw :guessed, chronic_parse_with_start('2 years from now', start)
185
+ nil
186
+ end
187
+ nil
90
188
  end
91
189
 
92
- def guess_special_end
93
- if token_types.same?([:special, :week]) && token_of_type(:special).start == :end then @next = chronic_parse_with_start('Saturday'); end
94
- if token_types.same?([:special, :month]) && token_of_type(:special).start == :end then @next = Date.civil(@start.year, @start.month, -1); end
95
- if token_types.same?([:special, :year]) && token_of_type(:special).start == :end then @next = Date.new(@start.year, 12, 31); end
190
+
191
+ def self.guess_special_beginning( tokens, start)
192
+ if Token.types(tokens).same?([:special, :week]) &&
193
+ Token.token_of_type(:special, tokens).start == :beginning
194
+ throw :guessed, chronic_parse_with_start('Sunday', start)
195
+ nil
196
+ end
197
+ if Token.types(tokens).same?([:special, :month]) &&
198
+ Token.token_of_type(:special, tokens).start == :beginning
199
+ throw :guessed, Date.civil(start.year, start.month + 1, 1)
200
+ nil
201
+ end
202
+ if Token.types(tokens).same?([:special, :year]) &&
203
+ Token.token_of_type(:special, tokens).start == :beginning
204
+ throw :guessed, Date.civil(start.year+1, 1, 1)
205
+ nil
206
+ end
207
+ nil
96
208
  end
97
209
 
98
- def guess_special_middle
99
- if token_types.same?([:special, :week]) && token_of_type(:special).start == :middle then @next = chronic_parse_with_start('Wednesday'); end
100
- if token_types.same?([:special, :month]) && token_of_type(:special).start == :middle then
101
- @next = (@start.day > 15 ? Date.civil(@start.year, @start.month + 1, 15) : Date.civil(@start.year, @start.month, 15))
210
+ def self.guess_special_middle( tokens, start)
211
+ if Token.types(tokens).same?([:special, :week]) &&
212
+ Token.token_of_type(:special, tokens).start == :middle
213
+ throw :guessed, chronic_parse_with_start('Wednesday', start)
214
+ nil
102
215
  end
103
- if token_types.same?([:special, :year]) && token_of_type(:special).start == :middle then
104
- @next = (@start.day > 15 && @start.month > 6 ? Date.new(@start.year+1, 6, 15) : Date.new(@start.year, 6, 15))
216
+
217
+ if Token.types(tokens).same?([:special, :month]) &&
218
+ Token.token_of_type(:special, tokens).start == :middle
219
+ _next = start.day > 15 ?
220
+ Date.civil(start.year, start.month + 1, 15) :
221
+ Date.civil(start.year, start.month, 15)
222
+ throw :guessed, _next
223
+ nil
224
+ end
225
+
226
+ if Token.types(tokens).same?([:special, :year]) &&
227
+ Token.token_of_type(:special, tokens).start == :middle
228
+ _next =
229
+ start.day > 15 && start.month > 6 ?
230
+ Date.new(start.year+1, 6, 15) :
231
+ Date.new(start.year, 6, 15)
232
+ throw :guessed, _next
233
+ nil
105
234
  end
235
+ nil
106
236
  end
107
237
 
108
- def token_of_type(type)
109
- @tokens.detect {|token| token.type == type}
238
+
239
+ def self.guess_special_end( tokens, start)
240
+ if Token.types(tokens).same?([:special, :week]) &&
241
+ (Token.token_of_type(:special, tokens).start == :end)
242
+ throw :guessed, chronic_parse_with_start('Saturday', start)
243
+ nil
244
+ end
245
+ if Token.types(tokens).same?([:special, :month]) &&
246
+ (Token.token_of_type(:special, tokens).start == :end)
247
+ throw :guessed, Date.civil(start.year, start.month, -1)
248
+ nil
249
+ end
250
+ if Token.types(tokens).same?([:special, :year]) &&
251
+ (Token.token_of_type(:special, tokens).start == :end)
252
+ throw :guessed, Date.new(start.year, 12, 31)
253
+ nil
254
+ end
255
+ nil
110
256
  end
111
257
 
112
- private
113
258
 
114
259
  # runs Chronic.parse with now being set to the specified start date for Tickle parsing
115
- def chronic_parse_with_start(exp)
116
- Tickle.dwrite("date expression: #{exp}, start: #{@start}")
117
- Chronic.parse(exp, :now => @start)
260
+ def self.chronic_parse_with_start(exp,start)
261
+ Chronic.parse(exp, :now => start)
118
262
  end
119
263
 
120
264
  # needed to handle the unique situation where a number or ordinal plus optional month or month name is passed that is EQUAL to the start date since Chronic returns that day.
121
- def handle_same_day_chronic_issue(year, month, day)
122
- Tickle.dwrite("year (#{year}), month (#{month}), day (#{day})")
123
- arg_date = (Date.new(year.to_i, month.to_i, day.to_i) == @start.to_date) ? Time.local(year, month+1, day) : Time.local(year, month, day)
124
- return arg_date
265
+ def self.handle_same_day_chronic_issue(year, month, day, start)
266
+ arg_date =
267
+ Date.new(year.to_i, month.to_i, day.to_i) == start.to_date ?
268
+ Time.local(year, month+1, day) :
269
+ Time.local(year, month, day)
270
+ arg_date
125
271
  end
126
-
127
-
128
- end
129
272
  end
@@ -0,0 +1,51 @@
1
+ module Tickle
2
+
3
+ require_relative "token.rb"
4
+
5
+ # static methods that are used across classes.
6
+ module Helpers
7
+
8
+ # Returns the next available month based on the current day of the month.
9
+ # For example, if get_next_month(15) is called and the start date is the 10th, then it will return the 15th of this month.
10
+ # However, if get_next_month(15) is called and the start date is the 18th, it will return the 15th of next month.
11
+ def self.get_next_month(number,start=nil)
12
+ start ||= @start || Time.now
13
+ month =
14
+ if number.to_i < start.day
15
+ start.month == 12 ?
16
+ 1 :
17
+ start.month + 1
18
+ else
19
+ start.month
20
+ end
21
+ end
22
+
23
+
24
+
25
+ # Return the number of days in a specified month.
26
+ # If no month is specified, current month is used.
27
+ def self.days_in_month(month=nil)
28
+ month ||= Date.today.month
29
+ days_in_mon = Date.civil(Date.today.year, month, -1).day
30
+ end
31
+
32
+
33
+ # Turns compound numbers, like 'twenty first' => 21
34
+ def self.combine_multiple_numbers(tokens)
35
+ if Token.types(tokens).include?(:number) &&
36
+ Token.types(tokens).include?(:ordinal)
37
+ number = Token.token_of_type(:number, tokens)
38
+ ordinal = Token.token_of_type(:ordinal, tokens)
39
+ combined_original = "#{number.original} #{ordinal.original}"
40
+ combined_word = (number.start.to_s[0] + ordinal.word)
41
+ combined_value = (number.start.to_s[0] + ordinal.start.to_s)
42
+ new_number_token = Token.new(combined_original, word: combined_word, type: :ordinal, start: combined_value, interval: 365)
43
+ tokens.reject! {|token| (token.type == :number || token.type == :ordinal)}
44
+ tokens << new_number_token
45
+ end
46
+ tokens
47
+ end
48
+
49
+
50
+ end # Helpers
51
+ end