tickle 0.0.1 → 0.0.2
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.
- 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
|