runt 0.7.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +19 -0
  2. data/.travis.yml +5 -0
  3. data/{CHANGES → CHANGES.txt} +24 -8
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -44
  6. data/README.md +79 -0
  7. data/Rakefile +6 -119
  8. data/doc/tutorial_schedule.md +365 -0
  9. data/doc/tutorial_sugar.md +170 -0
  10. data/doc/tutorial_te.md +155 -0
  11. data/lib/runt.rb +36 -21
  12. data/lib/runt/dprecision.rb +4 -2
  13. data/lib/runt/pdate.rb +101 -95
  14. data/lib/runt/schedule.rb +18 -0
  15. data/lib/runt/sugar.rb +41 -9
  16. data/lib/runt/temporalexpression.rb +246 -30
  17. data/lib/runt/version.rb +3 -0
  18. data/runt.gemspec +24 -0
  19. data/site/.cvsignore +1 -0
  20. data/site/dcl-small.gif +0 -0
  21. data/site/index-rubforge-www.html +72 -0
  22. data/site/index.html +75 -60
  23. data/site/runt-logo.gif +0 -0
  24. data/site/runt-logo.psd +0 -0
  25. data/test/baseexpressiontest.rb +10 -8
  26. data/test/combinedexpressionstest.rb +166 -158
  27. data/test/daterangetest.rb +4 -6
  28. data/test/diweektest.rb +32 -32
  29. data/test/dprecisiontest.rb +2 -4
  30. data/test/everytetest.rb +6 -0
  31. data/test/expressionbuildertest.rb +2 -3
  32. data/test/icalendartest.rb +3 -6
  33. data/test/minitest_helper.rb +7 -0
  34. data/test/pdatetest.rb +21 -6
  35. data/test/redaytest.rb +3 -0
  36. data/test/reyeartest.rb +1 -1
  37. data/test/runttest.rb +5 -8
  38. data/test/scheduletest.rb +13 -14
  39. data/test/sugartest.rb +28 -6
  40. data/test/{spectest.rb → temporaldatetest.rb} +14 -4
  41. data/test/{rspectest.rb → temporalrangetest.rb} +4 -4
  42. data/test/test_runt.rb +11 -0
  43. data/test/weekintervaltest.rb +106 -0
  44. metadata +161 -116
  45. data/README +0 -106
  46. data/doc/tutorial_schedule.rdoc +0 -393
  47. data/doc/tutorial_sugar.rdoc +0 -143
  48. data/doc/tutorial_te.rdoc +0 -190
  49. data/setup.rb +0 -1331
