tickle 0.0.5 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +122 -55
- data/VERSION +1 -1
- data/git-flow-version +1 -1
- data/lib/tickle.rb +3 -2
- data/lib/tickle/handler.rb +48 -60
- data/lib/tickle/repeater.rb +49 -30
- data/lib/tickle/tickle.rb +114 -30
- data/test/test_parsing.rb +67 -9
- data/tickle.gemspec +2 -2
- metadata +4 -4
data/README.rdoc
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
http://github.com/noctivityinc/tickle
|
3
3
|
by Joshua Lippiner, Noctivity
|
4
4
|
|
5
|
+
** LEGACY WARNING
|
6
|
+
|
7
|
+
If you starting using Tickle pre version 0.1.X, you will need to update your code to either include the :next_only => true option or read correctly from the options hash. Sorry.
|
8
|
+
|
5
9
|
-- DESCRIPTION
|
6
10
|
|
7
11
|
Tickle is a natural language parser for recurring events.
|
@@ -30,82 +34,143 @@ thoughtbot's shoulda (gem install shoulda)
|
|
30
34
|
|
31
35
|
You can parse strings containing a natural language interval using the Tickle.parse method.
|
32
36
|
|
33
|
-
|
37
|
+
You can either pass a string prefixed with the word "every, each or 'on the'" or simply the time frame.
|
34
38
|
|
35
|
-
|
39
|
+
Tickle.parse returns a hash containing the following keys:
|
40
|
+
* next = the next occurrence of the event. This is NEVER today as its always the next date in the future.
|
41
|
+
* starting = the date all calculations as based on. If not passed as an option, the start date is right now.
|
42
|
+
* until = the last date you want this event to run until.
|
43
|
+
* expression = this is the natural language expression to store to run through tickle later to get the next occurrence.
|
36
44
|
|
37
45
|
Tickle returns nil if it cannot parse the string cannot be parsed.
|
38
46
|
|
39
47
|
Tickle HEAVILY uses chronic for parsing both the event and the start date.
|
40
48
|
|
49
|
+
-- OPTIONS
|
50
|
+
|
51
|
+
There are two ways to pass options: natural language or an options hash.
|
52
|
+
|
53
|
+
NATURAL LANGUAGE:
|
54
|
+
Pass a start date with the word "starting, start, stars" (e.g. Tickle.parse('every 3 days starting next friday'))
|
55
|
+
Pass an end date with the word "until, end, ends, ending" (e.g. Tickle.parse('every 3 days until next friday'))
|
56
|
+
Pass both at the same time (e.g. "starting May 5th repeat every other week until December 1")
|
57
|
+
|
58
|
+
OPTIONS HASH
|
59
|
+
Valid options are:
|
60
|
+
* start - must be a valid date. (e.g. Tickle.parse('every other day', {:start => Date.new(2010,8,1) }))
|
61
|
+
* until - must be a valid date. (e.g. Tickle.parse('every other day', {:until => Date.new(2010,10,1) }))
|
62
|
+
* next_only - legacy switch to ONLY return the next occurrence as a date and not return a hash
|
63
|
+
|
64
|
+
-- SUPER IMPORTANT NOTE ABOUT NEXT OCCURRENCE & START DATE
|
65
|
+
|
66
|
+
You may notice when parsing an expression with a start date that the next occurrence IS the start date passed. This is DESIGNED BEHAVIOR.
|
67
|
+
|
68
|
+
Here's why - assume your user says "remind me every 3 weeks starting Dec 1" and today is May 8th. Well the first reminder needs to be sent on Dec 1, not Dec 21 (three weeks later).
|
69
|
+
|
70
|
+
If you don't like that, fork and have fun but don't say I didn't warn ya.
|
71
|
+
|
41
72
|
-- EXAMPLES
|
42
73
|
|
43
74
|
require 'rubygems'
|
44
75
|
require 'tickle'
|
76
|
+
|
77
|
+
.Time.now
|
78
|
+
2010-05-07 14:01:19 -0400
|
79
|
+
|
80
|
+
SIMPLE
|
81
|
+
Tickle.parse('day') #=> {:next=>2010-05-08 14:01:19 -0400, :recurrence_expression=>"day", :starting=>2010-05-07 14:01:19 -0400}
|
82
|
+
Tickle.parse('day') #=> {:next=>2010-05-08 14:01:19 -0400, :recurrence_expression=>"day", :starting=>2010-05-07 14:01:19 -0400}
|
83
|
+
Tickle.parse('week') #=> {:next=>2010-05-14 14:01:19 -0400, :recurrence_expression=>"week", :starting=>2010-05-07 14:01:19 -0400}
|
84
|
+
Tickle.parse('Month') #=> {:next=>2010-06-07 14:01:19 -0400, :recurrence_expression=>"month", :starting=>2010-05-07 14:01:19 -0400}
|
85
|
+
Tickle.parse('year') #=> {:next=>2011-05-07 14:01:19 -0400, :recurrence_expression=>"year", :starting=>2010-05-07 14:01:19 -0400}
|
86
|
+
Tickle.parse('daily') #=> {:next=>2010-05-08 14:01:19 -0400, :recurrence_expression=>"daily", :starting=>2010-05-07 14:01:19 -0400}
|
87
|
+
Tickle.parse('weekly') #=> {:next=>2010-05-14 14:01:19 -0400, :recurrence_expression=>"weekly", :starting=>2010-05-07 14:01:19 -0400}
|
88
|
+
Tickle.parse('monthly') #=> {:next=>2010-06-07 14:01:19 -0400, :recurrence_expression=>"monthly", :starting=>2010-05-07 14:01:19 -0400}
|
89
|
+
Tickle.parse('yearly') #=> {:next=>2011-05-07 14:01:19 -0400, :recurrence_expression=>"yearly", :starting=>2010-05-07 14:01:19 -0400}
|
90
|
+
Tickle.parse('3 days') #=> {:next=>2010-05-10 14:01:19 -0400, :recurrence_expression=>"3 days", :starting=>2010-05-07 14:01:19 -0400}
|
91
|
+
Tickle.parse('3 weeks') #=> {:next=>2010-05-28 14:01:19 -0400, :recurrence_expression=>"3 weeks", :starting=>2010-05-07 14:01:19 -0400}
|
92
|
+
Tickle.parse('3 months') #=> {:next=>2010-08-08 14:01:19 -0400, :recurrence_expression=>"3 months", :starting=>2010-05-07 14:01:19 -0400}
|
93
|
+
Tickle.parse('3 years') #=> {:next=>2013-05-06 14:01:19 -0400, :recurrence_expression=>"3 years", :starting=>2010-05-07 14:01:19 -0400}
|
94
|
+
Tickle.parse('other day') #=> {:next=>2010-05-09 14:01:19 -0400, :recurrence_expression=>"other day", :starting=>2010-05-07 14:01:19 -0400}
|
95
|
+
Tickle.parse('other week') #=> {:next=>2010-05-21 14:01:19 -0400, :recurrence_expression=>"other week", :starting=>2010-05-07 14:01:19 -0400}
|
96
|
+
Tickle.parse('other month') #=> {:next=>2010-07-07 14:01:19 -0400, :recurrence_expression=>"other month", :starting=>2010-05-07 14:01:19 -0400}
|
97
|
+
Tickle.parse('other year') #=> {:next=>2012-05-07 14:01:19 -0400, :recurrence_expression=>"other year", :starting=>2010-05-07 14:01:19 -0400}
|
98
|
+
Tickle.parse('Monday') #=> {:next=>2010-05-10 12:00:00 -0400, :recurrence_expression=>"monday", :starting=>2010-05-07 14:01:19 -0400}
|
99
|
+
Tickle.parse('Wednesday') #=> {:next=>2010-05-12 12:00:00 -0400, :recurrence_expression=>"wednesday", :starting=>2010-05-07 14:01:19 -0400}
|
100
|
+
Tickle.parse('Friday') #=> {:next=>2010-05-14 12:00:00 -0400, :recurrence_expression=>"friday", :starting=>2010-05-07 14:01:19 -0400}
|
101
|
+
Tickle.parse('May') #=> {:next=>2011-05-01 12:00:00 -0400, :recurrence_expression=>"may", :starting=>2010-05-07 14:01:19 -0400}
|
102
|
+
Tickle.parse('june') #=> {:next=>2010-06-01 12:00:00 -0400, :recurrence_expression=>"june", :starting=>2010-05-07 14:01:19 -0400}
|
103
|
+
Tickle.parse('beginning of the week') #=> {:next=>2010-05-09 12:00:00 -0400, :recurrence_expression=>"beginning of the week", :starting=>2010-05-07 14:01:19 -0400}
|
104
|
+
Tickle.parse('middle of the week') #=> {:next=>2010-05-12 12:00:00 -0400, :recurrence_expression=>"middle of the week", :starting=>2010-05-07 14:01:19 -0400}
|
105
|
+
Tickle.parse('end of the week') #=> {:next=>2010-05-08 12:00:00 -0400, :recurrence_expression=>"end of the week", :starting=>2010-05-07 14:01:19 -0400}
|
106
|
+
Tickle.parse('beginning of the month') #=> {:next=>2010-06-01 00:00:00 -0400, :recurrence_expression=>"beginning of the month", :starting=>2010-05-07 14:01:19 -0400}
|
107
|
+
Tickle.parse('middle of the month') #=> {:next=>2010-05-15 00:00:00 -0400, :recurrence_expression=>"middle of the month", :starting=>2010-05-07 14:01:19 -0400}
|
108
|
+
Tickle.parse('end of the month') #=> {:next=>2010-05-31 00:00:00 -0400, :recurrence_expression=>"end of the month", :starting=>2010-05-07 14:01:19 -0400}
|
109
|
+
Tickle.parse('beginning of the year') #=> {:next=>2011-01-01 00:00:00 -0500, :recurrence_expression=>"beginning of the year", :starting=>2010-05-07 14:01:19 -0400}
|
110
|
+
Tickle.parse('middle of the year') #=> {:next=>2010-06-15 00:00:00 -0400, :recurrence_expression=>"middle of the year", :starting=>2010-05-07 14:01:19 -0400}
|
111
|
+
Tickle.parse('end of the year') #=> {:next=>2010-12-31 00:00:00 -0500, :recurrence_expression=>"end of the year", :starting=>2010-05-07 14:01:19 -0400}
|
112
|
+
Tickle.parse('the 3rd of May') #=> {:next=>2011-05-03 12:00:00 -0400, :recurrence_expression=>"the 3rd of may", :starting=>2010-05-07 14:01:19 -0400}
|
113
|
+
Tickle.parse('the 3rd of February') #=> {:next=>2011-02-03 12:00:00 -0500, :recurrence_expression=>"the 3rd of february", :starting=>2010-05-07 14:01:19 -0400}
|
114
|
+
Tickle.parse('the 3rd of February 2012') #=> {:next=>2012-02-03 12:00:00 -0500, :recurrence_expression=>"the 3rd of february 2012", :starting=>2010-05-07 14:01:19 -0400}
|
115
|
+
Tickle.parse('the 3rd of Feb 2012') #=> {:next=>2012-02-03 12:00:00 -0500, :recurrence_expression=>"the 3rd of feb 2012", :starting=>2010-05-07 14:01:19 -0400}
|
116
|
+
Tickle.parse('the 4th of the month') #=> {:next=>2010-06-04 12:00:00 -0400, :recurrence_expression=>"the 4th of the month", :starting=>2010-05-07 14:01:19 -0400}
|
117
|
+
Tickle.parse('the 10th of the month') #=> {:next=>2010-05-10 12:00:00 -0400, :recurrence_expression=>"the 10th of the month", :starting=>2010-05-07 14:01:19 -0400}
|
118
|
+
Tickle.parse('the tenth of the month') #=> {:next=>2010-05-10 12:00:00 -0400, :recurrence_expression=>"the tenth of the month", :starting=>2010-05-07 14:01:19 -0400}
|
119
|
+
Tickle.parse('the first of the month') #=> {:next=>2010-06-01 12:00:00 -0400, :recurrence_expression=>"the first of the month", :starting=>2010-05-07 14:01:19 -0400}
|
120
|
+
Tickle.parse('the thirtieth') #=> {:next=>2010-05-30 12:00:00 -0400, :recurrence_expression=>"the thirtieth", :starting=>2010-05-07 14:01:19 -0400}
|
121
|
+
Tickle.parse('the fifth') #=> {:next=>2010-06-05 12:00:00 -0400, :recurrence_expression=>"the fifth", :starting=>2010-05-07 14:01:19 -0400}
|
122
|
+
Tickle.parse('the 3rd Sunday of May') #=> {:next=>2010-05-16 12:00:00 -0400, :recurrence_expression=>"the 3rd sunday of may", :starting=>2010-05-07 14:01:19 -0400}
|
123
|
+
Tickle.parse('the 3rd Sunday of the month') #=> {:next=>2010-06-20 12:00:00 -0400, :recurrence_expression=>"the 3rd sunday of the month", :starting=>2010-05-07 14:01:19 -0400}
|
124
|
+
Tickle.parse('the 23rd of June') #=> {:next=>2010-06-23 12:00:00 -0400, :recurrence_expression=>"the 23rd of june", :starting=>2010-05-07 14:01:19 -0400}
|
125
|
+
Tickle.parse('the twenty third of June') #=> {:next=>2010-06-23 12:00:00 -0400, :recurrence_expression=>"the twenty third of june", :starting=>2010-05-07 14:01:19 -0400}
|
126
|
+
Tickle.parse('the thirty first of August') #=> {:next=>2010-08-31 12:00:00 -0400, :recurrence_expression=>"the thirty first of august", :starting=>2010-05-07 14:01:19 -0400}
|
127
|
+
Tickle.parse('the twenty first') #=> {:next=>2010-05-21 12:00:00 -0400, :recurrence_expression=>"the twenty first", :starting=>2010-05-07 14:01:19 -0400}
|
128
|
+
Tickle.parse('the twenty first of the month') #=> {:next=>2010-05-21 12:00:00 -0400, :recurrence_expression=>"the twenty first of the month", :starting=>2010-05-07 14:01:19 -0400}
|
45
129
|
|
46
|
-
|
47
|
-
2010-
|
48
|
-
|
49
|
-
Tickle.parse('
|
50
|
-
Tickle.parse('every
|
51
|
-
Tickle.parse('every
|
52
|
-
Tickle.parse('every
|
53
|
-
Tickle.parse('every
|
54
|
-
Tickle.parse('
|
55
|
-
Tickle.parse('
|
56
|
-
Tickle.parse('
|
57
|
-
Tickle.parse('
|
58
|
-
Tickle.parse('every
|
59
|
-
Tickle.parse('every
|
60
|
-
Tickle.parse('every 3 months') #=> 2010-07-23 12:05:51 -0400
|
61
|
-
Tickle.parse('every 3 years') #=> 2013-04-23 12:05:51 -0400
|
62
|
-
Tickle.parse('every other day') #=> 2010-04-26 12:05:51 -0400
|
63
|
-
Tickle.parse('every other week') #=> 2010-05-08 12:05:51 -0400
|
64
|
-
Tickle.parse('every other month') #=> 2010-06-24 12:05:51 -0400
|
65
|
-
Tickle.parse('every other year') #=> 2012-04-24 12:05:51 -0400
|
66
|
-
Tickle.parse('every other day starting May 1st') #=> 2010-05-01 12:00:00 -0400
|
67
|
-
Tickle.parse('every other week starting this Sunday') #=> 2010-04-25 12:00:00 -0400
|
68
|
-
Tickle.parse('every Monday') #=> 2010-04-26 12:00:00 -0400
|
69
|
-
Tickle.parse('every Wednesday') #=> 2010-04-28 12:00:00 -0400
|
70
|
-
Tickle.parse('every Friday') #=> 2010-04-30 12:00:00 -0400
|
71
|
-
Tickle.parse('every May') #=> 2010-05-01 12:00:00 -0400
|
72
|
-
Tickle.parse('every june') #=> 2010-06-01 12:00:00 -0400
|
73
|
-
Tickle.parse('beginning of the week') #=> 2010-04-25 12:00:00 -0400
|
74
|
-
Tickle.parse('middle of the week') #=> 2010-04-28 12:00:00 -0400
|
75
|
-
Tickle.parse('end of the week') #=> 2010-05-01 12:00:00 -0400
|
76
|
-
Tickle.parse('beginning of the month') #=> 2010-05-01 12:00:00 -0400
|
77
|
-
Tickle.parse('middle of the month') #=> 2010-05-15 12:00:00 -0400
|
78
|
-
Tickle.parse('end of the month') #=> 2010-04-30 00:00:00 -0400
|
79
|
-
Tickle.parse('beginning of the year') #=> 2011-01-01 12:00:00 -0500
|
80
|
-
Tickle.parse('middle of the year') #=> 2010-06-15 00:00:00 -0400
|
81
|
-
Tickle.parse('end of the year') #=> 2010-12-31 00:00:00 -0500
|
82
|
-
Tickle.parse('the 3rd of May') #=> 2010-05-03 12:00:00 -0400
|
83
|
-
Tickle.parse('the 3rd of February') #=> 2011-02-03 12:00:00 -0500
|
84
|
-
Tickle.parse('the 10th of the month') #=> 2010-05-10 12:00:00 -0400
|
85
|
-
Tickle.parse('the tenth of the month') #=> 2010-05-10 12:00:00 -0400
|
86
|
-
Tickle.parse('the first of the month') #=> 2010-05-01 12:00:00 -0400
|
87
|
-
Tickle.parse('the thirtieth') #=> 2010-04-30 12:00:00 -0400
|
88
|
-
Tickle.parse('the fifth') #=> 2010-05-05 12:00:00 -0400
|
89
|
-
Tickle.parse('the 3rd Sunday of May') #=> 2010-05-16 12:00:00 -0400
|
90
|
-
Tickle.parse('the 3rd Sunday of the month') #=> 2010-05-16 12:00:00 -0400
|
91
|
-
Tickle.parse('the 23rd of June') #=> 2010-06-23 12:00:00 -0400
|
92
|
-
Tickle.parse('the twenty third of June') #=> 2010-06-23 12:00:00 -0400
|
93
|
-
Tickle.parse('the thirty first of August') #=> 2010-08-31 12:00:00 -0400
|
94
|
-
Tickle.parse('the twenty first') #=> 2010-05-21 12:00:00 -0400
|
95
|
-
Tickle.parse('the twenty first of the month') #=> 2010-05-21 12:00:00 -0400
|
130
|
+
COMPLEX
|
131
|
+
Tickle.parse('starting Monday repeat every month') #=> {:next=>2010-05-10 12:00:00 -0400, :recurrence_expression=>"month", :starting=>2010-05-10 12:00:00 -0400}
|
132
|
+
Tickle.parse('starting May 13th repeat every week') #=> {:next=>2010-05-13 12:00:00 -0400, :recurrence_expression=>"week", :starting=>2010-05-13 12:00:00 -0400}
|
133
|
+
Tickle.parse('starting May 13th repeat every other day') #=> {:next=>2010-05-13 12:00:00 -0400, :recurrence_expression=>"other day", :starting=>2010-05-13 12:00:00 -0400}
|
134
|
+
Tickle.parse('every week starts this wednesday') #=> {:next=>2010-05-12 12:00:00 -0400, :recurrence_expression=>"week", :starting=>2010-05-12 12:00:00 -0400}
|
135
|
+
Tickle.parse('every other day starts the 1st May') #=> {:next=>2011-05-01 12:00:00 -0400, :recurrence_expression=>"other day", :starting=>2011-05-01 12:00:00 -0400}
|
136
|
+
Tickle.parse('every other day starting May 6') #=> {:next=>2010-05-09 14:01:19 -0400, :recurrence_expression=>"other day", :starting=>2010-05-07 14:01:19 -0400}
|
137
|
+
Tickle.parse('every week starting this wednesday') #=> {:next=>2010-05-12 12:00:00 -0400, :recurrence_expression=>"week", :starting=>2010-05-12 12:00:00 -0400}
|
138
|
+
Tickle.parse('every other day starting the 1st May') #=> {:next=>2011-05-01 12:00:00 -0400, :recurrence_expression=>"other day", :starting=>2011-05-01 12:00:00 -0400}
|
139
|
+
Tickle.parse('every other day starting May 1st 2011') #=> {:next=>2011-05-01 12:00:00 -0400, :recurrence_expression=>"other day", :starting=>2011-05-01 12:00:00 -0400}
|
140
|
+
Tickle.parse('every other week starting this Sunday') #=> {:next=>2010-05-09 12:00:00 -0400, :recurrence_expression=>"other week", :starting=>2010-05-09 12:00:00 -0400}
|
141
|
+
Tickle.parse('every week starting this wednesday until June 5th') #=> {:next=>2010-05-14 14:01:19 -0400, :recurrence_expression=>"week", :starting=>2010-05-07 14:01:19 -0400, :until=>2010-06-05 12:00:00 -0400}
|
142
|
+
Tickle.parse('every week starting this wednesday ends June 5th') #=> {:next=>2010-05-14 14:01:19 -0400, :recurrence_expression=>"week", :starting=>2010-05-07 14:01:19 -0400, :until=>2010-06-05 12:00:00 -0400}
|
143
|
+
Tickle.parse('every week starting this wednesday ending June 5th') #=> {:next=>2010-05-14 14:01:19 -0400, :recurrence_expression=>"week", :starting=>2010-05-07 14:01:19 -0400, :until=>2010-06-05 12:00:00 -0400}
|
96
144
|
|
145
|
+
WITH OPTIONS HASH
|
146
|
+
.Tickle.parse('May 1st 2011, {:next_only=>true}') #=> 2011-05-01 12:00:00 -0400
|
147
|
+
Tickle.parse('every 3 days, {:start=>Mon, 17 May 2010}') #=> {:next=>2010-05-17 00:00:00 -0400, :recurrence_expression=>"every 3 days", :starting=>2010-05-17 00:00:00 -0400}
|
148
|
+
Tickle.parse('every 3 weeks, {:start=>Mon, 17 May 2010}') #=> {:next=>2010-05-17 00:00:00 -0400, :recurrence_expression=>"every 3 weeks", :starting=>2010-05-17 00:00:00 -0400}
|
149
|
+
Tickle.parse('every 3 months, {:start=>Mon, 17 May 2010}') #=> {:next=>2010-05-17 00:00:00 -0400, :recurrence_expression=>"every 3 months", :starting=>2010-05-17 00:00:00 -0400}
|
150
|
+
Tickle.parse('every 3 years, {:start=>Mon, 17 May 2010}') #=> {:next=>2010-05-17 00:00:00 -0400, :recurrence_expression=>"every 3 years", :starting=>2010-05-17 00:00:00 -0400}
|
151
|
+
Tickle.parse('every 3 days, {:start=>Mon, 17 May 2010, :until=>Thu, 07 Oct 2010}') #=> {:next=>2010-05-17 00:00:00 -0400, :recurrence_expression=>"every 3 days", :starting=>2010-05-17 00:00:00 -0400, :until=>2010-10-07 00:00:00 -0400}
|
152
|
+
Tickle.parse('every 3 weeks, {:start=>Mon, 17 May 2010, :until=>Thu, 07 Oct 2010}') #=> {:next=>2010-05-17 00:00:00 -0400, :recurrence_expression=>"every 3 weeks", :starting=>2010-05-17 00:00:00 -0400, :until=>2010-10-07 00:00:00 -0400}
|
153
|
+
Tickle.parse('3 months, {:until=>Thu, 07 Oct 2010}') #=> {:next=>2010-08-08 14:01:19 -0400, :recurrence_expression=>"3 months", :starting=>2010-05-07 14:01:19 -0400, :until=>2010-10-07 00:00:00 -0400}
|
154
|
+
.
|
97
155
|
|
98
156
|
-- USING IN APP
|
99
157
|
|
100
158
|
To use in your app, we recommend adding two attributes to your database model:
|
101
159
|
* next_occurrence
|
102
|
-
*
|
160
|
+
* tickle_expression
|
103
161
|
|
104
162
|
Then call Tickle.parse(query) when you need to and save the results accordingly. In your
|
105
163
|
code, each day, simply check to see if today is >= next_occurrence and, if so, run your block.
|
106
164
|
|
107
|
-
After it completes, call Tickle.parse(
|
165
|
+
After it completes, call Tickle.parse(tickle_expression) again to update the next occurrence of the event.
|
166
|
+
|
108
167
|
|
168
|
+
-- TESTING
|
169
|
+
|
170
|
+
Tickle comes with a full testing suite that tests and shows sample output for simple, complex, options hash and invalid arguments.
|
171
|
+
|
172
|
+
Please note, the tests were designed to output the parsed expression and not return true. This allows (and sorry, forces you) to have to skim through the
|
173
|
+
results and queries to ensure they are working correctly. You can also add your own for fun.
|
109
174
|
|
110
175
|
-- LIMITATIONS
|
111
176
|
|
@@ -117,6 +182,8 @@ HUGE shout-out to both the creator of Chronic, Tom Preston-Werner (http://chroni
|
|
117
182
|
|
118
183
|
Without their work and code structure I'd be lost.
|
119
184
|
|
185
|
+
As always, BIG shout-out to the RVM Master himself, Wayne Seguin, for putting up with me and Ruby from day one. Ask Wayne to make you some Ciabatta bread next time you see him
|
186
|
+
|
120
187
|
|
121
188
|
== Note on Patches/Pull Requests
|
122
189
|
|
@@ -126,7 +193,7 @@ Without their work and code structure I'd be lost.
|
|
126
193
|
future version unintentionally.
|
127
194
|
* Commit, do not mess with rakefile, version, or history.
|
128
195
|
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
129
|
-
* Send me a pull request. Bonus points for
|
196
|
+
* Send me a pull request. Bonus points for time-based branches.
|
130
197
|
|
131
198
|
== Copyright
|
132
199
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.1.3
|
data/git-flow-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
GITFLOW_VERSION=0.
|
1
|
+
GITFLOW_VERSION=0.1.3
|
data/lib/tickle.rb
CHANGED
@@ -6,7 +6,8 @@
|
|
6
6
|
#
|
7
7
|
#=============================================================================
|
8
8
|
|
9
|
-
|
9
|
+
|
10
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__)) # For use/testing when no gem is installed
|
10
11
|
|
11
12
|
require 'date'
|
12
13
|
require 'time'
|
@@ -17,7 +18,7 @@ require 'tickle/handler'
|
|
17
18
|
require 'tickle/repeater'
|
18
19
|
|
19
20
|
module Tickle
|
20
|
-
VERSION = "0.
|
21
|
+
VERSION = "0.1.3"
|
21
22
|
|
22
23
|
def self.debug; false; end
|
23
24
|
|
data/lib/tickle/handler.rb
CHANGED
@@ -2,75 +2,62 @@ module Tickle
|
|
2
2
|
class << self
|
3
3
|
|
4
4
|
def guess()
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
5
|
+
guess_unit_types
|
6
|
+
guess_weekday unless @next
|
7
|
+
guess_month_names unless @next
|
8
|
+
guess_number_and_unit unless @next
|
9
|
+
guess_ordinal unless @next
|
10
|
+
guess_ordinal_and_unit unless @next
|
11
|
+
guess_special unless @next
|
12
12
|
|
13
|
-
#
|
14
|
-
@next
|
15
|
-
|
16
|
-
# check to see if the start date is > NOW and, if so, set the next occurrence = start
|
17
|
-
@next = @start if @start.to_time > Time.now
|
13
|
+
# check to see if next is less than now and, if so, set it to next year
|
14
|
+
@next = Time.local(@next.year + 1, @next.month, @next.day, @next.hour, @next.min, @next.sec) if @next && @next.to_date < @start.to_date
|
18
15
|
|
19
16
|
# return the next occurrence
|
20
|
-
return @next.to_time if
|
17
|
+
return @next.to_time if @next
|
21
18
|
end
|
22
19
|
|
23
20
|
def guess_unit_types
|
24
|
-
interval =
|
21
|
+
interval = 1 if token_types.same?([:day])
|
25
22
|
interval = 7 if token_types.same?([:week])
|
26
|
-
interval =
|
23
|
+
interval = Tickle.days_in_month if token_types.same?([:month])
|
27
24
|
interval = 365 if token_types.same?([:year])
|
28
|
-
interval
|
25
|
+
compute_next(interval)
|
29
26
|
end
|
30
27
|
|
31
28
|
def guess_weekday
|
32
|
-
if token_types.same?([:weekday])
|
33
|
-
@start = Chronic.parse(token_of_type(:weekday).start.to_s)
|
34
|
-
interval = 7
|
35
|
-
end
|
36
|
-
interval
|
29
|
+
@next = chronic_parse("#{token_of_type(:weekday).start.to_s}") if token_types.same?([:weekday])
|
37
30
|
end
|
38
31
|
|
39
32
|
def guess_month_names
|
40
|
-
if token_types.same?([:month_name])
|
41
|
-
@start = Chronic.parse("#{token_of_type(:month_name).start.to_s} 1")
|
42
|
-
interval = 30
|
43
|
-
end
|
44
|
-
interval
|
33
|
+
@next = chronic_parse("#{token_of_type(:month_name).start.to_s} 1") if token_types.same?([:month_name])
|
45
34
|
end
|
46
35
|
|
47
36
|
def guess_number_and_unit
|
48
37
|
interval = token_of_type(:number).interval if token_types.same?([:number, :day])
|
49
38
|
interval = (token_of_type(:number).interval * 7) if token_types.same?([:number, :week])
|
50
|
-
interval = (token_of_type(:number).interval *
|
39
|
+
interval = (token_of_type(:number).interval * Tickle.days_in_month) if token_types.same?([:number, :month])
|
51
40
|
interval = (token_of_type(:number).interval * 365) if token_types.same?([:number, :year])
|
52
|
-
interval
|
41
|
+
compute_next(interval)
|
53
42
|
end
|
54
43
|
|
55
44
|
def guess_ordinal
|
56
|
-
|
57
|
-
interval
|
45
|
+
@next = chronic_parse("#{token_of_type(:ordinal).word} day in #{Date::MONTHNAMES[get_next_month(token_of_type(:ordinal).start)]}") if token_types.same?([:ordinal])
|
58
46
|
end
|
59
47
|
|
60
48
|
def guess_ordinal_and_unit
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
interval
|
49
|
+
@next = chronic_parse("#{token_of_type(:ordinal).word} day in #{token_of_type(:month_name).start.to_s} ") if token_types.same?([:ordinal, :month_name])
|
50
|
+
@next = chronic_parse("#{token_of_type(:ordinal).word} day in #{Date::MONTHNAMES[get_next_month(token_of_type(:ordinal).start)]}") if token_types.same?([:ordinal, :month])
|
51
|
+
@next = chronic_parse("#{token_of_type(:ordinal).word} #{token_of_type(:weekday).start.to_s} in #{token_of_type(:month_name).start.to_s}") if token_types.same?([:ordinal, :weekday, :month_name])
|
52
|
+
@next = chronic_parse("#{token_of_type(:ordinal).word} #{token_of_type(:weekday).start.to_s} in #{Date::MONTHNAMES[get_next_month(token_of_type(:ordinal).start)]}") if token_types.same?([:ordinal, :weekday, :month])
|
53
|
+
@next = chronic_parse("#{token_of_type(:month_name).word} #{token_of_type(:ordinal).start} #{token_of_type(:specific_year).word}") if token_types.same?([:ordinal, :month_name, :specific_year])
|
67
54
|
end
|
68
55
|
|
69
56
|
def guess_special
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
57
|
+
guess_special_other
|
58
|
+
guess_special_beginning unless @next
|
59
|
+
guess_special_middle unless @next
|
60
|
+
guess_special_end unless @next
|
74
61
|
end
|
75
62
|
|
76
63
|
private
|
@@ -78,36 +65,31 @@ module Tickle
|
|
78
65
|
def guess_special_other
|
79
66
|
interval = 2 if token_types.same?([:special, :day]) && token_of_type(:special).start == :other
|
80
67
|
interval = 14 if token_types.same?([:special, :week]) && token_of_type(:special).start == :other
|
81
|
-
if token_types.same?([:special, :month]) && token_of_type(:special).start == :other
|
82
|
-
if token_types.same?([:special, :year]) && token_of_type(:special).start == :other
|
83
|
-
interval
|
68
|
+
@next = chronic_parse('2 months from now') if token_types.same?([:special, :month]) && token_of_type(:special).start == :other
|
69
|
+
@next = chronic_parse('2 years from now') if token_types.same?([:special, :year]) && token_of_type(:special).start == :other
|
70
|
+
compute_next(interval)
|
84
71
|
end
|
85
72
|
|
86
73
|
def guess_special_beginning
|
87
|
-
if token_types.same?([:special, :week]) && token_of_type(:special).start == :beginning then
|
88
|
-
if token_types.same?([:special, :month]) && token_of_type(:special).start == :beginning then
|
89
|
-
if token_types.same?([:special, :year]) && token_of_type(:special).start == :beginning then
|
90
|
-
interval
|
74
|
+
if token_types.same?([:special, :week]) && token_of_type(:special).start == :beginning then @next = chronic_parse('Sunday'); end
|
75
|
+
if token_types.same?([:special, :month]) && token_of_type(:special).start == :beginning then @next = Date.civil(@start.year, @start.month + 1, 1); end
|
76
|
+
if token_types.same?([:special, :year]) && token_of_type(:special).start == :beginning then @next = Date.civil(@start.year+1, 1, 1); end
|
91
77
|
end
|
92
78
|
|
93
79
|
def guess_special_end
|
94
|
-
if token_types.same?([:special, :week]) && token_of_type(:special).start == :end then
|
95
|
-
if token_types.same?([:special, :month]) && token_of_type(:special).start == :end then
|
96
|
-
if token_types.same?([:special, :year]) && token_of_type(:special).start == :end then
|
97
|
-
interval
|
80
|
+
if token_types.same?([:special, :week]) && token_of_type(:special).start == :end then @next = chronic_parse('Saturday'); end
|
81
|
+
if token_types.same?([:special, :month]) && token_of_type(:special).start == :end then @next = Date.civil(@start.year, @start.month, -1); end
|
82
|
+
if token_types.same?([:special, :year]) && token_of_type(:special).start == :end then @next = Date.new(@start.year, 12, 31); end
|
98
83
|
end
|
99
84
|
|
100
85
|
def guess_special_middle
|
101
|
-
if token_types.same?([:special, :week]) && token_of_type(:special).start == :middle then
|
86
|
+
if token_types.same?([:special, :week]) && token_of_type(:special).start == :middle then @next = chronic_parse('Wednesday'); end
|
102
87
|
if token_types.same?([:special, :month]) && token_of_type(:special).start == :middle then
|
103
|
-
|
104
|
-
@start = (Date.today.day > 15 ? Chronic.parse('15th day of next month') : Date.new(Date.today.year, Date.today.month, 15))
|
88
|
+
@next = (@start.day > 15 ? Date.civil(@start.year, @start.month + 1, 15) : Date.civil(@start.year, @start.month, 15))
|
105
89
|
end
|
106
90
|
if token_types.same?([:special, :year]) && token_of_type(:special).start == :middle then
|
107
|
-
|
108
|
-
@start = (Date.today.day > 15 && Date.today.month > 6 ? Date.new(Date.today.year+1, 6, 15) : Date.new(Date.today.year, 6, 15))
|
91
|
+
@next = (@start.day > 15 && @start.month > 6 ? Date.new(@start.year+1, 6, 15) : Date.new(@start.year, 6, 15))
|
109
92
|
end
|
110
|
-
interval
|
111
93
|
end
|
112
94
|
|
113
95
|
def token_of_type(type)
|
@@ -116,10 +98,16 @@ module Tickle
|
|
116
98
|
|
117
99
|
private
|
118
100
|
|
119
|
-
def
|
120
|
-
|
121
|
-
|
101
|
+
def compute_next(interval)
|
102
|
+
# defines the next occurrence of this tickle if not set in a guess routine
|
103
|
+
@next ||= @start + (interval * 60 * 60 * 24) if interval
|
122
104
|
end
|
105
|
+
|
106
|
+
def chronic_parse(exp)
|
107
|
+
puts "date expression: #{exp}" if Tickle.debug
|
108
|
+
Chronic.parse(exp, :now => @start)
|
109
|
+
end
|
110
|
+
|
123
111
|
|
124
112
|
end
|
125
113
|
end
|
data/lib/tickle/repeater.rb
CHANGED
@@ -4,10 +4,11 @@ class Tickle::Repeater < Chronic::Tag #:nodoc:
|
|
4
4
|
# for each token
|
5
5
|
tokens.each do |token|
|
6
6
|
token = self.scan_for_numbers(token)
|
7
|
-
token = self.scan_for_ordinal_names(token)
|
8
|
-
token = self.scan_for_ordinals(token)
|
7
|
+
token = self.scan_for_ordinal_names(token) unless token.type
|
8
|
+
token = self.scan_for_ordinals(token) unless token.type
|
9
9
|
token = self.scan_for_month_names(token) unless token.type
|
10
10
|
token = self.scan_for_day_names(token) unless token.type
|
11
|
+
token = self.scan_for_year_name(token) unless token.type
|
11
12
|
token = self.scan_for_special_text(token) unless token.type
|
12
13
|
token = self.scan_for_units(token) unless token.type
|
13
14
|
end
|
@@ -15,46 +16,52 @@ class Tickle::Repeater < Chronic::Tag #:nodoc:
|
|
15
16
|
end
|
16
17
|
|
17
18
|
def self.scan_for_numbers(token)
|
18
|
-
|
19
|
-
token.update(:number,
|
19
|
+
regex = /\b(\d\d?)\b/
|
20
|
+
token.update(:number, token.word.gsub(regex,'\1').to_i, token.word.gsub(regex,'\1').to_i) if token.word =~ regex
|
20
21
|
token
|
21
22
|
end
|
22
23
|
|
23
24
|
def self.scan_for_ordinal_names(token)
|
24
25
|
scanner = {/first/ => '1st',
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
26
|
+
/second/ => '2nd',
|
27
|
+
/third/ => '3rd',
|
28
|
+
/fourth/ => '4th',
|
29
|
+
/fifth/ => '5th',
|
30
|
+
/sixth/ => '6th',
|
31
|
+
/seventh/ => '7th',
|
32
|
+
/eighth/ => '8th',
|
33
|
+
/ninth/ => '9th',
|
34
|
+
/tenth/ => '10th',
|
35
|
+
/eleventh/ => '11th',
|
36
|
+
/twelfth/ => '12th',
|
37
|
+
/thirteenth/ => '13th',
|
38
|
+
/fourteenth/ => '14th',
|
39
|
+
/fifteenth/ => '15th',
|
40
|
+
/sixteenth/ => '16th',
|
41
|
+
/seventeenth/ => '17th',
|
42
|
+
/eighteenth/ => '18th',
|
43
|
+
/nineteenth/ => '19th',
|
44
|
+
/twentieth/ => '20th',
|
45
|
+
/thirtieth/ => '30th',
|
45
46
|
}
|
46
47
|
scanner.keys.each do |scanner_item|
|
47
|
-
|
48
|
+
if scanner_item =~ token.original
|
49
|
+
token.word = scanner[scanner_item]
|
50
|
+
token.update(:ordinal, numericize_ordinals(scanner[scanner_item]), Tickle.days_in_month(Tickle.get_next_month(numericize_ordinals(scanner[scanner_item]))))
|
51
|
+
end
|
48
52
|
end
|
49
53
|
token
|
50
54
|
end
|
51
|
-
|
55
|
+
|
52
56
|
def self.scan_for_ordinals(token)
|
53
57
|
regex = /\b(\d*)(st|nd|rd|th)\b/
|
54
|
-
|
58
|
+
if token.original =~ regex
|
59
|
+
token.word = token.original
|
60
|
+
token.update(:ordinal, numericize_ordinals(token.word), Tickle.days_in_month(Tickle.get_next_month(token.word)))
|
61
|
+
end
|
55
62
|
token
|
56
63
|
end
|
57
|
-
|
64
|
+
|
58
65
|
def self.scan_for_month_names(token)
|
59
66
|
scanner = {/^jan\.?(uary)?$/ => :january,
|
60
67
|
/^feb\.?(ruary)?$/ => :february,
|
@@ -91,6 +98,12 @@ class Tickle::Repeater < Chronic::Tag #:nodoc:
|
|
91
98
|
token
|
92
99
|
end
|
93
100
|
|
101
|
+
def self.scan_for_year_name(token)
|
102
|
+
regex = /\b\d{4}\b/
|
103
|
+
token.update(:specific_year, token.original.gsub(regex,'\1'), 365) if token.original =~ regex
|
104
|
+
token
|
105
|
+
end
|
106
|
+
|
94
107
|
def self.scan_for_special_text(token)
|
95
108
|
scanner = {/^other$/ => :other,
|
96
109
|
/^begin(ing|ning)?$/ => :beginning,
|
@@ -109,8 +122,8 @@ class Tickle::Repeater < Chronic::Tag #:nodoc:
|
|
109
122
|
/^fortnights?$/ => {:type => :fortnight, :interval => 365, :start => :today},
|
110
123
|
/^week(ly)?s?$/ => {:type => :week, :interval => 7, :start => :today},
|
111
124
|
/^weekends?$/ => {:type => :weekend, :interval => 7, :start => :saturday},
|
112
|
-
/^days?$/ => {:type => :day, :interval =>
|
113
|
-
/^daily?$/ => {:type => :day, :interval =>
|
125
|
+
/^days?$/ => {:type => :day, :interval => 0, :start => :today},
|
126
|
+
/^daily?$/ => {:type => :day, :interval => 0, :start => :today}}
|
114
127
|
scanner.keys.each do |scanner_item|
|
115
128
|
if scanner_item =~ token.word
|
116
129
|
token.update(scanner[scanner_item][:type], scanner[scanner_item][:start], scanner[scanner_item][:interval]) if scanner_item =~ token.word
|
@@ -119,4 +132,10 @@ class Tickle::Repeater < Chronic::Tag #:nodoc:
|
|
119
132
|
token
|
120
133
|
end
|
121
134
|
|
135
|
+
# Convert ordinal words to numeric ordinals (third => 3rd)
|
136
|
+
def self.numericize_ordinals(text) #:nodoc:
|
137
|
+
text = text.gsub(/\b(\d*)(st|nd|rd|th)\b/, '\1')
|
138
|
+
end
|
139
|
+
|
140
|
+
|
122
141
|
end
|
data/lib/tickle/tickle.rb
CHANGED
@@ -3,49 +3,119 @@ module Tickle
|
|
3
3
|
|
4
4
|
def parse(text, specified_options = {})
|
5
5
|
# get options and set defaults if necessary
|
6
|
-
default_options = {:start => Time.now}
|
6
|
+
default_options = {:start => Time.now, :next_only => false, :until => nil}
|
7
7
|
options = default_options.merge specified_options
|
8
8
|
|
9
|
+
# ensure an expression was provided
|
10
|
+
raise(InvalidArgumentException, 'date expression is required') unless text
|
11
|
+
|
9
12
|
# ensure the specified options are valid
|
10
13
|
specified_options.keys.each do |key|
|
11
14
|
raise(InvalidArgumentException, "#{key} is not a valid option key.") unless default_options.keys.include?(key)
|
12
15
|
end
|
13
16
|
raise(InvalidArgumentException, ':start specified is not a valid datetime.') unless (is_date(specified_options[:start]) || Chronic.parse(specified_options[:start])) if specified_options[:start]
|
14
17
|
|
18
|
+
# check to see if a valid datetime was passed
|
19
|
+
return text if text.is_a?(Date) || text.is_a?(Time)
|
20
|
+
|
15
21
|
# check to see if this event starts some other time and reset now
|
16
|
-
event
|
17
|
-
|
18
|
-
@
|
22
|
+
event = scan_expression(text, options)
|
23
|
+
|
24
|
+
raise(InvalidDateExpression, "the start date (#{@start.to_date}) for a recurring event cannot occur in the past ") if @start.to_date < Date.today
|
25
|
+
raise(InvalidDateExpression, "the start date (#{@start.to_date}) cannot occur after the end date") if @until && @start.to_date > @until.to_date
|
19
26
|
|
20
|
-
#
|
21
|
-
|
27
|
+
# no need to guess at expression if the start_date is in the future
|
28
|
+
best_guess = nil
|
29
|
+
if @start.to_time > Time.now
|
30
|
+
best_guess = @start
|
31
|
+
else
|
32
|
+
# put the text into a normal format to ease scanning using Chronic
|
33
|
+
event = pre_filter(event)
|
22
34
|
|
23
|
-
|
24
|
-
|
35
|
+
# split into tokens
|
36
|
+
@tokens = base_tokenize(event)
|
25
37
|
|
26
|
-
|
27
|
-
|
38
|
+
# process each original word for implied word
|
39
|
+
post_tokenize
|
28
40
|
|
29
|
-
|
30
|
-
@tokens = Repeater.scan(@tokens)
|
41
|
+
# @tokens.each {|x| puts x.inspect} if Tickle.debug
|
31
42
|
|
32
|
-
|
33
|
-
|
43
|
+
# scan the tokens with each token scanner
|
44
|
+
@tokens = Repeater.scan(@tokens)
|
34
45
|
|
35
|
-
|
36
|
-
|
46
|
+
# remove all tokens without a type
|
47
|
+
@tokens.reject! {|token| token.type.nil? }
|
37
48
|
|
38
|
-
|
49
|
+
# combine number and ordinals into single number
|
50
|
+
combine_multiple_numbers
|
39
51
|
|
40
|
-
|
52
|
+
@tokens.each {|x| puts x.inspect} if Tickle.debug
|
53
|
+
|
54
|
+
best_guess = guess
|
55
|
+
end
|
56
|
+
|
57
|
+
raise(InvalidDateExpression, "the next occurrence takes place after the end date specified") if @until && best_guess.to_date > @until.to_date
|
58
|
+
|
59
|
+
if !best_guess
|
60
|
+
return nil
|
61
|
+
elsif options[:next_only] != true
|
62
|
+
h = {:next => best_guess.to_time, :recurrence_expression => event.strip}
|
63
|
+
h.merge!({:starting => @start.to_time}) if @start
|
64
|
+
h.merge!({:until => @until.to_time}) if @until
|
65
|
+
return h
|
66
|
+
else
|
67
|
+
return best_guess
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# scans the expression for a variety of natural formats, such as 'every thursday starting tomorrow until May 15th
|
72
|
+
def scan_expression(text, options)
|
73
|
+
starting = ending = nil
|
74
|
+
|
75
|
+
start_every_regex = /^(start(?:s|ing)?)\s(.*)(\s(?:every|each|on|repeat)(?:s|ing)?)(.*)/i
|
76
|
+
every_start_regex = /^(every|each|on|repeat(?:the)?)\s(.*)(\s(?:start)(?:s|ing)?)(.*)/i
|
77
|
+
if text =~ start_every_regex
|
78
|
+
starting = text.match(start_every_regex)[2]
|
79
|
+
text = text.match(start_every_regex)[4]
|
80
|
+
event, ending = process_for_ending(text)
|
81
|
+
elsif text =~ every_start_regex
|
82
|
+
event = text.match(every_start_regex)[2]
|
83
|
+
text = text.match(every_start_regex)[4]
|
84
|
+
starting, ending = process_for_ending(text)
|
85
|
+
else
|
86
|
+
event, ending = process_for_ending(text)
|
87
|
+
end
|
88
|
+
|
89
|
+
@start = (starting && Tickle.parse(pre_filter(starting), {:next_only => true}) || options[:start])
|
90
|
+
@until = (ending && Tickle.parse(pre_filter(ending), {:next_only => true}) || options[:until])
|
91
|
+
@next = nil
|
92
|
+
return event
|
93
|
+
end
|
94
|
+
|
95
|
+
def inspect_matches
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
# process the remaining expression to see if an until, end, ending is specified
|
100
|
+
def process_for_ending(text)
|
101
|
+
regex = /^(.*)(\s(?:end|until)(?:s|ing)?)(.*)/i
|
102
|
+
if text =~ regex
|
103
|
+
return text.match(regex)[1], text.match(regex)[3]
|
104
|
+
else
|
105
|
+
return text, nil
|
106
|
+
end
|
41
107
|
end
|
42
108
|
|
43
109
|
# Normalize natural string removing prefix language
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
110
|
+
def pre_filter(text)
|
111
|
+
return nil unless text
|
112
|
+
|
113
|
+
text.gsub!(/every(\s)?/, '')
|
114
|
+
text.gsub!(/each(\s)?/, '')
|
115
|
+
text.gsub!(/repeat(s|ing)?(\s)?/, '')
|
116
|
+
text.gsub!(/on the(\s)?/, '')
|
117
|
+
text.gsub!(/([^\w\d\s])+/, '')
|
118
|
+
text.downcase.strip
|
49
119
|
end
|
50
120
|
|
51
121
|
# Split the text on spaces and convert each word into
|
@@ -85,12 +155,7 @@ module Tickle
|
|
85
155
|
normalized_text.gsub!(/\btonight\b/, 'this night')
|
86
156
|
normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
|
87
157
|
normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
|
88
|
-
normalized_text
|
89
|
-
end
|
90
|
-
|
91
|
-
# Convert ordinal words to numeric ordinals (third => 3rd)
|
92
|
-
def numericize_ordinals(text) #:nodoc:
|
93
|
-
text = text.gsub(/\b(\d*)(st|nd|rd|th)\b/, '\1')
|
158
|
+
normalized_text
|
94
159
|
end
|
95
160
|
|
96
161
|
# Turns compound numbers, like 'twenty first' => 21
|
@@ -98,8 +163,10 @@ module Tickle
|
|
98
163
|
if [:number, :ordinal].all? {|type| token_types.include? type}
|
99
164
|
number = token_of_type(:number)
|
100
165
|
ordinal = token_of_type(:ordinal)
|
166
|
+
combined_original = "#{number.original} #{ordinal.original}"
|
167
|
+
combined_word = (number.start.to_s[0] + ordinal.word)
|
101
168
|
combined_value = (number.start.to_s[0] + ordinal.start.to_s)
|
102
|
-
new_number_token = Token.new(
|
169
|
+
new_number_token = Token.new(combined_original, combined_word, :ordinal, combined_value, 365)
|
103
170
|
@tokens.reject! {|token| (token.type == :number || token.type == :ordinal)}
|
104
171
|
@tokens << new_number_token
|
105
172
|
end
|
@@ -109,6 +176,17 @@ module Tickle
|
|
109
176
|
def token_types
|
110
177
|
@tokens.map(&:type)
|
111
178
|
end
|
179
|
+
|
180
|
+
protected
|
181
|
+
|
182
|
+
def get_next_month(number)
|
183
|
+
month = number.to_i < Date.today.day ? (Date.today.month == 12 ? 1 : Date.today.month + 1) : Date.today.month
|
184
|
+
end
|
185
|
+
|
186
|
+
def days_in_month(month=nil)
|
187
|
+
month ||= Date.today.month
|
188
|
+
days_in_mon = Date.civil(Date.today.year, month, -1).day
|
189
|
+
end
|
112
190
|
end
|
113
191
|
|
114
192
|
class Token #:nodoc:
|
@@ -134,4 +212,10 @@ module Tickle
|
|
134
212
|
class InvalidArgumentException < Exception
|
135
213
|
|
136
214
|
end
|
215
|
+
|
216
|
+
# This exception is raised if there is an issue with the parsing
|
217
|
+
# output from the date expression provided
|
218
|
+
class InvalidDateExpression < Exception
|
219
|
+
|
220
|
+
end
|
137
221
|
end
|
data/test/test_parsing.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
|
-
require 'helper'
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/helper')
|
2
2
|
require 'time'
|
3
3
|
require 'test/unit'
|
4
4
|
|
5
5
|
class TestParsing < Test::Unit::TestCase
|
6
6
|
|
7
7
|
def setup
|
8
|
-
|
9
8
|
end
|
10
9
|
|
11
|
-
def
|
10
|
+
def test_parse_best_guess_simple
|
12
11
|
puts "Time.now"
|
13
12
|
p Time.now
|
14
13
|
|
@@ -33,8 +32,6 @@ class TestParsing < Test::Unit::TestCase
|
|
33
32
|
parse_now('every other week')
|
34
33
|
parse_now('every other month')
|
35
34
|
parse_now('every other year')
|
36
|
-
parse_now('every other day starting May 1st')
|
37
|
-
parse_now('every other week starting this Sunday')
|
38
35
|
|
39
36
|
parse_now('every Monday')
|
40
37
|
parse_now('every Wednesday')
|
@@ -56,14 +53,17 @@ class TestParsing < Test::Unit::TestCase
|
|
56
53
|
parse_now('end of the year')
|
57
54
|
|
58
55
|
parse_now('the 3rd of May')
|
59
|
-
parse_now('the 3rd of February'
|
56
|
+
parse_now('the 3rd of February')
|
57
|
+
parse_now('the 3rd of February 2012')
|
58
|
+
parse_now('the 3rd of Feb, 2012')
|
60
59
|
|
60
|
+
parse_now('the 4th of the month')
|
61
61
|
parse_now('the 10th of the month')
|
62
62
|
parse_now('the tenth of the month')
|
63
63
|
|
64
64
|
parse_now('the first of the month')
|
65
65
|
parse_now('the thirtieth')
|
66
|
-
parse_now('the fifth'
|
66
|
+
parse_now('the fifth')
|
67
67
|
|
68
68
|
parse_now('the 3rd Sunday of May')
|
69
69
|
parse_now('the 3rd Sunday of the month')
|
@@ -72,11 +72,48 @@ class TestParsing < Test::Unit::TestCase
|
|
72
72
|
parse_now('the twenty third of June')
|
73
73
|
parse_now('the thirty first of August')
|
74
74
|
|
75
|
-
|
76
75
|
parse_now('the twenty first')
|
77
76
|
parse_now('the twenty first of the month')
|
78
77
|
end
|
79
78
|
|
79
|
+
def test_parse_best_guess_complex
|
80
|
+
puts "Time.now"
|
81
|
+
p Time.now
|
82
|
+
|
83
|
+
parse_now('starting Monday repeat every month')
|
84
|
+
parse_now('starting May 13th repeat every week')
|
85
|
+
parse_now('starting May 13th repeat every other day')
|
86
|
+
|
87
|
+
parse_now('every week starts this wednesday')
|
88
|
+
parse_now('every other day starts the 1st May')
|
89
|
+
parse_now('every other day starting May 6')
|
90
|
+
parse_now('every week starting this wednesday')
|
91
|
+
parse_now('every other day starting the 1st May')
|
92
|
+
|
93
|
+
parse_now("every other day starting May 1st #{Date.today.year + 1}")
|
94
|
+
parse_now('every other week starting this Sunday')
|
95
|
+
|
96
|
+
parse_now('every week starting this wednesday until June 5th')
|
97
|
+
parse_now('every week starting this wednesday ends June 5th')
|
98
|
+
parse_now('every week starting this wednesday ending June 5th')
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_tickle_args
|
103
|
+
parse_now('May 1st, 2011', {:next_only => true})
|
104
|
+
|
105
|
+
start_date = Date.civil(Date.today.year, Date.today.month, Date.today.day + 10)
|
106
|
+
parse_now('every 3 days', {:start => start_date})
|
107
|
+
parse_now('every 3 weeks', {:start => start_date})
|
108
|
+
parse_now('every 3 months', {:start => start_date})
|
109
|
+
parse_now('every 3 years', {:start => start_date})
|
110
|
+
|
111
|
+
end_date = Date.civil(Date.today.year, Date.today.month+5, Date.today.day)
|
112
|
+
parse_now('every 3 days', {:start => start_date, :until => end_date})
|
113
|
+
parse_now('every 3 weeks', {:start => start_date, :until => end_date})
|
114
|
+
parse_now('every 3 months', {:until => end_date})
|
115
|
+
end
|
116
|
+
|
80
117
|
def test_argument_validation
|
81
118
|
assert_raise(Tickle::InvalidArgumentException) do
|
82
119
|
time = Tickle.parse("may 27", :today => 'something odd')
|
@@ -85,12 +122,33 @@ class TestParsing < Test::Unit::TestCase
|
|
85
122
|
assert_raise(Tickle::InvalidArgumentException) do
|
86
123
|
time = Tickle.parse("may 27", :foo => :bar)
|
87
124
|
end
|
125
|
+
|
126
|
+
assert_raise(Tickle::InvalidArgumentException) do
|
127
|
+
time = Tickle.parse(nil)
|
128
|
+
end
|
129
|
+
|
130
|
+
assert_raise(Tickle::InvalidDateExpression) do
|
131
|
+
past_date = Date.civil(Date.today.year, Date.today.month, Date.today.day - 1)
|
132
|
+
time = Tickle.parse("every other day", {:start => past_date})
|
133
|
+
end
|
134
|
+
|
135
|
+
assert_raise(Tickle::InvalidDateExpression) do
|
136
|
+
start_date = Date.civil(Date.today.year, Date.today.month, Date.today.day + 10)
|
137
|
+
end_date = Date.civil(Date.today.year, Date.today.month, Date.today.day + 5)
|
138
|
+
time = Tickle.parse("every other day", :start => start_date, :until => end_date)
|
139
|
+
end
|
140
|
+
|
141
|
+
assert_raise(Tickle::InvalidDateExpression) do
|
142
|
+
end_date = Date.civil(Date.today.year, Date.today.month+2, Date.today.day)
|
143
|
+
parse_now('every 3 months', {:until => end_date})
|
144
|
+
end
|
88
145
|
end
|
89
146
|
|
90
147
|
private
|
91
148
|
def parse_now(string, options={})
|
92
149
|
out = Tickle.parse(string, {}.merge(options))
|
93
|
-
puts ("Tickle.parse('#{string}') #=> #{out}")
|
150
|
+
puts (options.empty? ? ("Tickle.parse('#{string}') #=> #{out}") : ("Tickle.parse('#{string}, #{options}') #=> #{out}"))
|
151
|
+
p '--' if Tickle.debug
|
94
152
|
out
|
95
153
|
end
|
96
154
|
end
|
data/tickle.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{tickle}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.1.3"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Joshua Lippiner"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-05-07}
|
13
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
14
|
s.email = %q{jlippiner@noctivity.com}
|
15
15
|
s.extra_rdoc_files = [
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 1
|
8
|
+
- 3
|
9
|
+
version: 0.1.3
|
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-
|
17
|
+
date: 2010-05-07 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|