├── .gitignore ├── .travis.yml ├── CHANGES.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── doc ├── tutorial_schedule.md ├── tutorial_sugar.md └── tutorial_te.md ├── examples ├── payment_report.rb ├── payment_reporttest.rb ├── reminder.rb ├── schedule_tutorial.rb └── schedule_tutorialtest.rb ├── lib ├── runt.rb └── runt │ ├── daterange.rb │ ├── dprecision.rb │ ├── expressionbuilder.rb │ ├── pdate.rb │ ├── schedule.rb │ ├── sugar.rb │ ├── temporalexpression.rb │ └── version.rb ├── runt.gemspec ├── site ├── .cvsignore ├── blue-robot3.css ├── dcl-small.gif ├── index-rubforge-www.html ├── index.html ├── logohover.png ├── runt-logo.gif └── runt-logo.psd └── test ├── aftertetest.rb ├── baseexpressiontest.rb ├── beforetetest.rb ├── collectiontest.rb ├── combinedexpressionstest.rb ├── daterangetest.rb ├── dayintervaltetest.rb ├── difftest.rb ├── dimonthtest.rb ├── diweektest.rb ├── dprecisiontest.rb ├── everytetest.rb ├── expressionbuildertest.rb ├── icalendartest.rb ├── intersecttest.rb ├── minitest_helper.rb ├── pdatetest.rb ├── redaytest.rb ├── remonthtest.rb ├── reweektest.rb ├── reyeartest.rb ├── runttest.rb ├── scheduletest.rb ├── sugartest.rb ├── temporaldatetest.rb ├── temporalexpressiontest.rb ├── temporalrangetest.rb ├── test_runt.rb ├── uniontest.rb ├── weekintervaltest.rb ├── wimonthtest.rb └── yeartetest.rb /.gitignore: -------------------------------------------------------------------------------- 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 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.3 5 | - 2.0.0 6 | - 2.2 7 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Runt Changelog 2 | 3 | ## Version 0.9.0 4 | 5 | * Special thanks to Bill Burcham for his repo contributions and help getting this release out the door 6 | 7 | * Replaced RDoc-based tutorials with Markdown files 8 | 9 | * Removed old-school setup.rb 10 | 11 | * Switched to MIT license 12 | 13 | * Switched to Bundler for gemification 14 | 15 | * Fixed compatiblity with Ruby 1.9 and Ruby 2.0 16 | 17 | * Merged several commits from Paydici GitHub repo which included various enhancements and fixes by multiple contributors 18 | 19 | * Moved to GitHub (finally) 20 | 21 | ## Version 0.7.0 22 | 23 | * Addded tutorial_sugar.rdoc for new builder and shortcut stuff 24 | 25 | * Added ExpressionBuilder class for creating expressions using more human-friendly syntax 26 | 27 | * 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 28 | 29 | * Removed deprecated "autorequire" property configuration from Rakefile 30 | 31 | * Added properly spelled constant Runt::Eighth to the Runt module 32 | 33 | * Added month constants defined in Date class to runt.rb for use by shortcuts 34 | 35 | * Added optional Runt module extension which provides some syntactic sugar for creating common expressions 36 | 37 | * Added tests and fixed broken to_s method in AfterTE and BeforeTE 38 | 39 | * Applied patch contributed by Justin Cunningham for optimizing performance of the TExprUtils#max_day_of_month method - source is taken verbatim from ActiveSupport::CoreExtensions::Time::Calculations::ClassMethods module days_in_month method 40 | 41 | * Changed runttest.rb to use local Time so test doesn't fail when run from another time zone 42 | 43 | * Fixed usage of deprecated methods in Date when accessing them from PDate subclass 44 | 45 | * Applied patches providing week precision and expanded RFC2445 compliance tests contributed by Larry Karnowski 46 | 47 | * Fixed bug #19982: REYear will match day in any month if @same_month_dates_provided with patch submitted by Riley Lynch 48 | 49 | ## Version 0.6.0 50 | 51 | * Refactored and improved temporal expression tests, moving them to separate files per class 52 | 53 | * Fixed bug #5741: REYear incorrect handling of default args 54 | 55 | * Fixed bug #5749: Fixed spans midnight for REDay (Revision 156), changed semantics when dealing with lower precision arguments 56 | 57 | * Fixed bug #10640: incorrect tutorial section for tutorial_te.rdoc 58 | 59 | * Fixed bug #10605: DateRange.empty? should be true for min == max 60 | 61 | * Finished Schedule API tutorial 62 | 63 | * Fixed bug #16143: Typo in Schedule RDoc 64 | 65 | * Added README (and related files) in the generated rdoc 66 | 67 | ## Version 0.5.0 68 | 69 | * Refactored Schedule implementation which greatly simplifies customization but does potentially break existing clients who relied on the ability to call add mulitple times (SEE BELOW) 70 | 71 | * Added update method to Schedule allowing clients to update existing expressions 72 | 73 | * Added select method to Schedule allowing clients to query Events using arbitrary criteria 74 | 75 | * Added events method to Schedule which returns an Array of the currrently held Events 76 | 77 | * Added time-related shortcuts to Runt module contributed by Ara T. Howard 78 | 79 | * Added ability to work with Time class contributed by Paul Wright 80 | 81 | * Implemented meaningful to_s methods for TExpr classes 82 | 83 | * Added include? method to Standard Library Date class allowing TemporalDate class better interaction with other expressions 84 | 85 | * Applied patch to fix PDate serialization bug contributed by Jodi Showers 86 | 87 | * Added BeforeTE, AfterTE, enhanced TExpr#dates method, RFC2445 (iCalendar) compliance unit test, contributed by Larry Karnowski 88 | 89 | * Applied patch by Gordon Thiesfeld which fixes broken Time class compatibility 90 | 91 | * Applied patch by Gordon Thiesfeld which allows REWeek expressions to span across two weeks 92 | 93 | * Removed unused context.rb and contexttest.rb 94 | 95 | * Removed unnecessary test suite alltests.rb 96 | 97 | ## Version 0.4.0 98 | 99 | * Added DayIntervalTE contributed by Ira Burton which matches every n days past a given date 100 | 101 | * Added YearTE requested by Pat Maddox 102 | 103 | ## Version 0.3.0 104 | 105 | * TExpr (finally!) becomes a Module instead of a superclass 106 | 107 | * Added overlap? method for all temporal expressions and DateRange 108 | 109 | * Added REMonth expression which matches a range of dates each month 110 | 111 | * Contributed by Emmett Shear: TExpr#dates method which returns an array of dates occurring within the supplied DateRange 112 | 113 | * Rakefile fixes: 114 | - test path allow gem to be installed with -t switch 115 | - usage of gzip which will break on Win32 116 | 117 | * Removed and then subsequently restored SpecTE 118 | 119 | * General clean-up, including: 120 | - renamed several methods on PDate 121 | - renamed 'dateprecisiontest.rb' to 'dprecisiontest.rb' 122 | 123 | * Fixed Object.id deprecation warning 124 | 125 | 126 | ## Version 0.2.0 127 | 128 | * Fixed Schedule class 129 | * Renamed Schedule#is_occurring? to Schedule#include? 130 | * RAA deployment 131 | * GEM deployment 132 | * Renamed file dateprecisiontest.rb to dprecisiontest.rb 133 | * Renamed several methods on PDate: 134 | - second -> sec 135 | - minute -> min 136 | - hour_of_day -> hour 137 | - day_of_month -> day 138 | 139 | ## Version 0.1.0 140 | 141 | * Inspired by suggestions[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/95465] on c.l.r., did massive renaming and refactoring to make source more Ruby-esque: 142 | 143 | - TemporalExpression class => TExpr 144 | - DatePrecision module => DPrecision (file name changed also) 145 | - TimePoint class => PDate (file name changed also) 146 | - Added transparent use of bitwise set operators ( & , | ) for building composite expressions 147 | - Added transparent use of - operator for diff expressions 148 | 149 | * Updated TE Tutorial to reflect new and improved syntax usage 150 | * Skipped several version numbers to celebrate 151 | 152 | ## Version 0.0.6 153 | 154 | * TE Tutorial 155 | * Website beautification 156 | * Credits 157 | 158 | ## Version 0.0.4 159 | 160 | * Improved Rake[http://rake.rubyforge.org] support 161 | * Better documentation: README, TODO, CHANGES, etc. 162 | * More Ruby-like source code layout and code organization inspired by Rake[http://rake.rubyforge.org] distribution 163 | * Dropped the obviously superfluous 'alpha' from versioning 164 | 165 | ## Version 0.0.1 166 | 167 | * Learned Ruby (or began trying, at any rate...) 168 | * RubyForge project setup 169 | * Basic implementation of the Java-based chronicJ[http://chronicJ.org] functionality in Ruby 170 | 171 | 172 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in runt.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 40 | # expressions of lesser precision. 41 | 42 | at_9 = DateTime.new(2013,5,12,9,0) # Sunday, May 12 at 9:00am 43 | daily_8_to_11_expr.include?(at_9) # => true 44 | # 45 | # On the next line, the given Date instance is "promoted" to the minute-level precision 46 | # required by the temporal expression so the time component defaults to 00:00 47 | # 48 | daily_8_to_11_expr.include?(a_monday) # => false 49 | 50 | # 51 | # Use an "AND" between two expressions to match 52 | # 53 | # (Monday OR Wednesday) AND (8am to 11am) 54 | # 55 | mon_or_wed_8_to_11_expr = mon_or_wed_expr & daily_8_to_11_expr 56 | 57 | mon_or_wed_8_to_11_expr.include?(a_monday) # => false - 00:00 is not between 8:00 and 11:00 58 | mon_or_wed_8_to_11_expr.include?(at_9) # => false - on Sunday 59 | mon_or_wed_8_to_11_expr.include?(a_wednesday) # => true - a Wednesday at 8:45 60 | 61 | ``` 62 | 63 | ## Tutorials 64 | 65 | * Basic temporal expression [tutorial](doc/tutorial_te.md) 66 | * Schedule [tutorial](doc/tutorial_schedule.md) 67 | * Runt syntatic sugar [tutorial](doc/tutorial_sugar.md) 68 | 69 | ## Etc... 70 | 71 | **Author:** Matthew Lipper 72 | 73 | **Requires:** Tested with J/Ruby 1.8.7, 1.9.3 and Ruby 2.0.0, 2.2 74 | 75 | **License:** Released under the MIT License (see LICENSE.txt). 76 | 77 | ## Warranty 78 | 79 | 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. 80 | 81 | Copyright © 2002-2013 ![DCL Logo](site/dcl-small.gif) 82 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.pattern = "test/*test.rb" 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /doc/tutorial_schedule.md: -------------------------------------------------------------------------------- 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 |
 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 | 
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 | 366 | -------------------------------------------------------------------------------- /doc/tutorial_sugar.md: -------------------------------------------------------------------------------- 1 | # Sugar 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 | * Starting with version 0.7.0, Runt provides some syntactic sugar for creating temporal expressions. Runt also provides a builder class for which can be used to create expressions in a more readable way than simply using `:new`. 6 | 7 | First, let's look at some of the new shorcuts for creating individual expressions. If you look at the `lib/runt/sugar.rb` file you find that the `Runt` module has been re-opened and some nutty stuff happens when `:method_missing` is called. 8 | 9 | For example, if you've included the `Runt` module, you can now create a `DIWeek` expression by calling a method whose name matches the following pattern: 10 | 11 | ``` 12 | /^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/ 13 | ``` 14 | 15 | So 16 | 17 | tuesday 18 | 19 | is equivalent to 20 | 21 | DIWeek.new(Tuesday) 22 | 23 | Here's a quick summary of patterns and the expressions they create. 24 | 25 | ### REDay 26 | 27 | **regex**: `/^daily_(d{1,2})_(d{2})([ap]m)*to*(d{1,2})_(d{2})([ap]m)$/` 28 | 29 | **example**: `daily_8_30am_to_10_00pm` 30 | 31 | **action**: `REDay.new(8,30,22,00)` 32 | 33 | 34 | ### REWeek 35 | 36 | **regex**: `/^weekly_(sunday|monday|tuesday|wednesday|thursday|friday|saturday)_to_(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/` 37 | 38 | **example**: `weekly_wednesday_to_friday` 39 | 40 | **action**: `REWeek.new(Wednesday, Friday)` 41 | 42 | 43 | ### REMonth 44 | 45 | **regex**: `/^monthly_(d{1,2})(?:st|nd|rd|th)_to_(d{1,2})(?:st|nd|rd|th)$/` 46 | 47 | **example**: `monthly_2nd_to_24th` 48 | 49 | **action**: `REMonth.new(2,24)` 50 | 51 | 52 | ### REYear 53 | 54 | **regex**: `/^yearly_(january|february|march|april|may|june|july|august|september|october|november|december)_(d{1,2})_to_(january|february|march|april|may|june|july|august|september|october|november|december)_(d{1,2})` 55 | 56 | **example**: `yearly_may_31_to_september_1` 57 | 58 | **action**: `REYear.new(May,31,September,1)` 59 | 60 | 61 | ### DIWeek 62 | 63 | **regex**: `/^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/` 64 | 65 | **example**: `friday` 66 | 67 | **action**: `DIWeek.new(Friday)` 68 | 69 | 70 | ### DIMonth 71 | 72 | **regex**: `/^(first|second|third|fourth|last|second_to_last)_(sunday|monday|tuesday|w ednesday|thursday|friday|saturday)$/` 73 | 74 | **example**: `last_friday` 75 | 76 | **action**: `DIMonth.new(Last,Friday)` 77 | 78 | 79 | There are also other methods defined (not via `:method_missing`) which provide shortcuts: 80 | 81 | ### AfterTE 82 | 83 | **method**: `after(date, inclusive=false)` 84 | 85 | **action**: `AfterTE.new(date, inclusive=false)` 86 | 87 | 88 | ### BeforeTE 89 | 90 | **method**: `before(date, inclusive=false)` 91 | 92 | **action**: `BeforeTE.new(date, inclusive=false)` 93 | 94 | 95 | Now let's look at the new `ExpressionBuilder` class. This class uses some simple methods and `instance_eval` to allow one to create composite temporal expressions in a more fluid style than `:new` and friends. The idea is that you define a block where method calls add to a composite expression using either "and", "or", or "not". 96 | 97 | ```ruby 98 | # Create a new builder 99 | d = ExpressionBuilder.new 100 | 101 | # Call define with a block 102 | expression = d.define do 103 | on REDay.new(8,45,9,30) 104 | on DIWeek.new(Friday) # "And" 105 | possibly DIWeek.new(Saturday) # "Or" 106 | except DIMonth.new(Last, Friday) # "Not" 107 | end 108 | 109 | # expression = "Daily 8:45am to 9:30 and Fridays or Saturday except not the last Friday of the month" 110 | ``` 111 | 112 | Hmmm, this is not really an improvement over 113 | 114 | ```ruby 115 | REDay.new(8,45,9,30) & DIWeek.new(Friday) | DIWeek.new(Saturday) - DIMonth.new(Last, Friday) 116 | ``` 117 | 118 | I know, let's try the new constructor aliases defined above! 119 | 120 | ```ruby 121 | expression = d.define do 122 | on daily_8_45am_to_9_30am 123 | on friday 124 | possibly saturday 125 | except last_friday 126 | end 127 | ``` 128 | 129 | Much better, except "on daily..." seems a little awkward. We can use `:occurs` which is aliased to `:on` for just such a scenario. 130 | 131 | ```ruby 132 | expression = d.define do 133 | occurs daily_8_45am_to_9_30am 134 | on friday 135 | possibly saturday 136 | except last_friday 137 | end 138 | ``` 139 | 140 | ExpressionBuilder creates expressions by evaluating a block passed to the `:define` method. From inside the block, methods `:occurs`, `:on`, `:every`, `:possibly`, and `:maybe` can be called with a temporal expression which will be added to a composite expression as follows: 141 | 142 | **:on** creates an "and" (`&`) 143 | 144 | **:possibly** creates an "or" (`|`) 145 | 146 | **:except** creates a "not" (`-`) 147 | 148 | **:every** alias for `:on` method 149 | 150 | **:occurs** alias for `:on` method 151 | 152 | **:maybe** alias for `:possibly` method 153 | 154 | 155 | Of course it's easy to open the builder class and add you own aliases if the ones provided don't work for you: 156 | 157 | ```ruby 158 | class ExpressionBuilder 159 | alias_method :potentially, :possibly 160 | # etc.... 161 | end 162 | ``` 163 | 164 | If there are shortcuts or macros that you think others would find useful, send in a pull request. 165 | 166 | 167 | *See Also:* 168 | 169 | * Temporal Expressions [tutorial](tutorial_te.md) 170 | * Schedule [tutorial](tutorial_schedule.md) 171 | -------------------------------------------------------------------------------- /doc/tutorial_te.md: -------------------------------------------------------------------------------- 1 | # Temporal Expressions Tutorial 2 | 3 | Based on a [pattern](http://martinfowler.com/apsupp/recurring.pdf) created by Martin Fowler, temporal expressions define points or ranges in time using *set expressions*. This means, an application developer can precisely describe recurring events without resorting to hacking out a big-ol' nasty enumerated list of dates. 4 | 5 | For example, say you wanted to schedule an event that occurred annually on the last Thursday of every August. You might start out by doing something like this: 6 | 7 | ```ruby 8 | require 'date' 9 | 10 | some_dates = [Date.new(2002,8,29),Date.new(2003,8,28),Date.new(2004,8,26)] 11 | ``` 12 | 13 | This is fine for two or three years, but what about for thirty years? What if you want to say every Monday, Tuesday and Friday, between 3 and 5pm for the next fifty years? 14 | 15 | As Fowler notes in his paper, TemporalExpressions(`TE`s for short) provide a simple pattern language for defining a given set of dates and/or times. They can be mixed and matched as necessary, providing modular component expressions that can be combined to define arbitrarily complex periods of time. 16 | 17 | ## Example 1 18 | **Define An Expression That Says: 'the last Thursday in August'** 19 | 20 | ```ruby 21 | require 'runt' 22 | require 'date' 23 | 24 | last_thursday = DIMonth.new(Last_of,Thursday) 25 | 26 | august = REYear.new(8) 27 | 28 | expr = last_thursday & august 29 | 30 | expr.include?(Date.new(2002,8,29)) #Thurs 8/29/02 => true 31 | expr.include?(Date.new(2003,8,28)) #Thurs 8/28/03 => true 32 | expr.include?(Date.new(2004,8,26)) #Thurs 8/26/04 => true 33 | 34 | expr.include?(Date.new(2004,3,18)) #Thurs 3/18/04 => false 35 | expr.include?(Date.new(2004,8,27)) #Fri 8/27/04 => false 36 | ``` 37 | 38 | A couple things are worth noting before we move on to more complicated expressions. 39 | 40 | Clients use temporal expressions by creating specific instances (`DIMonth` == day in month, `REYear` == range each year) and then, optionally, combining them using various familiar operators `( & , | , - )`. 41 | 42 | Semantically, the `&` operator on line 8 behaves much like the standard Ruby short-circuit operator `&&`. However, instead of returning a boolean value, a new composite `TE` is instead created and returned. This new expression is the logical intersection of everything matched by **both** arguments `&`. 43 | 44 | In the example above, line 4: 45 | 46 | ```ruby 47 | last_thursday = DIMonth.new(Last_of,Thursday) 48 | ``` 49 | 50 | will match the last Thursday of **any** month and line 6: 51 | 52 | ```ruby 53 | august = REYear.new(8) 54 | ``` 55 | 56 | will match **any** date or date range occurring within the month of August. Thus, combining them, you have 'the last Thursday' **AND** 'the month of August'. 57 | 58 | By contrast: 59 | 60 | ```ruby 61 | expr = DIMonth.new(Last_of,Thursday) | REYear.new(8) 62 | ``` 63 | 64 | will all match dates and ranges occurring within 'the last Thursday' **OR** 'the month of August'. 65 | 66 | Now what? You can see that calling the `#include?` method will let you know whether the expression you've defined includes a given date (or, in some cases, a range, or another TE). This is much like the way you use the standard `Range#include?`. 67 | 68 | ## Example 2 69 | **Define: 'Street Cleaning Rules/Alternate Side Parking in NYC'** 70 | 71 | In his [paper](http://martinfowler.com/apsupp/recurring.pdf), Fowler uses Boston parking regulations to illustrate some examples. Since I'm from New York City, and Boston-related examples might cause an allergic reaction, I'll use NYC's street cleaning and parking [calendar](http://www.nyc.gov/html/dot/html/motorist/scrintro.html#street) 72 | instead. Since I'm not *completely* insane, I'll only use a small subset of the City's actual rules. 73 | 74 | On my block, parking is prohibited on the north side of the street Monday, Wednesday, and Friday between the hours of 8am to 11am, and on Tuesday and Thursday from 11:30am to 2pm...let's start by selecting days in the week. 75 | 76 | Monday **OR** Wednesday **OR** Friday: 77 | 78 | ```ruby 79 | mon_wed_fri = DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri) 80 | 81 | mon_wed_fri.include?( DateTime.new(2004,3,10,19,15) ) # Wed => true 82 | mon_wed_fri.include?( DateTime.new(2004,3,14,9,00) ) # Sun => false 83 | ``` 84 | 85 | 8am to 11am: 86 | 87 | ```ruby 88 | eight_to_eleven = REDay.new(8,00,11,00) 89 | ``` 90 | combine the two: 91 | 92 | ```ruby 93 | expr1 = mon_wed_fri & eight_to_eleven 94 | ``` 95 | 96 | and, logically speaking, we now have '(Mon **OR** Wed **OR** Fri) **AND** (8am to 11am)'. We're halfway there. 97 | 98 | Tuesdays and Thursdays: 99 | 100 | ```ruby 101 | tues_thurs = DIWeek.new(Tue) | DIWeek.new(Thu) 102 | ``` 103 | 104 | 11:30am to 2pm: 105 | 106 | ```ruby 107 | eleven_thirty_to_two = REDay.new(11,30,14,00) 108 | 109 | eleven_thirty_to_two.include?( DateTime.new(2004,3,8,12,00) ) # Noon => true 110 | eleven_thirty_to_two.include?( DateTime.new(2004,3,11,00,00) ) # Midnite => false 111 | 112 | expr2 = tues_thurs & eleven_thirty_to_two 113 | ``` 114 | 115 | `expr2` says '(Tues **OR** Thurs) **AND** (11:30am to 2pm)'. 116 | 117 | and finally: 118 | 119 | ```ruby 120 | ticket = expr1 | expr2 121 | ``` 122 | 123 | Or, logically, ((Mon **OR** Wed **OR** Fri) **AND** (8am to 11am)) **OR** ((Tues OR Thurs) **AND** (11:30am to 2pm)) 124 | 125 | Let's re-write this without all the noise: 126 | 127 | ```ruby 128 | expr1 = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00) 129 | 130 | expr2 = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00) 131 | 132 | ticket = expr1 | expr2 133 | 134 | ticket.include?( DateTime.new(2004,3,11,12,15) ) # => true 135 | 136 | ticket.include?( DateTime.new(2004,3,10,9,15) ) # => true 137 | 138 | ticket.include?( DateTime.new(2004,3,10,8,00) ) # => true 139 | 140 | ticket.include?( DateTime.new(2004,3,11,1,15) ) # => false 141 | ``` 142 | 143 | Sigh...now if I can only get my dad to remember this... 144 | 145 | These are simple examples, but they demonstrate how temporal expressions can be used instead of an enumerated list of date values to define patterns of recurrence. There are many other temporal expressions, and, more importantly, once you get the hang of it, it's easy to write your own. 146 | 147 | Fowler's [paper](http://martinfowler.com/apsupp/recurring.pdf) also goes on to describe another element of this pattern: the `Schedule`. See the schedule [tutorial](tutorial_schedule.md) for details. 148 | 149 | *See Also:* 150 | 151 | * Schedule [tutorial](tutorial_schedule.md) 152 | * Sugar [tutorial](tutorial_sugar.md) 153 | * Martin Fowler's recurring event [pattern](http://martinfowler.com/apsupp/recurring.pdf) 154 | * Other temporal [patterns](http://martinfowler.com/eaaDev/timeNarrative.html) 155 | 156 | -------------------------------------------------------------------------------- /examples/payment_report.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'runt' 4 | 5 | class Report 6 | 7 | attr_reader :schedule 8 | 9 | def initialize(schedule) 10 | @schedule = schedule 11 | end 12 | def list(range) 13 | result = {} 14 | range.each do |dt| 15 | events = @schedule.events(dt) 16 | result[dt]=events unless events.empty? 17 | end 18 | result 19 | end 20 | end 21 | 22 | class Payment < Runt::Event 23 | attr_accessor :amount 24 | def initialize(id, amount) 25 | super(id) 26 | @amount = amount 27 | end 28 | end 29 | 30 | 31 | if __FILE__ == $0 32 | 33 | include Runt 34 | 35 | schedule = Schedule.new 36 | 37 | # Gas payment on the first Wednesday of every month 38 | gas_payment = Payment.new("Gas", 234) 39 | gas_expr = DIMonth.new(First, Wednesday) 40 | schedule.add(gas_payment, gas_expr) 41 | 42 | # Insurance payment every year on January 7th 43 | insurance_payment = Payment.new("Insurance", 345) 44 | insurance_expr = REYear.new(1, 7, 1, 7) 45 | schedule.add(insurance_payment, insurance_expr) 46 | 47 | # Run a report 48 | report = Report.new(schedule) 49 | result = report.list(PDate.day(2008, 1, 1)..PDate.day(2008,1,31)) 50 | result.keys.sort.each do |dt| 51 | unless result[dt].empty? then 52 | print "#{dt.ctime} - " 53 | result[dt].each do |event| 54 | puts "#{event.id}, $#{event.amount}" 55 | end 56 | end 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /examples/payment_reporttest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'test/unit' 4 | require 'runt' 5 | require 'payment_report' 6 | 7 | class ReportTest < Test::Unit::TestCase 8 | 9 | include Runt 10 | 11 | def setup 12 | @schedule = Schedule.new 13 | 14 | # Gas payment on the first Wednesday of every month 15 | @gas_payment = Payment.new("Gas", 234) 16 | @gas_expr = DIMonth.new(First, Wednesday) 17 | @schedule.add(@gas_payment, @gas_expr) 18 | 19 | # Insurance payment every year on January 7th 20 | @insurance_payment = Payment.new("Insurance", 345) 21 | @insurance_expr = REYear.new(1, 7, 1, 7) 22 | @schedule.add(@insurance_payment, @insurance_expr) 23 | @report = Report.new(@schedule) 24 | end 25 | def test_initialize 26 | assert_equal @schedule, @report.schedule 27 | end 28 | def test_list 29 | range = PDate.day(2008, 1, 1)..PDate.day(2008,1,31) 30 | result = @report.list(range) 31 | assert_equal(2, result.size) 32 | assert_equal(@gas_payment, result[PDate.day(2008, 1, 2)][0]) 33 | assert_equal(@insurance_payment, result[PDate.day(2008, 1, 7)][0]) 34 | end 35 | end 36 | 37 | class PaymentTest < Test::Unit::TestCase 38 | 39 | include Runt 40 | 41 | def test_initialize 42 | p = Payment.new "Foo", 12 43 | assert_equal "Foo", p.id 44 | assert_equal 12, p.amount 45 | end 46 | 47 | end 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/reminder.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | # NOTE this is slightly broken; it is in the process of being fixed 4 | base = File.basename(Dir.pwd) 5 | if base == "examples" || base =~ /runt/ 6 | Dir.chdir("..") if base == "examples" 7 | $LOAD_PATH.unshift(Dir.pwd + '/lib') 8 | Dir.chdir("examples") if base =~ /runt/ 9 | end 10 | 11 | 12 | 13 | 14 | require 'runt' 15 | 16 | class Reminder 17 | include Runt 18 | 19 | def initialize(schedule) 20 | @schedule=schedule 21 | end 22 | 23 | def next_times(event,end_point,now=Time.now) 24 | @schedule.dates(event,DateRange.new(now,end_point)) 25 | end 26 | end 27 | 28 | # start of range whose occurrences we want to list 29 | # TODO fix Runt so this can be done with Time instead 30 | # e.g., now=Time.now 31 | #now=Time.parse("13:00") 32 | #now.date_precision=Runt::DPrecision::MIN 33 | now=Runt::PDate.min(2006,12,8,13,00) 34 | 35 | # end of range 36 | soon=(now + 10.minutes) 37 | 38 | # Sanity check 39 | print "start: #{now.to_s} (#{now.date_precision}) end: #{soon.to_s} (#{soon.date_precision})\n" 40 | 41 | # 42 | # Schedule used to house TemporalExpression describing the recurrence from 43 | # which we'd list to generate a list of dates. In this example, some Event 44 | # occuring every 5 minutes. 45 | # 46 | schedule=Runt::Schedule.new 47 | 48 | # Some event whose schedule we're interested in 49 | event=Runt::Event.new("whatever") 50 | 51 | # Add the event to the schedule ( 52 | # NOTE: any Object that is a sensible Hash key can be used 53 | schedule.add(event,Runt::EveryTE.new(now,5.minutes)) 54 | 55 | # Example domain Object using Runt 56 | reminder=Reminder.new(schedule) 57 | 58 | # Call our domain Object with the start and end times and the event 59 | # in which we're interested 60 | #puts "times (inclusive) = #{reminder.next_times(event,soon,now).join('\n')}" 61 | 62 | puts "times (inclusive):" 63 | reminder.next_times(event,soon,now).each{|t| puts t} 64 | -------------------------------------------------------------------------------- /examples/schedule_tutorial.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'runt' 4 | 5 | class Reminder 6 | 7 | TO = "me@myselfandi.com" 8 | FROM = "reminder@daemon.net" 9 | SUBJECT = "Move your car!" 10 | TEXT = "Warning: " 11 | 12 | attr_reader :schedule, :mail_server 13 | 14 | def initialize(schedule,mail_server) 15 | @schedule = schedule 16 | @mail_server = mail_server 17 | end 18 | def run(date) 19 | result = self.check(date) 20 | self.send(result) if !result.empty? 21 | end 22 | def check(date) 23 | puts "Checking the schedule..." if $DEBUG 24 | return @schedule.events(date) 25 | end 26 | def send(events) 27 | text = TEXT + events.join(', ') 28 | return @mail_server.send(TO, FROM, SUBJECT, text) 29 | end 30 | end 31 | 32 | class MailServer 33 | Struct.new("Email",:to,:from,:subject,:text) 34 | def send(to, from, subject, text) 35 | puts "Sending message TO: #{to} FROM: #{from} RE: #{subject}..." if $DEBUG 36 | Struct::Email.new(to, from, subject, text) 37 | # etc... 38 | end 39 | end 40 | 41 | 42 | if __FILE__ == $0 43 | 44 | include Runt 45 | 46 | schedule = Schedule.new 47 | north_event = Event.new("north side") 48 | north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00) 49 | schedule.add(north_event, north_expr) 50 | south_event = Event.new("south side") 51 | south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00) 52 | schedule.add(south_event, south_expr) 53 | reminder = Reminder.new(schedule, MailServer.new) 54 | while true 55 | sleep 15.minutes 56 | reminder.run Time.now 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /examples/schedule_tutorialtest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'test/unit' 4 | require 'runt' 5 | require 'schedule_tutorial' 6 | 7 | class ReminderTest < Test::Unit::TestCase 8 | 9 | include Runt 10 | 11 | def setup 12 | @schedule = Schedule.new 13 | @north_event = Event.new("north side of the street will be ticketed") 14 | north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00) 15 | @schedule.add(@north_event, north_expr) 16 | @south_event = Event.new("south side of the street will be ticketed") 17 | south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00) 18 | @schedule.add(@south_event, south_expr) 19 | @mail_server = MailServer.new 20 | @reminder = Reminder.new(@schedule, @mail_server) 21 | @saturday_at_10 = PDate.min(2007,11,24,10,0,0) 22 | @monday_at_10 = PDate.min(2007,11,26,10,0,0) 23 | @tuesday_at_noon = PDate.min(2007,11,27,12,0,0) 24 | end 25 | def test_initalize 26 | assert_same @schedule, @reminder.schedule, "Expected #{@schedule} instead was #{@reminder.schedule}" 27 | assert_same @mail_server, @reminder.mail_server, "Expected #{@mail_server} instead was #{@reminder.mail_server}" 28 | end 29 | def test_send 30 | params = [@north_event, @south_event] 31 | result = @reminder.send(params) 32 | assert_email result, Reminder::TEXT + params.join(', ') 33 | end 34 | def test_check 35 | assert_equal 1, @reminder.check(@monday_at_10).size, "Unexpected size #{@reminder.check(@monday_at_10).size} returned" 36 | assert_same @north_event, @reminder.check(@monday_at_10)[0], "Expected Event #{@north_event}. Got #{@reminder.check(@monday_at_10)[0]}." 37 | assert_equal 1, @reminder.check(@tuesday_at_noon).size, "Unexpected size #{@reminder.check(@tuesday_at_noon).size} returned" 38 | assert_same @south_event, @reminder.check(@tuesday_at_noon)[0], "Expected Event #{@south_event}. Got #{@reminder.check(@tuesday_at_noon)[0]}." 39 | assert @reminder.check(@saturday_at_10).empty?, "Expected empty Array. Got #{@reminder.check(@saturday_at_10)}" 40 | end 41 | def test_run 42 | result = @reminder.run(@monday_at_10) 43 | assert_email result, Reminder::TEXT + @north_event.to_s 44 | end 45 | def assert_email(result, text) 46 | assert_equal Reminder::TO, result.to, "Unexpected value for 'to' field of Email Struct: #{result.to}" 47 | assert_equal Reminder::FROM, result.from, "Unexpected value for 'from' field of Email Struct: #{result.from}" 48 | assert_equal Reminder::SUBJECT, result.subject, "Unexpected value for 'subject' field of Email Struct: #{result.subject}" 49 | assert_equal text, result.text, "Unexpected value for 'text' field of Email Struct: #{result.text}" 50 | end 51 | end 52 | 53 | -------------------------------------------------------------------------------- /lib/runt.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # :title:Runt -- Ruby Temporal Expressions 4 | # 5 | # == Runt -- Ruby Temporal Expressions 6 | # 7 | # The usage and design patterns expressed in this library are mostly...*uhm*.. 8 | # entirely..*cough*...based on a series of 9 | # articles[http://www.martinfowler.com] by Martin Fowler. 10 | # 11 | # It highly recommended that anyone using Runt (or writing 12 | # object-oriented software :) take a moment to peruse the wealth of useful info 13 | # that Fowler has made publicly available: 14 | # 15 | # * An excellent introductory summation of temporal patterns[http://martinfowler.com/ap2/timeNarrative.html] 16 | # * Recurring event pattern[http://martinfowler.com/apsupp/recurring.pdf] 17 | # 18 | # Also, for those of you (like me, for example) still chained in your cubicle and forced 19 | # to write Java[http://java.sun.com] code, check out the original version of 20 | # project called ChronicJ[http://chronicj.org]. 21 | # 22 | # --- 23 | # Author:: Matthew Lipper (mailto:mlipper@gmail.com) 24 | # Copyright:: Copyright (c) 2004 Digital Clash, LLC 25 | # License:: See LICENSE.txt 26 | # 27 | # = Warranty 28 | # 29 | # This software is provided "as is" and without any express or 30 | # implied warranties, including, without limitation, the implied 31 | # warranties of merchantibility and fitness for a particular 32 | # purpose. 33 | 34 | require 'yaml' 35 | require 'time' 36 | require 'date' 37 | require "runt/version" 38 | require "runt/dprecision" 39 | require "runt/pdate" 40 | require "runt/temporalexpression" 41 | require "runt/schedule" 42 | require "runt/daterange" 43 | require "runt/sugar" 44 | require "runt/expressionbuilder" 45 | 46 | # 47 | # The Runt module is the main namespace for all Runt modules and classes. Using 48 | # require statements, it makes the entire Runt library available.It also 49 | # defines some new constants and exposes some already defined in the standard 50 | # library classes Date and DateTime. 51 | # 52 | # See also runt/sugar_rb which re-opens this module and adds 53 | # some additional functionality 54 | # 55 | # See also date.rb 56 | # 57 | module Runt 58 | 59 | class << self 60 | 61 | def day_name(number) 62 | Date::DAYNAMES[number] 63 | end 64 | 65 | def month_name(number) 66 | Date::MONTHNAMES[number] 67 | end 68 | 69 | def format_time(date) 70 | date.strftime('%I:%M%p') 71 | end 72 | 73 | def format_date(date) 74 | date.ctime 75 | end 76 | 77 | # 78 | # Cut and pasted from activesupport-1.2.5/lib/inflector.rb 79 | # 80 | def ordinalize(number) 81 | if (number.to_i==-1) 82 | 'last' 83 | elsif (number.to_i==-2) 84 | 'second to last' 85 | elsif (11..13).include?(number.to_i % 100) 86 | "#{number}th" 87 | else 88 | case number.to_i % 10 89 | when 1 then "#{number}st" 90 | when 2 then "#{number}nd" 91 | when 3 then "#{number}rd" 92 | else "#{number}th" 93 | end 94 | end 95 | end 96 | 97 | end 98 | 99 | #Yes it's true, I'm a big idiot! 100 | Sunday = Date::DAYNAMES.index("Sunday") 101 | Monday = Date::DAYNAMES.index("Monday") 102 | Tuesday = Date::DAYNAMES.index("Tuesday") 103 | Wednesday = Date::DAYNAMES.index("Wednesday") 104 | Thursday = Date::DAYNAMES.index("Thursday") 105 | Friday = Date::DAYNAMES.index("Friday") 106 | Saturday = Date::DAYNAMES.index("Saturday") 107 | Sun = Date::ABBR_DAYNAMES.index("Sun") 108 | Mon = Date::ABBR_DAYNAMES.index("Mon") 109 | Tue = Date::ABBR_DAYNAMES.index("Tue") 110 | Wed = Date::ABBR_DAYNAMES.index("Wed") 111 | Thu = Date::ABBR_DAYNAMES.index("Thu") 112 | Fri = Date::ABBR_DAYNAMES.index("Fri") 113 | Sat = Date::ABBR_DAYNAMES.index("Sat") 114 | January = Date::MONTHNAMES.index("January") 115 | February = Date::MONTHNAMES.index("February") 116 | March = Date::MONTHNAMES.index("March") 117 | April = Date::MONTHNAMES.index("April") 118 | May = Date::MONTHNAMES.index("May") 119 | June = Date::MONTHNAMES.index("June") 120 | July = Date::MONTHNAMES.index("July") 121 | August = Date::MONTHNAMES.index("August") 122 | September = Date::MONTHNAMES.index("September") 123 | October = Date::MONTHNAMES.index("October") 124 | November = Date::MONTHNAMES.index("November") 125 | December = Date::MONTHNAMES.index("December") 126 | First = 1 127 | Second = 2 128 | Third = 3 129 | Fourth = 4 130 | Fifth = 5 131 | Sixth = 6 132 | Seventh = 7 133 | Eighth = 8 134 | Eigth = 8 # Will be removed in v0.9.0 135 | Ninth = 9 136 | Tenth = 10 137 | 138 | private 139 | class ApplyLast #:nodoc: 140 | def initialize 141 | @negate=Proc.new{|n| n*-1} 142 | end 143 | def [](arg) 144 | @negate.call(arg) 145 | end 146 | end 147 | LastProc = ApplyLast.new 148 | 149 | public 150 | Last = LastProc[First] 151 | Last_of = LastProc[First] 152 | Second_to_last = LastProc[Second] 153 | 154 | end 155 | 156 | # 157 | # Add precision +Runt::DPrecision+ to standard library classes Date and DateTime 158 | # (which is a subclass of Date). Also, add an include? method for interoperability 159 | # with +Runt::TExpr+ classes 160 | # 161 | class Date 162 | 163 | include Runt 164 | 165 | alias_method :include?, :eql? 166 | 167 | attr_accessor :date_precision 168 | 169 | def date_precision 170 | if @date_precision.nil? then 171 | if self.class == DateTime then 172 | @date_precision = Runt::DPrecision::SEC 173 | else 174 | @date_precision = Runt::DPrecision::DAY 175 | end 176 | end 177 | @date_precision 178 | end 179 | end 180 | 181 | # 182 | # Add the ability to use Time class 183 | # 184 | # Contributed by Paul Wright 185 | # 186 | class Time 187 | 188 | include Runt 189 | 190 | attr_accessor :date_precision 191 | alias_method :old_initialize, :initialize 192 | def initialize(*args) 193 | if(args[0].instance_of?(Runt::DPrecision::Precision)) 194 | @precision=args.shift 195 | else 196 | @precision=Runt::DPrecision::SEC 197 | end 198 | old_initialize(*args) 199 | end 200 | 201 | alias :old_to_yaml :to_yaml 202 | def to_yaml(options) 203 | if self.instance_variables.empty? 204 | self.old_to_yaml(options) 205 | else 206 | Time.old_parse(self.to_s).old_to_yaml(options) 207 | end 208 | end 209 | 210 | class << self 211 | alias_method :old_parse, :parse 212 | def parse(*args) 213 | precision=Runt::DPrecision::DEFAULT 214 | if(args[0].instance_of?(Runt::DPrecision::Precision)) 215 | precision=args.shift 216 | end 217 | _parse=old_parse(*args) 218 | _parse.date_precision=precision 219 | _parse 220 | end 221 | end 222 | 223 | def date_precision 224 | return @date_precision unless @date_precision.nil? 225 | return Runt::DPrecision::DEFAULT 226 | end 227 | end 228 | 229 | # 230 | # Useful shortcuts! 231 | # 232 | # Originally contributed by Ara T. Howard who is pretty sure he got the idea from 233 | # somewhere else. :-) 234 | # 235 | # Patched by rgeerts in GH pull-request #17 236 | # 237 | class Numeric #:nodoc: 238 | def microseconds() Float(self * (10 ** -6)) end unless self.instance_methods.include?(:microseconds) 239 | def milliseconds() Float(self * (10 ** -3)) end unless self.instance_methods.include?(:milliseconds) 240 | def seconds() self end unless self.instance_methods.include?(:seconds) 241 | def minutes() 60 * seconds end unless self.instance_methods.include?(:minutes) 242 | def hours() 60 * minutes end unless self.instance_methods.include?(:hours) 243 | def days() 24 * hours end unless self.instance_methods.include?(:days) 244 | def weeks() 7 * days end unless self.instance_methods.include?(:weeks) 245 | def months() 30 * days end unless self.instance_methods.include?(:months) 246 | def years() 365 * days end unless self.instance_methods.include?(:years) 247 | def decades() 10 * years end unless self.instance_methods.include?(:decades) 248 | # This causes RDoc to hurl: 249 | %w[ 250 | microseconds milliseconds seconds minutes hours days weeks months years decades 251 | ].each{|m| alias_method m.chop, m} 252 | end 253 | 254 | -------------------------------------------------------------------------------- /lib/runt/daterange.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'date' 4 | require 'runt' 5 | 6 | 7 | module Runt 8 | # :title:DateRange 9 | # == DateRange 10 | # 11 | # 12 | # Based the range[http://martinfowler.com/ap2/range.html] pattern by Martin Fowler. 13 | # 14 | # 15 | # 16 | # Author:: Matthew Lipper 17 | class DateRange < Range 18 | 19 | include DPrecision 20 | 21 | attr_reader :start_expr, :end_expr 22 | 23 | def initialize(start_expr, end_expr,exclusive=false) 24 | super(start_expr, end_expr,exclusive) 25 | @start_expr, @end_expr = start_expr, end_expr 26 | end 27 | 28 | def include?(obj) 29 | return super(obj.min) && super(obj.max) if obj.kind_of? Range 30 | return super(obj) 31 | end 32 | 33 | def overlap?(obj) 34 | return true if( member?(obj) || include?(obj.min) || include?(obj.max) ) 35 | return true if( obj.kind_of?(Range) && obj.include?(self) ) 36 | false 37 | end 38 | 39 | def empty? 40 | return @start_expr >= @end_expr 41 | end 42 | 43 | def gap(obj) 44 | 45 | return EMPTY if self.overlap? obj 46 | 47 | lower=nil 48 | higher=nil 49 | 50 | if((self<=>obj)<0) 51 | lower=self 52 | higher=obj 53 | else 54 | lower=obj 55 | higher=self 56 | end 57 | 58 | return DateRange.new((lower.end_expr+1),(higher.start_expr-1)) 59 | end 60 | 61 | def <=>(other) 62 | return @start_expr <=> other.start_expr if(@start_expr != other.start_expr) 63 | return @end_expr <=> other.end_expr 64 | end 65 | 66 | def min; @start_expr end 67 | def max; @end_expr end 68 | def to_s; @start_expr.to_s + " " + @end_expr.to_s end 69 | 70 | 71 | EMPTY = DateRange.new(PDate.day(2004,2,2),PDate.day(2004,2,1)) 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/runt/dprecision.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'runt' 4 | require 'date' 5 | 6 | module Runt 7 | 8 | # :title:DPrecision 9 | # == DPrecision 10 | # Module providing automatic precisioning of Date, DateTime, and PDate classes. 11 | # 12 | # Inspired by a pattern[http://martinfowler.com/ap2/timePoint.html] by Martin Fowler. 13 | # 14 | # 15 | # Author:: Matthew Lipper 16 | module DPrecision 17 | 18 | def DPrecision.to_p(date,prec=DEFAULT) 19 | has_p = date.respond_to?(:date_precision) 20 | #puts "DPrecision.to_p(#{date.class}<#{has_p ? date.date_precision : nil}>,#{prec})" 21 | return date if PDate == date.class && (prec == date.date_precision) 22 | case prec 23 | when MIN then PDate.min(*DPrecision.explode(date,prec)) 24 | when DAY then PDate.day(*DPrecision.explode(date,prec)) 25 | when HOUR then PDate.hour(*DPrecision.explode(date,prec)) 26 | when WEEK then PDate.week(*DPrecision.explode(date,prec)) 27 | when MONTH then PDate.month(*DPrecision.explode(date,prec)) 28 | when YEAR then PDate.year(*DPrecision.explode(date,prec)) 29 | when SEC then PDate.sec(*DPrecision.explode(date,prec)) 30 | when MILLI then date #raise "Not implemented." 31 | else PDate.default(*DPrecision.explode(date,prec)) 32 | end 33 | end 34 | 35 | def DPrecision.explode(date,prec) 36 | result = [date.year,date.month,date.day] 37 | if(date.respond_to?(:hour)) 38 | result << date.hour << date.min << date.sec 39 | else 40 | result << 0 << 0 << 0 41 | end 42 | result 43 | end 44 | 45 | #Simple value class for keeping track of precisioned dates 46 | class Precision 47 | include Comparable 48 | 49 | attr_reader :precision 50 | private_class_method :new 51 | 52 | #Some constants w/arbitrary integer values used internally for comparisions 53 | YEAR_PREC = 0 54 | MONTH_PREC = 1 55 | WEEK_PREC = 2 56 | DAY_PREC = 3 57 | HOUR_PREC = 4 58 | MIN_PREC = 5 59 | SEC_PREC = 6 60 | MILLI_PREC = 7 61 | 62 | #String values for display 63 | LABEL = { YEAR_PREC => "YEAR", 64 | MONTH_PREC => "MONTH", 65 | WEEK_PREC => "WEEK", 66 | DAY_PREC => "DAY", 67 | HOUR_PREC => "HOUR", 68 | MIN_PREC => "MINUTE", 69 | SEC_PREC => "SECOND", 70 | MILLI_PREC => "MILLISECOND"} 71 | 72 | #Minimun values that precisioned fields get set to 73 | FIELD_MIN = { YEAR_PREC => 1, 74 | MONTH_PREC => 1, 75 | WEEK_PREC => 1, 76 | DAY_PREC => 1, 77 | HOUR_PREC => 0, 78 | MIN_PREC => 0, 79 | SEC_PREC => 0, 80 | MILLI_PREC => 0} 81 | 82 | def Precision.year 83 | new(YEAR_PREC) 84 | end 85 | 86 | def Precision.month 87 | new(MONTH_PREC) 88 | end 89 | 90 | def Precision.week 91 | new(WEEK_PREC) 92 | end 93 | 94 | def Precision.day 95 | new(DAY_PREC) 96 | end 97 | 98 | def Precision.hour 99 | new(HOUR_PREC) 100 | end 101 | 102 | def Precision.min 103 | new(MIN_PREC) 104 | end 105 | 106 | def Precision.sec 107 | new(SEC_PREC) 108 | end 109 | 110 | def Precision.millisec 111 | new(MILLI_PREC) 112 | end 113 | 114 | def min_value() 115 | FIELD_MIN[@precision] 116 | end 117 | 118 | def initialize(prec) 119 | @precision = prec 120 | end 121 | 122 | def <=>(other) 123 | self.precision <=> other.precision 124 | end 125 | 126 | def ===(other) 127 | self.precision == other.precision 128 | end 129 | 130 | def to_s 131 | "DPrecision::#{self.label}" 132 | end 133 | 134 | def label 135 | LABEL[@precision] 136 | end 137 | end 138 | 139 | #Pseudo Singletons: 140 | YEAR = Precision.year 141 | MONTH = Precision.month 142 | WEEK = Precision.week 143 | DAY = Precision.day 144 | HOUR = Precision.hour 145 | MIN = Precision.min 146 | SEC = Precision.sec 147 | MILLI = Precision.millisec 148 | DEFAULT=MIN 149 | 150 | end 151 | 152 | end 153 | -------------------------------------------------------------------------------- /lib/runt/expressionbuilder.rb: -------------------------------------------------------------------------------- 1 | require 'runt' 2 | 3 | # Convenience class for building temporal expressions in a more 4 | # human-friendly way. Used in conjunction with shortcuts defined in the 5 | # sugar.rb file, this allows one to create expressions like the following: 6 | # 7 | # b = ExpressionBuilder.new 8 | # expr = b.define do 9 | # occurs daily_8_30am_to_9_45am 10 | # on tuesday 11 | # possibly wednesday 12 | # end 13 | # 14 | # This equivalent to: 15 | # 16 | # expr = REDay.new(8,30,9,45) & DIWeek.new(Tuesday) | DIWeek.new(Wednesday) 17 | # 18 | # ExpressionBuilder creates expressions by evaluating a block passed to the 19 | # :define method. From inside the block, methods :occurs, :on, :every, :possibly, 20 | # and :maybe can be called with a temporal expression which will be added to 21 | # a composite expression as follows: 22 | # 23 | # * :on - creates an "and" (&) 24 | # * :possibly - creates an "or" (|) 25 | # * :except - creates a "not" (-) 26 | # * :every - alias for :on method 27 | # * :occurs - alias for :on method 28 | # * :maybe - alias for :possibly method 29 | # 30 | class ExpressionBuilder 31 | 32 | include Runt 33 | 34 | attr_accessor :ctx 35 | 36 | def initialize 37 | @ctx = nil 38 | end 39 | 40 | def define(&block) 41 | instance_eval(&block) 42 | end 43 | 44 | def on(expr) 45 | add(expr, :&) 46 | end 47 | 48 | def add(expr, op) 49 | @ctx ||= expr 50 | @ctx = @ctx.send(op, expr) unless @ctx == expr 51 | @ctx # explicit return, previous line may not execute 52 | end 53 | 54 | def except(expr) 55 | add(expr, :-) 56 | end 57 | 58 | def possibly(expr) 59 | add(expr, :|) 60 | end 61 | 62 | alias_method :every, :on 63 | alias_method :occurs, :on 64 | alias_method :maybe, :possibly 65 | end 66 | -------------------------------------------------------------------------------- /lib/runt/pdate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'date' 4 | require 'runt' 5 | 6 | 7 | module Runt 8 | 9 | 10 | # :title:PDate 11 | # == PDate 12 | # Date and DateTime with explicit precision. 13 | # 14 | # Based the pattern[http://martinfowler.com/ap2/timePoint.html] by Martin Fowler. 15 | # 16 | # 17 | # Author:: Matthew Lipper 18 | class PDate < DateTime 19 | include Comparable 20 | include DPrecision 21 | 22 | attr_accessor :date_precision 23 | 24 | class << self 25 | 26 | def civil(*args) 27 | precision=nil 28 | if(args[0].instance_of?(DPrecision::Precision)) 29 | precision = args.shift 30 | else 31 | return PDate::sec(*args) 32 | end 33 | pdate = super(*args) 34 | pdate.date_precision = precision 35 | pdate 36 | end 37 | 38 | def parse(*args) 39 | opts = args.last.is_a?(Hash) ? args.pop : {} 40 | pdate = super(*args) 41 | pdate.date_precision = opts[:precision] || opts[:date_precision] 42 | pdate 43 | end 44 | 45 | alias_method :new, :civil 46 | 47 | end 48 | 49 | def include?(expr) 50 | eql?(expr) 51 | end 52 | 53 | def + (n) 54 | raise TypeError, 'expected numeric' unless n.kind_of?(Numeric) 55 | ndays = n 56 | case @date_precision 57 | when YEAR then 58 | return DPrecision::to_p(PDate::civil(year+n,month,day),@date_precision) 59 | when MONTH then 60 | return DPrecision::to_p((self.to_date>>n),@date_precision) 61 | when WEEK then 62 | ndays = n*7 63 | when DAY then 64 | ndays = n 65 | when HOUR then 66 | ndays = n*(1.to_r/24) 67 | when MIN then 68 | ndays = n*(1.to_r/1440) 69 | when SEC then 70 | ndays = n*(1.to_r/86400) 71 | when MILLI then 72 | ndays = n*(1.to_r/86400000) 73 | end 74 | DPrecision::to_p((self.to_date + ndays),@date_precision) 75 | end 76 | 77 | def - (x) 78 | case x 79 | when Numeric then 80 | return self+(-x) 81 | when Date then 82 | return super(DPrecision::to_p(x,@date_precision)) 83 | end 84 | raise TypeError, 'expected numeric or date' 85 | end 86 | 87 | def <=> (other) 88 | result = nil 89 | raise "I'm broken #{self.to_s}" if @date_precision.nil? 90 | if(!other.nil? && other.respond_to?("date_precision") && other.date_precision>@date_precision) 91 | result = super(DPrecision::to_p(other,@date_precision)) 92 | else 93 | result = super(other) 94 | end 95 | puts "self<#{self.to_s}><=>other<#{other.to_s}> => #{result}" if $DEBUG 96 | result 97 | end 98 | 99 | def succ 100 | result = self + 1 101 | end 102 | 103 | def to_date 104 | (self.date_precision > DAY) ? DateTime.new(self.year,self.month,self.day,self.hour,self.min,self.sec) : Date.new(self.year, self.month, self.day) 105 | end 106 | 107 | def PDate.year(yr,*ignored) 108 | PDate.civil(YEAR, yr, MONTH.min_value, DAY.min_value ) 109 | end 110 | 111 | def PDate.month( yr,mon,*ignored ) 112 | PDate.civil(MONTH, yr, mon, DAY.min_value ) 113 | end 114 | 115 | def PDate.week( yr,mon,day,*ignored ) 116 | #LJK: need to calculate which week this day implies, 117 | #and then move the day back to the *first* day in that week; 118 | #note that since rfc2445 defaults to weekstart=monday, I'm 119 | #going to use commercial day-of-week 120 | raw = PDate.day(yr, mon, day) 121 | cooked = PDate.commercial(raw.cwyear, raw.cweek, 1) 122 | PDate.civil(WEEK, cooked.year, cooked.month, cooked.day) 123 | end 124 | 125 | def PDate.day( yr,mon,day,*ignored ) 126 | PDate.civil(DAY, yr, mon, day ) 127 | end 128 | 129 | def PDate.hour( yr,mon,day,hr=HOUR.min_value,*ignored ) 130 | PDate.civil(HOUR, yr, mon, day,hr,MIN.min_value, SEC.min_value) 131 | end 132 | 133 | def PDate.min( yr,mon,day,hr=HOUR.min_value,min=MIN.min_value,*ignored ) 134 | PDate.civil(MIN, yr, mon, day,hr,min, SEC.min_value) 135 | end 136 | 137 | def PDate.sec( yr,mon,day,hr=HOUR.min_value,min=MIN.min_value,sec=SEC.min_value,*ignored ) 138 | PDate.civil(SEC, yr, mon, day,hr,min, sec) 139 | end 140 | 141 | def PDate.millisecond( yr,mon,day,hr,min,sec,ms,*ignored ) 142 | PDate.civil(SEC, yr, mon, day,hr,min, sec, ms, *ignored) 143 | #raise "Not implemented yet." 144 | end 145 | 146 | def PDate.default(*args) 147 | PDate.civil(DEFAULT, *args) 148 | end 149 | 150 | #FIXME: marshall broken in 1.9 151 | # 152 | # Custom dump which preserves DatePrecision 153 | # 154 | # Author:: Jodi Showers 155 | # 156 | def marshal_dump 157 | [date_precision, ajd, start, offset] 158 | end 159 | 160 | #FIXME: marshall broken in 1.9 161 | # 162 | # Custom load which preserves DatePrecision 163 | # 164 | # Author:: Jodi Showers 165 | # 166 | def marshal_load(dumped_obj) 167 | @date_precision, @ajd, @sg, @of=dumped_obj 168 | end 169 | 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/runt/schedule.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | module Runt 4 | 5 | 6 | # Implementation of a pattern[http://martinfowler.com/apsupp/recurring.pdf] 7 | # for recurring calendar events created by Martin Fowler. 8 | class Schedule 9 | 10 | def initialize 11 | @elems = Hash.new 12 | self 13 | end 14 | 15 | # Schedule event to occur using the given expression. 16 | # NOTE: version 0.5.0 no longer uses an Array of ScheduleElements 17 | # internally to hold data. This would only matter to clients if they 18 | # they depended on the ability to call add multiple times for the same 19 | # event. Use the update method instead. 20 | def add(event, expression) 21 | @elems[event]=expression 22 | end 23 | 24 | # For the given date range, returns an Array of PDate objects at which 25 | # the supplied event is scheduled to occur. 26 | def dates(event, date_range) 27 | result=[] 28 | date_range.each do |date| 29 | result.push date if include?(event,date) 30 | end 31 | result 32 | end 33 | 34 | def scheduled_dates(date_range) 35 | @elems.values.collect{|expr| expr.dates(date_range)}.flatten.sort.uniq 36 | end 37 | 38 | # Return true or false depend on if the supplied event is scheduled to occur on the 39 | # given date. 40 | def include?(event, date) 41 | return false unless @elems.include?(event) 42 | return 0<(self.select{|ev,xpr| ev.eql?(event)&&xpr.include?(date);}).size 43 | end 44 | 45 | # 46 | # Returns all Events whose Temporal Expression includes the given date/expression 47 | # 48 | def events(date) 49 | self.select{|ev,xpr| xpr.include?(date);} 50 | end 51 | 52 | # 53 | # Selects events using the user supplied block/Proc. The Proc must accept 54 | # two parameters: an Event and a TemporalExpression. It will be called 55 | # with each existing Event-expression pair at which point it can choose 56 | # to include the Event in the final result by returning true or to filter 57 | # it by returning false. 58 | # 59 | def select(&block) 60 | result=[] 61 | @elems.each_pair{|event,xpr| result.push(event) if block.call(event,xpr);} 62 | result 63 | end 64 | 65 | # 66 | # Call the supplied block/Proc with the currently configured 67 | # TemporalExpression associated with the supplied Event. 68 | # 69 | def update(event,&block) 70 | block.call(@elems[event]) 71 | end 72 | 73 | def date_to_event_hash(event_attribute=:id) 74 | start_date = end_date = nil 75 | @elems.keys.each do |event| 76 | start_date = event.start_date if start_date.nil? || start_date > event.start_date 77 | end_date = event.end_date if end_date.nil? || end_date < event.end_date 78 | end 79 | 80 | scheduled_dates(DateRange.new(start_date, end_date)).inject({}) do |h, date| 81 | h[date] = events(date).collect{|e| e.send(event_attribute)} 82 | h 83 | end 84 | end 85 | end 86 | 87 | # TODO: Extend event to take other attributes 88 | 89 | class Event 90 | 91 | attr_reader :id 92 | 93 | def initialize(id) 94 | raise Exception, "id argument cannot be nil" unless !id.nil? 95 | @id=id 96 | end 97 | 98 | def to_s; @id.to_s end 99 | 100 | def == (other) 101 | return true if other.kind_of?(Event) && @id==other.id 102 | end 103 | 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /lib/runt/sugar.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # 5 | # == Overview 6 | # 7 | # This file provides an optional extension to the Runt module which 8 | # provides convenient shortcuts for commonly used temporal expressions. 9 | # 10 | # Several methods for creating new temporal expression instances are added 11 | # to a client class by including the Runt module. 12 | # 13 | # === Shortcuts 14 | # 15 | # Shortcuts are implemented by pattern matching done in method_missing for 16 | # the Runt module. Generally speaking, range expressions start with "daily_", 17 | # "weekly_", "yearly_", etc. 18 | # 19 | # Times use the format /\d{1,2}_\d{2}[ap]m/ where the first digits represent hours 20 | # and the second digits represent minutes. Note that hours are always within the 21 | # range of 1-12 and may be one or two digits. Minutes are always two digits 22 | # (e.g. '03' not just '3') and are always followed by am or pm (lowercase). 23 | # 24 | # 25 | # class MyClass 26 | # include Runt 27 | # 28 | # def some_method 29 | # # Daily from 4:02pm to 10:20pm or anytime Tuesday 30 | # expr = daily_4_02pm_to_10_20pm() | tuesday() 31 | # ... 32 | # end 33 | # ... 34 | # end 35 | # 36 | # The following documents the syntax for particular temporal expression classes. 37 | # 38 | # === REDay 39 | # 40 | # daily___to__ 41 | # 42 | # Example: 43 | # 44 | # self.daily_10_00am_to_1:30pm() 45 | # 46 | # is equivilant to 47 | # 48 | # REDay.new(10,00,13,30) 49 | # 50 | # === REWeek 51 | # 52 | # weekly__to_ 53 | # 54 | # Example: 55 | # 56 | # self.weekly_tuesday_to_thrusday() 57 | # 58 | # is equivilant to 59 | # 60 | # REWeek.new(Tuesday, Thrusday) 61 | # 62 | # === REMonth 63 | # 64 | # monthly__to_ 65 | # 66 | # Example: 67 | # 68 | # self.monthly_23rd_to_29th() 69 | # 70 | # is equivilant to 71 | # 72 | # REMonth.new(23, 29) 73 | # 74 | # === REYear 75 | # 76 | # self.yearly___to__() 77 | # 78 | # Example: 79 | # 80 | # self.yearly_march_15_to_june_1() 81 | # 82 | # is equivilant to 83 | # 84 | # REYear.new(March, 15, June, 1) 85 | # 86 | # === DIWeek 87 | # 88 | # self.() 89 | # 90 | # Example: 91 | # 92 | # self.friday() 93 | # 94 | # is equivilant to 95 | # 96 | # DIWeek.new(Friday) 97 | # 98 | # === DIMonth 99 | # 100 | # self._() 101 | # 102 | # Example: 103 | # 104 | # self.first_saturday() 105 | # self.last_tuesday() 106 | # 107 | # is equivilant to 108 | # 109 | # DIMonth.new(First, Saturday) 110 | # DIMonth.new(Last, Tuesday) 111 | # 112 | # === AfterTE 113 | # 114 | # self.after(date [, inclusive]) 115 | # 116 | # Example: 117 | # 118 | # self.after(date) 119 | # self.after(date, true) 120 | # 121 | # is equivilant to 122 | # 123 | # AfterTE.new(date) 124 | # AfterTE.new(date, true) 125 | # 126 | # === BeforeTE 127 | # 128 | # self.before(date [, inclusive]) 129 | # 130 | # Example: 131 | # 132 | # self.before(date) 133 | # self.before(date, true) 134 | # 135 | # is equivilant to 136 | # 137 | # BeforeTE.new(date) 138 | # BeforeTE.new(date, true) 139 | # 140 | require 'runt' 141 | 142 | module Runt 143 | MONTHS = '(january|february|march|april|may|june|july|august|september|october|november|december)' 144 | DAYS = '(sunday|monday|tuesday|wednesday|thursday|friday|saturday)' 145 | WEEK_OF_MONTH_ORDINALS = '(first|second|third|fourth|last|second_to_last)' 146 | ORDINAL_SUFFIX = '(?:st|nd|rd|th)' 147 | ORDINAL_ABBR = '(st|nd|rd|th)' 148 | class << self 149 | def const(string) 150 | self.const_get(string.capitalize) 151 | end 152 | end 153 | 154 | def method_missing(name, *args, &block) 155 | #puts "method_missing(#{name},#{args},#{block}) => #{result}" 156 | case name.to_s 157 | when /^daily_(\d{1,2})_(\d{2})([ap]m)_to_(\d{1,2})_(\d{2})([ap]m)$/ 158 | # REDay 159 | st_hr, st_min, st_m, end_hr, end_min, end_m = $1, $2, $3, $4, $5, $6 160 | args = parse_time(st_hr, st_min, st_m) 161 | args.concat(parse_time(end_hr, end_min, end_m)) 162 | return REDay.new(*args) 163 | when Regexp.new('^weekly_' + DAYS + '_to_' + DAYS + '$') 164 | # REWeek 165 | st_day, end_day = $1, $2 166 | return REWeek.new(Runt.const(st_day), Runt.const(end_day)) 167 | when Regexp.new('^monthly_(\d{1,2})' + ORDINAL_SUFFIX + '_to_(\d{1,2})' \ 168 | + ORDINAL_SUFFIX + '$') 169 | # REMonth 170 | st_day, end_day = $1, $2 171 | return REMonth.new(st_day, end_day) 172 | when Regexp.new('^yearly_' + MONTHS + '_(\d{1,2})_to_' + MONTHS + '_(\d{1,2})$') 173 | # REYear 174 | st_mon, st_day, end_mon, end_day = $1, $2, $3, $4 175 | return REYear.new(Runt.const(st_mon), st_day, Runt.const(end_mon), end_day) 176 | when Regexp.new('^' + DAYS + '$') 177 | # DIWeek 178 | return DIWeek.new(Runt.const(name.to_s)) 179 | when Regexp.new(WEEK_OF_MONTH_ORDINALS + '_' + DAYS) 180 | # DIMonth 181 | ordinal, day = $1, $2 182 | return DIMonth.new(Runt.const(ordinal), Runt.const(day)) 183 | else 184 | # You're hosed 185 | super 186 | end 187 | end 188 | 189 | # Shortcut for AfterTE(date, ...).new 190 | def after(date, inclusive=false) 191 | AfterTE.new(date, inclusive) 192 | end 193 | 194 | # Shortcut for BeforeTE(date, ...).new 195 | def before(date, inclusive=false) 196 | BeforeTE.new(date, inclusive) 197 | end 198 | 199 | def parse_time(hour, minute, ampm) 200 | hour = hour.to_i + 12 if ampm =~ /pm/ 201 | [hour.to_i, minute.to_i] 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /lib/runt/temporalexpression.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'date' 4 | require 'runt/dprecision' 5 | require 'runt/pdate' 6 | require 'pp' 7 | 8 | # 9 | # Author:: Matthew Lipper 10 | 11 | module Runt 12 | 13 | # 14 | # 'TExpr' is short for 'TemporalExpression' and are inspired by the recurring event 15 | # pattern[http://martinfowler.com/apsupp/recurring.pdf] 16 | # described by Martin Fowler. Essentially, they provide a pattern language for 17 | # specifying recurring events using set expressions. 18 | # 19 | # See also [tutorial_te.rdoc] 20 | module TExpr 21 | 22 | # Returns true or false depending on whether this TExpr includes the supplied 23 | # date expression. 24 | def include?(date_expr); false end 25 | 26 | def to_s; "TExpr" end 27 | 28 | def or (arg) 29 | 30 | if self.kind_of?(Union) 31 | self.add(arg) 32 | else 33 | yield Union.new.add(self).add(arg) 34 | end 35 | 36 | end 37 | 38 | def and (arg) 39 | 40 | if self.kind_of?(Intersect) 41 | self.add(arg) 42 | else 43 | yield Intersect.new.add(self).add(arg) 44 | end 45 | 46 | end 47 | 48 | def minus (arg) 49 | yield Diff.new(self,arg) 50 | end 51 | 52 | def | (expr) 53 | self.or(expr){|adjusted| adjusted } 54 | end 55 | 56 | def & (expr) 57 | self.and(expr){|adjusted| adjusted } 58 | end 59 | 60 | def - (expr) 61 | self.minus(expr){|adjusted| adjusted } 62 | end 63 | 64 | # Contributed by Emmett Shear: 65 | # Returns an Array of Date-like objects which occur within the supplied 66 | # DateRange. Will stop calculating dates once a number of dates equal 67 | # to the optional attribute limit are found. (A limit of zero will collect 68 | # all matching dates in the date range.) 69 | def dates(date_range, limit=0) 70 | result = [] 71 | date_range.each do |date| 72 | result << date if self.include? date 73 | if limit > 0 and result.size == limit 74 | break 75 | end 76 | end 77 | result 78 | end 79 | 80 | end 81 | 82 | # Base class for TExpr classes that can be composed of other 83 | # TExpr objects imlpemented using the Composite(GoF) pattern. 84 | class Collection 85 | 86 | include TExpr 87 | 88 | attr_reader :expressions 89 | 90 | def initialize(expressions = []) 91 | @expressions = expressions 92 | end 93 | 94 | def ==(other) 95 | if other.is_a?(Collection) 96 | o_exprs = other.expressions.dup 97 | expressions.each do |e| 98 | return false unless i = o_exprs.index(e) 99 | o_exprs.delete_at(i) 100 | end 101 | o_exprs.each {|e| return false unless i == expressions.index(e)} 102 | return true 103 | else 104 | super(other) 105 | end 106 | end 107 | 108 | def add(anExpression) 109 | @expressions.push anExpression 110 | self 111 | end 112 | 113 | # Will return true if the supplied object overlaps with the range used to 114 | # create this instance 115 | def overlap?(date_expr) 116 | @expressions.each do | interval | 117 | return true if date_expr.overlap?(interval) 118 | end 119 | false 120 | end 121 | 122 | def to_s 123 | if !@expressions.empty? && block_given? 124 | first_expr, next_exprs = yield 125 | result = '' 126 | @expressions.map do |expr| 127 | if @expressions.first===expr 128 | result = first_expr + expr.to_s 129 | else 130 | result = result + next_exprs + expr.to_s 131 | end 132 | end 133 | result 134 | else 135 | 'empty' 136 | end 137 | end 138 | 139 | def display 140 | puts "I am a #{self.class} containing:" 141 | @expressions.each do |ex| 142 | pp "#{ex.class}" 143 | end 144 | end 145 | 146 | 147 | end 148 | 149 | # Composite TExpr that will be true if any of it's 150 | # component expressions are true. 151 | class Union < Collection 152 | 153 | def include?(aDate) 154 | @expressions.each do |expr| 155 | return true if expr.include?(aDate) 156 | end 157 | false 158 | end 159 | 160 | def to_s 161 | super {['every ',' or ']} 162 | end 163 | end 164 | 165 | # Composite TExpr that will be true only if all it's 166 | # component expressions are true. 167 | class Intersect < Collection 168 | 169 | def include?(aDate) 170 | result = false 171 | @expressions.each do |expr| 172 | return false unless (result = expr.include?(aDate)) 173 | end 174 | result 175 | end 176 | 177 | def to_s 178 | super {['every ', ' and ']} 179 | end 180 | end 181 | 182 | # TExpr that will be true only if the first of 183 | # its two contained expressions is true and the second is false. 184 | class Diff 185 | 186 | include TExpr 187 | 188 | attr_reader :expr1, :expr2 189 | 190 | def initialize(expr1, expr2) 191 | @expr1 = expr1 192 | @expr2 = expr2 193 | end 194 | 195 | def ==(o) 196 | o.is_a?(Diff) ? expr1 == o.expr1 && expr2 == o.expr2 : super(o) 197 | end 198 | 199 | def include?(aDate) 200 | return false unless (@expr1.include?(aDate) && !@expr2.include?(aDate)) 201 | true 202 | end 203 | 204 | def to_s 205 | @expr1.to_s + ' except for ' + @expr2.to_s 206 | end 207 | end 208 | 209 | # TExpr that provides for inclusion of an arbitrary date. 210 | class TemporalDate 211 | 212 | include TExpr 213 | 214 | attr_reader :date_expr 215 | 216 | def initialize(date_expr) 217 | @date_expr = date_expr 218 | end 219 | 220 | def ==(o) 221 | o.is_a?(TemporalDate) ? date_expr == o.date_expr : super(o) 222 | end 223 | 224 | # Will return true if the supplied object is == to that which was used to 225 | # create this instance 226 | def include?(date_expr) 227 | return date_expr.include?(@date_expr) if date_expr.respond_to?(:include?) 228 | return true if @date_expr == date_expr 229 | false 230 | end 231 | 232 | def to_s 233 | @date_expr.to_s 234 | end 235 | 236 | end 237 | 238 | # TExpr that provides a thin wrapper around built-in Ruby Range functionality 239 | # facilitating inclusion of an arbitrary range in a temporal expression. 240 | # 241 | # See also: Range 242 | class TemporalRange < TemporalDate 243 | 244 | ## Will return true if the supplied object is included in the range used to 245 | ## create this instance 246 | def include?(date_expr) 247 | return @date_expr.include?(date_expr) 248 | end 249 | 250 | def ==(o) 251 | o.is_a?(TemporalRange) ? date_expr == o.date_expr : super(o) 252 | end 253 | 254 | # Will return true if the supplied object overlaps with the range used to 255 | # create this instance 256 | def overlap?(date_expr) 257 | @date_expr.each do | interval | 258 | return true if date_expr.include?(interval) 259 | end 260 | false 261 | end 262 | 263 | end 264 | 265 | ####################################################################### 266 | # Utility methods common to some expressions 267 | 268 | module TExprUtils 269 | def week_in_month(day_in_month) 270 | ((day_in_month - 1) / 7) + 1 271 | end 272 | 273 | def days_left_in_month(date) 274 | return max_day_of_month(date) - date.day 275 | end 276 | 277 | def max_day_of_month(date) 278 | # Contributed by Justin Cunningham who took it verbatim from the Rails 279 | # ActiveSupport::CoreExtensions::Time::Calculations::ClassMethods module 280 | # days_in_month method. 281 | month = date.month 282 | year = date.year 283 | if month == 2 284 | !year.nil? && 285 | (year % 4 == 0) && 286 | ((year % 100 != 0) || 287 | (year % 400 == 0)) ? 29 : 28 288 | elsif month <= 7 289 | month % 2 == 0 ? 30 : 31 290 | else 291 | month % 2 == 0 ? 31 : 30 292 | end 293 | end 294 | 295 | def week_matches?(index,date) 296 | if(index > 0) 297 | return week_from_start_matches?(index,date) 298 | else 299 | return week_from_end_matches?(index,date) 300 | end 301 | end 302 | 303 | def week_from_start_matches?(index,date) 304 | week_in_month(date.day)==index 305 | end 306 | 307 | def week_from_end_matches?(index,date) 308 | n = days_left_in_month(date) + 1 309 | week_in_month(n)==index.abs 310 | end 311 | 312 | end 313 | 314 | # TExpr that provides support for building a temporal 315 | # expression using the form: 316 | # 317 | # DIMonth.new(1,0) 318 | # 319 | # where the first argument is the week of the month and the second 320 | # argument is the wday of the week as defined by the 'wday' method 321 | # in the standard library class Date. 322 | # 323 | # A negative value for the week of the month argument will count 324 | # backwards from the end of the month. So, to match the last Saturday 325 | # of the month 326 | # 327 | # DIMonth.new(-1,6) 328 | # 329 | # Using constants defined in the base Runt module, you can re-write 330 | # the first example above as: 331 | # 332 | # DIMonth.new(First,Sunday) 333 | # 334 | # and the second as: 335 | # 336 | # DIMonth.new(Last,Saturday) 337 | # 338 | # See also: Date, Runt 339 | class DIMonth 340 | 341 | include TExpr 342 | include TExprUtils 343 | 344 | attr_reader :day_index, :week_of_month_index 345 | 346 | def initialize(week_of_month_index,day_index) 347 | @day_index = day_index 348 | @week_of_month_index = week_of_month_index 349 | end 350 | 351 | def ==(o) 352 | o.is_a?(DIMonth) ? day_index == o.day_index && week_of_month_index == o.week_of_month_index : super(o) 353 | end 354 | 355 | def include?(date) 356 | ( day_matches?(date) ) && ( week_matches?(@week_of_month_index,date) ) 357 | end 358 | 359 | def to_s 360 | "#{Runt.ordinalize(@week_of_month_index)} #{Runt.day_name(@day_index)} of the month" 361 | end 362 | 363 | private 364 | def day_matches?(date) 365 | @day_index == date.wday 366 | end 367 | 368 | end 369 | 370 | # TExpr that matches days of the week where the first argument 371 | # is an integer denoting the ordinal day of the week. Valid values are 0..6 where 372 | # 0 == Sunday and 6==Saturday 373 | # 374 | # For example: 375 | # 376 | # DIWeek.new(0) 377 | # 378 | # Using constants defined in the base Runt module, you can re-write 379 | # the first example above as: 380 | # 381 | # DIWeek.new(Sunday) 382 | # 383 | # See also: Date, Runt 384 | class DIWeek 385 | 386 | include TExpr 387 | 388 | VALID_RANGE = 0..6 389 | 390 | attr_reader :ordinal_weekday 391 | 392 | def initialize(ordinal_weekday) 393 | unless VALID_RANGE.include?(ordinal_weekday) 394 | raise ArgumentError, 'invalid ordinal day of week' 395 | end 396 | @ordinal_weekday = ordinal_weekday 397 | end 398 | 399 | def ==(o) 400 | o.is_a?(DIWeek) ? ordinal_weekday == o.ordinal_weekday : super(o) 401 | end 402 | 403 | def include?(date) 404 | @ordinal_weekday == date.wday 405 | end 406 | 407 | def to_s 408 | "#{Runt.day_name(@ordinal_weekday)}" 409 | end 410 | 411 | end 412 | 413 | # TExpr that matches days of the week within one 414 | # week only. 415 | # 416 | # If start and end day are equal, the entire week will match true. 417 | # 418 | # See also: Date 419 | class REWeek 420 | 421 | include TExpr 422 | 423 | VALID_RANGE = 0..6 424 | 425 | attr_reader :start_day, :end_day 426 | 427 | # Creates a REWeek using the supplied start 428 | # day(range = 0..6, where 0=>Sunday) and an optional end 429 | # day. If an end day is not supplied, the maximum value 430 | # (6 => Saturday) is assumed. 431 | def initialize(start_day,end_day=6) 432 | validate(start_day,end_day) 433 | @start_day = start_day 434 | @end_day = end_day 435 | end 436 | 437 | def ==(o) 438 | o.is_a?(REWeek) ? start_day == o.start_day && end_day == o.end_day : super(o) 439 | end 440 | 441 | def include?(date) 442 | return true if all_week? 443 | if @start_day < @end_day 444 | @start_day<=date.wday && @end_day>=date.wday 445 | else 446 | (@start_day<=date.wday && 6 >=date.wday) || (0 <=date.wday && @end_day >=date.wday) 447 | end 448 | end 449 | 450 | def to_s 451 | return "all week" if all_week? 452 | "#{Runt.day_name(@start_day)} through #{Runt.day_name(@end_day)}" 453 | end 454 | 455 | private 456 | 457 | def all_week? 458 | return true if @start_day==@end_day 459 | end 460 | 461 | def validate(start_day,end_day) 462 | unless VALID_RANGE.include?(start_day)&&VALID_RANGE.include?(end_day) 463 | raise ArgumentError, 'start and end day arguments must be in the range #{VALID_RANGE.to_s}.' 464 | end 465 | end 466 | end 467 | 468 | # 469 | # TExpr that matches date ranges within a single year. Assumes that the start 470 | # and end parameters occur within the same year. 471 | # 472 | # 473 | class REYear 474 | 475 | # Sentinel value used to denote that no specific day was given to create 476 | # the expression. 477 | NO_DAY = 0 478 | 479 | include TExpr 480 | 481 | attr_accessor :start_month, :start_day, :end_month, :end_day 482 | 483 | # 484 | # == Synopsis 485 | # 486 | # REYear.new(start_month [, (start_day | end_month), ...] 487 | # 488 | # == Args 489 | # 490 | # One or two arguments given:: 491 | # 492 | # +start_month+:: 493 | # Start month. Valid values are 1..12. When no other parameters are given 494 | # this value will be used for the end month as well. Matches the entire 495 | # month through the ending month. 496 | # +end_month+:: 497 | # End month. Valid values are 1..12. When given in two argument form 498 | # will match through the entire month. 499 | # 500 | # Three or four arguments given:: 501 | # 502 | # +start_month+:: 503 | # Start month. Valid values are 1..12. 504 | # +start_day+:: 505 | # Start day. Valid values are 1..31, depending on the month. 506 | # +end_month+:: 507 | # End month. Valid values are 1..12. If a fourth argument is not given, 508 | # this value will cover through the entire month. 509 | # +end_day+:: 510 | # End day. Valid values are 1..31, depending on the month. 511 | # 512 | # == Description 513 | # 514 | # Create a new REYear expression expressing a range of months or days 515 | # within months within a year. 516 | # 517 | # == Usage 518 | # 519 | # # Creates the range March 12th through May 23rd 520 | # expr = REYear.new(3,12,5,23) 521 | # 522 | # # Creates the range March 1st through May 31st 523 | # expr = REYear.new(3,5) 524 | # 525 | # # Creates the range March 12th through May 31st 526 | # expr = REYear.new(3,12,5) 527 | # 528 | # # Creates the range March 1st through March 30th 529 | # expr = REYear.new(3) 530 | # 531 | def initialize(start_month, *args) 532 | @start_month = start_month 533 | if (args.nil? || args.size == NO_DAY) then 534 | # One argument given 535 | @end_month = start_month 536 | @start_day = NO_DAY 537 | @end_day = NO_DAY 538 | else 539 | case args.size 540 | when 1 541 | @end_month = args[0] 542 | @start_day = NO_DAY 543 | @end_day = NO_DAY 544 | when 2 545 | @start_day = args[0] 546 | @end_month = args[1] 547 | @end_day = NO_DAY 548 | when 3 549 | @start_day = args[0] 550 | @end_month = args[1] 551 | @end_day = args[2] 552 | else 553 | raise "Invalid number of var args: 1 or 3 expected, #{args.size} given" 554 | end 555 | end 556 | @same_month_dates_provided = (@start_month == @end_month) && (@start_day!=NO_DAY && @end_day != NO_DAY) 557 | end 558 | 559 | def ==(o) 560 | o.is_a?(REYear) ? start_day == o.start_day && end_day == o.end_day && start_month == o.start_month && end_month == o.end_month : super(o) 561 | end 562 | 563 | def include?(date) 564 | 565 | return same_start_month_include_day?(date) \ 566 | && same_end_month_include_day?(date) if @same_month_dates_provided 567 | 568 | is_between_months?(date) || 569 | (same_start_month_include_day?(date) || 570 | same_end_month_include_day?(date)) 571 | end 572 | 573 | def save 574 | "Runt::REYear.new(#{@start_month}, #{@start_day}, #{@end_month}, #{@end_day})" 575 | end 576 | 577 | def to_s 578 | "#{Runt.month_name(@start_month)} #{Runt.ordinalize(@start_day)} " + 579 | "through #{Runt.month_name(@end_month)} #{Runt.ordinalize(@end_day)}" 580 | end 581 | 582 | private 583 | def is_between_months?(date) 584 | (date.mon > @start_month) && (date.mon < @end_month) 585 | end 586 | 587 | def same_end_month_include_day?(date) 588 | return false unless (date.mon == @end_month) 589 | (@end_day == NO_DAY) || (date.day <= @end_day) 590 | end 591 | 592 | def same_start_month_include_day?(date) 593 | return false unless (date.mon == @start_month) 594 | (@start_day == NO_DAY) || (date.day >= @start_day) 595 | end 596 | 597 | end 598 | 599 | # TExpr that matches periods of the day with minute 600 | # precision. If the start hour is greater than the end hour, than end hour 601 | # is assumed to be on the following day. 602 | # 603 | # NOTE: By default, this class will match any date expression whose 604 | # precision is less than or equal to DPrecision::DAY. To override 605 | # this behavior, pass the optional fifth constructor argument the 606 | # value: false. 607 | # 608 | # When the less_precise_match argument is true, the 609 | # date-like object passed to :include? will be "promoted" to 610 | # DPrecision::MINUTE if it has a precision of DPrecision::DAY or 611 | # less. 612 | # 613 | class REDay 614 | 615 | include TExpr 616 | 617 | CURRENT=28 618 | NEXT=29 619 | ANY_DATE=PDate.day(2002,8,CURRENT) 620 | 621 | attr_reader :range, :spans_midnight 622 | 623 | def initialize(start_hour, start_minute, end_hour, end_minute, less_precise_match=true) 624 | 625 | start_time = PDate.min(ANY_DATE.year,ANY_DATE.month, 626 | ANY_DATE.day,start_hour,start_minute) 627 | 628 | if(@spans_midnight = spans_midnight?(start_hour, end_hour)) then 629 | end_time = get_next(end_hour,end_minute) 630 | else 631 | end_time = get_current(end_hour,end_minute) 632 | end 633 | 634 | @range = start_time..end_time 635 | @less_precise_match = less_precise_match 636 | end 637 | 638 | def ==(o) 639 | o.is_a?(REDay) ? spans_midnight == o.spans_midnight && range == o.range : super(o) 640 | end 641 | 642 | def include?(date) 643 | # 644 | # If @less_precise_match == true and the precision of the argument 645 | # is day or greater, then the result is always true 646 | return true if @less_precise_match && less_precise?(date) 647 | 648 | date_to_use = ensure_precision(date) 649 | 650 | if(@spans_midnight&&date_to_use.hour<12) then 651 | #Assume next day 652 | return @range.include?(get_next(date_to_use.hour,date_to_use.min)) 653 | end 654 | 655 | #Same day 656 | return @range.include?(get_current(date_to_use.hour,date_to_use.min)) 657 | end 658 | 659 | def to_s 660 | "from #{Runt.format_time(@range.begin)} to #{Runt.format_time(@range.end)} daily" 661 | end 662 | 663 | private 664 | 665 | def less_precise?(date) 666 | date.date_precision <= DPrecision::DAY 667 | end 668 | 669 | def ensure_precision(date) 670 | return date unless less_precise?(date) 671 | DPrecision.to_p(date,DPrecision::MIN) 672 | end 673 | 674 | def spans_midnight?(start_hour, end_hour) 675 | return end_hour < start_hour 676 | end 677 | 678 | def get_current(hour,minute) 679 | PDate.min(ANY_DATE.year,ANY_DATE.month,CURRENT,hour,minute) 680 | end 681 | 682 | def get_next(hour,minute) 683 | PDate.min(ANY_DATE.year,ANY_DATE.month,NEXT,hour,minute) 684 | end 685 | 686 | end 687 | 688 | # TExpr that matches the week in a month. For example: 689 | # 690 | # WIMonth.new(1) 691 | # 692 | # See also: Date 693 | # FIXME .dates mixin seems functionally broken 694 | class WIMonth 695 | 696 | include TExpr 697 | include TExprUtils 698 | 699 | VALID_RANGE = -2..5 700 | 701 | attr_reader :ordinal 702 | 703 | def initialize(ordinal) 704 | unless VALID_RANGE.include?(ordinal) 705 | raise ArgumentError, 'invalid ordinal week of month' 706 | end 707 | @ordinal = ordinal 708 | end 709 | 710 | def ==(o) 711 | o.is_a?(WIMonth) ? ordinal == o.ordinal : super(o) 712 | end 713 | 714 | def include?(date) 715 | week_matches?(@ordinal,date) 716 | end 717 | 718 | def to_s 719 | "#{Runt.ordinalize(@ordinal)} week of any month" 720 | end 721 | 722 | end 723 | 724 | # TExpr that matches a range of dates within a month. For example: 725 | # 726 | # REMonth.(12,28) 727 | # 728 | # matches from the 12th thru the 28th of any month. If end_day==0 729 | # or is not given, start_day will define the range with that single day. 730 | # 731 | # See also: Date 732 | class REMonth 733 | 734 | include TExpr 735 | 736 | attr_reader :range 737 | 738 | def initialize(start_day, end_day=0) 739 | end_day=start_day if end_day==0 740 | @range = start_day..end_day 741 | end 742 | 743 | def ==(o) 744 | o.is_a?(REMonth) ? range == o.range : super(o) 745 | end 746 | 747 | def include?(date) 748 | @range.include? date.mday 749 | end 750 | 751 | def to_s 752 | "from the #{Runt.ordinalize(@range.begin)} to the #{Runt.ordinalize(@range.end)} monthly" 753 | end 754 | 755 | end 756 | 757 | # 758 | # Using the precision from the supplied start argument and the its date value, 759 | # matches every n number of time units thereafter. 760 | # 761 | class EveryTE 762 | 763 | include TExpr 764 | 765 | attr_reader :start, :interval, :precision 766 | 767 | def initialize(start,n,precision=nil) 768 | @start=start 769 | @interval=n 770 | # Use the precision of the start date by default 771 | @precision=precision || @start.date_precision 772 | end 773 | 774 | def ==(o) 775 | o.is_a?(EveryTE) ? start == o.start && precision == o.precision && interval == o.interval : super(o) 776 | end 777 | 778 | def include?(date) 779 | i=DPrecision.to_p(@start,@precision) 780 | d=DPrecision.to_p(date,@precision) 781 | while i<=d 782 | return true if i.eql?(d) 783 | i=i+@interval 784 | end 785 | false 786 | end 787 | 788 | def to_s 789 | "every #{@interval} #{@precision.label.downcase}s starting #{Runt.format_date(@start)}" 790 | end 791 | 792 | end 793 | 794 | # Using day precision dates, matches every n number of days after a given 795 | # base date. All date arguments are converted to DPrecision::DAY precision. 796 | # 797 | # Contributed by Ira Burton 798 | class DayIntervalTE 799 | 800 | include TExpr 801 | 802 | attr_reader :interval, :base_date 803 | 804 | def initialize(base_date,n) 805 | @base_date = DPrecision.to_p(base_date,DPrecision::DAY) 806 | @interval = n 807 | end 808 | 809 | def ==(o) 810 | o.is_a?(DayIntervalTE) ? base_date == o.base_date && interval == o.interval : super(o) 811 | end 812 | 813 | def include?(date) 814 | return ((DPrecision.to_p(date,DPrecision::DAY) - @base_date).to_i % @interval == 0) 815 | end 816 | 817 | def to_s 818 | "every #{Runt.ordinalize(@interval)} day after #{Runt.format_date(@base_date)}" 819 | end 820 | 821 | end 822 | 823 | # 824 | # This class creates an expression which matches dates occuring during the weeks 825 | # alternating at the given interval begining on the week containing the date 826 | # used to create the instance. 827 | # 828 | # WeekInterval.new(starting_date, interval) 829 | # 830 | # Weeks are defined as Sunday to Saturday, as opposed to the commercial week 831 | # which starts on a Monday. For example, 832 | # 833 | # every_other_week = WeekInterval.new(Date.new(2013,04,24), 2) 834 | # 835 | # will match any date that occurs during every other week begining with the 836 | # week of 2013-04-21 (2013-04-24 is a Wednesday and 2013-04-21 is the Sunday 837 | # that begins the containing week). 838 | # 839 | # # Sunday of starting week 840 | # every_other_week.include?(Date.new(2013,04,21)) #==> true 841 | # # Saturday of starting week 842 | # every_other_week.include?(Date.new(2013,04,27)) #==> true 843 | # # First week _after_ start week 844 | # every_other_week.include?(Date.new(2013,05,01)) #==> false 845 | # # Second week _after_ start week 846 | # every_other_week.include?(Date.new(2013,05,06)) #==> true 847 | # 848 | # NOTE: The idea and tests for this class were originally contributed as the 849 | # REWeekWithIntervalTE class by Jeff Whitmire. The behavior of the original class 850 | # provided both the matching of every n weeks and the specification of specific 851 | # days of that week in a single class. This class only provides the matching 852 | # of every n weeks. The exact functionality of the original class is easy to create 853 | # using the Runt set operators and the DIWeek class: 854 | # 855 | # # Old way 856 | # tu_thurs_every_third_week = REWeekWithIntervalTE.new(Date.new(2013,04,24),2,[2,4]) 857 | # 858 | # # New way 859 | # tu_thurs_every_third_week = 860 | # WeekInterval.new(Date.new(2013,04,24),2) & (DIWeek.new(Tuesday) | DIWeek.new(Thursday)) 861 | # 862 | # Notice that the compound expression (in parens after the "&") can be replaced 863 | # or combined with any other appropriate temporal expression to provide different 864 | # functionality (REWeek to provide a range of days, REDay to provide certain times, etc...). 865 | # 866 | # Contributed by Jeff Whitmire 867 | class WeekInterval 868 | include TExpr 869 | def initialize(start_date,interval=2) 870 | @start_date = DPrecision.to_p(start_date,DPrecision::DAY) 871 | # convert base_date to the start of the week 872 | @base_date = @start_date - @start_date.wday 873 | @interval = interval 874 | end 875 | 876 | def include?(date) 877 | return false if @base_date > date 878 | ((adjust_for_year(date) - week_num(@base_date)) % @interval) == 0 879 | end 880 | 881 | def to_s 882 | "every #{Runt.ordinalize(@interval)} week starting with the week containing #{Runt.format_date(@start_date)}" 883 | end 884 | 885 | private 886 | def week_num(date) 887 | # %U - Week number of the year. The week starts with Sunday. (00..53) 888 | date.strftime("%U").to_i 889 | end 890 | def max_week_num(year) 891 | d = Date.new(year,12,31) 892 | max = week_num(d) 893 | while max < 52 894 | d = d - 1 895 | max = week_num(d) 896 | end 897 | max 898 | end 899 | def adjust_for_year(date) 900 | # Exclusive range: if date.year == @base_date.year, this will be empty 901 | range_of_years = @base_date.year...date.year 902 | in_same_year = range_of_years.to_a.empty? 903 | # Week number of the given date argument 904 | week_number = week_num(date) 905 | # Default (most common case) date argument is in same year as @base_date 906 | # and the week number is also part of the same year. This starting value 907 | # is also necessary for the case where they're not in the same year. 908 | adjustment = week_number 909 | if in_same_year && (week_number < week_num(@base_date)) then 910 | # The given date occurs within the same year 911 | # but is actually week number 1 of the next year 912 | adjustment = adjustment + max_week_num(date.year) 913 | elsif !in_same_year then 914 | # Date occurs in different year 915 | range_of_years.each do |year| 916 | # Max week number taking into account we are not using commercial week 917 | adjustment = adjustment + max_week_num(year) 918 | end 919 | end 920 | adjustment 921 | end 922 | end 923 | 924 | # Simple expression which returns true if the supplied arguments 925 | # occur within the given year. 926 | # 927 | class YearTE 928 | 929 | include TExpr 930 | 931 | attr_reader :year 932 | 933 | def initialize(year) 934 | @year = year 935 | end 936 | 937 | def ==(o) 938 | o.is_a?(YearTE) ? year == o.year : super(o) 939 | end 940 | 941 | def include?(date) 942 | return date.year == @year 943 | end 944 | 945 | def to_s 946 | "during the year #{@year}" 947 | end 948 | 949 | end 950 | 951 | # Matches dates that occur before a given date. 952 | class BeforeTE 953 | 954 | include TExpr 955 | 956 | attr_reader :date, :inclusive 957 | 958 | def initialize(date, inclusive=false) 959 | @date = date 960 | @inclusive = inclusive 961 | end 962 | 963 | def ==(o) 964 | o.is_a?(BeforeTE) ? date == o.date && inclusive == o.inclusive : super(o) 965 | end 966 | 967 | def include?(date) 968 | return false unless date 969 | return (date < @date) || (@inclusive && @date == date) 970 | end 971 | 972 | def to_s 973 | "before #{Runt.format_date(@date)}" 974 | end 975 | 976 | end 977 | 978 | # Matches dates that occur after a given date. 979 | class AfterTE 980 | 981 | include TExpr 982 | 983 | attr_reader :date, :inclusive 984 | 985 | def initialize(date, inclusive=false) 986 | @date = date 987 | @inclusive = inclusive 988 | end 989 | 990 | def ==(o) 991 | o.is_a?(AfterTE) ? date == o.date && inclusive == o.inclusive : super(o) 992 | end 993 | 994 | 995 | def include?(date) 996 | return (date > @date) || (@inclusive && @date == date) 997 | end 998 | 999 | def to_s 1000 | "after #{Runt.format_date(@date)}" 1001 | end 1002 | 1003 | end 1004 | 1005 | end 1006 | -------------------------------------------------------------------------------- /lib/runt/version.rb: -------------------------------------------------------------------------------- 1 | module Runt 2 | VERSION = "0.9.0" 3 | end 4 | -------------------------------------------------------------------------------- /runt.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'runt/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "runt" 8 | spec.version = Runt::VERSION 9 | spec.authors = ["Matthew Lipper"] 10 | spec.email = ["mlipper@gmail.com"] 11 | spec.description = %q{Runt is a Ruby implementation of temporal patterns by Martin Fowler. Runt provides an API for working with recurring events using set expressions.} 12 | spec.summary = %q{Ruby Temporal Expressions} 13 | spec.homepage = "http://github.com/mlipper/runt" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.3" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "minitest" 24 | end 25 | -------------------------------------------------------------------------------- /site/.cvsignore: -------------------------------------------------------------------------------- 1 | doc -------------------------------------------------------------------------------- /site/blue-robot3.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | color:#333; 4 | background-color:#CCCCCC; 5 | margin:20px; 6 | padding:0px; 7 | font:11px verdana, arial, helvetica, sans-serif; 8 | } 9 | h1 { 10 | margin:0px 0px 15px 0px; 11 | padding:0px; 12 | font-size:14px; 13 | font-weight:900; 14 | color:#ccc; 15 | } 16 | h2 { 17 | font:bold 12px/14px verdana, arial, helvetica, sans-serif; 18 | margin:0px 0px 5px 0px; 19 | padding:0px; 20 | } 21 | p { 22 | font:11px/20px verdana, arial, helvetica, sans-serif; 23 | margin:0px 0px 16px 0px; 24 | padding:0px; 25 | } 26 | .Content>p {margin:0px;} 27 | .Content>p+p {text-indent:30px;} 28 | 29 | a { 30 | color:#09c; 31 | font-size:11px; 32 | font-family:verdana, arial, helvetica, sans-serif; 33 | font-weight:600; 34 | text-decoration:none; 35 | } 36 | a:link {color:#09c;} 37 | a:visited {color:#07a;} 38 | a:hover {background-color:#eee;} 39 | 40 | 41 | /* All the content boxes belong to the content class. */ 42 | .content { 43 | position:relative; /* Position is declared "relative" to gain control of stacking order (z-index). */ 44 | width:auto; 45 | min-width:120px; 46 | margin:0px 210px 20px 170px; 47 | border:1px solid black; 48 | background-color:white; 49 | padding:10px; 50 | z-index:3; /* This allows the content to overlap the right menu in narrow windows in good browsers. */ 51 | } 52 | 53 | /* 2004-03-12 MSL */ 54 | .ruby-link { 55 | color: rgb(255, 0, 0); 56 | } 57 | 58 | .keyword { 59 | font-weight:bold; 60 | } 61 | 62 | 63 | #navAlpha { 64 | position:absolute; 65 | width:150px; 66 | top:20px; 67 | left:20px; 68 | border:1px dashed black; 69 | background-color:#eee; 70 | padding:10px; 71 | z-index:2; 72 | 73 | /* Here is the ugly brilliant hack that protects IE5/Win from its own stupidity. 74 | Thanks to Tantek Celik for the hack and to Eric Costello for publicizing it. 75 | IE5/Win incorrectly parses the "\"}"" value, prematurely closing the style 76 | declaration. The incorrect IE5/Win value is above, while the correct value is 77 | below. See http://glish.com/css/hacks.asp for details. */ 78 | voice-family: "\"}\""; 79 | voice-family:inherit; 80 | width:128px; 81 | } 82 | /* I've heard this called the "be nice to Opera 5" rule. Basically, it feeds correct 83 | length values to user agents that exhibit the parsing error exploited above yet get 84 | the CSS box model right and understand the CSS2 parent-child selector. ALWAYS include 85 | a "be nice to Opera 5" rule every time you use the Tantek Celik hack (above). */ 86 | body>#navAlpha {width:128px;} 87 | 88 | #navBeta { 89 | position:absolute; 90 | width:190px; 91 | top:20px; 92 | right:20px; 93 | border:1px dashed black; 94 | background-color:#eee; 95 | padding:10px; 96 | z-index:1; 97 | /* Again, the ugly brilliant hack. */ 98 | voice-family: "\"}\""; 99 | voice-family:inherit; 100 | width:168px; 101 | } 102 | /* Again, "be nice to Opera 5". */ 103 | body>#navBeta {width:168px;} 104 | 105 | /* 2004-03-12 MSL */ 106 | 107 | #contentFooter { 108 | text-align: center; 109 | font-size:10px; 110 | } 111 | 112 | #contentFooter a { 113 | font-size:10px; 114 | } 115 | 116 | 117 | #logo { 118 | border: 0px solid ; 119 | width: 97px; 120 | height: 33px; 121 | vertical-align: -50%; 122 | } 123 | 124 | 125 | #dcl-logo { 126 | border: 0px solid ; 127 | width: 140px; 128 | height: 29px; 129 | vertical-align: -110%; 130 | margin: 2px; 131 | } 132 | -------------------------------------------------------------------------------- /site/dcl-small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlipper/runt/d0dab62fa45571e50e2018dbe1e12a9fe2752f61/site/dcl-small.gif -------------------------------------------------------------------------------- /site/index-rubforge-www.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Runt - Ruby Temporal Expressions 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Ruby Temporal Expressions