@@ -0,0 +1,170 @@
1
+ # Sugar Tutorial
2
+
3
+ * This tutorial assumes you are familiar with use of the Runt API to create temporal expressions. If you're unfamiliar with how and why to write temporal expressions, take a look at the temporal expression [tutorial](tutorial_te.md).
4
+
5
+ * Starting with version 0.7.0, Runt provides some syntactic sugar for creating temporal expressions. Runt also provides a builder class for which can be used to create expressions in a more readable way than simply using `:new`.
6
+
7
+ First, let's look at some of the new shorcuts for creating individual expressions. If you look at the `lib/runt/sugar.rb` file you find that the `Runt` module has been re-opened and some nutty stuff happens when `:method_missing` is called.
8
+
9
+ For example, if you've included the `Runt` module, you can now create a `DIWeek` expression by calling a method whose name matches the following pattern:
10
+
11
+ ```
12
+ /^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/
13
+ ```
14
+
15
+ So
16
+
17
+ tuesday
18
+
19
+ is equivalent to
20
+
21
+ DIWeek.new(Tuesday)
22
+
23
+ Here's a quick summary of patterns and the expressions they create.
24
+
25
+ ### REDay
26
+
27
+ **regex**: `/^daily_(d{1,2})_(d{2})([ap]m)*to*(d{1,2})_(d{2})([ap]m)$/`
28
+
29
+ **example**: `daily_8_30am_to_10_00pm`
30
+
31
+ **action**: `REDay.new(8,30,22,00)`
32
+
33
+
34
+ ### REWeek
35
+
36
+ **regex**: `/^weekly_(sunday|monday|tuesday|wednesday|thursday|friday|saturday)_to_(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/`
37
+
38
+ **example**: `weekly_wednesday_to_friday`
39
+
40
+ **action**: `REWeek.new(Wednesday, Friday)`
41
+
42
+
43
+ ### REMonth
44
+
45
+ **regex**: `/^monthly_(d{1,2})(?:st|nd|rd|th)_to_(d{1,2})(?:st|nd|rd|th)$/`
46
+
47
+ **example**: `monthly_2nd_to_24th`
48
+
49
+ **action**: `REMonth.new(2,24)`
50
+
51
+
52
+ ### REYear
53
+
54
+ **regex**: `/^yearly_(january|february|march|april|may|june|july|august|september|october|november|december)_(d{1,2})_to_(january|february|march|april|may|june|july|august|september|october|november|december)_(d{1,2})`
55
+
56
+ **example**: `yearly_may_31_to_september_1`
57
+
58
+ **action**: `REYear.new(May,31,September,1)`
59
+
60
+
61
+ ### DIWeek
62
+
63
+ **regex**: `/^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/`
64
+
65
+ **example**: `friday`
66
+
67
+ **action**: `DIWeek.new(Friday)`
68
+
69
+
70
+ ### DIMonth
71
+
72
+ **regex**: `/^(first|second|third|fourth|last|second_to_last)_(sunday|monday|tuesday|w ednesday|thursday|friday|saturday)$/`
73
+
74
+ **example**: `last_friday`
75
+
76
+ **action**: `DIMonth.new(Last,Friday)`
77
+
78
+
79
+ There are also other methods defined (not via `:method_missing`) which provide shortcuts:
80
+
81
+ ### AfterTE
82
+
83
+ **method**: `after(date, inclusive=false)`
84
+
85
+ **action**: `AfterTE.new(date, inclusive=false)`
86
+
87
+
88
+ ### BeforeTE
89
+
90
+ **method**: `before(date, inclusive=false)`
91
+
92
+ **action**: `BeforeTE.new(date, inclusive=false)`
93
+
94
+
95
+ Now let's look at the new `ExpressionBuilder` class. This class uses some simple methods and `instance_eval` to allow one to create composite temporal expressions in a more fluid style than `:new` and friends. The idea is that you define a block where method calls add to a composite expression using either "and", "or", or "not".
96
+
97
+ ```ruby
98
+ # Create a new builder
99
+ b = ExpressionBuilder.new
100
+
101
+ # Call define with a block
102
+ expression = d.define do
103
+ on REDay.new(8,45,9,30)
104
+ on DIWeek.new(Friday) # "And"
105
+ possibly DIWeek.new(Saturday) # "Or"
106
+ except DIMonth.new(Last, Friday) # "Not"
107
+ end
108
+
109
+ # expression = "Daily 8:45am to 9:30 and Fridays or Saturday except not the last Friday of the month"
110
+ ```
111
+
112
+ Hmmm, this is not really an improvement over
113
+
114
+ ```ruby
115
+ REDay.new(8,45,9,30) & DIWeek.new(Friday) | DIWeek.new(Saturday) - DIMonth.new(Last, Friday)
116
+ ```
117
+
118
+ I know, let's try the new constructor aliases defined above!
119
+
120
+ ```ruby
121
+ expression = d.define do
122
+ on daily_8_45am_to_9_30am
123
+ on friday
124
+ possibly saturday
125
+ except last_friday
126
+ end
127
+ ```
128
+
129
+ Much better, except "on daily..." seems a little awkward. We can use `:occurs` which is aliased to `:on` for just such a scenario.
130
+
131
+ ```ruby
132
+ expression = d.define do
133
+ occurs daily_8_45am_to_9_30am
134
+ on friday
135
+ possibly saturday
136
+ except last_friday
137
+ end
138
+ ```
139
+
140
+ ExpressionBuilder creates expressions by evaluating a block passed to the `:define` method. From inside the block, methods `:occurs`, `:on`, `:every`, `:possibly`, and `:maybe` can be called with a temporal expression which will be added to a composite expression as follows:
141
+
142
+ **:on** creates an "and" (`&`)
143
+
144
+ **:possibly** creates an "or" (`|`)
145
+
146
+ **:except** creates a "not" (`-`)
147
+
148
+ **:every** alias for `:on` method
149
+
150
+ **:occurs** alias for `:on` method
151
+
152
+ **:maybe** alias for `:possibly` method
153
+
154
+
155
+ Of course it's easy to open the builder class and add you own aliases if the ones provided don't work for you:
156
+
157
+ ```ruby
158
+ class ExpressionBuilder
159
+ alias_method :potentially, :possibly
160
+ # etc....
161
+ end
162
+ ```
163
+
164
+ If there are shortcuts or macros that you think others would find useful, send in a pull request.
165
+
166
+
167
+ *See Also:*
168
+
169
+ * Temporal Expressions [tutorial](tutorial_te.md)
170
+ * Schedule [tutorial](tutorial_schedule.md)
@@ -0,0 +1,155 @@
1
+ # Temporal Expressions Tutorial
2
+
3
+ Based on a [pattern](http://martinfowler.com/apsupp/recurring.pdf) created by Martin Fowler, temporal expressions define points or ranges in time using *set expressions*. This means, an application developer can precisely describe recurring events without resorting to hacking out a big-ol' nasty enumerated list of dates.
4
+
5
+ For example, say you wanted to schedule an event that occurred annually on the last Thursday of every August. You might start out by doing something like this:
6
+
7
+ ```ruby
8
+ require 'date'
9
+
10
+ some_dates = [Date.new(2002,8,29),Date.new(2003,8,28),Date.new(2004,8,26)]
11
+ ```
12
+
13
+ This is fine for two or three years, but what about for thirty years? What if you want to say every Monday, Tuesday and Friday, between 3 and 5pm for the next fifty years?
14
+
15
+ As Fowler notes in his paper, TemporalExpressions(`TE`s for short) provide a simple pattern language for defining a given set of dates and/or times. They can be mixed and matched as necessary, providing modular component expressions that can be combined to define arbitrarily complex periods of time.
16
+
17
+ ## Example 1
18
+ **Define An Expression That Says: 'the last Thursday in August'**
19
+
20
+ ```ruby
21
+ require 'runt'
22
+ require 'date'
23
+
24
+ last_thursday = DIMonth.new(Last_of,Thursday)
25
+
26
+ august = REYear.new(8)
27
+
28
+ expr = last_thursday & august
29
+
30
+ expr.include?(Date.new(2002,8,29)) #Thurs 8/29/02 => true
31
+ expr.include?(Date.new(2003,8,28)) #Thurs 8/28/03 => true
32
+ expr.include?(Date.new(2004,8,26)) #Thurs 8/26/04 => true
33
+
34
+ expr.include?(Date.new(2004,3,18)) #Thurs 3/18/04 => false
35
+ expr.include?(Date.new(2004,8,27)) #Fri 8/27/04 => false
36
+ ```
37
+
38
+ A couple things are worth noting before we move on to more complicated expressions.
39
+
40
+ Clients use temporal expressions by creating specific instances (`DIMonth` == day in month, `REYear` == range each year) and then, optionally, combining them using various familiar operators `( & , | , - )`.
41
+
42
+ Semantically, the `&` operator on line 8 behaves much like the standard Ruby short-circuit operator `&&`. However, instead of returning a boolean value, a new composite `TE` is instead created and returned. This new expression is the logical intersection of everything matched by **both** arguments `&`.
43
+
44
+ In the example above, line 4:
45
+
46
+ ```ruby
47
+ last_thursday = DIMonth.new(Last_of,Thursday)
48
+ ```
49
+
50
+ will match the last Thursday of **any** month and line 6:
51
+
52
+ ```ruby
53
+ august = REYear.new(8)
54
+ ```
55
+
56
+ will match **any** date or date range occurring within the month of August. Thus, combining them, you have 'the last Thursday' **AND** 'the month of August'.
57
+
58
+ By contrast:
59
+
60
+ ```ruby
61
+ expr = DIMonth.new(Last_of,Thursday) | REYear.new(8)
62
+ ```
63
+
64
+ will all match dates and ranges occurring within 'the last Thursday' **OR** 'the month of August'.
65
+
66
+ Now what? You can see that calling the `#include?` method will let you know whether the expression you've defined includes a given date (or, in some cases, a range, or another TE). This is much like the way you use the standard `Range#include?`.
67
+
68
+ ## Example 2
69
+ **Define: 'Street Cleaning Rules/Alternate Side Parking in NYC'**
70
+
71
+ In his [paper](http://martinfowler.com/apsupp/recurring.pdf), Fowler uses Boston parking regulations to illustrate some examples. Since I'm from New York City, and Boston-related examples might cause an allergic reaction, I'll use NYC's street cleaning and parking [calendar](http://www.nyc.gov/html/dot/html/motorist/scrintro.html#street)
72
+ instead. Since I'm not *completely* insane, I'll only use a small subset of the City's actual rules.
73
+
74
+ On my block, parking is prohibited on the north side of the street Monday, Wednesday, and Friday between the hours of 8am to 11am, and on Tuesday and Thursday from 11:30am to 2pm...let's start by selecting days in the week.
75
+
76
+ Monday **OR** Wednesday **OR** Friday:
77
+
78
+ ```ruby
79
+ mon_wed_fri = DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)
80
+
81
+ mon_wed_fri.include?( DateTime.new(2004,3,10,19,15) ) # Wed => true
82
+ mon_wed_fri.include?( DateTime.new(2004,3,14,9,00) ) # Sun => false
83
+ ```
84
+
85
+ 8am to 11am:
86
+
87
+ ```ruby
88
+ eight_to_eleven = REDay.new(8,00,11,00)
89
+ ```
90
+ combine the two:
91
+
92
+ ```ruby
93
+ expr1 = mon_wed_fri & eight_to_eleven
94
+ ```
95
+
96
+ and, logically speaking, we now have '(Mon **OR** Wed **OR** Fri) **AND** (8am to 11am)'. We're halfway there.
97
+
98
+ Tuesdays and Thursdays:
99
+
100
+ ```ruby
101
+ tues_thurs = DIWeek.new(Tue) | DIWeek.new(Thu)
102
+ ```
103
+
104
+ 11:30am to 2pm:
105
+
106
+ ```ruby
107
+ eleven_thirty_to_two = REDay.new(11,30,14,00)
108
+
109
+ eleven_thirty_to_two.include?( DateTime.new(2004,3,8,12,00) ) # Noon => true
110
+ eleven_thirty_to_two.include?( DateTime.new(2004,3,11,00,00) ) # Midnite => false
111
+
112
+ expr2 = tues_thurs & eleven_thirty_to_two
113
+ ```
114
+
115
+ `expr2` says '(Tues **OR** Thurs) **AND** (11:30am to 2pm)'.
116
+
117
+ and finally:
118
+
119
+ ```ruby
120
+ ticket = expr1 | expr2
121
+ ```
122
+
123
+ Or, logically, ((Mon **OR** Wed **OR** Fri) **AND** (8am to 11am)) **OR** ((Tues OR Thurs) **AND** (11:30am to 2pm))
124
+
125
+ Let's re-write this without all the noise:
126
+
127
+ ```ruby
128
+ expr1 = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
129
+
130
+ expr2 = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
131
+
132
+ ticket = expr1 | expr2
133
+
134
+ ticket.include?( DateTime.new(2004,3,11,12,15) ) # => true
135
+
136
+ ticket.include?( DateTime.new(2004,3,10,9,15) ) # => true
137
+
138
+ ticket.include?( DateTime.new(2004,3,10,8,00) ) # => true
139
+
140
+ ticket.include?( DateTime.new(2004,3,11,1,15) ) # => false
141
+ ```
142
+
143
+ Sigh...now if I can only get my dad to remember this...
144
+
145
+ These are simple examples, but they demonstrate how temporal expressions can be used instead of an enumerated list of date values to define patterns of recurrence. There are many other temporal expressions, and, more importantly, once you get the hang of it, it's easy to write your own.
146
+
147
+ Fowler's [paper](http://martinfowler.com/apsupp/recurring.pdf) also goes on to describe another element of this pattern: the `Schedule`. See the schedule [tutorial](tutorial_schedule.md) for details.
148
+
149
+ *See Also:*
150
+
151
+ * Schedule [tutorial](tutorial_schedule.md)
152
+ * Sugar [tutorial](tutorial_sugar.md)
153
+ * Martin Fowler's recurring event [pattern](http://martinfowler.com/apsupp/recurring.pdf)
154
+ * Other temporal [patterns](http://martinfowler.com/eaaDev/timeNarrative.html)
155
+
@@ -31,9 +31,11 @@
31
31
  # warranties of merchantibility and fitness for a particular
32
32
  # purpose.
33
33
 
34
+ require 'yaml'
34
35
  require 'time'
35
36
  require 'date'
36
37
  require 'date/format'
38
+ require "runt/version"
37
39
  require "runt/dprecision"
38
40
  require "runt/pdate"
39
41
  require "runt/temporalexpression"
@@ -85,9 +87,9 @@ module Runt
85
87
  "#{number}th"
86
88
  else
87
89
  case number.to_i % 10
88
- when 1: "#{number}st"
89
- when 2: "#{number}nd"
90
- when 3: "#{number}rd"
90
+ when 1 then "#{number}st"
91
+ when 2 then "#{number}nd"
92
+ when 3 then "#{number}rd"
91
93
  else "#{number}th"
92
94
  end
93
95
  end
@@ -154,22 +156,26 @@ end
154
156
 
155
157
  #
156
158
  # Add precision +Runt::DPrecision+ to standard library classes Date and DateTime
157
- # (which is a subclass of Date). Also, add an inlcude? method for interoperability
159
+ # (which is a subclass of Date). Also, add an include? method for interoperability
158
160
  # with +Runt::TExpr+ classes
159
161
  #
160
162
  class Date
161
163
 
162
164
  include Runt
163
165
 
164
- attr_accessor :date_precision
166
+ alias_method :include?, :eql?
165
167
 
166
- def include?(expr)
167
- eql?(expr)
168
- end
168
+ attr_accessor :date_precision
169
169
 
170
170
  def date_precision
171
- return @date_precision unless @date_precision.nil?
172
- return Runt::DPrecision::DEFAULT
171
+ if @date_precision.nil? then
172
+ if self.class == DateTime then
173
+ @date_precision = Runt::DPrecision::SEC
174
+ else
175
+ @date_precision = Runt::DPrecision::DAY
176
+ end
177
+ end
178
+ @date_precision
173
179
  end
174
180
  end
175
181
 
@@ -188,11 +194,20 @@ class Time
188
194
  if(args[0].instance_of?(Runt::DPrecision::Precision))
189
195
  @precision=args.shift
190
196
  else
191
- @precision=Runt::DPrecision::DEFAULT
197
+ @precision=Runt::DPrecision::SEC
192
198
  end
193
199
  old_initialize(*args)
194
200
  end
195
201
 
202
+ alias :old_to_yaml :to_yaml
203
+ def to_yaml(options)
204
+ if self.instance_variables.empty?
205
+ self.old_to_yaml(options)
206
+ else
207
+ Time.old_parse(self.to_s).old_to_yaml(options)
208
+ end
209
+ end
210
+
196
211
  class << self
197
212
  alias_method :old_parse, :parse
198
213
  def parse(*args)
@@ -219,16 +234,16 @@ end
219
234
  # somewhere else. :-)
220
235
  #
221
236
  class Numeric #:nodoc:
222
- def microseconds() Float(self * (10 ** -6)) end
223
- def milliseconds() Float(self * (10 ** -3)) end
224
- def seconds() self end
225
- def minutes() 60 * seconds end
226
- def hours() 60 * minutes end
227
- def days() 24 * hours end
228
- def weeks() 7 * days end
229
- def months() 30 * days end
230
- def years() 365 * days end
231
- def decades() 10 * years end
237
+ def microseconds() Float(self * (10 ** -6)) end unless self.instance_methods.include?('microseconds')
238
+ def milliseconds() Float(self * (10 ** -3)) end unless self.instance_methods.include?('milliseconds')
239
+ def seconds() self end unless self.instance_methods.include?('seconds')
240
+ def minutes() 60 * seconds end unless self.instance_methods.include?('minutes')
241
+ def hours() 60 * minutes end unless self.instance_methods.include?('hours')
242
+ def days() 24 * hours end unless self.instance_methods.include?('days')
243
+ def weeks() 7 * days end unless self.instance_methods.include?('weeks')
244
+ def months() 30 * days end unless self.instance_methods.include?('months')
245
+ def years() 365 * days end unless self.instance_methods.include?('years')
246
+ def decades() 10 * years end unless self.instance_methods.include?('decades')
232
247
  # This causes RDoc to hurl:
233
248
  %w[
234
249
  microseconds milliseconds seconds minutes hours days weeks months years decades
@@ -16,7 +16,9 @@ module Runt
16
16
  module DPrecision
17
17
 
18
18
  def DPrecision.to_p(date,prec=DEFAULT)
19
-
19
+ has_p = date.respond_to?(:date_precision)
20
+ #puts "DPrecision.to_p(#{date.class}<#{has_p ? date.date_precision : nil}>,#{prec})"
21
+ return date if PDate == date.class && (prec == date.date_precision)
20
22
  case prec
21
23
  when MIN then PDate.min(*DPrecision.explode(date,prec))
22
24
  when DAY then PDate.day(*DPrecision.explode(date,prec))
@@ -32,7 +34,7 @@ module Runt
32
34
 
33
35
  def DPrecision.explode(date,prec)
34
36
  result = [date.year,date.month,date.day]
35
- if(date.respond_to?("hour"))
37
+ if(date.respond_to?(:hour))
36
38
  result << date.hour << date.min << date.sec
37
39
  else
38
40
  result << 0 << 0 << 0