runt 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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]
|