tickle 0.0.5 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- Tickle.parse returns the next occurrence of the event.
37
+ You can either pass a string prefixed with the word "every, each or 'on the'" or simply the time frame.
34
38
 
35
- You can either pass a string prefixed with the word "every, each or 'on the'" or simply the time frame. You can also pass a start date with the word "starting" (e.g. Tickle.parse('every 3 days starting next friday'))
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
- Time.now
47
- 2010-04-24 12:05:51 -0400
48
-
49
- Tickle.parse('each day') #=> 2010-04-24 12:05:51 -0400
50
- Tickle.parse('every day') #=> 2010-04-24 12:05:51 -0400
51
- Tickle.parse('every week') #=> 2010-05-01 12:05:51 -0400
52
- Tickle.parse('every Month') #=> 2010-05-24 12:05:51 -0400
53
- Tickle.parse('every year') #=> 2011-04-24 12:05:51 -0400
54
- Tickle.parse('daily') #=> 2010-04-24 12:05:51 -0400
55
- Tickle.parse('weekly') #=> 2010-05-01 12:05:51 -0400
56
- Tickle.parse('monthly') #=> 2010-05-24 12:05:51 -0400
57
- Tickle.parse('yearly') #=> 2011-04-24 12:05:51 -0400
58
- Tickle.parse('every 3 days') #=> 2010-04-27 12:05:51 -0400
59
- Tickle.parse('every 3 weeks') #=> 2010-05-15 12:05:51 -0400
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
- * tickle_query
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(tickle_query) again to update the next occurrence of the event.
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 topic branches.
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.0.5
1
+ 0.1.3
@@ -1 +1 @@
1
- GITFLOW_VERSION=0.0.5
1
+ GITFLOW_VERSION=0.1.3
@@ -6,7 +6,8 @@
6
6
  #
7
7
  #=============================================================================
8
8
 
9
- $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
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.0.3"
21
+ VERSION = "0.1.3"
21
22
 
22
23
  def self.debug; false; end
23
24
 
@@ -2,75 +2,62 @@ module Tickle
2
2
  class << self
3
3
 
4
4
  def guess()
5
- interval = guess_unit_types
6
- interval ||= guess_weekday
7
- interval ||= guess_month_names
8
- interval ||= guess_number_and_unit
9
- interval ||= guess_ordinal
10
- interval ||= guess_ordinal_and_unit
11
- interval ||= guess_special
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
- # defines the next occurrence of this tickle if not set in a guess routine
14
- @next ||= @start + (interval * 60 * 60 * 24) if interval
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 interval
17
+ return @next.to_time if @next
21
18
  end
22
19
 
23
20
  def guess_unit_types
24
- interval = 0 if token_types.same?([:day])
21
+ interval = 1 if token_types.same?([:day])
25
22
  interval = 7 if token_types.same?([:week])
