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.
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
+