runt 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +107 -71
- data/LICENSE.txt +43 -43
- data/README +100 -94
- data/Rakefile +119 -120
- data/TODO +11 -20
- data/doc/tutorial_schedule.rdoc +84 -51
- data/doc/tutorial_te.rdoc +190 -190
- data/lib/runt.rb +219 -110
- data/lib/runt/daterange.rb +74 -74
- data/lib/runt/dprecision.rb +141 -137
- data/lib/runt/pdate.rb +153 -126
- data/lib/runt/schedule.rb +88 -89
- data/lib/runt/temporalexpression.rb +695 -524
- data/setup.rb +1331 -1331
- data/site/blue-robot3.css +131 -131
- data/site/index.html +94 -93
- data/site/runt-logo.psd +0 -0
- data/test/daterangetest.rb +87 -87
- data/test/dprecisiontest.rb +55 -45
- data/test/icalendartest.rb +524 -0
- data/test/pdatetest.rb +117 -104
- data/test/runttest.rb +101 -0
- data/test/scheduletest.rb +148 -88
- data/test/temporalexpressiontest.rb +612 -402
- metadata +56 -43
- data/test/alltests.rb +0 -10
data/Rakefile
CHANGED
@@ -1,120 +1,119 @@
|
|
1
|
-
# Rakefile for runt -*- ruby -*-
|
2
|
-
|
3
|
-
begin
|
4
|
-
require 'rubygems'
|
5
|
-
require 'rake/gempackagetask'
|
6
|
-
rescue Exception
|
7
|
-
nil
|
8
|
-
end
|
9
|
-
require 'rake'
|
10
|
-
require 'rake/clean'
|
11
|
-
require 'rake/testtask'
|
12
|
-
require 'rake/rdoctask'
|
13
|
-
require 'rake/contrib/sshpublisher'
|
14
|
-
require 'rake/contrib/rubyforgepublisher'
|
15
|
-
require 'fileutils'
|
16
|
-
|
17
|
-
#####################################################################
|
18
|
-
# Constants
|
19
|
-
#####################################################################
|
20
|
-
|
21
|
-
# Build Settings
|
22
|
-
PKG_VERSION = "0.
|
23
|
-
|
24
|
-
# Files to be included in Runt distribution
|
25
|
-
PKG_FILES = FileList[
|
26
|
-
'setup.rb',
|
27
|
-
'[A-Z]*',
|
28
|
-
'lib/**/*.rb',
|
29
|
-
'test/**/*.rb',
|
30
|
-
'doc/**/*',
|
31
|
-
'site/**/*'
|
32
|
-
].exclude("*.ses")
|
33
|
-
|
34
|
-
if(RUBY_PLATFORM =~ /win32/i)
|
35
|
-
PKG_EXEC_TAR = false
|
36
|
-
else
|
37
|
-
PKG_EXEC_TAR = true
|
38
|
-
end
|
39
|
-
|
40
|
-
# build directory
|
41
|
-
TARGET_DIR = "target"
|
42
|
-
|
43
|
-
#####################################################################
|
44
|
-
# Targets
|
45
|
-
#####################################################################
|
46
|
-
|
47
|
-
task :default => [:test]
|
48
|
-
task :clobber => [:clobber_build_dir]
|
49
|
-
|
50
|
-
# Make the build directory
|
51
|
-
directory TARGET_DIR
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
CLOBBER.include(TARGET_DIR)
|
57
|
-
end
|
58
|
-
|
59
|
-
Rake::RDocTask.new do |rd|
|
60
|
-
rd.rdoc_dir="#{TARGET_DIR}/doc"
|
61
|
-
rd.options << "-S"
|
62
|
-
rd.rdoc_files.include('lib
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
t.
|
68
|
-
t.
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
file "copy_site"
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
s.
|
96
|
-
s.
|
97
|
-
s.
|
98
|
-
s.
|
99
|
-
s.
|
100
|
-
s.
|
101
|
-
s.
|
102
|
-
s.
|
103
|
-
s.
|
104
|
-
s.
|
105
|
-
s.
|
106
|
-
s.
|
107
|
-
s.
|
108
|
-
s.
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
pkg.
|
118
|
-
|
119
|
-
|
120
|
-
end
|
1
|
+
# Rakefile for runt -*- ruby -*-
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rubygems'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
rescue Exception
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
require 'rake'
|
10
|
+
require 'rake/clean'
|
11
|
+
require 'rake/testtask'
|
12
|
+
require 'rake/rdoctask'
|
13
|
+
require 'rake/contrib/sshpublisher'
|
14
|
+
require 'rake/contrib/rubyforgepublisher'
|
15
|
+
require 'fileutils'
|
16
|
+
|
17
|
+
#####################################################################
|
18
|
+
# Constants
|
19
|
+
#####################################################################
|
20
|
+
|
21
|
+
# Build Settings
|
22
|
+
PKG_VERSION = "0.5.0"
|
23
|
+
|
24
|
+
# Files to be included in Runt distribution
|
25
|
+
PKG_FILES = FileList[
|
26
|
+
'setup.rb',
|
27
|
+
'[A-Z]*',
|
28
|
+
'lib/**/*.rb',
|
29
|
+
'test/**/*.rb',
|
30
|
+
'doc/**/*',
|
31
|
+
'site/**/*'
|
32
|
+
].exclude("*.ses")
|
33
|
+
|
34
|
+
if(RUBY_PLATFORM =~ /win32/i)
|
35
|
+
PKG_EXEC_TAR = false
|
36
|
+
else
|
37
|
+
PKG_EXEC_TAR = true
|
38
|
+
end
|
39
|
+
|
40
|
+
# build directory
|
41
|
+
TARGET_DIR = "target"
|
42
|
+
|
43
|
+
#####################################################################
|
44
|
+
# Targets
|
45
|
+
#####################################################################
|
46
|
+
|
47
|
+
task :default => [:test]
|
48
|
+
task :clobber => [:clobber_build_dir]
|
49
|
+
|
50
|
+
# Make the build directory
|
51
|
+
directory TARGET_DIR
|
52
|
+
|
53
|
+
# Calling this task directly doesn't work?
|
54
|
+
desc "Clobber the entire build directory."
|
55
|
+
task :clobber_build_dir do |t|
|
56
|
+
CLOBBER.include(TARGET_DIR)
|
57
|
+
end
|
58
|
+
|
59
|
+
Rake::RDocTask.new do |rd|
|
60
|
+
rd.rdoc_dir="#{TARGET_DIR}/doc"
|
61
|
+
rd.options << "-S"
|
62
|
+
rd.rdoc_files.include('lib/*','doc/*.rdoc','README','CHANGES','TODO','LICENSE.txt')
|
63
|
+
end
|
64
|
+
|
65
|
+
Rake::TestTask.new do |t|
|
66
|
+
t.libs << "test"
|
67
|
+
t.pattern = 'test/*test.rb'
|
68
|
+
t.verbose = false
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "Copy html files for the Runt website to the build directory."
|
72
|
+
file "copy_site" => TARGET_DIR
|
73
|
+
file "copy_site" do
|
74
|
+
cp_r Dir["site/*.{html,gif,png,css}"], TARGET_DIR
|
75
|
+
end
|
76
|
+
|
77
|
+
desc "Publish the Documentation to RubyForge."
|
78
|
+
task :publish => [:rdoc,:copy_site,:clobber_package] do |t|
|
79
|
+
publisher = Rake::CompositePublisher.new
|
80
|
+
publisher.add Rake::SshDirPublisher.new("mlipper@rubyforge.org", "/var/www/gforge-projects/runt",TARGET_DIR)
|
81
|
+
publisher.upload
|
82
|
+
end
|
83
|
+
|
84
|
+
desc "Publish the Documentation to the build dir."
|
85
|
+
task :test_publish => [:rdoc,:copy_site,:clobber_package] do |t|
|
86
|
+
puts "YAY! We've tested publish! YAY!"
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
if ! defined?(Gem)
|
91
|
+
puts "Package Target requires RubyGEMs"
|
92
|
+
else
|
93
|
+
spec = Gem::Specification.new do |s|
|
94
|
+
s.platform = Gem::Platform::RUBY
|
95
|
+
s.summary = "Ruby Temporal Expressions."
|
96
|
+
s.name = 'runt'
|
97
|
+
s.version = PKG_VERSION
|
98
|
+
s.requirements << 'none'
|
99
|
+
s.require_path = 'lib'
|
100
|
+
s.autorequire = 'runt'
|
101
|
+
s.files = PKG_FILES.to_a
|
102
|
+
s.author = 'Matthew Lipper'
|
103
|
+
s.email = 'mlipper@gmail.com'
|
104
|
+
s.homepage = 'http://runt.rubyforge.org'
|
105
|
+
s.has_rdoc = true
|
106
|
+
s.test_files = Dir['test/*test.rb']
|
107
|
+
s.rubyforge_project = 'runt'
|
108
|
+
s.description = <<EOF
|
109
|
+
Runt is a Ruby version of temporal patterns by
|
110
|
+
Martin Fowler. Runt provides an API for scheduling
|
111
|
+
recurring events using set-like semantics.
|
112
|
+
EOF
|
113
|
+
end
|
114
|
+
|
115
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
116
|
+
pkg.need_zip = true
|
117
|
+
pkg.need_tar = PKG_EXEC_TAR
|
118
|
+
end
|
119
|
+
end
|
data/TODO
CHANGED
@@ -1,20 +1,11 @@
|
|
1
|
-
= Runt - Ruby Temporal Expressions -- To Do List
|
2
|
-
|
3
|
-
Send suggestions, questions, threats, etc. for this list to Matt[mailto:
|
4
|
-
|
5
|
-
=== To Do
|
6
|
-
|
7
|
-
*
|
8
|
-
|
9
|
-
*
|
10
|
-
|
11
|
-
*
|
12
|
-
- Marshall
|
13
|
-
- database...create storable grammar, expression stack?
|
14
|
-
|
15
|
-
* Fix REWeek so that ordinal day values where the end day < start day does not fail
|
16
|
-
|
17
|
-
* Make Schedule a Module instead of a class
|
18
|
-
|
19
|
-
* Address performance issues due to implementation (in particular, PDate#<=>)
|
20
|
-
|
1
|
+
= Runt - Ruby Temporal Expressions -- To Do List
|
2
|
+
|
3
|
+
Send suggestions, questions, threats, etc. for this list to Matt[mailto:mlipper@gmail.com]
|
4
|
+
|
5
|
+
=== To Do
|
6
|
+
|
7
|
+
* Tutorials
|
8
|
+
|
9
|
+
* Better examples
|
10
|
+
|
11
|
+
* Laundry
|
data/doc/tutorial_schedule.rdoc
CHANGED
@@ -1,51 +1,84 @@
|
|
1
|
-
= Schedule Tutorial
|
2
|
-
|
3
|
-
If you haven't done so already, took a look the temporal expression
|
4
|
-
tutorial[http://runt.rubyforge.org/doc/files/doc/tutorial_te_rdoc.html].
|
5
|
-
|
6
|
-
So, you've defined some temporal expressions, now what? In his
|
7
|
-
paper[http://martinfowler.com/apsupp/recurring.pdf] about recurring events,
|
8
|
-
Martin Fowler also discusses a simple schedule API which is used, surprisingly
|
9
|
-
enough, to build a schedule.
|
10
|
-
|
11
|
-
We're not going to cover the pattern itself in this tutorial as Fowler already
|
12
|
-
does a nice job. Luckily, because it is such a simple pattern (once you invent
|
13
|
-
it!), you'll be able understand it even if you decide not to skim the
|
14
|
-
aforementioned paper[http://martinfowler.com/apsupp/recurring.pdf].
|
15
|
-
|
16
|
-
In the last tutorial we learned about the exciting world of NYC street cleaning
|
17
|
-
regulations. For the uptown side of my block, the sign says:
|
18
|
-
|
19
|
-
#############################
|
20
|
-
# #
|
21
|
-
# NO PARKING #
|
22
|
-
# #
|
23
|
-
# Mon, Wed, Fri 8am-11am #
|
24
|
-
# #
|
25
|
-
# T,Th 11:30am-2:00pm #
|
26
|
-
# #
|
27
|
-
# Violators will be towed! #
|
28
|
-
# #
|
29
|
-
#############################
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
We created a temporal expression to match this time period, like so:
|
35
|
-
|
36
|
-
expr1
|
37
|
-
|
38
|
-
expr2
|
39
|
-
|
40
|
-
ticket
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
1
|
+
= Schedule Tutorial
|
2
|
+
|
3
|
+
If you haven't done so already, took a look the temporal expression
|
4
|
+
tutorial[http://runt.rubyforge.org/doc/files/doc/tutorial_te_rdoc.html].
|
5
|
+
|
6
|
+
So, you've defined some temporal expressions, now what? In his
|
7
|
+
paper[http://martinfowler.com/apsupp/recurring.pdf] about recurring events,
|
8
|
+
Martin Fowler also discusses a simple schedule API which is used, surprisingly
|
9
|
+
enough, to build a schedule.
|
10
|
+
|
11
|
+
We're not going to cover the pattern itself in this tutorial as Fowler already
|
12
|
+
does a nice job. Luckily, because it is such a simple pattern (once you invent
|
13
|
+
it!), you'll be able understand it even if you decide not to skim the
|
14
|
+
aforementioned paper[http://martinfowler.com/apsupp/recurring.pdf].
|
15
|
+
|
16
|
+
In the last tutorial we learned about the exciting world of NYC street cleaning
|
17
|
+
regulations. For the uptown side of my block, the sign says:
|
18
|
+
|
19
|
+
#############################
|
20
|
+
# #
|
21
|
+
# NO PARKING #
|
22
|
+
# #
|
23
|
+
# Mon, Wed, Fri 8am-11am #
|
24
|
+
# #
|
25
|
+
# T,Th 11:30am-2:00pm #
|
26
|
+
# #
|
27
|
+
# Violators will be towed! #
|
28
|
+
# #
|
29
|
+
#############################
|
30
|
+
# #
|
31
|
+
# #
|
32
|
+
# #
|
33
|
+
|
34
|
+
We created a temporal expression to match this time period, like so:
|
35
|
+
|
36
|
+
expr1=(DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
|
37
|
+
|
38
|
+
expr2=(DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
|
39
|
+
|
40
|
+
ticket=expr1 | expr2
|
41
|
+
|
42
|
+
What we need at this point is a way to associate this expression with the rest
|
43
|
+
of our domain model. For this purpose, we can use a Schedule and an associated
|
44
|
+
Event both of which are supplied by Runt.
|
45
|
+
|
46
|
+
A Schedule holds zero or more Event/TemporalExpression pairs, allowing clients
|
47
|
+
to easily query and update TemporalExpressions as well perform certain range
|
48
|
+
operations as we will see in a moment.
|
49
|
+
|
50
|
+
An Event is simply a container for domain data. Although Runt uses Events
|
51
|
+
by default, Schedules will happily house any kind of Object. Internally, a
|
52
|
+
Schedule is really just a Hash where the keys are whatever it is you are
|
53
|
+
scheduling and the values are the TemporalExpressions you create.
|
54
|
+
|
55
|
+
|
56
|
+
def add(obj, expression)
|
57
|
+
@elems[obj]=expression
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
For the remainder of this tutorial I will refer to Events specifically, but
|
62
|
+
just remember that anything reasonably Hash-keyable will do.
|
63
|
+
|
64
|
+
|
65
|
+
So, let's pretend that I own a car. Since I don't want to get a ticket, I
|
66
|
+
decide to create a little cron-like utility daemon to remind myself whenever
|
67
|
+
it's time for me to move it.
|
68
|
+
|
69
|
+
class Ron
|
70
|
+
|
71
|
+
|
72
|
+
def run
|
73
|
+
loop
|
74
|
+
|
75
|
+
etc...
|
76
|
+
|
77
|
+
=.....FINISH ME!......
|
78
|
+
|
79
|
+
|
80
|
+
<em>See Also:</em>
|
81
|
+
|
82
|
+
* Fowler's recurring event pattern[http://martinfowler.com/apsupp/recurring.pdf]
|
83
|
+
|
84
|
+
* Other temporal patterns[http://martinfowler.com/ap2/timeNarrative.html]
|
data/doc/tutorial_te.rdoc
CHANGED
@@ -1,190 +1,190 @@
|
|
1
|
-
= Temporal Expressions Tutorial
|
2
|
-
|
3
|
-
Based on a pattern[http://martinfowler.com/apsupp/recurring.pdf]
|
4
|
-
created by Martin Fowler, temporal expressions define points or ranges
|
5
|
-
in time using <em>set expressions</em>. This means, an application
|
6
|
-
developer can precisely describe recurring events without resorting to
|
7
|
-
hacking out a big-ol' nasty enumerated list of dates.
|
8
|
-
|
9
|
-
For example, say you wanted to schedule an event that occurred
|
10
|
-
annually on the last Thursday of every August. You might start out by
|
11
|
-
doing something like this:
|
12
|
-
|
13
|
-
require 'date'
|
14
|
-
|
15
|
-
some_dates = [Date.new(2002,8,29),Date.new(2003,8,28),Date.new(2004,8,26)]
|
16
|
-
|
17
|
-
...etc.
|
18
|
-
|
19
|
-
This is fine for two or three years, but what about for thirty years?
|
20
|
-
What if you want to say every Monday, Tuesday and Friday, between 3
|
21
|
-
and 5pm for the next fifty years? *Ouch*.
|
22
|
-
|
23
|
-
As Fowler notes in his paper, TemporalExpressions(<tt>TE</tt>s for
|
24
|
-
short) provide a simple pattern language for defining a given set of
|
25
|
-
dates and/or times. They can be 'mixed-and- matched' as necessary,
|
26
|
-
providing an incremental, modular and expanding expressive power.
|
27
|
-
|
28
|
-
Alrighty, then...less talkin', more tutorin'!
|
29
|
-
|
30
|
-
=== Example 1
|
31
|
-
<b>Define An Expression That Says: 'the last Thursday in August'</b>
|
32
|
-
|
33
|
-
1 require 'runt'
|
34
|
-
2 require 'date'
|
35
|
-
3
|
36
|
-
4 last_thursday = DIMonth.new(Last_of,Thursday)
|
37
|
-
5
|
38
|
-
6 august = REYear.new(8)
|
39
|
-
7
|
40
|
-
8 expr = last_thursday & august
|
41
|
-
9
|
42
|
-
10 expr.include?(Date.new(2002,8,29)) #Thurs 8/29/02 => true
|
43
|
-
11 expr.include?(Date.new(2003,8,28)) #Thurs 8/28/03 => true
|
44
|
-
12 expr.include?(Date.new(2004,8,26)) #Thurs 8/26/04 => true
|
45
|
-
13 expr.include?(Date.new(2004,3,18)) #Thurs 3/18/04 => true
|
46
|
-
14
|
47
|
-
15 expr.include?(Date.new(2004,8,27)) #Fri 8/27/04 => false
|
48
|
-
|
49
|
-
A couple things are worth noting before we move on to more complicated
|
50
|
-
expressions.
|
51
|
-
|
52
|
-
Clients use temporal expressions by creating specific instances
|
53
|
-
(DIMonth == day in month, REYear == range each year) and then,
|
54
|
-
optionally, combining them using various familiar operators
|
55
|
-
<tt>( & , | , - )</tt>.
|
56
|
-
|
57
|
-
Semantically, the '&' operator on line 8 behaves much like the
|
58
|
-
standard Ruby short-circuit operator '&&'. However, instead of
|
59
|
-
returning a boolean value, a new composite <tt>TE</tt> is instead
|
60
|
-
created and returned. This new expression is the logical
|
61
|
-
intersection of everything matched by <b>both</b> arguments '&'.
|
62
|
-
|
63
|
-
In the example above, line 4:
|
64
|
-
|
65
|
-
|
66
|
-
last_thursday = DIMonth.new(Last_of,Thursday)
|
67
|
-
|
68
|
-
|
69
|
-
will match the last Thursday of <b>any</b> month and line 6:
|
70
|
-
|
71
|
-
|
72
|
-
august = REYear.new(8)
|
73
|
-
|
74
|
-
|
75
|
-
will match <b>any</b> date or date range occurring within the month of
|
76
|
-
August. Thus, combining them, you have 'the last Thursday' <b>AND</b>
|
77
|
-
'the month of August'.
|
78
|
-
|
79
|
-
By contrast:
|
80
|
-
|
81
|
-
|
82
|
-
expr = DIMonth.new(Last_of,Thursday) | REYear.new(8)
|
83
|
-
|
84
|
-
|
85
|
-
will all match dates and ranges occurring within 'the last Thursday'
|
86
|
-
<b>OR</b> 'the month of August'.
|
87
|
-
|
88
|
-
|
89
|
-
Now what? Beginning on line 11, you can see that calling the
|
90
|
-
<tt>#include?</tt> method will let you know whether the expression you've
|
91
|
-
defined includes a given date (or, in some cases, a range, or another
|
92
|
-
TE). This is much like the way you use the standard <tt>Range#include?</tt>.
|
93
|
-
|
94
|
-
=== Example 2
|
95
|
-
<b>Define: 'Street Cleaning Rules/Alternate Side Parking in NYC'</b>
|
96
|
-
|
97
|
-
In his paper[http://martinfowler.com/apsupp/recurring.pdf], Fowler
|
98
|
-
uses Boston parking regulations to illustrate some examples. Since I'm
|
99
|
-
from New York City, and Boston-related examples might cause an
|
100
|
-
allergic reaction, I'll use NYC's street cleaning and parking
|
101
|
-
calendar[http://www.nyc.gov/html/dot/html/motorist/scrintro.html#street]
|
102
|
-
instead. Since I'm not <em>completely</em> insane, I'll only use a
|
103
|
-
small subset of the City's actual rules.
|
104
|
-
|
105
|
-
On my block, parking is prohibited on the north side of the street
|
106
|
-
Monday, Wednesday, and Friday between the hours of 8am to 11am, and on
|
107
|
-
Tuesday and Thursday from 11:30am to 2pm
|
108
|
-
|
109
|
-
Hmmm...let's start by selecting days in the week.
|
110
|
-
|
111
|
-
Monday <b>OR</b> Wednesday <b>OR</b> Friday:
|
112
|
-
|
113
|
-
mon_wed_fri = DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)
|
114
|
-
|
115
|
-
#Sanity check
|
116
|
-
mon_wed_fri.include?( DateTime.new(2004,3,10,19,15) ) # Wed => true
|
117
|
-
mon_wed_fri.include?( DateTime.new(2004,3,14,9,00) ) # Sun => false
|
118
|
-
|
119
|
-
8am to 11am:
|
120
|
-
|
121
|
-
eight_to_eleven = REDay.new(8,00,11,00)
|
122
|
-
|
123
|
-
combine the two:
|
124
|
-
|
125
|
-
expr1 = mon_wed_fri & eight_to_eleven
|
126
|
-
|
127
|
-
and, logically speaking, we now have '(Mon <b>OR</b> Wed <b>OR</b> Fri)
|
128
|
-
<b>AND</b> (8am to 11am)'. We're halfway there.
|
129
|
-
|
130
|
-
Tuesdays and Thursdays:
|
131
|
-
|
132
|
-
tues_thurs = DIWeek.new(Tue) | DIWeek.new(Thu)
|
133
|
-
|
134
|
-
11:30am to 2pm:
|
135
|
-
eleven_thirty_to_two = REDay.new(11,30,14,00)
|
136
|
-
|
137
|
-
#Sanity check
|
138
|
-
eleven_thirty_to_two.include?( DateTime.new(2004,3,8,12,00) ) # Noon => true
|
139
|
-
eleven_thirty_to_two.include?( DateTime.new(2004,3,11,00,00) ) # Midnite => false
|
140
|
-
|
141
|
-
expr2 = tues_thurs & eleven_thirty_to_two
|
142
|
-
|
143
|
-
<tt>expr2</tt> says '(Tues <b>OR</b> Thurs) <b>AND</b> (11:30am to 2pm)'.
|
144
|
-
|
145
|
-
and finally:
|
146
|
-
|
147
|
-
ticket = expr1 | expr2
|
148
|
-
|
149
|
-
|
150
|
-
Or, logically, ((Mon <b>OR</b> Wed <b>OR</b> Fri) <b>AND</b> (8am to
|
151
|
-
11am)) <b>OR</b> ((Tues OR Thurs) <b>AND</b> (11:30am to 2pm))
|
152
|
-
|
153
|
-
|
154
|
-
Let's re-write this without all the noise:
|
155
|
-
|
156
|
-
|
157
|
-
expr1 = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
|
158
|
-
|
159
|
-
expr2 = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
|
160
|
-
|
161
|
-
ticket = expr1 | expr2
|
162
|
-
|
163
|
-
|
164
|
-
ticket.include?( DateTime.new(2004,3,11,12,15) ) # => true
|
165
|
-
|
166
|
-
ticket.include?( DateTime.new(2004,3,10,9,15) ) # => true
|
167
|
-
|
168
|
-
ticket.include?( DateTime.new(2004,3,10,8,00) ) # => true
|
169
|
-
|
170
|
-
ticket.include?( DateTime.new(2004,3,11,1,15) ) # => false
|
171
|
-
|
172
|
-
|
173
|
-
Sigh...now if I can only get my dad to remember this...
|
174
|
-
|
175
|
-
|
176
|
-
These are simple examples, but they demonstrate how temporal
|
177
|
-
expressions can be used instead of an enumerated list of date values
|
178
|
-
to define patterns of recurrence. There are many other temporal
|
179
|
-
expressions, and, more importantly, once you get the hang of it, it's
|
180
|
-
easy to write your own.
|
181
|
-
|
182
|
-
Fowler's paper[http://martinfowler.com/apsupp/recurring.pdf] also goes
|
183
|
-
on to describe another element of this pattern: the <tt>Schedule</tt>.
|
184
|
-
See the schedule tutorial[http://runt.rubyforge.org/doc/files/doc/tutorial_schedule_rdoc.html] for details.
|
185
|
-
|
186
|
-
<em>See Also:</em>
|
187
|
-
|
188
|
-
* Fowler's recurring event pattern[http://martinfowler.com/apsupp/recurring.pdf]
|
189
|
-
|
190
|
-
* Other temporal patterns[http://martinfowler.com/ap2/timeNarrative.html]
|
1
|
+
= Temporal Expressions Tutorial
|
2
|
+
|
3
|
+
Based on a pattern[http://martinfowler.com/apsupp/recurring.pdf]
|
4
|
+
created by Martin Fowler, temporal expressions define points or ranges
|
5
|
+
in time using <em>set expressions</em>. This means, an application
|
6
|
+
developer can precisely describe recurring events without resorting to
|
7
|
+
hacking out a big-ol' nasty enumerated list of dates.
|
8
|
+
|
9
|
+
For example, say you wanted to schedule an event that occurred
|
10
|
+
annually on the last Thursday of every August. You might start out by
|
11
|
+
doing something like this:
|
12
|
+
|
13
|
+
require 'date'
|
14
|
+
|
15
|
+
some_dates = [Date.new(2002,8,29),Date.new(2003,8,28),Date.new(2004,8,26)]
|
16
|
+
|
17
|
+
...etc.
|
18
|
+
|
19
|
+
This is fine for two or three years, but what about for thirty years?
|
20
|
+
What if you want to say every Monday, Tuesday and Friday, between 3
|
21
|
+
and 5pm for the next fifty years? *Ouch*.
|
22
|
+
|
23
|
+
As Fowler notes in his paper, TemporalExpressions(<tt>TE</tt>s for
|
24
|
+
short) provide a simple pattern language for defining a given set of
|
25
|
+
dates and/or times. They can be 'mixed-and- matched' as necessary,
|
26
|
+
providing an incremental, modular and expanding expressive power.
|
27
|
+
|
28
|
+
Alrighty, then...less talkin', more tutorin'!
|
29
|
+
|
30
|
+
=== Example 1
|
31
|
+
<b>Define An Expression That Says: 'the last Thursday in August'</b>
|
32
|
+
|
33
|
+
1 require 'runt'
|
34
|
+
2 require 'date'
|
35
|
+
3
|
36
|
+
4 last_thursday = DIMonth.new(Last_of,Thursday)
|
37
|
+
5
|
38
|
+
6 august = REYear.new(8)
|
39
|
+
7
|
40
|
+
8 expr = last_thursday & august
|
41
|
+
9
|
42
|
+
10 expr.include?(Date.new(2002,8,29)) #Thurs 8/29/02 => true
|
43
|
+
11 expr.include?(Date.new(2003,8,28)) #Thurs 8/28/03 => true
|
44
|
+
12 expr.include?(Date.new(2004,8,26)) #Thurs 8/26/04 => true
|
45
|
+
13 expr.include?(Date.new(2004,3,18)) #Thurs 3/18/04 => true
|
46
|
+
14
|
47
|
+
15 expr.include?(Date.new(2004,8,27)) #Fri 8/27/04 => false
|
48
|
+
|
49
|
+
A couple things are worth noting before we move on to more complicated
|
50
|
+
expressions.
|
51
|
+
|
52
|
+
Clients use temporal expressions by creating specific instances
|
53
|
+
(DIMonth == day in month, REYear == range each year) and then,
|
54
|
+
optionally, combining them using various familiar operators
|
55
|
+
<tt>( & , | , - )</tt>.
|
56
|
+
|
57
|
+
Semantically, the '&' operator on line 8 behaves much like the
|
58
|
+
standard Ruby short-circuit operator '&&'. However, instead of
|
59
|
+
returning a boolean value, a new composite <tt>TE</tt> is instead
|
60
|
+
created and returned. This new expression is the logical
|
61
|
+
intersection of everything matched by <b>both</b> arguments '&'.
|
62
|
+
|
63
|
+
In the example above, line 4:
|
64
|
+
|
65
|
+
|
66
|
+
last_thursday = DIMonth.new(Last_of,Thursday)
|
67
|
+
|
68
|
+
|
69
|
+
will match the last Thursday of <b>any</b> month and line 6:
|
70
|
+
|
71
|
+
|
72
|
+
august = REYear.new(8)
|
73
|
+
|
74
|
+
|
75
|
+
will match <b>any</b> date or date range occurring within the month of
|
76
|
+
August. Thus, combining them, you have 'the last Thursday' <b>AND</b>
|
77
|
+
'the month of August'.
|
78
|
+
|
79
|
+
By contrast:
|
80
|
+
|
81
|
+
|
82
|
+
expr = DIMonth.new(Last_of,Thursday) | REYear.new(8)
|
83
|
+
|
84
|
+
|
85
|
+
will all match dates and ranges occurring within 'the last Thursday'
|
86
|
+
<b>OR</b> 'the month of August'.
|
87
|
+
|
88
|
+
|
89
|
+
Now what? Beginning on line 11, you can see that calling the
|
90
|
+
<tt>#include?</tt> method will let you know whether the expression you've
|
91
|
+
defined includes a given date (or, in some cases, a range, or another
|
92
|
+
TE). This is much like the way you use the standard <tt>Range#include?</tt>.
|
93
|
+
|
94
|
+
=== Example 2
|
95
|
+
<b>Define: 'Street Cleaning Rules/Alternate Side Parking in NYC'</b>
|
96
|
+
|
97
|
+
In his paper[http://martinfowler.com/apsupp/recurring.pdf], Fowler
|
98
|
+
uses Boston parking regulations to illustrate some examples. Since I'm
|
99
|
+
from New York City, and Boston-related examples might cause an
|
100
|
+
allergic reaction, I'll use NYC's street cleaning and parking
|
101
|
+
calendar[http://www.nyc.gov/html/dot/html/motorist/scrintro.html#street]
|
102
|
+
instead. Since I'm not <em>completely</em> insane, I'll only use a
|
103
|
+
small subset of the City's actual rules.
|
104
|
+
|
105
|
+
On my block, parking is prohibited on the north side of the street
|
106
|
+
Monday, Wednesday, and Friday between the hours of 8am to 11am, and on
|
107
|
+
Tuesday and Thursday from 11:30am to 2pm
|
108
|
+
|
109
|
+
Hmmm...let's start by selecting days in the week.
|
110
|
+
|
111
|
+
Monday <b>OR</b> Wednesday <b>OR</b> Friday:
|
112
|
+
|
113
|
+
mon_wed_fri = DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)
|
114
|
+
|
115
|
+
#Sanity check
|
116
|
+
mon_wed_fri.include?( DateTime.new(2004,3,10,19,15) ) # Wed => true
|
117
|
+
mon_wed_fri.include?( DateTime.new(2004,3,14,9,00) ) # Sun => false
|
118
|
+
|
119
|
+
8am to 11am:
|
120
|
+
|
121
|
+
eight_to_eleven = REDay.new(8,00,11,00)
|
122
|
+
|
123
|
+
combine the two:
|
124
|
+
|
125
|
+
expr1 = mon_wed_fri & eight_to_eleven
|
126
|
+
|
127
|
+
and, logically speaking, we now have '(Mon <b>OR</b> Wed <b>OR</b> Fri)
|
128
|
+
<b>AND</b> (8am to 11am)'. We're halfway there.
|
129
|
+
|
130
|
+
Tuesdays and Thursdays:
|
131
|
+
|
132
|
+
tues_thurs = DIWeek.new(Tue) | DIWeek.new(Thu)
|
133
|
+
|
134
|
+
11:30am to 2pm:
|
135
|
+
eleven_thirty_to_two = REDay.new(11,30,14,00)
|
136
|
+
|
137
|
+
#Sanity check
|
138
|
+
eleven_thirty_to_two.include?( DateTime.new(2004,3,8,12,00) ) # Noon => true
|
139
|
+
eleven_thirty_to_two.include?( DateTime.new(2004,3,11,00,00) ) # Midnite => false
|
140
|
+
|
141
|
+
expr2 = tues_thurs & eleven_thirty_to_two
|
142
|
+
|
143
|
+
<tt>expr2</tt> says '(Tues <b>OR</b> Thurs) <b>AND</b> (11:30am to 2pm)'.
|
144
|
+
|
145
|
+
and finally:
|
146
|
+
|
147
|
+
ticket = expr1 | expr2
|
148
|
+
|
149
|
+
|
150
|
+
Or, logically, ((Mon <b>OR</b> Wed <b>OR</b> Fri) <b>AND</b> (8am to
|
151
|
+
11am)) <b>OR</b> ((Tues OR Thurs) <b>AND</b> (11:30am to 2pm))
|
152
|
+
|
153
|
+
|
154
|
+
Let's re-write this without all the noise:
|
155
|
+
|
156
|
+
|
157
|
+
expr1 = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
|
158
|
+
|
159
|
+
expr2 = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
|
160
|
+
|
161
|
+
ticket = expr1 | expr2
|
162
|
+
|
163
|
+
|
164
|
+
ticket.include?( DateTime.new(2004,3,11,12,15) ) # => true
|
165
|
+
|
166
|
+
ticket.include?( DateTime.new(2004,3,10,9,15) ) # => true
|
167
|
+
|
168
|
+
ticket.include?( DateTime.new(2004,3,10,8,00) ) # => true
|
169
|
+
|
170
|
+
ticket.include?( DateTime.new(2004,3,11,1,15) ) # => false
|
171
|
+
|
172
|
+
|
173
|
+
Sigh...now if I can only get my dad to remember this...
|
174
|
+
|
175
|
+
|
176
|
+
These are simple examples, but they demonstrate how temporal
|
177
|
+
expressions can be used instead of an enumerated list of date values
|
178
|
+
to define patterns of recurrence. There are many other temporal
|
179
|
+
expressions, and, more importantly, once you get the hang of it, it's
|
180
|
+
easy to write your own.
|
181
|
+
|
182
|
+
Fowler's paper[http://martinfowler.com/apsupp/recurring.pdf] also goes
|
183
|
+
on to describe another element of this pattern: the <tt>Schedule</tt>.
|
184
|
+
See the schedule tutorial[http://runt.rubyforge.org/doc/files/doc/tutorial_schedule_rdoc.html] for details.
|
185
|
+
|
186
|
+
<em>See Also:</em>
|
187
|
+
|
188
|
+
* Fowler's recurring event pattern[http://martinfowler.com/apsupp/recurring.pdf]
|
189
|
+
|
190
|
+
* Other temporal patterns[http://martinfowler.com/ap2/timeNarrative.html]
|