15 |

Runt is an implementation of select temporal patterns by Martin Fowler in the super-fantastic Ruby language. 16 | Runt provides: 17 |

    18 |
  • ability to define recurring events using simple, set-like expressions
  • 19 |
  • an interfaced-based API for creating schedules for arbitrary events/objects
  • 20 |
  • precisioned date types using Time Points
  • 21 |
  • date Ranges
  • 22 |
  • everlasting peace and/or eternal life
  • 23 |
24 |

25 |

Visit our project page provided by the generous folks at Rubyforge.

26 |
27 | 28 |
29 | 30 |

Getting Started

31 |

32 | Check out the temporal expressions tutorial. Then try 33 | schedule tutorial and finally, 34 | sugar tutorial for some useful shortcuts. 35 |

36 |
37 | 38 |
39 | <<©2008, Digital Clash LLC>> 40 |
"The weather is here, wish you were beautiful..." 41 |
42 | 43 | 57 | 58 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | We've moved! 6 | 77 | 78 | 79 |
80 |

We've moved!

81 |

The Runt project is now hosted on GitHub.

82 | 85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /site/logohover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlipper/runt/d0dab62fa45571e50e2018dbe1e12a9fe2752f61/site/logohover.png -------------------------------------------------------------------------------- /site/runt-logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlipper/runt/d0dab62fa45571e50e2018dbe1e12a9fe2752f61/site/runt-logo.gif -------------------------------------------------------------------------------- /site/runt-logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlipper/runt/d0dab62fa45571e50e2018dbe1e12a9fe2752f61/site/runt-logo.psd -------------------------------------------------------------------------------- /test/aftertetest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for AfterTE class 6 | # Author:: Matthew Lipper 7 | 8 | class AfterTETest < BaseExpressionTest 9 | 10 | include TExpr 11 | 12 | def test_include_inclusive 13 | expr = AfterTE.new(@pdate_20071030, true) 14 | assert !expr.include?(@date_20050101), "Should not include an earlier date" 15 | assert expr.include?(@pdate_20071114), "Should include a later date" 16 | assert expr.include?(@pdate_20071030), "Should include the same date" 17 | end 18 | 19 | def test_include_non_inclusive 20 | expr = AfterTE.new(@pdate_20071030) 21 | assert !expr.include?(@date_20050101), "Should not include an earlier date" 22 | assert expr.include?(@pdate_20071114), "Should include a later date" 23 | assert !expr.include?(@pdate_20071030), "Should not include the same date" 24 | end 25 | 26 | def test_to_s 27 | expr = AfterTE.new(@pdate_20071114) 28 | assert_equal "after #{Runt.format_date(@pdate_20071114)}", expr.to_s 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /test/baseexpressiontest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'minitest_helper.rb' 4 | 5 | $DEBUG=false 6 | 7 | # Base test case for refactored temporal expression unit tests 8 | # Author:: Matthew Lipper 9 | 10 | class BaseExpressionTest < MiniTest::Unit::TestCase 11 | 12 | include Runt 13 | include DPrecision 14 | 15 | def setup 16 | @stub1 = StubExpression.new(false, "stub 1", false) 17 | @stub2 = StubExpression.new(false, "stub 2", false) 18 | @pdate_198606 = PDate.month(1986,6) # June, 1986 19 | @pdate_20040531 = PDate.day(2004,5,31) # Monday, May 31st, 2004 20 | @pdate_20040704 = PDate.day(2004,7,4) # Sunday, July 4th, 2004 21 | @pdate_20060914 = PDate.day(2006,9,14) # Thursday, September 14th, 2006 22 | @pdate_20060921 = PDate.day(2006,9,21) # Thursday, September 21st, 2006 23 | @pdate_20071008 = PDate.day(2007,10,8) # Monday, October 8th, 2007 24 | @pdate_20071024 = PDate.day(2007,10,24) # Wednesday, October 24th, 2007 25 | @pdate_20071028 = PDate.day(2007,10,28) # Sunday, October 28th, 2007 26 | @pdate_20071030 = PDate.day(2007,10,30) # Tuesday, October 30th, 2007 27 | @pdate_20071114 = PDate.day(2007,11,14) # Wednesday, November 14th, 2007 28 | @pdate_20081112 = PDate.day(2008,11,12) # Wednesday, November 12th, 2008 29 | @pdate_1922041816 = PDate.hour(1922,4,18,16) # 4pm, Tuesday, April 18th, 1922 30 | @pdate_1975060512 = PDate.hour(1975,6,5,12) # 12pm, Thursday, June 5th, 1975 31 | @pdate_2004090600 = PDate.hour(2004,9,6,0) # 12am, Monday, September 6th, 2004 32 | @pdate_2012050803 = PDate.hour(2012,5,8,3) # 3am, Tuesday, May 8th, 2012 33 | @pdate_2012050815 = PDate.hour(2012,5,8,15) # 3pm, Tuesday, May 8th, 2012 34 | @pdate_200401282100 = PDate.min(2004,1,28,21,0) # 9:00pm, Wednesday, January 28th, 2004 35 | @pdate_200401280000 = PDate.min(2004,1,28,0,0) # 12:00am, Wednesday, January 28th, 2004 36 | @pdate_200401280001 = PDate.min(2004,1,28,0,1) # 12:01am, Wednesday, January 28th, 2004 37 | @pdate_200405010806 = PDate.min(2004,5,1,8,6) # 8:06am, Saturday, May 1st, 2004 38 | @pdate_200405030906 = PDate.min(2004,5,3,9,6) # 9:06am, Monday, May 3rd, 2004 39 | @pdate_200405040806 = PDate.min(2004,5,4,8,6) # 8:06am, Tuesday, May 4th, 2004 40 | @pdate_200605291012 = PDate.min(2006,5,29,10,12) 41 | @pdate_200605301400 = PDate.min(2006,5,30,14,00) 42 | @pdate_200609211001 = PDate.min(2006,9,21,10,1) 43 | @pdate_200609211002 = PDate.min(2006,9,21,10,2) 44 | @pdate_20071116100030 = PDate.sec(2007,11,16,10,0,30) 45 | @date_19611101 = Date.civil(1961,11,1) 46 | @date_20040109 = Date.civil(2004,1,9) # Friday, January 9th 2004 47 | @date_20040116 = Date.civil(2004,1,16) # Friday, January 16th 2004 48 | @date_20040125 = Date.civil(2004,1,25) # Sunday, January 25th 2004 49 | @date_20040501 = Date.civil(2004,5,1) 50 | @date_20040806 = Date.civil(2004,8,6) 51 | @date_20050101 = Date.civil(2005,1,1) 52 | @date_20050102 = Date.civil(2005,1,2) 53 | @date_20050109 = Date.civil(2005,1,9) 54 | @date_20050116 = Date.civil(2005,1,16) 55 | @date_20050123 = Date.civil(2005,1,23) 56 | @date_20050130 = Date.civil(2005,1,30) 57 | @date_20050131 = Date.civil(2005,1,31) 58 | @date_20050228 = Date.civil(2005,2,28) 59 | @date_20051231 = Date.civil(2005,12,31) 60 | @date_20060504 = Date.civil(2006,5,4) 61 | @datetime_200403081200 = DateTime.new(2004,3,8,12,0) # 12:00pm, Monday, March 8th, 2004 62 | @datetime_200403100800 = DateTime.new(2004,3,10,8,00) 63 | @datetime_200403100915 = DateTime.new(2004,3,10,9,15) 64 | @datetime_200403101915 = DateTime.new(2004,3,10,19,15) # 7:15pm, Wednesday, March 10th, 2004 65 | @datetime_200403110000 = DateTime.new(2004,3,11,0,0) # 12:00am, Thursday, March 11th, 2004 66 | @datetime_200403110115 = DateTime.new(2004,3,11,1,15) 67 | @datetime_200403111215 = DateTime.new(2004,3,11,12,15) 68 | @datetime_200403140900 = DateTime.new(2004,3,14,9,00) # 9:00am, Sunday, March 14th, 2004 69 | @datetime_200709161007 = DateTime.new(2007,9,16,10,7) 70 | @time_20070925115959 = Time.mktime(2007,9,25,11,59,59) # 11:59:59am, Tuesday, September 25th, 2007 71 | @time_20070926000000 = Time.mktime(2007,9,26,0,0,0) # 12:00:00am, Wednesday, September 26th, 2007 72 | @time_20070927065959 = Time.mktime(2007,9,27,6,59,59) # 6:59:59am, Thursday, September 27th, 2007 73 | @time_20070927115900 = Time.mktime(2007,9,27,11,59,0) # 11:59:00am, Thursday, September 27th, 2007 74 | @time_20070928000000 = Time.mktime(2007,9,28,0,0,0) # 12:00:00am, Friday, September 28th, 2007 75 | @time_20070929110000 = Time.mktime(2007,9,29,11,0,0) # 11:00:00am, Saturday, September 29th, 2007 76 | @time_20070929000000 = Time.mktime(2007,9,29,0,0,0) # 12:00:00am, Saturday, September 29th, 2007 77 | @time_20070929235959 = Time.mktime(2007,9,29,23,59,59) # 11:59:59pm, Saturday, September 29th, 2007 78 | @time_20070930235959 = Time.mktime(2007,9,30,23,59,59) # 11:59:59am, Sunday, September 30th, 2007 79 | end 80 | 81 | # override default_test keeps it from growling about no other tests in this file 82 | def default_test; end 83 | 84 | def assert_raise_message(types, matcher, message = nil, &block) 85 | args = [types].flatten + [message] 86 | exception = assert_raise(*args, &block) 87 | assert_match matcher, exception.message, message 88 | end 89 | end 90 | 91 | class StubExpression 92 | include Runt 93 | include TExpr 94 | attr_accessor :match, :string, :overlap, :args 95 | def initialize(match=false, string="StubExpression",overlap=false) 96 | @match=match 97 | @string=string 98 | @overlap=overlap 99 | @args=[] 100 | end 101 | def include?(arg) 102 | @args << arg 103 | @match 104 | end 105 | def overlap?(arg) 106 | @args << arg 107 | @overlap 108 | end 109 | def to_s 110 | @string 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /test/beforetetest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for BeforeTE class 6 | # Author:: Matthew Lipper 7 | 8 | class BeforeTETest < BaseExpressionTest 9 | 10 | include TExpr 11 | 12 | def test_include_inclusive 13 | expr = BeforeTE.new(@pdate_20071030, true) 14 | assert expr.include?(@date_20050101), "Should include an earlier date" 15 | assert !expr.include?(@pdate_20071114), "Should not include a later date" 16 | assert expr.include?(@pdate_20071030), "Should include the same date" 17 | end 18 | 19 | def test_include_non_inclusive 20 | expr = BeforeTE.new(@pdate_20071030) 21 | assert expr.include?(@date_20050101), "Should include an earlier date" 22 | assert !expr.include?(@pdate_20071114), "Should not include a later date" 23 | assert !expr.include?(@pdate_20071030), "Should not include the same date" 24 | end 25 | 26 | def test_to_s 27 | expr = BeforeTE.new(@pdate_20071114) 28 | assert_equal "before #{Runt.format_date(@pdate_20071114)}", expr.to_s 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /test/collectiontest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for Collection class 6 | # Author:: Matthew Lipper 7 | 8 | class CollectionTest < BaseExpressionTest 9 | 10 | def setup 11 | super 12 | @expr = Collection.new 13 | end 14 | 15 | def test_initialize 16 | assert !@expr.expressions.nil?, "Newly created Collection should have a non-nil @expressions attribute" 17 | assert @expr.expressions.empty?, "Newly created Collection should have an empty @expressions attribute" 18 | end 19 | 20 | def test_include 21 | #base class that should always return false 22 | assert !@expr.include?(StubExpression.new(true)), "Collection#include? should always return false" 23 | end 24 | 25 | 26 | def test_to_s 27 | assert_equal 'empty', @expr.to_s 28 | assert_equal 'empty', @expr.to_s{['b','oo']} 29 | dim = StubExpression.new(false,"Mock1") 30 | @expr.expressions << dim 31 | assert_equal 'ff' + dim.to_s, @expr.to_s{['ff','nn']} 32 | red = StubExpression.new(false, "Mock2") 33 | @expr.expressions << red 34 | assert_equal 'ff' + dim.to_s + 'nn' + red.to_s, @expr.to_s{['ff','nn']} 35 | wim = StubExpression.new(false, "Mock3") 36 | @expr.expressions << wim 37 | assert_equal 'ff' + dim.to_s + 'nn' + red.to_s + 'nn' + wim.to_s, @expr.to_s{['ff','nn']} 38 | end 39 | 40 | def test_add 41 | e1 = StubExpression.new 42 | e2 = StubExpression.new 43 | assert @expr.expressions.empty?, "Empty Collection should not include any expressions" 44 | result = @expr.add(e1) 45 | assert_same @expr, result, "Collection#add method should return self instance" 46 | assert @expr.expressions.include?(e1), "Collection should include added expression" 47 | @expr.add(e2) 48 | assert @expr.expressions.include?(e2), "Collection should include added expression" 49 | assert_same e2, @expr.expressions.pop, "Collection should keep added expressions in stack order" 50 | assert_same e1, @expr.expressions.pop, "Collection should keep added expressions in stack order" 51 | end 52 | 53 | def test_overlap 54 | stub = StubExpression.new(false, "stubby", true) # start with "always overlap?" stub 55 | assert !@expr.overlap?(stub), "Empty Collection should never overlap" 56 | @expr.add StubExpression.new 57 | assert @expr.overlap?(stub), "Collection should overlap with given stub argument" 58 | assert_same stub.args[0], @expr.expressions.first, "First expression should be given to stub in the first call to overlap?" 59 | stub.overlap = false 60 | assert !@expr.overlap?(stub), "Collection should not overlap with given stub argument" 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /test/combinedexpressionstest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for composite temporal expressions 6 | # Author:: Matthew Lipper 7 | 8 | class CombinedExpressionTest < BaseExpressionTest 9 | 10 | def test_dates_on_last_fri_or_first_tues 11 | date_range = @date_20050101..@date_20051231 12 | expr = DIMonth.new(Last, Friday) | DIMonth.new(First, Tuesday) 13 | dates = expr.dates(date_range) 14 | assert_equal 24, dates.size, "Expected 24 dates in 2005 which were either on the last Friday or first Tuesday of the month" 15 | month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 16 | dates.each do |d| 17 | unless (d.wday == 2 and d.day < 8) or \ 18 | (d.wday == 5 and d.day > month_days[d.month-1] - 8) 19 | # Fail 20 | assert false, d.to_s 21 | end 22 | end 23 | end 24 | 25 | def test_january_except_from_7th_to_15th 26 | date_range = @date_20050101..@date_20050131 27 | dates = (REYear.new(1, 1, 1, 31) - REMonth.new(7, 15)).dates(date_range) 28 | assert_equal 22, dates.size, "Expected 22 dates: 1/1-1/6, 1/16-1/31" 29 | end 30 | 31 | def test_work_day 32 | # Should match for 9am to 5pm except for 12pm to 1pm 33 | expr = REDay.new(9,0,17,0) - REDay.new(12,0,13,0) 34 | assert expr.include?(@pdate_200405030906), "Expression #{expr.to_s} should include #{@pdate_200405030906.to_s}" 35 | assert !expr.include?(@pdate_1975060512), "Expression #{expr.to_s} should not include #{@pdate_1975060512.to_s}" 36 | end 37 | 38 | def test_monday_tuesday_8am_to_9am 39 | expr = REWeek.new(Mon,Fri) & REDay.new(8,0,9,0) 40 | assert expr.include?(@pdate_200405040806), "Expression #{expr.to_s} should include #{@pdate_200405040806.to_s}" 41 | assert !expr.include?(@pdate_200405010806), "Expression #{expr.to_s} should not include #{@pdate_200405010806.to_s}" 42 | assert !expr.include?(@pdate_200405030906), "Expression #{expr.to_s} should not include #{@pdate_200405030906.to_s}" 43 | end 44 | 45 | 46 | def test_midnight_to_9am_or_tuesday 47 | expr = REDay.new(0,0,9,0) | DIWeek.new(Tuesday) 48 | assert expr.include?(@pdate_20071030), "Expression #{expr.to_s} should include #{@pdate_20071030.to_s}" 49 | assert expr.include?(@pdate_2012050803), "Expression #{expr.to_s} should include #{@pdate_2012050803.to_s}" 50 | assert !expr.include?(@pdate_20071116100030), "Expression #{expr.to_s} should not include #{@pdate_20071116100030.to_s}" 51 | end 52 | 53 | def test_wednesday_thru_saturday_6_to_12am 54 | expr = REWeek.new(Wed, Sat) & REDay.new(6,0,12,00) 55 | assert !expr.include?(@time_20070926000000), "Expression #{expr.to_s} should include #{@time_20070926000000.to_s}" 56 | assert expr.include?(@time_20070927065959), "Expression #{expr.to_s} should include #{@time_20070927065959.to_s}" 57 | assert !expr.include?(@time_20070928000000), "Expression #{expr.to_s} should include #{@time_20070928000000.to_s}" 58 | assert expr.include?(@time_20070929110000), "Expression #{expr.to_s} should include #{@time_20070929110000.to_s}" 59 | end 60 | 61 | def test_memorial_day 62 | # Monday through Friday, from 9am to 5pm 63 | job = REWeek.new(Mon,Fri) & REDay.new(9,00,17,00) 64 | # Memorial Day (U.S.) 65 | memorial_day = REYear.new(5) & DIMonth.new(Last,Monday) 66 | # May 29th, 2006 67 | last_monday_in_may = @pdate_200605291012 68 | # Before 69 | assert job.include?(last_monday_in_may), "Expression #{job.to_s} should include #{last_monday_in_may.to_s}" 70 | assert job.include?(@pdate_200605301400), "Expression #{job.to_s} should include #{@pdate_200605301400.to_s}" 71 | # Add Diff expression 72 | job_with_holiday = job - last_monday_in_may 73 | assert !job_with_holiday.include?(last_monday_in_may), "Expression #{job_with_holiday.to_s} should not include #{last_monday_in_may.to_s}" 74 | # Still have to work on Tuesday 75 | assert job.include?(@pdate_200605301400), "Expression #{job.to_s} should include #{@pdate_200605301400.to_s}" 76 | end 77 | 78 | def test_summertime 79 | #This is a hack..... 80 | #In the U.S., Memorial Day begins the last Monday of May 81 | # 82 | #The month of May 83 | may=REYear.new(5) 84 | #Monday through Saturday 85 | monday_to_saturday = REWeek.new(1,6) 86 | #Last week of (any) month 87 | last_week_in = WIMonth.new(Last_of) 88 | #So, to say 'starting from the last Monday in May', 89 | #we need to select just that last week of May begining with 90 | #the Monday of that week 91 | last_week_of_may = may & monday_to_saturday & last_week_in 92 | 93 | #This is another hack similar to the above, except instead of selecting a range 94 | #starting at the begining of the month, we need to select only the time period in 95 | #September up until Labor Day. 96 | # 97 | #In the U.S., Labor Day is the first Monday in September 98 | # 99 | #The month of September 100 | september=REYear.new(9) 101 | #First week of (any) month 102 | first_week_in = WIMonth.new(First) 103 | entire_first_week_of_september = september & first_week_in 104 | #To exclude everything in the first week which occurs on or after Monday. 105 | first_week_of_september=entire_first_week_of_september - monday_to_saturday 106 | #June through August 107 | june_through_august=REYear.new(6, 1, 8) 108 | assert june_through_august.include?(@pdate_20040704), "Expression #{june_through_august.to_s} should include #{@pdate_20040704.to_s}" 109 | #Finally! 110 | summer_time = last_week_of_may | first_week_of_september | june_through_august 111 | 112 | #Will work, but will be incredibly slow: 113 | # assert(summer_time.include?(PDate.min(2004,5,31,0,0))) 114 | assert summer_time.include?(@pdate_20040531), "Expression #{summer_time.to_s} should include #{@pdate_20040704.to_s}" 115 | assert summer_time.include?(@pdate_20040704), "Expression #{summer_time.to_s} should include #{@pdate_20040704.to_s}" 116 | #also works...also slow: 117 | # assert(!summer_time.include?(PDate.min(2004,9,6,0,0))) 118 | assert !summer_time.include?(@pdate_2004090600), "Expression #{summer_time.to_s} should not include #{@pdate_2004090600.to_s}" 119 | 120 | end 121 | def test_nyc_parking_te 122 | 123 | #Monday, Wednesday, Friday 124 | mon_wed_fri = DIWeek.new(Mon) | \ 125 | DIWeek.new(Wed) | \ 126 | DIWeek.new(Fri) 127 | 128 | assert mon_wed_fri.include?(@datetime_200403101915), "Expression #{mon_wed_fri.to_s} should include #{@datetime_200403101915.to_s}" 129 | assert !mon_wed_fri.include?(@datetime_200403140900), "Expression #{mon_wed_fri.to_s} should not include #{@datetime_200403140900.to_s}" 130 | # 8am to 11am 131 | eight_to_eleven = REDay.new(8,00,11,00) 132 | # => Mon,Wed,Fri - 8am to 11am 133 | expr1 = mon_wed_fri & eight_to_eleven 134 | # Tuesdays, Thursdays 135 | tues_thurs = DIWeek.new(Tue) | DIWeek.new(Thu) 136 | # 11:30am to 2pm 137 | eleven_thirty_to_two = REDay.new(11,30,14,00) 138 | assert eleven_thirty_to_two.include?(@datetime_200403081200), "Expression #{eleven_thirty_to_two.to_s} should include #{@datetime_200403081200.to_s}" 139 | assert !eleven_thirty_to_two.include?(@datetime_200403110000), "Expression #{eleven_thirty_to_two.to_s} should not include #{@datetime_200403110000.to_s}" 140 | # => Tues,Thurs - 11:30am to 2pm 141 | expr2 = tues_thurs & eleven_thirty_to_two 142 | # 143 | # No parking: Mon Wed Fri, 8am - 11am 144 | # Tu Thu, 11:30am - 2pm 145 | parking_ticket = expr1 | expr2 146 | assert parking_ticket.include?(@datetime_200403111215), "Expression #{parking_ticket.to_s} should include #{@datetime_200403111215.to_s}" 147 | assert parking_ticket.include?(@datetime_200403100915), "Expression #{parking_ticket.to_s} should include #{@datetime_200403100915.to_s}" 148 | assert parking_ticket.include?(@datetime_200403100800), "Expression #{parking_ticket.to_s} should include #{@datetime_200403100800.to_s}" 149 | assert !parking_ticket.include?(@datetime_200403110115), "Expression #{parking_ticket.to_s} should not include #{@datetime_200403110115.to_s}" 150 | 151 | # The preceeding example can be condensed to: 152 | # e1 = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00) 153 | # e2 = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00) 154 | # ticket = e1 | e2 155 | end 156 | 157 | def test_dates_every_four_hours_on_tuesdays 158 | # 3am, Tuesday, May 8th, 2012 to 3am Wednesday, May 9th, 2012 159 | range = DateRange.new(@pdate_2012050803, PDate.new(2012,5,9,15)) 160 | every_four_hours = EveryTE.new(@pdate_2012050803,4) 161 | tuesday = DIWeek.new(Tuesday) 162 | every_four_hours_on_tuesday = every_four_hours & tuesday 163 | result = every_four_hours_on_tuesday.dates(range) 164 | end 165 | 166 | end 167 | -------------------------------------------------------------------------------- /test/daterangetest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'minitest_helper' 4 | 5 | # Unit tests for DateRange class 6 | # 7 | # Author:: Matthew Lipper 8 | class DateRangeTest < MiniTest::Unit::TestCase 9 | 10 | include Runt 11 | 12 | def test_sub_range 13 | r_start = PDate.min(2004,2,29,16,24) 14 | r_end = PDate.min(2004,3,2,4,22) 15 | range = DateRange.new(r_start,r_end) 16 | assert(range.min==r_start) 17 | assert(range.max==r_end) 18 | assert(range.include?(r_start+1)) 19 | assert(range.include?(r_end-1)) 20 | sub_range = DateRange.new((r_start+1),(r_end-1)) 21 | assert(range.include?(sub_range)) 22 | end 23 | 24 | def test_date 25 | r_start = PDate.min(1979,12,31,23,57) 26 | r_end = PDate.min(1980,1,1,0,2) 27 | range = DateRange.new(r_start,r_end) 28 | assert(range.min==r_start) 29 | assert(range.max==r_end) 30 | assert(range.include?(r_start+1)) 31 | assert(range.include?(r_end-1)) 32 | sub_range = DateRange.new((r_start+1),(r_end-1)) 33 | assert(range.include?(sub_range)) 34 | end 35 | 36 | def test_spaceship_operator 37 | r_start = PDate.min(1984,8,31,22,00) 38 | r_end = PDate.min(1984,9,15,0,2) 39 | range = DateRange.new(r_start,r_end) 40 | assert(-1==(range<=>(DateRange.new(r_start+2,r_end+5)))) 41 | assert(1==(range<=>(DateRange.new(r_start-24,r_end+5)))) 42 | assert(0==(range<=>(DateRange.new(r_start,r_end)))) 43 | end 44 | 45 | def test_overlap 46 | r_start = PDate.month(2010,12) 47 | r_end = PDate.month(2011,12) 48 | range = DateRange.new(r_start,r_end) 49 | o_start = PDate.month(2010,11) 50 | o_end = PDate.month(2012,2) 51 | o_range = DateRange.new(o_start,o_end) 52 | assert(range.overlap?(o_range)) 53 | assert(o_range.overlap?(range)) 54 | assert(o_range.overlap?(DateRange.new(r_start,o_end))) 55 | assert(o_range.overlap?(DateRange.new(o_start,r_end))) 56 | 57 | # September 18th - 19th, 2005, 8am - 10am 58 | expr1=DateRange.new(PDate.day(2005,9,18),PDate.day(2005,9,19)) 59 | # September 19th - 20th, 2005, 9am - 11am 60 | expr2=DateRange.new(PDate.day(2005,9,19),PDate.day(2005,9,20)) 61 | 62 | assert(expr1.overlap?(expr2)) 63 | end 64 | 65 | def test_empty 66 | r_start = PDate.hour(2004,2,10,0) 67 | r_end = PDate.hour(2004,2,9,23) 68 | empty_range = DateRange.new(r_start,r_end) 69 | assert(empty_range.empty?) 70 | assert(DateRange::EMPTY.empty?) 71 | # start == end should be empty 72 | assert DateRange.new(r_start,r_start).empty?, "Range should be empty when start == end" 73 | end 74 | 75 | def test_gap 76 | r_start = PDate.day(2000,6,12) 77 | r_end = PDate.day(2000,6,14) 78 | range = DateRange.new(r_start,r_end) 79 | g_start = PDate.day(2000,6,18) 80 | g_end = PDate.day(2000,6,20) 81 | g_range = DateRange.new(g_start,g_end) 82 | the_gap=range.gap(g_range) 83 | assert(the_gap.start_expr==(r_end+1)) 84 | assert(the_gap.end_expr==(g_start-1)) 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /test/dayintervaltetest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for DayIntervalTE class 6 | # Author:: Matthew Lipper 7 | 8 | class DayIntervalTETest < BaseExpressionTest 9 | 10 | 11 | def test_every_8_days 12 | date = @date_20040116 13 | # Match every 8 days 14 | expr = DayIntervalTE.new(date, 8) 15 | assert expr.include?(date + 8), "Expression #{expr.to_s} should include #{(date + 8).to_s}" 16 | assert expr.include?(date + 16), "Expression #{expr.to_s} should include #{(date + 16).to_s}" 17 | assert expr.include?(date + 64), "Expression #{expr.to_s} should include #{(date + 64).to_s}" 18 | assert !expr.include?(date + 4), "Expression #{expr.to_s} should not include #{(date + 4).to_s}" 19 | # FIXME This test fails 20 | #assert !expr.include?(date - 8), "Expression #{expr.to_s} should not include #{(date - 8).to_s}" 21 | end 22 | 23 | def test_every_2_days 24 | date = @datetime_200709161007 25 | expr = DayIntervalTE.new(date, 2) 26 | assert expr.include?(date + 2), "Expression #{expr.to_s} should include #{(date + 2).to_s}" 27 | assert expr.include?(date + 4), "Expression #{expr.to_s} should include #{(date + 4).to_s}" 28 | assert !expr.include?(date + 3), "Expression #{expr.to_s} should not include #{(date + 3).to_s}" 29 | end 30 | 31 | def test_to_s 32 | every_four_days = DayIntervalTE.new(Date.new(2006,2,26), 4) 33 | assert_equal "every 4th day after #{Runt.format_date(Date.new(2006,2,26))}", every_four_days.to_s 34 | end 35 | 36 | 37 | end 38 | -------------------------------------------------------------------------------- /test/difftest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for Diff class 6 | # Author:: Matthew Lipper 7 | 8 | class DiffTest < BaseExpressionTest 9 | 10 | def setup 11 | super 12 | @diff = Diff.new(@stub1, @stub2) 13 | @date = @pdate_20071008 14 | end 15 | 16 | def test_initialize 17 | assert_same @stub1, @diff.expr1, "Expected #{@stub1} instance used to create expression. Instead got #{@diff.expr1}" 18 | assert_same @stub2, @diff.expr2, "Expected #{@stub2} instance used to create expression. Instead got #{@diff.expr2}" 19 | end 20 | 21 | def test_to_s 22 | assert_equal @stub1.to_s + ' except for ' + @stub2.to_s, @diff.to_s 23 | end 24 | 25 | def test_include 26 | # Diff will match only if expr1 && !expr2 27 | @stub1.match = false 28 | @stub2.match = false 29 | assert !@diff.include?(@date), "Diff instance should not include any dates" 30 | @stub2.match = true 31 | assert !@diff.include?(@date), "Diff instance should not include any dates" 32 | @stub1.match = true 33 | assert !@diff.include?(@date), "Diff instance should not include any dates" 34 | @stub2.match = false 35 | assert @diff.include?(@date), "Diff instance should include any dates" 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/dimonthtest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for DIMonth class 6 | # Author:: Matthew Lipper 7 | 8 | class DIMonthTest < BaseExpressionTest 9 | def setup 10 | super 11 | @date_range = @date_20050101..@date_20051231 12 | end 13 | 14 | ### 15 | # Dates functionality & tests contributed by Emmett Shear 16 | ### 17 | def test_dates_mixin_first_tuesday 18 | dates = DIMonth.new(First, Tuesday).dates(@date_range) 19 | assert dates.size == 12 20 | dates.each do |d| 21 | assert @date_range.include?(d) 22 | assert d.wday == 2 # tuesday 23 | assert d.day < 8 # in the first week 24 | end 25 | end 26 | 27 | def test_dates_mixin_last_friday 28 | dates = DIMonth.new(Last, Friday).dates(@date_range) 29 | assert dates.size == 12 30 | month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 31 | dates.each do |d| 32 | assert @date_range.include?(d) 33 | assert d.wday == 5 # friday 34 | assert d.day > month_days[d.month-1] - 8 # last week 35 | end 36 | end 37 | 38 | def test_third_friday_of_the_month 39 | expr = DIMonth.new(Third,Friday) 40 | assert expr.include?(@date_20040116), "Third Friday of the month should include #{@date_20040116.to_s}" 41 | assert !expr.include?(@date_20040109), "Third Friday of the month should not include #{@date_20040109.to_s}" 42 | end 43 | 44 | def test_second_friday_of_the_month 45 | expr = DIMonth.new(Second,Friday) 46 | assert expr.include?(@date_20040109), "Second Friday of the month should include #{@date_20040109.to_s}" 47 | assert !expr.include?(@date_20040116), "Second Friday of the month should not include #{@date_20040116.to_s}" 48 | end 49 | 50 | def test_last_sunday_of_the_month 51 | expr = DIMonth.new(Last_of,Sunday) 52 | assert expr.include?(@date_20040125), "Last Sunday of the month should include #{@date_20040125}" 53 | end 54 | 55 | def test_to_s 56 | assert_equal 'last Sunday of the month', DIMonth.new(Last_of,Sunday).to_s 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /test/diweektest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for DIWeek class 6 | # Author:: Matthew Lipper 7 | 8 | class DIWeekTest < BaseExpressionTest 9 | 10 | def test_day_in_week_te 11 | expr = DIWeek.new(Friday) 12 | assert expr.include?(@date_20040109), "'Friday' should include #{@date_20040109} which is a Friday" 13 | assert expr.include?(@date_20040116), "'Friday' should include #{@date_20040116} which is a Friday" 14 | assert !expr.include?(@date_20040125), "'Friday' should not include #{@date_20040125} which is a Sunday" 15 | end 16 | 17 | def test_day_in_week_te_to_s 18 | assert_equal 'Friday', DIWeek.new(Friday).to_s 19 | end 20 | 21 | def test_day_in_week_dates 22 | expr = DIWeek.new(Sunday) 23 | dates = expr.dates(@date_20050101..@date_20050131) 24 | assert dates.size == 5, "There are five Sundays in January, 2005: found #{dates.size}" 25 | assert dates.include?(@date_20050102), "Should include #{@date_20050102}" 26 | assert dates.include?(@date_20050109), "Should include #{@date_20050109}" 27 | assert dates.include?(@date_20050116), "Should include #{@date_20050116}" 28 | assert dates.include?(@date_20050123), "Should include #{@date_20050123}" 29 | assert dates.include?(@date_20050130), "Should include #{@date_20050130}" 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /test/dprecisiontest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'minitest_helper' 4 | 5 | # Unit tests for DPrecision class 6 | # 7 | # Author:: Matthew Lipper 8 | 9 | class DPrecisionTest < MiniTest::Unit::TestCase 10 | 11 | include Runt 12 | 13 | def test_comparable 14 | assert(DPrecision::YEAR#{no_prec_datetime.date_precision}<--" 42 | assert(month_prec==DPrecision.to_p(no_prec_datetime,DPrecision::MONTH)) 43 | end 44 | 45 | def test_label 46 | assert_equal(DPrecision::YEAR.label,"YEAR") 47 | assert_equal(DPrecision::MONTH.label,"MONTH") 48 | assert_equal(DPrecision::WEEK.label,"WEEK") 49 | assert_equal(DPrecision::DAY.label,"DAY") 50 | assert_equal(DPrecision::HOUR.label,"HOUR") 51 | assert_equal(DPrecision::MIN.label,"MINUTE") 52 | assert_equal(DPrecision::SEC.label,"SECOND") 53 | assert_equal(DPrecision::MILLI.label,"MILLISECOND") 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /test/everytetest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for EveryTE class 6 | # Author:: Matthew Lipper 7 | 8 | class EveryTETest < BaseExpressionTest 9 | 10 | def test_every_other_week 11 | date = @pdate_20081112 12 | expr = EveryTE.new(date, 14, DPrecision::DAY) 13 | assert !expr.include?(date + 7) 14 | assert expr.include?(date + 14) 15 | end 16 | 17 | def test_every_2_minutes 18 | date = @pdate_200401282100 19 | expr=EveryTE.new(date, 2) 20 | assert expr.include?(date + 2), "Expression #{expr.to_s} should include #{(date + 2).to_s}" 21 | assert expr.include?(date + 4), "Expression #{expr.to_s} should include #{(date + 4).to_s}" 22 | assert !expr.include?(date - 2), "Expression #{expr.to_s} should not include #{(date - 2).to_s}" 23 | end 24 | 25 | def test_every_3_days 26 | # Match every 3 days begining 2007-11-14 27 | date = @pdate_20071114 28 | expr=EveryTE.new(date, 3) 29 | assert expr.include?(date + 6), "Expression #{expr.to_s} should include #{(date + 6).to_s}" 30 | assert expr.include?(date + 9), "Expression #{expr.to_s} should include #{(date + 9).to_s}" 31 | assert !expr.include?(date + 1), "Expression #{expr.to_s} should not include #{(date + 1).to_s}" 32 | assert !expr.include?(date - 3), "Expression #{expr.to_s} should not include #{(date - 3).to_s}" 33 | end 34 | 35 | 36 | def test_to_s 37 | date=@pdate_20071116100030 38 | every_thirty_seconds=EveryTE.new(date, 30) 39 | assert_equal "every 30 seconds starting #{Runt.format_date(date)}", every_thirty_seconds.to_s 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /test/expressionbuildertest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'minitest_helper' 4 | 5 | class ExpressionBuilderTest < MiniTest::Unit::TestCase 6 | 7 | def setup 8 | @builder = ExpressionBuilder.new 9 | end 10 | 11 | def test_define_should_instance_eval_a_block 12 | @builder.define do 13 | @ctx = "meow" 14 | end 15 | assert_equal "meow", @builder.ctx, "Expected instance variable @ctx to be set by block" 16 | end 17 | 18 | def test_add_should_initialize_empty_ctx_with_expression 19 | result = @builder.add('expr',:to_s) 20 | assert_equal 'expr', result, "Result should equal string given to add method" 21 | assert_equal 'expr', @builder.ctx, "Builder context should equal result" 22 | end 23 | 24 | def test_add_should_send_op_to_ctx_with_expression 25 | @builder.add('abc',:to_s) 26 | result = @builder.add('def',:concat) 27 | assert_equal 'abcdef', result, "Result should equal concatenated strings given to add method" 28 | assert_equal 'abcdef', @builder.ctx, "Builder context should equal result" 29 | end 30 | 31 | def test_on_should_call_add_with_expression_and_ampersand 32 | @builder.add(1,:to_s) 33 | result = @builder.on(3) # result = 1 & 3 34 | assert_equal 1, result, "Result should equal 1 == 1 & 3" 35 | assert_equal 1, @builder.ctx, "Builder context should equal result" 36 | end 37 | 38 | def test_except_should_call_add_with_expression_and_minus 39 | @builder.add(1,:to_s) 40 | result = @builder.except(3) # result = 1 - 3 41 | assert_equal -2, result, "Result should equal -2 == 1 - 3" 42 | assert_equal -2, @builder.ctx, "Builder context should equal result" 43 | end 44 | 45 | def test_possibly_should_call_add_with_expression_and_pipe 46 | @builder.add(1, :to_s) 47 | result = @builder.possibly(2) # result = 1 | 2 48 | assert_equal 3, result, "Result should equal 3 == 1 | 2" 49 | assert_equal 3, @builder.ctx, "Builder context should equal result" 50 | end 51 | 52 | def test_builder_created_expression_should_equal_manually_created_expression 53 | manual = Runt::REDay.new(8,45,9,30) & Runt::DIWeek.new(Runt::Friday) | \ 54 | Runt::DIWeek.new(Runt::Saturday) - Runt::DIMonth.new(Runt::Last, Runt::Friday) 55 | expression = @builder.define do 56 | on daily_8_45am_to_9_30am 57 | on friday 58 | possibly saturday 59 | except last_friday 60 | end 61 | assert_equal manual.to_s, expression.to_s, "Expected #{manual.to_s} but was #{expression.to_s}" 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/intersecttest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for Intersect class 6 | # Author:: Matthew Lipper 7 | 8 | class IntersectTest < BaseExpressionTest 9 | 10 | def setup 11 | super 12 | @intersect = Intersect.new 13 | @date = @pdate_20071008 14 | end 15 | 16 | def test_to_s 17 | assert_equal 'empty', @intersect.to_s 18 | @intersect.add(@stub1) 19 | assert_equal 'every ' + @stub1.to_s, @intersect.to_s 20 | @intersect.add(@stub2) 21 | assert_equal 'every ' + @stub1.to_s + ' and ' + @stub2.to_s, @intersect.to_s 22 | end 23 | 24 | 25 | def test_include 26 | assert !@intersect.include?(@date), "Empty Intersect instance should not include any dates" 27 | @intersect.add(@stub1).add(@stub2) # both expressions will return false 28 | assert !@intersect.include?(@date), "Intersect instance should not include any dates" 29 | @stub2.match = true 30 | assert !@intersect.include?(@date), "Intersect instance should not include any dates" 31 | @stub1.match = true 32 | assert @intersect.include?(@date), "Intersect instance should include any dates" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/minitest_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'runt' 4 | require 'date' 5 | require 'time' 6 | require 'rubygems' # Needed for minitest on 1.8.7 7 | require 'minitest/autorun' 8 | -------------------------------------------------------------------------------- /test/pdatetest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'minitest_helper' 4 | 5 | # Unit tests for PDate class 6 | # Author:: Matthew Lipper 7 | class PDateTest < MiniTest::Unit::TestCase 8 | 9 | include Runt 10 | 11 | def setup 12 | # 2010 (August - ignored) 13 | @year_prec = PDate.year(2010,8) 14 | #August, 2004 15 | @month_prec = PDate.month(2004,8) 16 | #January 25th, 2004 (11:39 am - ignored) 17 | @week_prec = PDate.week(2004,1,25,11,39) 18 | #January 25th, 2004 (11:39 am - ignored) 19 | @day_prec = PDate.day(2004,1,25,11,39) 20 | #11:59(:04 - ignored), December 31st, 1999 21 | @minute_prec = PDate.min(1999,12,31,23,59,4) 22 | #12:00:10 am, March 1st, 2004 23 | @second_prec = PDate.sec(2004,3,1,0,0,10) 24 | end 25 | 26 | def test_pdate_with_native_range 27 | start_dt = PDate.min(2013,04,22,8,0) 28 | middle_dt = PDate.min(2013,04,22,8,2) 29 | end_dt = PDate.min(2013,04,22,8,04) 30 | range = start_dt..end_dt 31 | assert(range.include?(middle_dt)) 32 | end 33 | 34 | def test_marshal 35 | # Thanks to Jodi Showers for finding/fixing this bug 36 | pdate=PDate.new(2004,2,29,22,13,2) 37 | refute_nil pdate.date_precision 38 | data=Marshal.dump pdate 39 | obj=Marshal.load data 40 | refute_nil obj.date_precision 41 | #FIXME: marshall broken in 1.9 42 | #assert(obj.eql?(pdate)) 43 | #assert(pdate.eql?(obj)) 44 | end 45 | 46 | def test_include 47 | pdate = PDate.new(2006,3,10) 48 | assert(pdate.include?(Date.new(2006,3,10))) 49 | date = Date.new(2006,3,10) 50 | assert(date.include?(PDate.new(2006,3,10))) 51 | end 52 | 53 | def test_new 54 | date = PDate.new(2004,2,29) 55 | assert(!date.date_precision.nil?) 56 | date_time = PDate.new(2004,2,29,22,13,2) 57 | assert(!date_time.date_precision.nil?) 58 | date2 = PDate.day(2004,2,29) 59 | assert(date==date2) 60 | date_time2 = PDate.sec(2004,2,29,22,13,2) 61 | assert(date_time==date_time2) 62 | end 63 | 64 | def test_plus 65 | assert(PDate.year(2022,12)==(@year_prec+12)) 66 | assert(PDate.month(2005,2)==(@month_prec+6)) 67 | assert(PDate.week(2004,2,1)==(@week_prec+1)) 68 | assert(PDate.day(2004,2,1)==(@day_prec+7)) 69 | assert(PDate.min(2000,1,1,0,0)==(@minute_prec+1)) 70 | assert(PDate.sec(2004,3,1,0,0,21)==(@second_prec+11)) 71 | end 72 | 73 | def test_minus 74 | assert(PDate.year(1998,12)==(@year_prec-12)) 75 | assert(PDate.month(2002,6)==(@month_prec-26)) 76 | assert(PDate.week(2004,1,11)==(@week_prec-2)) 77 | #Hmmm...FIXME? @day_prec-26 == 12/31?? 78 | assert(PDate.day(2003,12,30)==(@day_prec-26)) 79 | assert(PDate.min(1999,12,31,21,57)==(@minute_prec-122)) 80 | assert(PDate.sec(2004,2,29,23,59,59)==(@second_prec-11)) 81 | end 82 | def test_spaceship_comparison_operator 83 | sec_prec = PDate.sec(2002,8,28,6,04,02) 84 | assert(PDate.year(1998,12)sec_prec) 86 | assert(PDate.week(2002,8,28)==sec_prec) 87 | assert(PDate.day(2002,8,28)==sec_prec) 88 | assert(PDate.min(1999,12,31,21,57)sec_prec) 91 | end 92 | def test_succ 93 | #~ fail("FIXME! Implement succ") 94 | end 95 | def test_range 96 | #11:50 pm (:22 seconds ignored), February 2nd, 2004 97 | min1 = PDate.min(2004,2,29,23,50,22) 98 | #12:02 am , March 1st, 2004 99 | min2 = PDate.min(2004,3,1,0,2) 100 | #Inclusive Range w/minute precision 101 | r_min = min1..min2 102 | assert( r_min.include?(PDate.min(2004,2,29,23,50,22)) ) 103 | assert( r_min.include?(PDate.min(2004,3,1,0,2)) ) 104 | assert( r_min.include?(PDate.min(2004,3,1,0,0)) ) 105 | assert( ! r_min.include?(PDate.min(2004,3,1,0,3)) ) 106 | #Exclusive Range w/minute precision 107 | r_min = min1...min2 108 | assert( r_min.include?(PDate.min(2004,2,29,23,50,22)) ) 109 | assert( !r_min.include?(PDate.min(2004,3,1,0,2)) ) 110 | end 111 | 112 | def test_create_with_class_methods 113 | #December 12th, 1968 114 | no_prec = PDate.civil(1968,12,12) 115 | #December 12th, 1968 (at 11:15 am - ignored) 116 | day_prec = PDate.day(1968,12,12,11,15) 117 | assert(no_prec==day_prec, "PDate instance does not equal precisioned instance.") 118 | #December 2004 (24th - ignored) 119 | month_prec1 = PDate.month(2004,12,24) 120 | #December 31st, 2004 (31st - ignored) 121 | month_prec2 = PDate.month(2004,12,31) 122 | assert(month_prec1==month_prec2, "PDate.month instances not equal.") 123 | #December 2004 124 | month_prec3 = PDate.month(2004,12) 125 | assert(month_prec1==month_prec3, "PDate.month instances not equal.") 126 | assert(month_prec2==month_prec3, "PDate.month instances not equal.") 127 | #December 2003 128 | month_prec4 = PDate.month(2003,12) 129 | assert(month_prec4!=month_prec1, "PDate.month instances not equal.") 130 | 131 | one_week = [ 132 | PDate.week(2004, 12, 20), # Monday 133 | PDate.week(2004, 12, 21), # Tuesday 134 | PDate.week(2004, 12, 22), # Wednesday 135 | PDate.week(2004, 12, 23), # Thursday 136 | PDate.week(2004, 12, 24), # Friday 137 | PDate.week(2004, 12, 25), # Saturday 138 | PDate.week(2004, 12, 26), # Sunday 139 | ] 140 | 141 | one_week.each do |week_prec1| 142 | one_week.each do |week_prec2| 143 | assert_equal week_prec1, week_prec2 144 | end 145 | end 146 | 147 | week_before = PDate.week(2004, 12, 19) 148 | week_after = PDate.week(2004, 12, 27) 149 | 150 | one_week.each do |week_prec| 151 | assert week_prec != week_before 152 | assert week_prec != week_after 153 | end 154 | end 155 | 156 | def test_parse_with_precision 157 | month_parsed = PDate.parse('April 2004', :precision => PDate::MONTH) 158 | assert_equal month_parsed, PDate.month(2004,04) 159 | refute_equal month_parsed, PDate.year(2004,04) 160 | end 161 | 162 | end 163 | -------------------------------------------------------------------------------- /test/redaytest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for REDay class 6 | # Author:: Matthew Lipper 7 | 8 | class REDayTest < BaseExpressionTest 9 | 10 | 11 | def test_noon_to_430 12 | #noon to 4:30pm 13 | expr = REDay.new(12,0,16,30) 14 | assert expr.include?(@pdate_2012050815), "Expression #{expr.to_s} should include #{@pdate_2012050815.to_s}" 15 | assert expr.include?(@pdate_1922041816), "Expression #{expr.to_s} should include #{@pdate_1922041816.to_s}" 16 | assert expr.include?(@pdate_1975060512), "Expression #{expr.to_s} should include #{@pdate_1975060512.to_s}" 17 | assert !expr.include?(@pdate_2012050803), "Expression #{expr.to_s} should not include #{@pdate_2012050803.to_s}" 18 | end 19 | def test_830_to_midnight 20 | expr = REDay.new(20,30,00,00) 21 | assert expr.include?(@pdate_200401282100), "Expression #{expr.to_s} should include #{@pdate_200401282100.to_s}" 22 | assert expr.include?(@pdate_200401280000), "Expression #{expr.to_s} should include #{@pdate_200401280000.to_s}" 23 | assert !expr.include?(@pdate_200401280001), "Expression #{expr.to_s} should not include #{@pdate_200401280001.to_s}" 24 | end 25 | 26 | def test_range_each_day_te_to_s 27 | assert_equal 'from 11:10PM to 01:20AM daily', REDay.new(23,10,1,20).to_s 28 | end 29 | 30 | def test_less_precise_argument_and_precision_policy 31 | expr = REDay.new(8,00,10,00) 32 | assert expr.include?(@pdate_20040531), \ 33 | "Expression #{expr.to_s} should include any lower precision argument by default" 34 | 35 | expr = REDay.new(8,00,10,00, false) 36 | assert !expr.include?(@pdate_20040531), \ 37 | "Expression #{expr.to_s} created with less_precise_match=false should not include any lower precision argument automatically" 38 | ## Date class which has no public hour or min methods should not cause an exception 39 | assert !expr.include?(@date_19611101), \ 40 | "Expression #{expr.to_s} created with less_precise_match=false should not hurl when given a Date instance" 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/remonthtest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for REMonth class 6 | # Author:: Matthew Lipper 7 | 8 | class REMonthTest < BaseExpressionTest 9 | 10 | 11 | def test_20th_thru_29th_of_every_month 12 | expr = REMonth.new(20,29) 13 | assert expr.include?(@date_20050123), "Expression #{expr.to_s} should include #{@date_20050123.to_s}" 14 | assert !expr.include?(@date_20050116), "Expression #{expr.to_s} should not include #{@date_20050116.to_s}" 15 | end 16 | 17 | def test_16th_of_every_month 18 | expr = REMonth.new(16) 19 | assert expr.include?(@date_20050116), "Expression #{expr.to_s} should include #{@date_20050116.to_s}" 20 | assert !expr.include?(@date_20050123), "Expression #{expr.to_s} should not include #{@date_20050123.to_s}" 21 | end 22 | 23 | def test_dates_mixin 24 | expr = REMonth.new(22, 26) 25 | dates = expr.dates(@pdate_20071024..@pdate_20071028) 26 | assert dates.size == 3, "Expected 2 dates and got #{dates.size}" 27 | # Use default Test::Unit assertion message 28 | assert_equal "2007-10-24T00:00:00+00:00", dates[0].to_s 29 | assert_equal "2007-10-25T00:00:00+00:00", dates[1].to_s 30 | assert_equal "2007-10-26T00:00:00+00:00", dates[2].to_s 31 | end 32 | 33 | def test_range_each_month_to_s 34 | assert_equal 'from the 2nd to the 5th monthly',REMonth.new(2,5).to_s 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /test/reweektest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for REWeek class 6 | # Author:: Matthew Lipper 7 | 8 | class REWeekTest < BaseExpressionTest 9 | 10 | 11 | def test_invalid_ctor_arg 12 | assert_raises(ArgumentError,"Expected ArgumentError for invalid day of week ordinal"){ REWeek.new(10,4) } 13 | end 14 | 15 | def test_monday_thru_friday 16 | expr = REWeek.new(Mon,Fri) 17 | assert expr.include?(@date_20040116), "Expression #{expr.to_s} should include #{@date_20040116.to_s}" 18 | assert !expr.include?(@date_20040125), "Expression #{expr.to_s} should not include #{@date_20040125.to_s}" 19 | end 20 | 21 | def test_friday_thru_tuesday 22 | expr = REWeek.new(Fri,Tue) 23 | assert expr.include?(@date_20040125), "Expression #{expr.to_s} should include #{@date_20040125.to_s}" 24 | assert !expr.include?(@pdate_20071024), "Expression #{expr.to_s} should not include #{@pdate_20071024.to_s}" 25 | end 26 | 27 | def test_range_each_week_te 28 | #Friday through Tuesday 29 | expr = REWeek.new(Friday,Tuesday) 30 | assert expr.include?(@time_20070928000000), "#{expr.inspect} should include Fri 12am" 31 | assert expr.include?(@time_20070925115959), "#{expr.inspect} should include Tue 11:59pm" 32 | assert !expr.include?(@time_20070926000000),"#{expr.inspect} should not include Wed 12am" 33 | assert !expr.include?(@time_20070927065959),"#{expr.inspect} should not include Thurs 6:59am" 34 | assert !expr.include?(@time_20070927115900),"#{expr.inspect} should not include Thurs 1159am" 35 | assert expr.include?(@time_20070929110000), "#{expr.inspect} should include Sat 11am" 36 | assert expr.include?(@time_20070929000000), "#{expr.inspect} should include Sat midnight" 37 | assert expr.include?(@time_20070929235959), "#{expr.inspect} should include Saturday one minute before midnight" 38 | assert expr.include?(@time_20070930235959), "#{expr.inspect} should include Sunday one minute before midnight" 39 | end 40 | 41 | def test_to_s 42 | assert_equal 'all week', REWeek.new(Tuesday,Tuesday).to_s 43 | assert_equal 'Thursday through Saturday', REWeek.new(Thursday,Saturday).to_s 44 | end 45 | 46 | def test_dates_mixin 47 | dates = REWeek.new(Tue,Wed).dates(@pdate_20071008..@pdate_20071030) 48 | assert dates.size == 7 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /test/reyeartest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for REYear class 6 | # Author:: Matthew Lipper 7 | 8 | class REYearTest < BaseExpressionTest 9 | 10 | def test_ctor_one_arg 11 | expr = REYear.new(11) 12 | assert expr.start_month == 11, "Start month should equal 11" 13 | assert expr.end_month == 11, "End month should equal 11" 14 | assert expr.start_day == REYear::NO_DAY, "Start day should equal constant NO_DAY" 15 | assert expr.end_day == REYear::NO_DAY, "End day should equal constant NO_DAY" 16 | end 17 | 18 | def test_ctor_two_args 19 | expr = REYear.new(11,12) 20 | assert expr.start_month == 11, "Start month should equal 11" 21 | assert expr.end_month == 12, "End month should equal 12" 22 | assert expr.start_day == REYear::NO_DAY, "Start day should equal constant NO_DAY" 23 | assert expr.end_day == REYear::NO_DAY, "End day should equal constant NO_DAY" 24 | end 25 | 26 | def test_ctor_three_args 27 | expr = REYear.new(10,21,12) 28 | assert expr.start_month == 10, "Start month should equal 10" 29 | assert expr.end_month == 12, "End month should equal 12" 30 | assert expr.start_day == 21, "Start day should equal 21" 31 | assert expr.end_day == REYear::NO_DAY, "End day should equal constant NO_DAY" 32 | end 33 | 34 | def test_ctor_four_args 35 | expr = REYear.new(10,21,12,3) 36 | assert expr.start_month == 10, "Start month should equal 10" 37 | assert expr.end_month == 12, "End month should equal 12" 38 | assert expr.start_day == 21, "Start day should equal 21" 39 | assert expr.end_day == 3, "End day should equal 3" 40 | end 41 | 42 | def test_specific_days_same_month 43 | expr = REYear.new(10,20,10,29) 44 | assert expr.include?(@pdate_20071028), "#{expr.to_s} should include #{@pdate_20071028}" 45 | assert !expr.include?(@pdate_20071114), "#{expr.to_s} should not include #{@pdate_20071114}" 46 | assert !expr.include?(@pdate_20071030), "#{expr.to_s} should not include #{@pdate_20071030}" 47 | assert !expr.include?(@pdate_20071008), "#{expr.to_s} should not include #{@pdate_20071008}" 48 | assert !expr.include?(@pdate_20060921), "#{expr.to_s} should not include #{@pdate_20060921}" 49 | end 50 | 51 | def test_specific_days_different_months 52 | expr = REYear.new(5,31,9,6) 53 | assert expr.include?(@pdate_198606), "#{expr.to_s} should include #{@pdate_198606}" 54 | assert expr.include?(@date_20040806), "#{expr.to_s} should include #{@date_20040806}" 55 | assert !expr.include?(@pdate_20071008), "#{expr.to_s} should not include #{@pdate_20071008}" 56 | end 57 | 58 | def test_default_days_different_months 59 | expr = REYear.new(11,12) 60 | assert expr.include?(@date_19611101), "#{expr.to_s} should include #{@date_19611101}" 61 | assert !expr.include?(@pdate_198606), "#{expr.to_s} should not include #{@pdate_198606}" 62 | end 63 | 64 | def test_all_defaults 65 | expr = REYear.new(8) 66 | assert expr.include?(@date_20040806), "#{expr.to_s} should include #{@date_20040806}" 67 | assert !expr.include?(@pdate_198606), "#{expr.to_s} should not include #{@pdate_198606}" 68 | assert !expr.include?(@date_19611101), "#{expr.to_s} should not include #{@date_19611101}" 69 | end 70 | 71 | def test_same_days_same_month 72 | # Bug #5741 73 | expr = REYear.new(9,21,9,21) 74 | assert expr.include?(@pdate_20060921), "#{expr.to_s} should include #{@pdate_20060921.to_s}" 75 | assert !expr.include?(@pdate_20060914), "#{expr.to_s} should not include #{@pdate_20060914.to_s}" 76 | end 77 | 78 | def test_to_s 79 | assert_equal 'June 1st through July 2nd', REYear.new(6, 1, 7, 2).to_s 80 | end 81 | 82 | def test_dates_mixin 83 | expr = REYear.new(4, 28, 5, 6) 84 | assert((expr.dates(@date_20040501..@date_20060504)).size == 22, "Should be 22 occurences in dates Array") 85 | end 86 | 87 | # From bug #5749 88 | def test_mixed_precision_combo 89 | # 10:00 am to 10:01 am 90 | ten_ish = REDay.new(10,0,10,1) 91 | # September 21st every year 92 | every_21_sept = REYear.new(9,21,9,21) 93 | # Between 10:00 am and 10:01 am every September 21st 94 | combo = ten_ish & every_21_sept 95 | assert combo.include?(@pdate_20060921), "Should include lower precision argument" 96 | assert combo.include?(@pdate_200609211001), "Should include matching precision argument which is in range" 97 | assert !combo.include?(@pdate_200609211002), "Should not include matching precision argument which is out of range" 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/runttest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'minitest_helper' 4 | 5 | class RuntModuleTest < MiniTest::Unit::TestCase 6 | 7 | def test_last 8 | assert Runt::Last == -1 9 | end 10 | 11 | def test_last_of 12 | assert Runt::Last_of == -1 13 | end 14 | 15 | def test_second_to_last 16 | assert Runt::Second_to_last == -2 17 | end 18 | 19 | def test_ordinals 20 | #1.upto(31){ |n| puts Runt.ordinalize(n); } 21 | assert_equal '1st', Runt.ordinalize(1) 22 | assert_equal '33rd', Runt.ordinalize(33) 23 | assert_equal '50th', Runt.ordinalize(50) 24 | assert_equal '2nd', Runt.ordinalize(2) 25 | assert_equal 'second to last', Runt.ordinalize(-2) 26 | assert_equal 'last', Runt.ordinalize(-1) 27 | end 28 | 29 | def test_day_name 30 | i=0 31 | Date::DAYNAMES.each do |n| 32 | assert_equal Date::DAYNAMES[i], Runt.day_name(i) 33 | i=i+1 34 | end 35 | end 36 | 37 | def test_month_name 38 | i=0 39 | Date::MONTHNAMES.each do |n| 40 | assert_equal Date::MONTHNAMES[i], Runt.month_name(i) 41 | i=i+1 42 | end 43 | end 44 | 45 | def test_strftime 46 | d=DateTime.new(2006,2,26,14,45) 47 | assert_equal '02:45PM', Runt.format_time(d) 48 | end 49 | 50 | def test_numeric_class_additions 51 | assert_equal 0.000001, 1.microsecond 52 | assert_equal 0.000001, 1.microseconds 53 | assert_equal 0.001, 1.millisecond 54 | assert_equal 0.001, 1.milliseconds 55 | assert_equal 7, 7.second 56 | assert_equal 7, 7.seconds 57 | assert_equal 60, 1.minute 58 | assert_equal 60, 1.minutes 59 | assert_equal 3600, 1.hour 60 | assert_equal 3600, 1.hours 61 | assert_equal 86400, 1.day 62 | assert_equal 86400, 1.days 63 | assert_equal 604800, 1.week 64 | assert_equal 604800, 1.weeks 65 | assert_equal 2592000, 1.month 66 | assert_equal 2592000, 1.months 67 | assert_equal 31536000, 1.year 68 | assert_equal 31536000, 1.years 69 | assert_equal 315360000, 1.decade 70 | assert_equal 315360000, 1.decades 71 | end 72 | 73 | def test_time_class_dprecision 74 | time=Time.parse('Monday 06 November 2006 07:38') 75 | assert_equal(Runt::DPrecision::DEFAULT,time.date_precision) 76 | end 77 | 78 | def test_date_class_dprecision 79 | date=Date.today 80 | assert_equal(Runt::DPrecision::DAY,date.date_precision) 81 | end 82 | 83 | def test_datetime_class_dprecision 84 | date=DateTime.civil 85 | assert_equal(Runt::DPrecision::SEC,date.date_precision) 86 | end 87 | 88 | def test_time_plus 89 | time=Time.local(2006, 12, 9, 5, 56, 12) 90 | # Default precision is minute 91 | assert_equal(Runt::PDate.min(2006,12,9,5,56),Runt::DPrecision.to_p(time)) 92 | refute_equal(Time.parse("Sat Dec 09 05:56:00 -0500 2006"),time) 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /test/scheduletest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'minitest_helper' 4 | 5 | # Unit tests for Schedule classes 6 | # Author:: Matthew Lipper 7 | class ScheduleTest < MiniTest::Unit::TestCase 8 | 9 | include Runt 10 | 11 | def setup 12 | # Jane is very busy these days. 13 | @sched=Schedule.new 14 | # Elmo's World is on TV: Mon-Fri 8am-8:30am 15 | @elmo=Event.new("Elmo's World") 16 | @elmo_broadcast=(REWeek.new(Mon,Fri) & REDay.new(8,00,8,30)) 17 | @sched.add(@elmo,@elmo_broadcast) 18 | #Oobi's on TV: Thu-Sat 8:30am-9am 19 | @oobi=Event.new("Oobi") 20 | @oobi_broadcast=(REWeek.new(Thu,Sat) & REDay.new(8,30,9,00)) 21 | @sched.add(@oobi,@oobi_broadcast) 22 | @during_elmo=PDate.new(2004,5,4,8,06) 23 | @not_during_elmo=PDate.new(2004,5,1,8,06) 24 | @during_oobi=PDate.new(2004,4,30,8,56) 25 | @not_during_oobi=PDate.new(2004,5,1,8,12) 26 | end 27 | 28 | def test_include 29 | # Check Elmo 30 | assert(@sched.include?(@elmo, @during_elmo)) 31 | assert(!@sched.include?(@elmo,@not_during_elmo)) 32 | assert(!@sched.include?(@elmo,@during_oobi)) 33 | # Check Oobi 34 | assert(@sched.include?(@oobi, @during_oobi)) 35 | assert(!@sched.include?(@oobi,@not_during_oobi)) 36 | assert(!@sched.include?(@oobi,@not_during_elmo)) 37 | end 38 | 39 | def test_select_all 40 | # select all 41 | all=@sched.select {|ev,xpr| true; } 42 | assert all.size==2 43 | assert all.include?(@elmo) 44 | assert all.include?(@oobi) 45 | end 46 | 47 | def test_select_none 48 | # select none 49 | assert((@sched.select {|ev,xpr| false; }).size==0) 50 | end 51 | 52 | def test_select_some 53 | # select oobi only 54 | some=@sched.select {|ev,xpr| @oobi.eql?(ev); } 55 | assert some.size==1 56 | assert !some.include?(@elmo) 57 | assert some.include?(@oobi) 58 | some.clear 59 | # select elmo only 60 | some=@sched.select {|ev,xpr| @elmo.eql?(ev); } 61 | assert some.size==1 62 | assert some.include?(@elmo) 63 | assert !some.include?(@oobi) 64 | end 65 | 66 | def test_events 67 | events=@sched.events(PDate.new(2006,12,4,11,15)) 68 | assert_equal 0,events.size 69 | # The Barney power hour which overlaps with Elmo 70 | barney=Event.new("Barney") 71 | @sched.add(barney,REDay.new(7,30,8,30)) 72 | events=@sched.events(PDate.new(2006,12,4,8,15)) 73 | assert_equal 2,events.size 74 | assert events.include?(barney) 75 | assert events.include?(@elmo) 76 | end 77 | 78 | def test_update 79 | @sched.update(Event.new("aaa")){|ev|assert_nil(ev)} 80 | @sched.update(@elmo){|ev|assert_equal(@elmo_broadcast,ev)} 81 | @sched.update(@oobi){|ev|assert_equal(@oobi_broadcast,ev)} 82 | end 83 | 84 | def test_select_old 85 | @sched=Schedule.new 86 | e1=Event.new("e1") 87 | assert(!@sched.include?(e1,nil)) 88 | #FIXME: Temporarily comment this out since it hangs JRuby 1.9 89 | #range=TemporalRange.new(DateRange.new(PDate.new(2006,12,3),PDate.new(2007,1,24))) 90 | #in_range=PDate.new(2007,1,4) 91 | #assert(range.include?(in_range)) 92 | #out_of_range=PDate.new(2006,1,4) 93 | #assert(!range.include?(out_of_range)) 94 | #@sched.add(e1,range) 95 | #assert(@sched.include?(e1,in_range)) 96 | #assert(!@sched.include?(e1,out_of_range)) 97 | end 98 | 99 | def test_dates 100 | # range: May 1st, 2004 to May 31st, 2004 101 | d_range = DateRange.new(PDate.day(2004,5,1), PDate.day(2004,5,31)) 102 | @sched = Schedule.new 103 | event = Event.new("Visit Ernie") 104 | # First and last Friday of the month 105 | expr1 = DIMonth.new(1,Fri) | DIMonth.new(-1,Fri) 106 | @sched.add(event,expr1) 107 | dates = @sched.dates(event,d_range) 108 | expected = [PDate.day(2004,5,7), PDate.day(2004,5,28)] 109 | assert_equal(expected,dates) 110 | end 111 | 112 | def test_using_a_schedule 113 | 114 | # September 18th - 19th, 2005, 8am - 10am 115 | expr1=TemporalRange.new(DateRange.new(PDate.day(2005,9,18),PDate.day(2005,9,19))) & REDay.new(8,0,10,0) 116 | assert(expr1.include?(PDate.min(2005,9,18,8,15))) 117 | # September 19th - 20th, 2005, 9am - 11am 118 | expr2=TemporalRange.new(DateRange.new(PDate.day(2005,9,19),PDate.day(2005,9,20))) & REDay.new(9,0,11,0) 119 | # Quick sanuty check 120 | assert(expr1.overlap?(expr2)) 121 | # Setup a @schedule w/first expression 122 | @sched = Schedule.new 123 | @sched.add(Event.new("Snafubar Opening"),expr1) 124 | resource = Resource.new(@sched) 125 | # Add a another overlapping event 126 | resource.add_event(Event.new("Yodeling Lesson"),expr2) 127 | # Create a new resource using the same schedule 128 | resource2 = Resource.new(@sched) 129 | # Add a another overlapping event and pass a block which should complain 130 | #resource.add_event(Event.new("Yodeling Lesson"),expr2) \ 131 | #{|e,s| raise "Resource not available at requested time(s)." \ 132 | # if (@schedule.overlap?(s))} 133 | end 134 | end 135 | 136 | class Resource 137 | def initialize(schedule) 138 | @schedule=schedule 139 | end 140 | def add_event(event,expr) 141 | if(block_given?) 142 | yield(event,expr) 143 | else 144 | @schedule.add(event,expr) 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /test/sugartest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'minitest_helper' 4 | 5 | class SugarTest < MiniTest::Unit::TestCase 6 | include Runt 7 | 8 | def setup 9 | @date = PDate.day(2008,7,1) 10 | end 11 | 12 | def test_method_missing_should_be_called_for_invalid_name 13 | begin 14 | self.some_tuesday 15 | rescue NoMethodError 16 | # YAY! 17 | end 18 | end 19 | 20 | def test_const_should_return_runt_constant 21 | assert_equal Runt::Monday, Runt.const('monday'), \ 22 | "Expected #{Runt::Monday} but was #{Runt.const('monday')}" 23 | end 24 | 25 | def test_method_missing_should_define_dimonth 26 | make_ordinals.each do |ordinal| 27 | make_days.each do |day| 28 | name = ordinal + '_' + day 29 | result = self.send(name) 30 | expected = DIMonth.new(Runt.const(ordinal), Runt.const(day)) 31 | assert_expression expected, result 32 | end 33 | end 34 | end 35 | 36 | def test_method_missing_should_define_diweek 37 | assert_expression(DIWeek.new(Monday), self.monday) 38 | assert_expression(DIWeek.new(Tuesday), self.tuesday) 39 | assert_expression(DIWeek.new(Wednesday), self.wednesday) 40 | assert_expression(DIWeek.new(Thursday), self.thursday) 41 | assert_expression(DIWeek.new(Friday), self.friday) 42 | assert_expression(DIWeek.new(Saturday), self.saturday) 43 | assert_expression(DIWeek.new(Sunday), self.sunday) 44 | end 45 | 46 | def test_parse_time 47 | assert_equal [13,2], parse_time('1','02','pm') 48 | assert_equal [1,2], parse_time('1','02','am') 49 | end 50 | 51 | def test_method_missing_should_define_re_day 52 | assert_expression(REDay.new(8,45,14,00), daily_8_45am_to_2_00pm) 53 | end 54 | 55 | def test_method_missing_should_define_re_week 56 | make_days.each do |st_day| 57 | make_days.each do |end_day| 58 | if Runt.const(st_day) <= Runt.const(end_day) then 59 | assert_expression REWeek.new(Runt.const(st_day), \ 60 | Runt.const(end_day)), self.send('weekly_' + st_day + '_to_' + end_day) 61 | end 62 | end 63 | end 64 | end 65 | 66 | def test_method_missing_should_define_re_month 67 | assert_expression(REMonth.new(3,14), monthly_3rd_to_14th) 68 | end 69 | def test_method_missing_should_define_re_year 70 | # Imperfect but "good enough" for now 71 | make_months.each do |st_month| 72 | make_months.each do |end_month| 73 | st_mon_number = Runt.const(st_month) 74 | end_mon_number = Runt.const(end_month) 75 | next if st_mon_number > end_mon_number 76 | st_day = rand(27) + 1 77 | end_day = rand(27) + 1 78 | if st_mon_number == end_mon_number && st_day > end_day then 79 | st_day, end_day = end_day, st_day 80 | end 81 | #puts "Checking #{st_month} #{st_day} - #{end_month} #{end_day}" 82 | assert_expression REYear.new(st_mon_number, st_day, end_mon_number, end_day), \ 83 | self.send('yearly_' + st_month + '_' + st_day.to_s + '_to_' + end_month + '_' + end_day.to_s) 84 | end 85 | end 86 | end 87 | 88 | def test_after_should_define_after_te_with_inclusive_parameter 89 | result = self.after(@date, true) 90 | assert_expression AfterTE.new(@date, true), result 91 | assert result.instance_variable_get("@inclusive") 92 | end 93 | 94 | def test_after_should_define_after_te_without_inclusive_parameter 95 | result = self.after(@date) 96 | assert_expression AfterTE.new(@date), result 97 | assert !result.instance_variable_get("@inclusive") 98 | end 99 | 100 | def test_before_should_define_before_te_with_inclusive_parameter 101 | result = self.before(@date, true) 102 | assert_expression BeforeTE.new(@date, true), result 103 | assert result.instance_variable_get("@inclusive") 104 | end 105 | 106 | def test_before_should_define_before_te_without_inclusive_parameter 107 | result = self.before(@date) 108 | assert_expression BeforeTE.new(@date), result 109 | assert !result.instance_variable_get("@inclusive") 110 | end 111 | 112 | private 113 | def assert_expression(expected, actual) 114 | assert_equal expected.to_s, actual.to_s, \ 115 | "Expected #{expected.to_s} but was #{actual.to_s}" 116 | end 117 | def make_ordinals 118 | Runt::WEEK_OF_MONTH_ORDINALS.delete('()').split('|') 119 | end 120 | def make_days 121 | Runt::DAYS.delete('()').split('|') 122 | end 123 | def make_months 124 | Runt::MONTHS.delete('()').split('|') 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/temporaldatetest.rb: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env ruby 3 | 4 | require 'baseexpressiontest' 5 | 6 | # Unit tests for TemporalDate class 7 | # Author:: Matthew Lipper 8 | 9 | class TemporalDateTest < BaseExpressionTest 10 | 11 | def setup 12 | super 13 | @spec = TemporalDate.new(@stub1) 14 | end 15 | 16 | def test_initialize 17 | assert_same @stub1, @spec.date_expr, "Expected #{@stub1}, instead got #{@spec.date_expr}" 18 | end 19 | 20 | def test_specific_date 21 | date_spec = TemporalDate.new(Date.new(2006,06,27)) 22 | 23 | assert !date_spec.include?(Date.new(2005,06,27)) 24 | assert !date_spec.include?(Date.new(2006,06,26)) 25 | assert date_spec.include?(Date.new(2006,06,27)) 26 | assert !date_spec.include?(Date.new(2006,06,28)) 27 | assert !date_spec.include?(Date.new(2007,06,27)) 28 | end 29 | 30 | def test_include_arg_has_include_method 31 | assert !@spec.include?(@stub2), "Expression should not include configured stub" 32 | @stub2.match = true 33 | assert @spec.include?(@stub2), "Expression should include configured stub" 34 | end 35 | 36 | def test_include_arg_without_include_method 37 | @spec = TemporalDate.new(4) 38 | assert !@spec.include?(3), "Expression #{@spec.to_s} should not include 3" 39 | assert @spec.include?(4), "Expression #{@spec.to_s} should include 4" 40 | end 41 | 42 | def test_to_s 43 | assert_equal @stub1.to_s, @spec.to_s 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /test/temporalexpressiontest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for TExpr classes 6 | # Author:: Matthew Lipper 7 | 8 | class TExprTest < BaseExpressionTest 9 | 10 | include TExpr 11 | 12 | def test_include 13 | assert !self.include?(true), "Default include? method should always return false" 14 | end 15 | 16 | def test_to_s 17 | assert_equal self.to_s, 'TExpr', "Default to_s method should always return 'TExpr'" 18 | end 19 | 20 | def test_or_from_union 21 | union = Union.new 22 | same_union = union.or(@stub1) 23 | assert_same union, same_union, "Expected same Union instance that received the or method" 24 | assert_same @stub1, union.expressions.first, "Union instance should have added the stub expression" 25 | end 26 | 27 | def test_or_from_nonunion 28 | result = @stub1.or(@stub2) {|e| e} 29 | assert_equal Runt::Union, result.class, "Expected an Union instance. Instead got #{result.class}" 30 | assert_same @stub1, result.expressions.first, "Result should be new Union instance containing both stub expressions" 31 | assert_same @stub2, result.expressions.last, "Result should be new Union instance containing both stub expressions" 32 | end 33 | 34 | def test_and_from_intersect 35 | intersect = Intersect.new 36 | result = intersect.and(@stub1) 37 | assert_same intersect, result, "Expected same Intersect instance that received the and method" 38 | assert_same @stub1, intersect.expressions.first, "Intersect instance should have added the stub expression" 39 | end 40 | 41 | def test_or_from_nonintersect 42 | result = @stub1.and(@stub2) {|e| e} 43 | assert_equal Runt::Intersect, result.class, "Expected an Intersect instance. Instead got #{result.class}" 44 | assert_same @stub1, result.expressions.first, "Result should be new Intersect instance containing both stub expressions" 45 | assert_same @stub2, result.expressions.last, "Result should be new Intersect instance containing both stub expressions" 46 | end 47 | 48 | def test_minus 49 | result = @stub1.minus(@stub2) {|e| e} 50 | assert_equal Runt::Diff, result.class, "Expected an Diff instance. Instead got #{result.class}" 51 | assert_same @stub1, result.expr1, "Expected first stub instance used to create Diff expression" 52 | assert_same @stub2, result.expr2, "Expected second stub instance used to create Diff expression" 53 | end 54 | 55 | def test_dates_no_limit 56 | # Normally, your range is made up of Date-like Objects 57 | range = 1..3 58 | assert @stub1.dates(range).empty?, "Expected empty Array of Objects returned from stub expression" 59 | @stub1.match = true 60 | result = @stub1.dates(range) 61 | assert_equal 1, result[0], "Expected Array of Objects given by range to be returned from stub expression" 62 | assert_equal 2, result[1], "Expected Array of Objects given by range to be returned from stub expression" 63 | assert_equal 3, result[2], "Expected Array of Objects given by range to be returned from stub expression" 64 | end 65 | 66 | def test_dates_with_limit 67 | range = 1..3 68 | assert @stub1.dates(range).empty?, "Expected empty Array of Objects returned from stub expression" 69 | @stub1.match = true 70 | result = @stub1.dates(range,2) 71 | assert_equal 2, result.size, "Expected Array of only 2 Objects. Got #{result.size}" 72 | assert_equal 1, result[0], "Expected Array of Objects given by range to be returned from stub expression" 73 | assert_equal 2, result[1], "Expected Array of Objects given by range to be returned from stub expression" 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /test/temporalrangetest.rb: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env ruby 3 | 4 | require 'baseexpressiontest' 5 | 6 | # Unit tests for TemporalRange class 7 | # Author:: Matthew Lipper 8 | 9 | class TemporalRangeTest < BaseExpressionTest 10 | 11 | def test_include 12 | rspec = TemporalRange.new(@stub1) 13 | assert !rspec.include?("Any Object"), "Expression should not include any given argument" 14 | @stub1.match = true 15 | assert rspec.include?("Any Object"), "Expression should include any given argument" 16 | end 17 | 18 | def test_overlap 19 | range = DateRange.new(@date_20050101, @date_20050109) 20 | rspec = TemporalRange.new(range) 21 | assert !rspec.include?(@date_20050116), "Expression #{rspec.to_s} should not include #{@date_20050116.to_s}" 22 | assert rspec.include?(@date_20050102), "Expression #{rspec.to_s} should include #{@date_20050102.to_s}" 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /test/test_runt.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | class TestRunt < MiniTest::Unit::TestCase 4 | def test_that_it_has_a_version_number 5 | refute_nil ::Runt::VERSION 6 | end 7 | 8 | def test_it_does_something_useful 9 | #assert false 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/uniontest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for Union class 6 | # Author:: Matthew Lipper 7 | 8 | class UnionTest < BaseExpressionTest 9 | 10 | def setup 11 | super 12 | @union = Union.new 13 | @date = @pdate_20071028 14 | end 15 | 16 | def test_include 17 | assert !@union.include?(@date), "Empty Union instance should not include any dates" 18 | @union.add(@stub1).add(@stub2) # both expressions will return false 19 | assert !@union.include?(@date), "Union instance should not include any dates" 20 | @stub2.match = true 21 | assert @union.include?(@date), "Union instance should include any dates" 22 | @stub2.match = false 23 | @stub1.match = true 24 | assert @union.include?(@date), "Union instance should include any dates" 25 | 26 | end 27 | 28 | def test_to_s 29 | assert_equal 'empty', @union.to_s 30 | @union.add(@stub1) 31 | assert_equal 'every ' + @stub1.to_s, @union.to_s 32 | @union.add(@stub2) 33 | assert_equal 'every ' + @stub1.to_s + ' or ' + @stub2.to_s, @union.to_s 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /test/weekintervaltest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | class WeekIntervalTest < BaseExpressionTest 6 | 7 | def test_every_other_week 8 | expr = WeekInterval.new(Date.new(2013,4,23),2) 9 | good_dates = [Date.new(2013,4,21), Date.new(2013,4,27),Date.new(2013,5,8)] 10 | good_dates.each do |date| 11 | assert(expr.include?(date),"Expr<#{expr}> should include #{date.ctime}") 12 | end 13 | bad_dates = [Date.new(2013,4,20), Date.new(2013,4,28)] 14 | bad_dates.each do |date| 15 | assert(!expr.include?(date),"Expr<#{expr}> should not include #{date.ctime}") 16 | end 17 | end 18 | 19 | def test_every_third_week_spans_a_year 20 | expr = WeekInterval.new(Date.new(2013,12,25),3) 21 | good_dates = [Date.new(2013,12,22),Date.new(2014,1,12)] 22 | good_dates.each do |date| 23 | assert(expr.include?(date),"Expr<#{expr}> should include #{date.ctime}") 24 | end 25 | bad_dates = [Date.new(2013,12,21), Date.new(2013,12,31),Date.new(2014,01,11),Date.new(2014,01,19)] 26 | bad_dates.each do |date| 27 | assert(!expr.include?(date),"Expr<#{expr}> should not include #{date.ctime}") 28 | end 29 | end 30 | 31 | def test_biweekly_with_sunday_start_with_diweek 32 | every_other_friday = WeekInterval.new(Date.new(2006,2,26), 2) & DIWeek.new(Friday) 33 | 34 | # should match the First friday and every other Friday after that 35 | good_dates = [Date.new(2006,3,3), Date.new(2006,3,17), Date.new(2006,3,31)] 36 | bad_dates = [Date.new(2006,3,1), Date.new(2006,3,10), Date.new(2006,3,24)] 37 | 38 | good_dates.each do |date| 39 | assert every_other_friday.include?(date), "Expression #{every_other_friday.to_s} should include #{date}" 40 | end 41 | 42 | bad_dates.each do |date| 43 | assert !every_other_friday.include?(date), "Expression #{every_other_friday.to_s} should not include #{date}" 44 | end 45 | end 46 | 47 | def test_biweekly_with_friday_start_with_diweek 48 | every_other_wednesday = WeekInterval.new(Date.new(2006,3,3), 2) & DIWeek.new(Wednesday) 49 | 50 | # should match the First friday and every other Friday after that 51 | good_dates = [Date.new(2006,3,1), Date.new(2006,3,15), Date.new(2006,3,29)] 52 | bad_dates = [Date.new(2006,3,2), Date.new(2006,3,8), Date.new(2006,3,22)] 53 | 54 | good_dates.each do |date| 55 | assert every_other_wednesday.include?(date), "Expression #{every_other_wednesday.to_s} should include #{date}" 56 | end 57 | 58 | bad_dates.each do |date| 59 | assert !every_other_wednesday.include?(date), "Expression #{every_other_wednesday.to_s} should not include #{date}" 60 | end 61 | end 62 | 63 | def test_tue_thur_every_third_week_with_diweek 64 | every_tth_every_3 = WeekInterval.new(Date.new(2006,5,1), 3) & (DIWeek.new(Tuesday) | DIWeek.new(Thursday)) 65 | 66 | # should match the First tuesday and thursday for week 1 and every 3 weeks thereafter 67 | good_dates = [Date.new(2006,5,2), Date.new(2006,5,4), Date.new(2006,5,23), Date.new(2006,5,25), Date.new(2006,6,13), Date.new(2006,6,15)] 68 | bad_dates = [Date.new(2006,5,3), Date.new(2006,5,9), Date.new(2006,5,18)] 69 | 70 | good_dates.each do |date| 71 | assert every_tth_every_3.include?(date), "Expression #{every_tth_every_3.to_s} should include #{date}" 72 | end 73 | 74 | bad_dates.each do |date| 75 | assert !every_tth_every_3.include?(date), "Expression #{every_tth_every_3.to_s} should not include #{date}" 76 | end 77 | 78 | range_start = Date.new(2006,5,1) 79 | range_end = Date.new(2006,8,1) 80 | expected_dates = [ 81 | Date.new(2006,5,2), Date.new(2006,5,4), 82 | Date.new(2006,5,23), Date.new(2006,5,25), 83 | Date.new(2006,6,13), Date.new(2006,6,15), 84 | Date.new(2006,7,4), Date.new(2006,7,6), 85 | Date.new(2006,7,25), Date.new(2006,7,27) 86 | ] 87 | 88 | dates = every_tth_every_3.dates(DateRange.new(range_start, range_end)) 89 | assert_equal dates, expected_dates 90 | end 91 | 92 | def test_to_s 93 | date = Date.new(2006,2,26) 94 | assert_equal "every 2nd week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 2).to_s 95 | assert_equal "every 3rd week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 3).to_s 96 | assert_equal "every 4th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 4).to_s 97 | assert_equal "every 5th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 5).to_s 98 | assert_equal "every 6th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 6).to_s 99 | assert_equal "every 7th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 7).to_s 100 | assert_equal "every 8th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 8).to_s 101 | assert_equal "every 9th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 9).to_s 102 | assert_equal "every 10th week starting with the week containing #{Runt.format_date(date)}", WeekInterval.new(date, 10).to_s 103 | end 104 | 105 | 106 | end 107 | -------------------------------------------------------------------------------- /test/wimonthtest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for WIMonth class 6 | # Author:: Matthew Lipper 7 | 8 | class WIMonthTest < BaseExpressionTest 9 | 10 | def setup 11 | super 12 | @date_range = @date_20050101..@date_20050228 13 | end 14 | 15 | 16 | def test_second_week_in_month 17 | expr = WIMonth.new(Second) 18 | assert expr.include?(@pdate_20071008), "#{expr.to_s} should include #{@pdate_20071008.to_s}" 19 | assert !expr.include?(@pdate_20071030), "#{expr.to_s} should not include #{@pdate_20071030.to_s}" 20 | end 21 | 22 | def test_last_week_in_month 23 | expr = WIMonth.new(Last_of) 24 | # Make sure of day precision or things will be unusably slow! 25 | assert expr.include?(@pdate_20071030), "#{expr.to_s} should include #{@pdate_20071030.to_s}" 26 | assert !expr.include?(@pdate_20071008), "#{expr.to_s} should not include #{@pdate_20071008.to_s}" 27 | end 28 | 29 | def test_second_to_last_week_in_month 30 | expr = WIMonth.new(Second_to_last) 31 | # Make sure of day precision or things will be unusably slow! 32 | assert expr.include?(@pdate_20071024), "#{expr.to_s} should include #{@pdate_20071024}" 33 | assert !expr.include?(@pdate_20071008), "#{expr.to_s} should not include #{@pdate_20071008}" 34 | end 35 | 36 | def test_dates_mixin_second_week_in_month 37 | dates = WIMonth.new(Second).dates(@date_range) 38 | assert dates.size == 14, "Expected 14 dates, was #{dates.size}" 39 | assert dates.first.mday == 8, "Expected first date.mday to be 8, was #{dates.first.mday}" 40 | assert dates.last.mday == 14, "Expected last date.mday to be 14, was #{dates.last.mday}" 41 | end 42 | 43 | def test_dates_mixin_last_week_in_month 44 | dates = WIMonth.new(Last).dates(@date_range) 45 | assert dates.size == 14, "Expected 14 dates, was #{dates.size}" 46 | assert dates.first.mday == 25, "Expected first date.mday to be 25, was #{dates.first.mday}" 47 | assert dates.last.mday == 28, "Expected last date.mday to be 28, was #{dates.last.mday}" 48 | end 49 | 50 | def test_week_in_month_to_s 51 | assert_equal 'last week of any month', WIMonth.new(Last).to_s 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /test/yeartetest.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'baseexpressiontest' 4 | 5 | # Unit tests for YearTE class 6 | # Author:: Matthew Lipper 7 | 8 | class YearTETest < BaseExpressionTest 9 | 10 | def test_2006 11 | expr = YearTE.new(2006) 12 | assert expr.include?(@pdate_20060914), "Expression #{expr.to_s} should include #{@pdate_20060914}" 13 | assert !expr.include?(@pdate_20071008), "Expression #{expr.to_s} should include #{@pdate_20071008}" 14 | assert expr.include?(@date_20060504), "Expression #{expr.to_s} should include #{@date_20060504}" 15 | assert !expr.include?(@date_20051231), "Expression #{expr.to_s} should include #{@date_20051231}" 16 | end 17 | 18 | def test_to_s 19 | assert_equal 'during the year 1934', YearTE.new(1934).to_s 20 | end 21 | 22 | end 23 | --------------------------------------------------------------------------------