tickle 1.2.0 → 2.0.0.rc1

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