26
- interval = 30 if token_types.same?([:month])
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]) then
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]) then
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 * 30) if token_types.same?([:number, :month])
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
- if token_types.same?([:ordinal]) then interval = 365; @next = Chronic.parse("#{token_of_type(:ordinal).start} day in #{Date::MONTHNAMES[get_next_month(token_of_type(:ordinal).start)]}"); end
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
- parse_text = ''
62
- if token_types.same?([:ordinal, :month_name]) then interval = 365; @next = Chronic.parse("#{token_of_type(:ordinal).original} day in #{token_of_type(:month_name).start.to_s}"); end
63
- if token_types.same?([:ordinal, :month]) then interval = 365; @next = Chronic.parse("#{token_of_type(:ordinal).start} day in #{Date::MONTHNAMES[get_next_month(token_of_type(:ordinal).start)]}"); end
64
- if token_types.same?([:ordinal, :weekday, :month_name]) then interval = 365; @next = Chronic.parse("#{token_of_type(:ordinal).original} #{token_of_type(:weekday).start.to_s} in #{token_of_type(:month_name).start.to_s}"); end
65
- if token_types.same?([:ordinal, :weekday, :month]) then interval = 365; @next = Chronic.parse("#{token_of_type(:ordinal).original} #{token_of_type(:weekday).start.to_s} in #{Date::MONTHNAMES[get_next_month(token_of_type(:ordinal).start)]}"); end
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
- interval = guess_special_other
71
- interval ||= guess_special_beginning
72
- interval ||= guess_special_middle
73
- interval ||= guess_special_end
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 then interval = 60; @next = Chronic.parse('2 months from now'); end
82
- if token_types.same?([:special, :year]) && token_of_type(:special).start == :other then interval = 730; @next = Chronic.parse('2 years from now'); end
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 interval = 7; @start = Chronic.parse('Sunday'); end
88
- if token_types.same?([:special, :month]) && token_of_type(:special).start == :beginning then interval = 30; @start = Chronic.parse('1st day next month'); end
89
- if token_types.same?([:special, :year]) && token_of_type(:special).start == :beginning then interval = 365; @start = Chronic.parse('1st day next year'); end
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 interval = 7; @start = Chronic.parse('Saturday'); end
95
- 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
96
- if token_types.same?([:special, :year]) && token_of_type(:special).start == :end then interval = 365; @start = Date.new(Date.today.year, 12, 31); end
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 interval = 7; @start = Chronic.parse('Wednesday'); end
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
- interval = 30;
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
- interval = 365;
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 get_next_month(ordinal)
120
- ord_to_int = ordinal.gsub(/\b(\d*)(st|nd|rd|th)\b/,'\1').to_i
121
- month = (ord_to_int < Date.today.day ? Date.today.month + 1 : Date.today.month)
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
@@ -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
- num = Float(token.word) rescue nil
19
- token.update(:number, num.to_i, num.to_i) if num
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
- /second/ => '2nd',
26
- /third/ => '3rd',
27
- /fourth/ => '4th',
28
- /fifth/ => '5th',
29
- /sixth/ => '6th',
30
- /seventh/ => '7th',
31
- /eighth/ => '8th',
32
- /ninth/ => '9th',
33
- /tenth/ => '10th',
34
- /eleventh/ => '11th',
35
- /twelfth/ => '12th',
36
- /thirteenth/ => '13th',
37
- /fourteenth/ => '14th',
38
- /fifteenth/ => '15th',
39
- /sixteenth/ => '16th',
40
- /seventeenth/ => '17th',
41
- /eighteenth/ => '18th',
42
- /nineteenth/ => '19th',
43
- /twentieth/ => '20th',
44
- /thirtieth/ => '30th',
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
- token.update(:ordinal, scanner[scanner_item], 365) if scanner_item =~ token.original
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
- token.update(:ordinal, token.original, 365) if !(token.original =~ regex).nil?
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 => 1, :start => :today},
113
- /^daily?$/ => {:type => :day, :interval => 1, :start => :today}}
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
@@ -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, starting = text.split('starting')
17
- @start = (Chronic.parse(starting) || options[:start])
18
- @next = nil
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
- # put the text into a normal format to ease scanning using Chronic
21
- event = pre_tokenize(event)
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
- # split into tokens
24
- @tokens = base_tokenize(event)
35
+ # split into tokens
36
+ @tokens = base_tokenize(event)
25
37
 
26
- # process each original word for implied word
27
- post_tokenize
38
+ # process each original word for implied word
39
+ post_tokenize
28
40
 
29
- # scan the tokens with each token scanner
30
- @tokens = Repeater.scan(@tokens)
41
+ # @tokens.each {|x| puts x.inspect} if Tickle.debug
31
42
 
32
- # remove all tokens without a type
33
- @tokens.reject! {|token| token.type.nil? }
43
+ # scan the tokens with each token scanner
44
+ @tokens = Repeater.scan(@tokens)
34
45
 
35
- # combine number and ordinals into single number
36
- combine_multiple_numbers
46
+ # remove all tokens without a type
47
+ @tokens.reject! {|token| token.type.nil? }
37
48
 
38
- @tokens.each {|x| puts x.inspect} if Tickle.debug
49
+ # combine number and ordinals into single number
50
+ combine_multiple_numbers
39
51
 
40
- return guess
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 pre_tokenize(text)
45
- normalized_text = text.gsub(/^every\s\b/, '')
46
- normalized_text = text.gsub(/^each\s\b/, '')
47
- normalized_text = text.gsub(/^on the\s\b/, '')
48
- normalized_text.downcase
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 = numericize_ordinals(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(combined_value, combined_value, :ordinal, combined_value, 365)
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
@@ -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 test_parse_best_guess
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', {:start => Date.new(2010, 03, 01).to_time})
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', {:start => Date.new(2010, 03, 15)})
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
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{tickle}
8
- s.version = "0.0.5"
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-04-25}
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
- - 0
8
- - 5
9
- version: 0.0.5
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-04-25 00:00:00 -04:00
17
+ date: 2010-05-07 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency