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,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ #doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.swp
19
+ .ruby-version
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.3
5
+ - 2.0.0
@@ -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 Spec class better interaction with other expressions
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in runt.gemspec
4
+ gemspec
@@ -1,44 +1,22 @@
1
- The Apache Software License, Version 1.1
2
-
3
- Copyright (c) 2003-2004 Digital Clash LLC. All rights
4
- reserved.
5
-
6
- Redistribution and use in source and binary forms, with or without
7
- modification, are permitted provided that the following conditions
8
- are met:
9
-
10
- 1. Redistributions of source code must retain the above copyright
11
- notice, this list of conditions and the following disclaimer.
12
-
13
- 2. Redistributions in binary form must reproduce the above copyright
14
- notice, this list of conditions and the following disclaimer in
15
- the documentation and/or other materials provided with the
16
- distribution.
17
-
18
- 3. The end-user documentation included with the redistribution,
19
- if any, must include the following acknowledgment:
20
- "This product includes software developed by the
21
- Runt team (http://runt.rubyforge.org/)."
22
- Alternately, this acknowledgment may appear in the software itself,
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.
@@ -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
+ ![DCL Logo](site/dcl-small.gif)
data/Rakefile CHANGED
@@ -1,122 +1,9 @@
1
- # Rakefile for runt -*- ruby -*-
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
2
3
 
3
- begin
4
- require 'rubygems'
5
- require 'rake/gempackagetask'
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
+