tickle 1.0.2 → 2.0.0rc3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +3 -1
- data/.travis.yml +12 -0
- data/CHANGES.md +25 -3
- data/Gemfile +14 -1
- data/LICENCE +1 -1
- data/README.md +1 -1
- data/Rakefile +3 -8
- data/benchmarks/main.rb +71 -0
- data/lib/ext/array.rb +6 -0
- data/lib/ext/date_and_time.rb +64 -0
- data/lib/ext/string.rb +39 -0
- data/lib/tickle.rb +23 -113
- data/lib/tickle/filters.rb +58 -0
- data/lib/tickle/handler.rb +223 -80
- data/lib/tickle/helpers.rb +51 -0
- data/lib/tickle/patterns.rb +115 -0
- data/lib/tickle/repeater.rb +232 -116
- data/lib/tickle/tickle.rb +97 -201
- data/lib/tickle/tickled.rb +173 -0
- data/lib/tickle/token.rb +94 -0
- data/lib/tickle/version.rb +2 -2
- data/spec/helpers_spec.rb +36 -0
- data/spec/patterns_spec.rb +240 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/tickle_spec.rb +551 -0
- data/spec/token_spec.rb +82 -0
- data/tickle.gemspec +4 -9
- metadata +34 -74
- data/lib/numerizer/numerizer.rb +0 -103
@@ -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
|
data/lib/tickle/handler.rb
CHANGED
@@ -1,129 +1,272 @@
|
|
1
|
-
module Tickle
|
2
|
-
|
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
|
11
|
+
def self.guess(tokens, start)
|
12
|
+
return nil if tokens.empty?
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
27
|
+
_next.to_time if _next
|
24
28
|
end
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
51
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
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
|
65
|
-
|
66
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
93
|
-
|
94
|
-
if
|
95
|
-
|
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
|
100
|
-
|
101
|
-
|
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
|
-
|
104
|
-
|
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
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
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
|