tickle 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +121 -15
- data/Rakefile +1 -1
- data/SCENARIOS.rdoc +12 -0
- data/VERSION +1 -1
- data/lib/tickle.rb +23 -2
- data/lib/tickle/handler.rb +98 -0
- data/lib/tickle/repeater.rb +85 -0
- data/lib/tickle/tickle.rb +73 -12
- data/test/helper.rb +2 -1
- data/test/test_parsing.rb +76 -0
- data/tickle.gemspec +63 -0
- metadata +14 -8
- data/test/test_tickle.rb +0 -7
data/README.rdoc
CHANGED
@@ -20,34 +20,140 @@ $ gem install tickle
|
|
20
20
|
|
21
21
|
Everything's at Github - http://github.com/noctivityinc/tickle
|
22
22
|
|
23
|
+
-- DEPENDENCIES
|
24
|
+
|
25
|
+
chronic gem (gem install chronic)
|
26
|
+
thoughtbot's shoulda (gem install shoulda)
|
27
|
+
|
23
28
|
== USAGE
|
24
29
|
|
25
30
|
You can parse strings containing a natural language interval using the Tickle.parse method.
|
26
31
|
|
32
|
+
Tickle.parse returns an array of first occurrence, next occurrence, interval between occurrences.
|
33
|
+
|
34
|
+
You can also pass a start date with the word "starting" (e.g. Tickle.parse('every 3 days starting next friday'))
|
35
|
+
|
36
|
+
Tickle HEAVILY uses chronic for parsing both the event and the start date.
|
37
|
+
|
38
|
+
-- EXAMPLES
|
39
|
+
|
27
40
|
require 'rubygems'
|
28
41
|
require 'tickle'
|
29
42
|
|
30
|
-
Time.now
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
Time.now
|
44
|
+
2010-04-22 16:38:12 -0400
|
45
|
+
|
46
|
+
Tickle.parse('each day')
|
47
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 1]
|
48
|
+
|
49
|
+
Tickle.parse('every day')
|
50
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 1]
|
51
|
+
|
52
|
+
Tickle.parse('every week')
|
53
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 7]
|
54
|
+
|
55
|
+
Tickle.parse('every Month')
|
56
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 30]
|
57
|
+
|
58
|
+
Tickle.parse('every year')
|
59
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 365]
|
60
|
+
|
61
|
+
Tickle.parse('daily')
|
62
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 1]
|
63
|
+
|
64
|
+
Tickle.parse('weekly')
|
65
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 7]
|
66
|
+
|
67
|
+
Tickle.parse('monthly')
|
68
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 30]
|
69
|
+
|
70
|
+
Tickle.parse('yearly')
|
71
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 365]
|
72
|
+
|
73
|
+
Tickle.parse('every 3 days')
|
74
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 3]
|
75
|
+
|
76
|
+
Tickle.parse('every 3 weeks')
|
77
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 21]
|
78
|
+
|
79
|
+
Tickle.parse('every 3 months')
|
80
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 90]
|
81
|
+
|
82
|
+
Tickle.parse('every 3 years')
|
83
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 1095]
|
84
|
+
|
85
|
+
Tickle.parse('every other day')
|
86
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 2]
|
87
|
+
|
88
|
+
Tickle.parse('every other week')
|
89
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 14]
|
90
|
+
|
91
|
+
Tickle.parse('every other month')
|
92
|
+
#=> [2010-04-22 16:38:12 -0400, 2010-06-22 16:38:12 -0400, 60]
|
93
|
+
|
94
|
+
Tickle.parse('every other year')
|
95
|
+
#=> [2010-04-22 16:38:12 -0400, 2012-04-22 16:38:12 -0400, 730]
|
96
|
+
|
97
|
+
Tickle.parse('every other day starting May 1st')
|
98
|
+
#=> [2010-05-01 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 2]
|
99
|
+
|
100
|
+
Tickle.parse('every other week starting this Sunday')
|
101
|
+
#=> [2010-04-25 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 14]
|
102
|
+
|
103
|
+
Tickle.parse('every Monday')
|
104
|
+
#=> [2010-04-26 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]
|
105
|
+
|
106
|
+
Tickle.parse('every Wednesday')
|
107
|
+
#=> [2010-04-28 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]
|
108
|
+
|
109
|
+
Tickle.parse('every Friday')
|
110
|
+
#=> [2010-04-23 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]
|
111
|
+
|
112
|
+
Tickle.parse('every May')
|
113
|
+
#=> [2010-05-01 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]
|
114
|
+
|
115
|
+
Tickle.parse('every june')
|
116
|
+
#=> [2010-06-01 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]
|
117
|
+
|
118
|
+
Tickle.parse('beginning of the week')
|
119
|
+
#=> [2010-04-25 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]
|
120
|
+
|
121
|
+
Tickle.parse('middle of the week')
|
122
|
+
#=> [2010-04-28 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]
|
123
|
+
|
124
|
+
Tickle.parse('end of the week')
|
125
|
+
#=> [2010-04-24 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]
|
126
|
+
|
127
|
+
Tickle.parse('beginning of the month')
|
128
|
+
#=> [2010-05-01 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]
|
129
|
+
|
130
|
+
Tickle.parse('middle of the month')
|
131
|
+
#=> [2010-05-15 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]
|
132
|
+
|
133
|
+
Tickle.parse('end of the month')
|
134
|
+
#=> [2010-04-30 00:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]
|
135
|
+
|
136
|
+
Tickle.parse('beginning of the year')
|
137
|
+
#=> [2011-01-01 12:00:00 -0500, 2012-04-22 16:38:12 -0400, 365]
|
138
|
+
|
139
|
+
Tickle.parse('middle of the year')
|
140
|
+
#=> [2010-06-15 00:00:00 -0400, 2012-04-22 16:38:12 -0400, 365]
|
141
|
+
|
142
|
+
Tickle.parse('end of the year')
|
143
|
+
#=> [2010-12-31 00:00:00 -0500, 2012-04-22 16:38:12 -0400, 365]
|
43
144
|
|
44
145
|
|
45
|
-
You can either pass a string prefixed with the word "every" or simply the time frame.
|
146
|
+
You can either pass a string prefixed with the word "every, each or 'on the'" or simply the time frame.
|
46
147
|
|
47
148
|
-- LIMITATIONS
|
48
149
|
|
49
|
-
Currently, Tickle only works for day intervals but feel free to fork and add time-based interval support
|
150
|
+
Currently, Tickle only works for day intervals but feel free to fork and add time-based interval support or send me a note if you really want me to add it.
|
151
|
+
|
152
|
+
== CREDIT
|
153
|
+
|
154
|
+
HUGE shout-out to both the creator of Chronic, Tom Preston-Werner (http://chronic.rubyforge.org/) as well as Brian Brownling who maintains a github version at http://github.com/mojombo/chronic.
|
50
155
|
|
156
|
+
Without their work and code structure I'd be lost.
|
51
157
|
|
52
158
|
|
53
159
|
== Note on Patches/Pull Requests
|
data/Rakefile
CHANGED
@@ -11,7 +11,7 @@ begin
|
|
11
11
|
gem.homepage = "http://github.com/noctivityinc/tickle"
|
12
12
|
gem.authors = ["Joshua Lippiner"]
|
13
13
|
gem.add_dependency('chronic', '>= 0.2.3')
|
14
|
-
gem.add_development_dependency "
|
14
|
+
gem.add_development_dependency "shoulda", ">= 2.10.3"
|
15
15
|
|
16
16
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
17
|
end
|
data/SCENARIOS.rdoc
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
every 2 weeks => word 'week' found plus a number so the interval = number * 7 and start_date = today
|
2
|
+
every second tuesday => => day of week found WITH number (ordinal converted to 2). interval = 30 with start_date = Chronic.parse(Ordinal Number Day Of Week 'in' Start Date Month)
|
3
|
+
every sunday => day of week found without number. interval = 7, start_date = next day of week occurrence
|
4
|
+
every other day => word 'day' found with word 'other.' interval = 2, start_date = today
|
5
|
+
every fourth thursday => day of week found WITH number (ordinal converted to 4). interval = 30 with start_date = next occurrence of 'event' as parsed by chronic
|
6
|
+
on the 15th of each month => 'each month' becomes interval = 30, number found + start month through chronic equals start date
|
7
|
+
on the 15th of November => month found with number. interval = 365, start_date = Chronic.parse(month + number)
|
8
|
+
on the second monday in April => month, day and number found. interval = 365, start_date = Chronic.parse(ordinal number form of number, day of week, month)
|
9
|
+
every November 15th => month found with number. interval = 365, start_date = Chronic.parse(month + number)
|
10
|
+
every day => word 'day' found without a number. interval = 1. start_day = today
|
11
|
+
every week => word 'week' found without a number. interval = 7. start_day = today
|
12
|
+
every month => word 'month' found without a number. interval = 30. start_day = today
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/lib/tickle.rb
CHANGED
@@ -8,13 +8,34 @@
|
|
8
8
|
|
9
9
|
$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
|
10
10
|
|
11
|
+
require 'date'
|
12
|
+
require 'time'
|
11
13
|
require 'chronic'
|
12
|
-
require 'numerizer/numerizer'
|
13
14
|
|
14
15
|
require 'tickle/tickle'
|
16
|
+
require 'tickle/handler'
|
17
|
+
require 'tickle/repeater'
|
15
18
|
|
16
19
|
module Tickle
|
17
|
-
VERSION = "0.0.
|
20
|
+
VERSION = "0.0.2"
|
18
21
|
|
19
22
|
def self.debug; false; end
|
23
|
+
|
24
|
+
def self.dwrite(msg)
|
25
|
+
puts msg if Tickle.debug
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Date
|
30
|
+
def days_in_month
|
31
|
+
d,m,y = mday,month,year
|
32
|
+
d += 1 while Date.valid_civil?(y,m,d)
|
33
|
+
d - 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Array
|
38
|
+
def same?(y)
|
39
|
+
self.sort == y.sort
|
40
|
+
end
|
20
41
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Tickle
|
2
|
+
class << self
|
3
|
+
|
4
|
+
def guess()
|
5
|
+
interval = guess_unit_types
|
6
|
+
interval ||= guess_weekday
|
7
|
+
interval ||= guess_weekday
|
8
|
+
interval ||= guess_month_names
|
9
|
+
interval ||= guess_number_and_unit
|
10
|
+
interval ||= guess_special
|
11
|
+
|
12
|
+
# defines the next occurrence of this tickle if not set in a guess routine
|
13
|
+
@next ||= @start + (interval * 60 * 60 * 24) if interval
|
14
|
+
return [@start.to_time, @next.to_time, interval] if interval
|
15
|
+
end
|
16
|
+
|
17
|
+
def guess_unit_types
|
18
|
+
interval = 1 if token_types.same?([:day])
|
19
|
+
interval = 7 if token_types.same?([:week])
|
20
|
+
interval = 30 if token_types.same?([:month])
|
21
|
+
interval = 365 if token_types.same?([:year])
|
22
|
+
interval
|
23
|
+
end
|
24
|
+
|
25
|
+
def guess_weekday
|
26
|
+
if token_types.same?([:weekday]) then
|
27
|
+
@start = Chronic.parse(token_of_type(:weekday).start.to_s)
|
28
|
+
interval = 7
|
29
|
+
end
|
30
|
+
interval
|
31
|
+
end
|
32
|
+
|
33
|
+
def guess_month_names
|
34
|
+
if token_types.same?([:month_name]) then
|
35
|
+
@start = Chronic.parse("#{token_of_type(:month_name).start.to_s} 1")
|
36
|
+
interval = 30
|
37
|
+
end
|
38
|
+
interval
|
39
|
+
end
|
40
|
+
|
41
|
+
def guess_number_and_unit
|
42
|
+
interval = token_of_type(:number).interval if token_types.same?([:number, :day])
|
43
|
+
interval = (token_of_type(:number).interval * 7) if token_types.same?([:number, :week])
|
44
|
+
interval = (token_of_type(:number).interval * 30) if token_types.same?([:number, :month])
|
45
|
+
interval = (token_of_type(:number).interval * 365) if token_types.same?([:number, :year])
|
46
|
+
interval
|
47
|
+
end
|
48
|
+
|
49
|
+
def guess_special
|
50
|
+
interval = guess_special_other
|
51
|
+
interval ||= guess_special_beginning
|
52
|
+
interval ||= guess_special_middle
|
53
|
+
interval ||= guess_special_end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def guess_special_other
|
59
|
+
interval = 2 if token_types.same?([:special, :day]) && token_of_type(:special).start == :other
|
60
|
+
interval = 14 if token_types.same?([:special, :week]) && token_of_type(:special).start == :other
|
61
|
+
if token_types.same?([:special, :month]) && token_of_type(:special).start == :other then interval = 60; @next = Chronic.parse('2 months from now'); end
|
62
|
+
if token_types.same?([:special, :year]) && token_of_type(:special).start == :other then interval = 730; @next = Chronic.parse('2 years from now'); end
|
63
|
+
interval
|
64
|
+
end
|
65
|
+
|
66
|
+
def guess_special_beginning
|
67
|
+
if token_types.same?([:special, :week]) && token_of_type(:special).start == :beginning then interval = 7; @start = Chronic.parse('Sunday'); end
|
68
|
+
if token_types.same?([:special, :month]) && token_of_type(:special).start == :beginning then interval = 30; @start = Chronic.parse('1st day next month'); end
|
69
|
+
if token_types.same?([:special, :year]) && token_of_type(:special).start == :beginning then interval = 365; @start = Chronic.parse('1st day next year'); end
|
70
|
+
interval
|
71
|
+
end
|
72
|
+
|
73
|
+
def guess_special_end
|
74
|
+
if token_types.same?([:special, :week]) && token_of_type(:special).start == :end then interval = 7; @start = Chronic.parse('Saturday'); end
|
75
|
+
if token_types.same?([:special, :month]) && token_of_type(:special).start == :end then interval = 30; @start = Date.new(Date.today.year, Date.today.month, Date.today.days_in_month); end
|
76
|
+
if token_types.same?([:special, :year]) && token_of_type(:special).start == :end then interval = 365; @start = Date.new(Date.today.year, 12, 31); end
|
77
|
+
interval
|
78
|
+
end
|
79
|
+
|
80
|
+
def guess_special_middle
|
81
|
+
if token_types.same?([:special, :week]) && token_of_type(:special).start == :middle then interval = 7; @start = Chronic.parse('Wednesday'); end
|
82
|
+
if token_types.same?([:special, :month]) && token_of_type(:special).start == :middle then
|
83
|
+
interval = 30;
|
84
|
+
@start = (Date.today.day >= 15 ? Chronic.parse('15th day of next month') : Date.new(Date.today.year, Date.today.month, 15))
|
85
|
+
end
|
86
|
+
if token_types.same?([:special, :year]) && token_of_type(:special).start == :middle then
|
87
|
+
interval = 365;
|
88
|
+
@start = (Date.today.day >= 15 && Date.today.month >= 6 ? Date.new(Date.today.year+1, 6, 15) : Date.new(Date.today.year, 6, 15))
|
89
|
+
end
|
90
|
+
interval
|
91
|
+
end
|
92
|
+
|
93
|
+
def token_of_type(type)
|
94
|
+
@tokens.detect {|token| token.type == type}
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class Tickle::Repeater < Chronic::Tag #:nodoc:
|
2
|
+
#
|
3
|
+
def self.scan(tokens)
|
4
|
+
# for each token
|
5
|
+
tokens.each do |token|
|
6
|
+
token = self.scan_for_numbers(token)
|
7
|
+
token = self.scan_for_month_names(token)
|
8
|
+
token = self.scan_for_day_names(token)
|
9
|
+
token = self.scan_for_special_text(token)
|
10
|
+
token = self.scan_for_units(token)
|
11
|
+
end
|
12
|
+
tokens
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.scan_for_numbers(token)
|
16
|
+
num = Float(token.word) rescue nil
|
17
|
+
token.update(:number, nil, num.to_i) if num
|
18
|
+
token
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.scan_for_month_names(token)
|
22
|
+
scanner = {/^jan\.?(uary)?$/ => :january,
|
23
|
+
/^feb\.?(ruary)?$/ => :february,
|
24
|
+
/^mar\.?(ch)?$/ => :march,
|
25
|
+
/^apr\.?(il)?$/ => :april,
|
26
|
+
/^may$/ => :may,
|
27
|
+
/^jun\.?e?$/ => :june,
|
28
|
+
/^jul\.?y?$/ => :july,
|
29
|
+
/^aug\.?(ust)?$/ => :august,
|
30
|
+
/^sep\.?(t\.?|tember)?$/ => :september,
|
31
|
+
/^oct\.?(ober)?$/ => :october,
|
32
|
+
/^nov\.?(ember)?$/ => :november,
|
33
|
+
/^dec\.?(ember)?$/ => :december}
|
34
|
+
scanner.keys.each do |scanner_item|
|
35
|
+
token.update(:month_name, scanner[scanner_item], 30) if scanner_item =~ token.word
|
36
|
+
end
|
37
|
+
token
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.scan_for_day_names(token)
|
41
|
+
scanner = {/^m[ou]n(day)?$/ => :monday,
|
42
|
+
/^t(ue|eu|oo|u|)s(day)?$/ => :tuesday,
|
43
|
+
/^tue$/ => :tuesday,
|
44
|
+
/^we(dnes|nds|nns)day$/ => :wednesday,
|
45
|
+
/^wed$/ => :wednesday,
|
46
|
+
/^th(urs|ers)day$/ => :thursday,
|
47
|
+
/^thu$/ => :thursday,
|
48
|
+
/^fr[iy](day)?$/ => :friday,
|
49
|
+
/^sat(t?[ue]rday)?$/ => :saturday,
|
50
|
+
/^su[nm](day)?$/ => :sunday}
|
51
|
+
scanner.keys.each do |scanner_item|
|
52
|
+
token.update(:weekday, scanner[scanner_item], 7) if scanner_item =~ token.word
|
53
|
+
end
|
54
|
+
token
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.scan_for_special_text(token)
|
58
|
+
scanner = {/^other$/ => :other,
|
59
|
+
/^begin(ing|ning)?$/ => :beginning,
|
60
|
+
/^start$/ => :beginning,
|
61
|
+
/^end$/ => :end,
|
62
|
+
/^mid(d)?le$/ => :middle}
|
63
|
+
scanner.keys.each do |scanner_item|
|
64
|
+
token.update(:special, scanner[scanner_item], 7) if scanner_item =~ token.word
|
65
|
+
end
|
66
|
+
token
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.scan_for_units(token)
|
70
|
+
scanner = {/^year(ly)?s?$/ => {:type => :year, :interval => 365, :start => :today},
|
71
|
+
/^month(ly)?s?$/ => {:type => :month, :interval => 30, :start => :today},
|
72
|
+
/^fortnights?$/ => {:type => :fortnight, :interval => 365, :start => :today},
|
73
|
+
/^week(ly)?s?$/ => {:type => :week, :interval => 7, :start => :today},
|
74
|
+
/^weekends?$/ => {:type => :weekend, :interval => 7, :start => :saturday},
|
75
|
+
/^days?$/ => {:type => :day, :interval => 1, :start => :today},
|
76
|
+
/^daily?$/ => {:type => :day, :interval => 1, :start => :today}}
|
77
|
+
scanner.keys.each do |scanner_item|
|
78
|
+
if scanner_item =~ token.word
|
79
|
+
token.update(scanner[scanner_item][:type], scanner[scanner_item][:start], scanner[scanner_item][:interval]) if scanner_item =~ token.word
|
80
|
+
end
|
81
|
+
end
|
82
|
+
token
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
data/lib/tickle/tickle.rb
CHANGED
@@ -1,25 +1,86 @@
|
|
1
1
|
module Tickle
|
2
2
|
class << self
|
3
|
-
|
3
|
+
|
4
4
|
def parse(text, specified_options = {})
|
5
5
|
# get options and set defaults if necessary
|
6
|
-
default_options = {:
|
6
|
+
default_options = {:start => Time.now}
|
7
7
|
options = default_options.merge specified_options
|
8
|
-
|
8
|
+
|
9
9
|
# ensure the specified options are valid
|
10
10
|
specified_options.keys.each do |key|
|
11
11
|
default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
|
12
12
|
end
|
13
|
-
Chronic.parse(
|
14
|
-
|
15
|
-
#
|
16
|
-
|
17
|
-
|
13
|
+
Chronic.parse(specified_options[:start]) || raise(InvalidArgumentException, ':start specified is not a valid datetime.') if specified_options[:start]
|
14
|
+
|
15
|
+
# remove every is specified
|
16
|
+
text = text.gsub(/^every\s\b/, '')
|
17
|
+
|
18
18
|
# put the text into a normal format to ease scanning using Chronic
|
19
|
+
text = pre_normalize(text)
|
19
20
|
text = Chronic.pre_normalize(text)
|
20
|
-
|
21
|
-
|
21
|
+
text = numericize_ordinals(text)
|
22
|
+
|
23
|
+
# check to see if this event starts some other time and reset now
|
24
|
+
event, starting = text.split('starting')
|
25
|
+
@start = (Chronic.parse(starting) || options[:start])
|
26
|
+
|
27
|
+
# split into tokens
|
28
|
+
@tokens = base_tokenize(event)
|
29
|
+
|
30
|
+
# scan the tokens with each token scanner
|
31
|
+
@tokens = Repeater.scan(@tokens)
|
32
|
+
|
33
|
+
# remove all tokens without a type
|
34
|
+
@tokens.reject! {|token| token.type.nil? }
|
35
|
+
|
36
|
+
# dwrite @tokens.inspect
|
37
|
+
|
38
|
+
return guess
|
22
39
|
end
|
23
|
-
|
40
|
+
|
41
|
+
# Normalize natural string removing prefix language
|
42
|
+
def pre_normalize(text)
|
43
|
+
normalized_text = text.gsub(/^every\s\b/, '')
|
44
|
+
normalized_text = text.gsub(/^each\s\b/, '')
|
45
|
+
normalized_text = text.gsub(/^on the\s\b/, '')
|
46
|
+
normalized_text
|
47
|
+
end
|
48
|
+
|
49
|
+
# Split the text on spaces and convert each word into
|
50
|
+
# a Token
|
51
|
+
def base_tokenize(text) #:nodoc:
|
52
|
+
text.split(' ').map { |word| Token.new(word) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Convert ordinal words to numeric ordinals (third => 3rd)
|
56
|
+
def numericize_ordinals(text) #:nodoc:
|
57
|
+
text = text.gsub(/\b(\d*)(st|nd|rd|th)\b/, '\1')
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns an array of types for all tokens
|
61
|
+
def token_types
|
62
|
+
@tokens.map(&:type)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Token #:nodoc:
|
67
|
+
attr_accessor :word, :type, :interval, :start
|
68
|
+
|
69
|
+
def initialize(word)
|
70
|
+
@word = word
|
71
|
+
@type = @interval = @start = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def update(type, start=nil, interval=nil)
|
75
|
+
@start = start
|
76
|
+
@type = type
|
77
|
+
@interval = interval
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# This exception is raised if an invalid argument is provided to
|
82
|
+
# any of Tickle's methods
|
83
|
+
class InvalidArgumentException < Exception
|
84
|
+
|
24
85
|
end
|
25
|
-
end
|
86
|
+
end
|
data/test/helper.rb
CHANGED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'time'
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
class TestParsing < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_parse_best_guess
|
12
|
+
puts "Time.now"
|
13
|
+
p Time.now
|
14
|
+
|
15
|
+
parse_now('each day')
|
16
|
+
|
17
|
+
parse_now('every day')
|
18
|
+
parse_now('every week')
|
19
|
+
parse_now('every Month')
|
20
|
+
parse_now('every year')
|
21
|
+
|
22
|
+
parse_now('daily')
|
23
|
+
parse_now('weekly')
|
24
|
+
parse_now('monthly')
|
25
|
+
parse_now('yearly')
|
26
|
+
|
27
|
+
parse_now('every 3 days')
|
28
|
+
parse_now('every 3 weeks')
|
29
|
+
parse_now('every 3 months')
|
30
|
+
parse_now('every 3 years')
|
31
|
+
|
32
|
+
parse_now('every other day')
|
33
|
+
parse_now('every other week')
|
34
|
+
parse_now('every other month')
|
35
|
+
parse_now('every other year')
|
36
|
+
parse_now('every other day starting May 1st')
|
37
|
+
parse_now('every other week starting this Sunday')
|
38
|
+
|
39
|
+
parse_now('every Monday')
|
40
|
+
parse_now('every Wednesday')
|
41
|
+
parse_now('every Friday')
|
42
|
+
|
43
|
+
parse_now('every May')
|
44
|
+
parse_now('every june')
|
45
|
+
|
46
|
+
parse_now('beginning of the week')
|
47
|
+
parse_now('middle of the week')
|
48
|
+
parse_now('end of the week')
|
49
|
+
|
50
|
+
parse_now('beginning of the month')
|
51
|
+
parse_now('middle of the month')
|
52
|
+
parse_now('end of the month')
|
53
|
+
|
54
|
+
parse_now('beginning of the year')
|
55
|
+
parse_now('middle of the year')
|
56
|
+
parse_now('end of the year')
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_argument_validation
|
60
|
+
assert_raise(Tickle::InvalidArgumentException) do
|
61
|
+
time = Tickle.parse("may 27", :today => 'something odd')
|
62
|
+
end
|
63
|
+
|
64
|
+
assert_raise(Tickle::InvalidArgumentException) do
|
65
|
+
time = Tickle.parse("may 27", :foo => :bar)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def parse_now(string, options={})
|
71
|
+
puts ("attempting to parse '#{string}'")
|
72
|
+
out = Tickle.parse(string, {}.merge(options))
|
73
|
+
p ("output: #{out}")
|
74
|
+
out
|
75
|
+
end
|
76
|
+
end
|
data/tickle.gemspec
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{tickle}
|
8
|
+
s.version = "0.0.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Joshua Lippiner"]
|
12
|
+
s.date = %q{2010-04-22}
|
13
|
+
s.description = %q{Tickle is a date/time helper gem to help parse natural language into a recurring pattern. Tickle is designed to be a compliment of Chronic and can interpret things such as "every 2 days, every Sunday, Sundays, Weekly, etc.}
|
14
|
+
s.email = %q{jlippiner@noctivity.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
".rvmrc",
|
23
|
+
"LICENSE",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"SCENARIOS.rdoc",
|
27
|
+
"VERSION",
|
28
|
+
"lib/numerizer/numerizer.rb",
|
29
|
+
"lib/tickle.rb",
|
30
|
+
"lib/tickle/handler.rb",
|
31
|
+
"lib/tickle/repeater.rb",
|
32
|
+
"lib/tickle/tickle.rb",
|
33
|
+
"test/helper.rb",
|
34
|
+
"test/test_parsing.rb",
|
35
|
+
"tickle.gemspec"
|
36
|
+
]
|
37
|
+
s.homepage = %q{http://github.com/noctivityinc/tickle}
|
38
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
s.rubygems_version = %q{1.3.6}
|
41
|
+
s.summary = %q{natural language parser for recurring events}
|
42
|
+
s.test_files = [
|
43
|
+
"test/helper.rb",
|
44
|
+
"test/test_parsing.rb"
|
45
|
+
]
|
46
|
+
|
47
|
+
if s.respond_to? :specification_version then
|
48
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
49
|
+
s.specification_version = 3
|
50
|
+
|
51
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
52
|
+
s.add_runtime_dependency(%q<chronic>, [">= 0.2.3"])
|
53
|
+
s.add_development_dependency(%q<shoulda>, [">= 2.10.3"])
|
54
|
+
else
|
55
|
+
s.add_dependency(%q<chronic>, [">= 0.2.3"])
|
56
|
+
s.add_dependency(%q<shoulda>, [">= 2.10.3"])
|
57
|
+
end
|
58
|
+
else
|
59
|
+
s.add_dependency(%q<chronic>, [">= 0.2.3"])
|
60
|
+
s.add_dependency(%q<shoulda>, [">= 2.10.3"])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 2
|
9
|
+
version: 0.0.2
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Joshua Lippiner
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-04-
|
17
|
+
date: 2010-04-22 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -32,15 +32,17 @@ dependencies:
|
|
32
32
|
type: :runtime
|
33
33
|
version_requirements: *id001
|
34
34
|
- !ruby/object:Gem::Dependency
|
35
|
-
name:
|
35
|
+
name: shoulda
|
36
36
|
prerelease: false
|
37
37
|
requirement: &id002 !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
segments:
|
42
|
-
-
|
43
|
-
|
42
|
+
- 2
|
43
|
+
- 10
|
44
|
+
- 3
|
45
|
+
version: 2.10.3
|
44
46
|
type: :development
|
45
47
|
version_requirements: *id002
|
46
48
|
description: Tickle is a date/time helper gem to help parse natural language into a recurring pattern. Tickle is designed to be a compliment of Chronic and can interpret things such as "every 2 days, every Sunday, Sundays, Weekly, etc.
|
@@ -59,12 +61,16 @@ files:
|
|
59
61
|
- LICENSE
|
60
62
|
- README.rdoc
|
61
63
|
- Rakefile
|
64
|
+
- SCENARIOS.rdoc
|
62
65
|
- VERSION
|
63
66
|
- lib/numerizer/numerizer.rb
|
64
67
|
- lib/tickle.rb
|
68
|
+
- lib/tickle/handler.rb
|
69
|
+
- lib/tickle/repeater.rb
|
65
70
|
- lib/tickle/tickle.rb
|
66
71
|
- test/helper.rb
|
67
|
-
- test/
|
72
|
+
- test/test_parsing.rb
|
73
|
+
- tickle.gemspec
|
68
74
|
has_rdoc: true
|
69
75
|
homepage: http://github.com/noctivityinc/tickle
|
70
76
|
licenses: []
|
@@ -97,4 +103,4 @@ specification_version: 3
|
|
97
103
|
summary: natural language parser for recurring events
|
98
104
|
test_files:
|
99
105
|
- test/helper.rb
|
100
|
-
- test/
|
106
|
+
- test/test_parsing.rb
|