runt 0.7.0 → 0.9.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/.gitignore +19 -0
- data/.travis.yml +5 -0
- data/{CHANGES → CHANGES.txt} +24 -8
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -44
- data/README.md +79 -0
- data/Rakefile +6 -119
- data/doc/tutorial_schedule.md +365 -0
- data/doc/tutorial_sugar.md +170 -0
- data/doc/tutorial_te.md +155 -0
- data/lib/runt.rb +36 -21
- data/lib/runt/dprecision.rb +4 -2
- data/lib/runt/pdate.rb +101 -95
- data/lib/runt/schedule.rb +18 -0
- data/lib/runt/sugar.rb +41 -9
- data/lib/runt/temporalexpression.rb +246 -30
- data/lib/runt/version.rb +3 -0
- data/runt.gemspec +24 -0
- data/site/.cvsignore +1 -0
- data/site/dcl-small.gif +0 -0
- data/site/index-rubforge-www.html +72 -0
- data/site/index.html +75 -60
- data/site/runt-logo.gif +0 -0
- data/site/runt-logo.psd +0 -0
- data/test/baseexpressiontest.rb +10 -8
- data/test/combinedexpressionstest.rb +166 -158
- data/test/daterangetest.rb +4 -6
- data/test/diweektest.rb +32 -32
- data/test/dprecisiontest.rb +2 -4
- data/test/everytetest.rb +6 -0
- data/test/expressionbuildertest.rb +2 -3
- data/test/icalendartest.rb +3 -6
- data/test/minitest_helper.rb +7 -0
- data/test/pdatetest.rb +21 -6
- data/test/redaytest.rb +3 -0
- data/test/reyeartest.rb +1 -1
- data/test/runttest.rb +5 -8
- data/test/scheduletest.rb +13 -14
- data/test/sugartest.rb +28 -6
- data/test/{spectest.rb → temporaldatetest.rb} +14 -4
- data/test/{rspectest.rb → temporalrangetest.rb} +4 -4
- data/test/test_runt.rb +11 -0
- data/test/weekintervaltest.rb +106 -0
- metadata +161 -116
- data/README +0 -106
- data/doc/tutorial_schedule.rdoc +0 -393
- data/doc/tutorial_sugar.rdoc +0 -143
- data/doc/tutorial_te.rdoc +0 -190
- data/setup.rb +0 -1331
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/{CHANGES → CHANGES.txt}
RENAMED
@@ -1,5 +1,21 @@
|
|
1
1
|
= Runt Changelog
|
2
2
|
|
3
|
+
== Version 0.9.0
|
4
|
+
|
5
|
+
* Replaced RDoc-based tutorials with Markdown files
|
6
|
+
|
7
|
+
* Removed old-school setup.rb
|
8
|
+
|
9
|
+
* Switched to MIT license
|
10
|
+
|
11
|
+
* Switched to Bundler for gemification
|
12
|
+
|
13
|
+
* Fixed compatiblity with Ruby 1.9 and Ruby 2.0
|
14
|
+
|
15
|
+
* Merged several commits from Paydici GitHub repo which included various enhancements and fixes by multiple contributors
|
16
|
+
|
17
|
+
* Moved to GitHub (finally)
|
18
|
+
|
3
19
|
== Version 0.7.0
|
4
20
|
|
5
21
|
* Addded tutorial_sugar.rdoc for new builder and shortcut stuff
|
@@ -9,7 +25,7 @@
|
|
9
25
|
* Fixed bug #20723: using modified patch contributed by Justin Cunningham. This partially reverted changes made by bug fix #5749, REDay by default now returns true for less precise arguments but accepts an optional constructor parameter which will override this behavior
|
10
26
|
|
11
27
|
* Removed deprecated "autorequire" property configuration from Rakefile
|
12
|
-
|
28
|
+
|
13
29
|
* Added properly spelled constant Runt::Eighth to the Runt module
|
14
30
|
|
15
31
|
* Added month constants defined in Date class to runt.rb for use by shortcuts
|
@@ -22,7 +38,7 @@
|
|
22
38
|
|
23
39
|
* Changed runttest.rb to use local Time so test doesn't fail when run from another time zone
|
24
40
|
|
25
|
-
* Fixed usage of deprecated methods in Date when accessing them from PDate subclass
|
41
|
+
* Fixed usage of deprecated methods in Date when accessing them from PDate subclass
|
26
42
|
|
27
43
|
* Applied patches providing week precision and expanded RFC2445 compliance tests contributed by Larry Karnowski
|
28
44
|
|
@@ -52,9 +68,9 @@
|
|
52
68
|
|
53
69
|
* Added update method to Schedule allowing clients to update existing expressions
|
54
70
|
|
55
|
-
* Added select method to Schedule allowing clients to query Events using arbitrary criteria
|
71
|
+
* Added select method to Schedule allowing clients to query Events using arbitrary criteria
|
56
72
|
|
57
|
-
* Added events method to Schedule which returns an Array of the currrently held Events
|
73
|
+
* Added events method to Schedule which returns an Array of the currrently held Events
|
58
74
|
|
59
75
|
* Added time-related shortcuts to Runt module contributed by Ara T. Howard
|
60
76
|
|
@@ -62,7 +78,7 @@
|
|
62
78
|
|
63
79
|
* Implemented meaningful to_s methods for TExpr classes
|
64
80
|
|
65
|
-
* Added include? method to Standard Library Date class allowing
|
81
|
+
* Added include? method to Standard Library Date class allowing TemporalDate class better interaction with other expressions
|
66
82
|
|
67
83
|
* Applied patch to fix PDate serialization bug contributed by Jodi Showers
|
68
84
|
|
@@ -84,12 +100,12 @@
|
|
84
100
|
|
85
101
|
== Version 0.3.0
|
86
102
|
|
87
|
-
* TExpr (finally!) becomes a Module instead of a superclass
|
103
|
+
* TExpr (finally!) becomes a Module instead of a superclass
|
88
104
|
|
89
105
|
* Added overlap? method for all temporal expressions and DateRange
|
90
106
|
|
91
107
|
* Added REMonth expression which matches a range of dates each month
|
92
|
-
|
108
|
+
|
93
109
|
* Contributed by Emmett Shear: TExpr#dates method which returns an array of dates occurring within the supplied DateRange
|
94
110
|
|
95
111
|
* Rakefile fixes:
|
@@ -114,7 +130,7 @@
|
|
114
130
|
* Renamed file dateprecisiontest.rb to dprecisiontest.rb
|
115
131
|
* Renamed several methods on PDate:
|
116
132
|
- second -> sec
|
117
|
-
- minute -> min
|
133
|
+
- minute -> min
|
118
134
|
- hour_of_day -> hour
|
119
135
|
- day_of_month -> day
|
120
136
|
|
data/Gemfile
ADDED
data/LICENSE.txt
CHANGED
@@ -1,44 +1,22 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
if and wherever such third-party acknowledgments normally appear.
|
24
|
-
|
25
|
-
4. The names "Runt" and "Digital Clash" not be used to endorse or
|
26
|
-
promote products derived from this software without prior written
|
27
|
-
permission. For written permission, please contact
|
28
|
-
info@digitalclash.com.
|
29
|
-
|
30
|
-
5. Products derived from this software may not be called "Runt",
|
31
|
-
"Digital Clash", nor may "Runt" or "Digital Clash" appear in
|
32
|
-
their name, without prior written permission of Digital Clash LLC.
|
33
|
-
|
34
|
-
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
|
35
|
-
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
36
|
-
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
37
|
-
DISCLAIMED. IN NO EVENT SHALL DIGITAL CLASH LLC OR ITS CONTRIBUTORS
|
38
|
-
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
39
|
-
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
40
|
-
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
41
|
-
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
42
|
-
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
43
|
-
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
44
|
-
THE POSSIBILITY OF SUCH DAMAGE.
|
1
|
+
Copyright (c) 2013 Matthew Lipper
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# RUNT -- Ruby Temporal Expressions
|
2
|
+
|
3
|
+
* Runt is a [Ruby](http://www.ruby-lang.org/en/) implementation of select temporal patterns by Martin Fowler described in this [paper](http://martinfowler.com/apsupp/recurring.pdf).
|
4
|
+
|
5
|
+
* Temporal expressions allow a developer to define patterns of date recurrence using set expressions.
|
6
|
+
|
7
|
+
## INSTALL
|
8
|
+
|
9
|
+
* gem install runt
|
10
|
+
|
11
|
+
|
12
|
+
## QUICK START
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'date'
|
16
|
+
require 'runt'
|
17
|
+
|
18
|
+
include Runt
|
19
|
+
|
20
|
+
a_monday = Date.new(2013,5,13) # Monday, May 13 - has "day-level" precision
|
21
|
+
a_wednesday = DateTime.new(2013,5,15,8,45) # Wednesday, May 15 at 8:45am - has "minute-level" precision
|
22
|
+
|
23
|
+
monday_expr = DIWeek.new(Mon) # Matches any Monday
|
24
|
+
monday_expr.include?(a_monday) # => true
|
25
|
+
monday_expr.include?(a_wednesday) # => false
|
26
|
+
|
27
|
+
wednesday_expr = DIWeek.new(Wed) # Matches any Wednesday
|
28
|
+
wednesday_expr.include?(a_monday) # => false
|
29
|
+
wednesday_expr.include?(a_wednesday) # => true
|
30
|
+
|
31
|
+
#
|
32
|
+
# Use an "OR" between two expressions
|
33
|
+
#
|
34
|
+
mon_or_wed_expr = monday_expr | wednesday_expr # Matches any Monday OR any Wednesday
|
35
|
+
mon_or_wed_expr.include?(a_monday) # => true
|
36
|
+
mon_or_wed_expr.include?(a_wednesday) # => true
|
37
|
+
|
38
|
+
daily_8_to_11_expr =REDay.new(8,00,11,00,false) # Matches from 8am to 11am on ANY date.
|
39
|
+
# The 'false' argument says not to auto-match expressions of lesser precision.
|
40
|
+
at_9 = DateTime.new(2013,5,12,9,0) # Sunday, May 12 at 9:00am
|
41
|
+
daily_8_to_11_expr.include?(at_9) # => true
|
42
|
+
#
|
43
|
+
# On the next line, the given Date instance is "promoted" to the minute-level precision
|
44
|
+
# required by the temporal expression so the time component defaults to 00:00
|
45
|
+
#
|
46
|
+
daily_8_to_11_expr.include?(a_monday) # => false
|
47
|
+
|
48
|
+
#
|
49
|
+
# Use an "AND" between two expressions to match
|
50
|
+
#
|
51
|
+
# (Monday OR Wednesday) AND (8am to 11am)
|
52
|
+
#
|
53
|
+
mon_or_wed_8_to_11_expr = mon_or_wed_expr & daily_8_to_11_expr
|
54
|
+
|
55
|
+
mon_or_wed_8_to_11_expr.include?(a_monday) # => false - 00:00 is not between 8:00 and 11:00
|
56
|
+
mon_or_wed_8_to_11_expr.include?(at_9) # => false - on Sunday
|
57
|
+
mon_or_wed_8_to_11_expr.include?(a_wednesday) # => true - a Wednesday at 8:45
|
58
|
+
|
59
|
+
```
|
60
|
+
|
61
|
+
## Tutorials
|
62
|
+
|
63
|
+
* Basic temporal expression [tutorial](doc/tutorial_te.md)
|
64
|
+
* Schedule [tutorial](doc/tutorial_schedule.md)
|
65
|
+
* Runt syntatic sugar [tutorial](doc/tutorial_sugar.md)
|
66
|
+
|
67
|
+
## Etc...
|
68
|
+
|
69
|
+
**Author:** Matthew Lipper <mlipper@gmail.com>
|
70
|
+
|
71
|
+
**Requires:** Tested with J/Ruby 1.8.7, 1.9.3 and Ruby 2.0.x
|
72
|
+
|
73
|
+
**License:** Released under the MIT License (see LICENSE.txt).
|
74
|
+
|
75
|
+
## Warranty
|
76
|
+
|
77
|
+
This software is provided "as is" and without any express or implied warranties, including, without limitation, the implied warranties of merchantibility and fitness for a particular purpose.
|
78
|
+
|
79
|
+

|
data/Rakefile
CHANGED
@@ -1,122 +1,9 @@
|
|
1
|
-
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
rescue Exception
|
7
|
-
nil
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.pattern = "test/*test.rb"
|
8
7
|
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
8
|
|
17
|
-
|
18
|
-
# Constants
|
19
|
-
#####################################################################
|
20
|
-
|
21
|
-
# Build Settings
|
22
|
-
PKG_VERSION = "0.7.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
|
-
'examples/**/*.rb',
|
31
|
-
'doc/**/*',
|
32
|
-
'site/**/*'
|
33
|
-
].exclude("*.ses")
|
34
|
-
|
35
|
-
if(RUBY_PLATFORM =~ /win32/i)
|
36
|
-
PKG_EXEC_TAR = false
|
37
|
-
else
|
38
|
-
PKG_EXEC_TAR = true
|
39
|
-
end
|
40
|
-
|
41
|
-
# build directory
|
42
|
-
TARGET_DIR = "target"
|
43
|
-
|
44
|
-
#####################################################################
|
45
|
-
# Targets
|
46
|
-
#####################################################################
|
47
|
-
|
48
|
-
task :default => [:test]
|
49
|
-
task :clobber => [:clobber_build_dir]
|
50
|
-
|
51
|
-
# Make the build directory
|
52
|
-
directory TARGET_DIR
|
53
|
-
|
54
|
-
# Calling this task directly doesn't work?
|
55
|
-
desc "Clobber the entire build directory."
|
56
|
-
task :clobber_build_dir do |t|
|
57
|
-
CLOBBER.include(TARGET_DIR)
|
58
|
-
end
|
59
|
-
|
60
|
-
Rake::RDocTask.new do |rd|
|
61
|
-
rd.rdoc_dir="#{TARGET_DIR}/doc"
|
62
|
-
rd.options << "-S"
|
63
|
-
rd.rdoc_files.include('lib/*','doc/*.rdoc','README','CHANGES','TODO','LICENSE.txt')
|
64
|
-
end
|
65
|
-
|
66
|
-
Rake::TestTask.new do |t|
|
67
|
-
t.libs << "test" << "examples"
|
68
|
-
t.pattern = '**/*test.rb'
|
69
|
-
t.verbose = false
|
70
|
-
t.warning = false
|
71
|
-
end
|
72
|
-
|
73
|
-
desc "Copy html files for the Runt website to the build directory."
|
74
|
-
file "copy_site" => TARGET_DIR
|
75
|
-
file "copy_site" do
|
76
|
-
cp_r Dir["site/*.{html,gif,png,css}"], TARGET_DIR
|
77
|
-
end
|
78
|
-
|
79
|
-
desc "Publish the Documentation to RubyForge."
|
80
|
-
task :publish => [:rdoc,:copy_site,:clobber_package] do |t|
|
81
|
-
publisher = Rake::CompositePublisher.new
|
82
|
-
publisher.add Rake::SshDirPublisher.new("mlipper@rubyforge.org", "/var/www/gforge-projects/runt",TARGET_DIR)
|
83
|
-
publisher.upload
|
84
|
-
end
|
85
|
-
|
86
|
-
desc "Publish the Documentation to the build dir."
|
87
|
-
task :test_publish => [:rdoc,:copy_site,:clobber_package] do |t|
|
88
|
-
puts "YAY! We've tested publish! YAY!"
|
89
|
-
end
|
90
|
-
|
91
|
-
|
92
|
-
if ! defined?(Gem)
|
93
|
-
puts "Package Target requires RubyGEMs"
|
94
|
-
else
|
95
|
-
spec = Gem::Specification.new do |s|
|
96
|
-
s.platform = Gem::Platform::RUBY
|
97
|
-
s.summary = "Ruby Temporal Expressions."
|
98
|
-
s.name = 'runt'
|
99
|
-
s.version = PKG_VERSION
|
100
|
-
s.requirements << 'none'
|
101
|
-
s.require_path = 'lib'
|
102
|
-
s.files = PKG_FILES.to_a
|
103
|
-
s.author = 'Matthew Lipper'
|
104
|
-
s.email = 'mlipper@gmail.com'
|
105
|
-
s.homepage = 'http://runt.rubyforge.org'
|
106
|
-
s.has_rdoc = true
|
107
|
-
s.rdoc_options += %w{--main README --title Runt}
|
108
|
-
s.extra_rdoc_files = FileList["README","CHANGES","TODO","LICENSE.txt","doc/*.rdoc"]
|
109
|
-
s.test_files = Dir['**/*test.rb']
|
110
|
-
s.rubyforge_project = 'runt'
|
111
|
-
s.description = <<EOF
|
112
|
-
Runt is a Ruby version of temporal patterns by
|
113
|
-
Martin Fowler. Runt provides an API for scheduling
|
114
|
-
recurring events using set-like semantics.
|
115
|
-
EOF
|
116
|
-
end
|
117
|
-
|
118
|
-
Rake::GemPackageTask.new(spec) do |pkg|
|
119
|
-
pkg.need_zip = true
|
120
|
-
pkg.need_tar = PKG_EXEC_TAR
|
121
|
-
end
|
122
|
-
end
|
9
|
+
task :default => :test
|
@@ -0,0 +1,365 @@
|
|
1
|
+
# Schedule 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
|
+
* In his [paper](http://martinfowler.com/apsupp/recurring.pdf) about recurring events, Martin Fowler also discusses a simple schedule API which is used, surprisingly enough, to build a schedule. We're not going to cover the pattern itself in this tutorial as Fowler already does a nice job. Because it is such a simple pattern (once you invent it!), you'll be able understand it even if you decide not to read his paper.
|
6
|
+
|
7
|
+
So, let's pretend that I own a car. Since I don't want to get a ticket, I decide to create an application which will tell me where and when I can park it on my street. (Since this is all make believe anyway, my car is a late 60's model black Ford Mustang with flame detailing (and on the back seat is one million dollars)).
|
8
|
+
|
9
|
+
We'll build a Runt Schedule that models the parking regulations. Our app will check this Schedule at regular intervals and send us reminders to move our car so we don't get a ticket. YAY!
|
10
|
+
|
11
|
+
First, let's visit the exciting world of NYC street cleaning regulations. Let's pretend the following rules are in place for our block:
|
12
|
+
|
13
|
+
* For the north side of the street, there is no parking Monday, Wednesday, or Friday, from 8am thru 11am
|
14
|
+
|
15
|
+
* For the south side of the street, there is no parking Tuesday or Thursday between 11:30am and 2pm
|
16
|
+
|
17
|
+
Thus...
|
18
|
+
|
19
|
+
<pre>
|
20
|
+
############################# #############################
|
21
|
+
# # # #
|
22
|
+
# NO PARKING # # NO PARKING #
|
23
|
+
# # # #
|
24
|
+
# Mon, Wed, Fri 8am-11am # # Tu, Th 11:30am-2:00pm #
|
25
|
+
# # # #
|
26
|
+
# # # #
|
27
|
+
# Violators will be towed! # # Violaters will be towed! #
|
28
|
+
# # # #
|
29
|
+
############################# #############################
|
30
|
+
# # # #
|
31
|
+
# # # #
|
32
|
+
# # # #
|
33
|
+
|
34
|
+
North side of the street South side of the street
|
35
|
+
</pre>
|
36
|
+
|
37
|
+
We'll start by creating temporal expressions which describe the verboten parking times:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
|
41
|
+
|
42
|
+
south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
|
43
|
+
```
|
44
|
+
|
45
|
+
What we need at this point is a way to write queries against these expressions to determine whether we need to send a reminder. For this purpose, we can use a Schedule and an associated Event, both of which are supplied by Runt.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
schedule = Schedule.new
|
49
|
+
```
|
50
|
+
|
51
|
+
A Schedule holds zero or more Event/TemporalExpression pairs, allowing clients to easily query and update TemporalExpressions as well perform certain range operations as we will see in a moment. We'll create two events, one for each side of the street:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
north_event = Event.new("north side")
|
55
|
+
|
56
|
+
south_event = Event.new("south side")
|
57
|
+
```
|
58
|
+
|
59
|
+
Now we add each event and its associated occurrence to our Schedule:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
schedule.add(north_event, north_expr)
|
63
|
+
|
64
|
+
schedule.add(south_event, south_expr)
|
65
|
+
```
|
66
|
+
|
67
|
+
An Event is simply a container for domain data. Although Runt uses Events by default, Schedules will happily house any kind of Object. Internally, a Schedule is really just a Hash where the keys are whatever it is you are scheduling and the values are the TemporalExpressions you create.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class Schedule
|
71
|
+
...
|
72
|
+
|
73
|
+
def add(obj, expression)
|
74
|
+
@elems[obj]=expression
|
75
|
+
end
|
76
|
+
...
|
77
|
+
```
|
78
|
+
|
79
|
+
Now that we have a Schedule configured, we need something to check it and then let us know if we need to move the car. For this, we'll create a simple class called Reminder which will function as the "main-able" part of our app. We'll start by creating an easily testable constructor which will be passed a Schedule instance (like the one we just created) and an SMTP server.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class Reminder
|
83
|
+
|
84
|
+
attr_reader :schedule, :mail_server
|
85
|
+
|
86
|
+
def initialize(schedule,mail_server)
|
87
|
+
@schedule = schedule
|
88
|
+
@mail_server = mail_server
|
89
|
+
end
|
90
|
+
...
|
91
|
+
```
|
92
|
+
|
93
|
+
Being rabid foaming-at-the-mouth Agilists, we'll of course also create a unit test to help flesh out the specifics of our new Reminder class. We'll create test fixtures using the Runt Objects described above.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class ReminderTest < Test::Unit::TestCase
|
97
|
+
|
98
|
+
include Runt
|
99
|
+
|
100
|
+
def setup
|
101
|
+
@schedule = Schedule.new
|
102
|
+
@north_event = Event.new("north side of the street will be ticketed")
|
103
|
+
north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
|
104
|
+
@schedule.add(@north_event, north_expr)
|
105
|
+
@south_event = Event.new("south side of the street will be ticketed")
|
106
|
+
south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
|
107
|
+
@schedule.add(@south_event, south_expr)
|
108
|
+
@mail_server = MailServer.new
|
109
|
+
@reminder = Reminder.new(@schedule, @mail_server)
|
110
|
+
@saturday_at_10 = PDate.min(2007,11,24,10,0,0)
|
111
|
+
@monday_at_10 = PDate.min(2007,11,26,10,0,0)
|
112
|
+
@tuesday_at_noon = PDate.min(2007,11,27,12,0,0)
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_initalize
|
116
|
+
assert_same @schedule, @reminder.schedule, "Expected #{@schedule} instead was #{@reminder.schedule}"
|
117
|
+
assert_same @mail_server, @reminder.mail_server, "Expected #{@mail_server} instead was #{@reminder.mail_server}"
|
118
|
+
end
|
119
|
+
...
|
120
|
+
```
|
121
|
+
|
122
|
+
For the purposes of this tutorial, the mail server will simply be a stub to illustrate how a real one might be used.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class MailServer
|
126
|
+
|
127
|
+
Struct.new("Email",:to,:from,:subject,:text)
|
128
|
+
|
129
|
+
def send(to, from, subject, text)
|
130
|
+
Struct::Email.new(to, from, subject, text)
|
131
|
+
# etc...
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
Next, let's add a method to our Reminder class which actually checks our schedule using a date which is passed in as a parameter.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class Reminder
|
141
|
+
...
|
142
|
+
def check(date)
|
143
|
+
return @schedule.events(date)
|
144
|
+
end
|
145
|
+
...
|
146
|
+
```
|
147
|
+
|
148
|
+
The Schedule#events method will return an Array of Event Objects for any events which occur at the date and time given by the method's argument. Usage is easily demonstrated by a test case which makes use of the fixtures created by the TestCase#setup method defined above.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
class ReminderTest < Test::Unit::TestCase
|
152
|
+
...
|
153
|
+
def test_check
|
154
|
+
assert_equal 1, @reminder.check(@monday_at_10).size, "Unexpected size #{@reminder.check(@monday_at_10).size} returned"
|
155
|
+
assert_same @north_event, @reminder.check(@monday_at_10)[0], "Expected Event #{@north_event}. Got #{@reminder.check(@monday_at_10)[0]}."
|
156
|
+
assert_equal 1, @reminder.check(@tuesday_at_noon).size, "Unexpected size #{@reminder.check(@tuesday_at_noon).size} returned"
|
157
|
+
assert_same @south_event, @reminder.check(@tuesday_at_noon)[0], "Expected Event #{@south_event}. Got #{@reminder.check(@tuesday_at_noon)[0]}."
|
158
|
+
assert @reminder.check(@saturday_at_10).empty?, "Expected empty Array. Got #{@reminder.check(@saturday_at_10)}"
|
159
|
+
end
|
160
|
+
...
|
161
|
+
```
|
162
|
+
|
163
|
+
There are other methods in the Schedule API which allow a client to query for information. Although we don't need them for this tutorial, I'll mention two briefly because they are generally useful. The first is Schedule#dates which will return an Array of PDate Objects which occur during the DateRange supplied as a parameter. The second is Schedule#include? which returns a boolean value indicating whether the Event occurs on the date which are both supplied as arguments.
|
164
|
+
|
165
|
+
Next, let's make use of the mail server argument given to the Reminder class in it's constructor. This is the method that will be called when a call to the Reminder#check method produces results.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
class Reminder
|
169
|
+
...
|
170
|
+
def send(date)
|
171
|
+
text = "Warning: " + events.join(', ')
|
172
|
+
return @mail_server.send(TO, FROM, SUBJECT, text)
|
173
|
+
end
|
174
|
+
...
|
175
|
+
```
|
176
|
+
|
177
|
+
Testing this is simple thanks to our MailServer stub which simply regurgitates the text argument it's passed as a result.
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class ReminderTest < Test::Unit::TestCase
|
181
|
+
...
|
182
|
+
def test_send
|
183
|
+
params = [@north_event, @south_event]
|
184
|
+
result = @reminder.send(params)
|
185
|
+
assert_email result, Reminder::TEXT + params.join(', ')
|
186
|
+
end
|
187
|
+
|
188
|
+
def assert_email(result, text)
|
189
|
+
assert_equal Reminder::TO, result.to, "Unexpected value for 'to' field of Email Struct: #{result.to}"
|
190
|
+
assert_equal Reminder::FROM, result.from, "Unexpected value for 'from' field of Email Struct: #{result.from}"
|
191
|
+
assert_equal Reminder::SUBJECT, result.subject, "Unexpected value for 'subject' field of Email Struct: #{result.subject}"
|
192
|
+
assert_equal text, result.text, "Unexpected value for 'text' field of Email Struct: #{result.text}"
|
193
|
+
end
|
194
|
+
...
|
195
|
+
```
|
196
|
+
|
197
|
+
Note the `ReminderTest#assert_email` method we've added to make assertions common to multiple test cases.
|
198
|
+
|
199
|
+
Now, let's tie the whole thing together with a method which which checks for occuring Events and (upon finding some) sends a reminder. This method is really the only one in the Reminder class that needs to be public.
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
class Reminder
|
203
|
+
...
|
204
|
+
def run(date)
|
205
|
+
result = self.check(date)
|
206
|
+
self.send(result) if !result.empty?
|
207
|
+
end
|
208
|
+
...
|
209
|
+
|
210
|
+
class ReminderTest < Test::Unit::TestCase
|
211
|
+
...
|
212
|
+
def test_send
|
213
|
+
params = [@north_event, @south_event]
|
214
|
+
result = @reminder.send(params)
|
215
|
+
assert_email result, Reminder::TEXT + params.join(', ')
|
216
|
+
end
|
217
|
+
...
|
218
|
+
```
|
219
|
+
|
220
|
+
Finally, we'll cheat a bit and stitch every thing together so it can be run from a command line.
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
include Runt
|
224
|
+
|
225
|
+
schedule = Schedule.new
|
226
|
+
north_event = Event.new("north side")
|
227
|
+
north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
|
228
|
+
schedule.add(north_event, north_expr)
|
229
|
+
south_event = Event.new("south side")
|
230
|
+
south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
|
231
|
+
schedule.add(south_event, south_expr)
|
232
|
+
reminder = Reminder.new(schedule, MailServer.new)
|
233
|
+
while true
|
234
|
+
sleep 15.minutes
|
235
|
+
reminder.run Time.now
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
So, here's all the code for this tutorial (it's in the Runt distribution under the examples folder):
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
### schedule_tutorial.rb ###
|
243
|
+
|
244
|
+
#!/usr/bin/ruby
|
245
|
+
|
246
|
+
require 'runt'
|
247
|
+
|
248
|
+
class Reminder
|
249
|
+
|
250
|
+
TO = "me@myselfandi.com"
|
251
|
+
FROM = "reminder@daemon.net"
|
252
|
+
SUBJECT = "Move your car!"
|
253
|
+
TEXT = "Warning: "
|
254
|
+
|
255
|
+
attr_reader :schedule, :mail_server
|
256
|
+
|
257
|
+
def initialize(schedule,mail_server)
|
258
|
+
@schedule = schedule
|
259
|
+
@mail_server = mail_server
|
260
|
+
end
|
261
|
+
def run(date)
|
262
|
+
result = self.check(date)
|
263
|
+
self.send(result) if !result.empty?
|
264
|
+
end
|
265
|
+
def check(date)
|
266
|
+
puts "Checking the schedule..." if $DEBUG
|
267
|
+
return @schedule.events(date)
|
268
|
+
end
|
269
|
+
def send(events)
|
270
|
+
text = TEXT + events.join(', ')
|
271
|
+
return @mail_server.send(TO, FROM, SUBJECT, text)
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
|
276
|
+
class MailServer
|
277
|
+
Struct.new("Email",:to,:from,:subject,:text)
|
278
|
+
def send(to, from, subject, text)
|
279
|
+
puts "Sending message TO: #{to} FROM: #{from} RE: #{subject}..." if $DEBUG
|
280
|
+
Struct::Email.new(to, from, subject, text)
|
281
|
+
# etc...
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
285
|
+
|
286
|
+
if __FILE__ == $0
|
287
|
+
|
288
|
+
include Runt
|
289
|
+
|
290
|
+
schedule = Schedule.new
|
291
|
+
north_event = Event.new("north side")
|
292
|
+
north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
|
293
|
+
schedule.add(north_event, north_expr)
|
294
|
+
south_event = Event.new("south side")
|
295
|
+
south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
|
296
|
+
schedule.add(south_event, south_expr)
|
297
|
+
reminder = Reminder.new(schedule, MailServer.new)
|
298
|
+
while true
|
299
|
+
sleep 15.minutes
|
300
|
+
reminder.run Time.now
|
301
|
+
end
|
302
|
+
|
303
|
+
end
|
304
|
+
|
305
|
+
### schedule_tutorialtest.rb ###
|
306
|
+
|
307
|
+
#!/usr/bin/ruby
|
308
|
+
|
309
|
+
require 'test/unit' require 'runt' require 'schedule_tutorial'
|
310
|
+
|
311
|
+
class ReminderTest < Test::Unit::TestCase
|
312
|
+
|
313
|
+
include Runt
|
314
|
+
|
315
|
+
def setup
|
316
|
+
@schedule = Schedule.new
|
317
|
+
@north_event = Event.new("north side of the street will be ticketed")
|
318
|
+
north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
|
319
|
+
@schedule.add(@north_event, north_expr)
|
320
|
+
@south_event = Event.new("south side of the street will be ticketed")
|
321
|
+
south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
|
322
|
+
@schedule.add(@south_event, south_expr)
|
323
|
+
@mail_server = MailServer.new
|
324
|
+
@reminder = Reminder.new(@schedule, @mail_server)
|
325
|
+
@saturday_at_10 = PDate.min(2007,11,24,10,0,0)
|
326
|
+
@monday_at_10 = PDate.min(2007,11,26,10,0,0)
|
327
|
+
@tuesday_at_noon = PDate.min(2007,11,27,12,0,0)
|
328
|
+
end
|
329
|
+
def test_initalize
|
330
|
+
assert_same @schedule, @reminder.schedule, "Expected #{@schedule} instead was #{@reminder.schedule}"
|
331
|
+
assert_same @mail_server, @reminder.mail_server, "Expected #{@mail_server} instead was #{@reminder.mail_server}"
|
332
|
+
end
|
333
|
+
def test_send
|
334
|
+
params = [@north_event, @south_event]
|
335
|
+
result = @reminder.send(params)
|
336
|
+
assert_email result, Reminder::TEXT + params.join(', ')
|
337
|
+
end
|
338
|
+
def test_check
|
339
|
+
assert_equal 1, @reminder.check(@monday_at_10).size, "Unexpected size #{@reminder.check(@monday_at_10).size} returned"
|
340
|
+
assert_same @north_event, @reminder.check(@monday_at_10)[0], "Expected Event #{@north_event}. Got #{@reminder.check(@monday_at_10)[0]}."
|
341
|
+
assert_equal 1, @reminder.check(@tuesday_at_noon).size, "Unexpected size #{@reminder.check(@tuesday_at_noon).size} returned"
|
342
|
+
assert_same @south_event, @reminder.check(@tuesday_at_noon)[0], "Expected Event #{@south_event}. Got #{@reminder.check(@tuesday_at_noon)[0]}."
|
343
|
+
assert @reminder.check(@saturday_at_10).empty?, "Expected empty Array. Got #{@reminder.check(@saturday_at_10)}"
|
344
|
+
end
|
345
|
+
def test_run
|
346
|
+
result = @reminder.run(@monday_at_10)
|
347
|
+
assert_email result, Reminder::TEXT + @north_event.to_s
|
348
|
+
end
|
349
|
+
def assert_email(result, text)
|
350
|
+
assert_equal Reminder::TO, result.to, "Unexpected value for 'to' field of Email Struct: #{result.to}"
|
351
|
+
assert_equal Reminder::FROM, result.from, "Unexpected value for 'from' field of Email Struct: #{result.from}"
|
352
|
+
assert_equal Reminder::SUBJECT, result.subject, "Unexpected value for 'subject' field of Email Struct: #{result.subject}"
|
353
|
+
assert_equal text, result.text, "Unexpected value for 'text' field of Email Struct: #{result.text}"
|
354
|
+
end
|
355
|
+
|
356
|
+
end
|
357
|
+
```
|
358
|
+
|
359
|
+
*See Also:*
|
360
|
+
|
361
|
+
* Temporal Expressions [tutorial](tutorial_te.md)
|
362
|
+
* Runt syntax sugar [tutorial](tutorial_sugar.md)
|
363
|
+
* Fowler's recurring event [pattern](http://martinfowler.com/apsupp/recurring.pdf)
|
364
|
+
* Other temporal [patterns](http://martinfowler.com/eaaDev/timeNarrative.html)
|
365
|
+
|