runt19 0.7.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/History.txt +153 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +44 -0
- data/Manifest.txt +112 -0
- data/README.md +29 -0
- data/README.txt +106 -0
- data/Rakefile +2 -0
- data/TODO +13 -0
- data/examples/payment_report.rb +59 -0
- data/examples/payment_reporttest.rb +49 -0
- data/examples/reminder.rb +63 -0
- data/examples/schedule_tutorial.rb +59 -0
- data/examples/schedule_tutorialtest.rb +52 -0
- data/lib/runt.rb +249 -0
- data/lib/runt/daterange.rb +74 -0
- data/lib/runt/dprecision.rb +150 -0
- data/lib/runt/expressionbuilder.rb +65 -0
- data/lib/runt/pdate.rb +165 -0
- data/lib/runt/schedule.rb +88 -0
- data/lib/runt/sugar.rb +171 -0
- data/lib/runt/temporalexpression.rb +795 -0
- data/lib/runt/version.rb +3 -0
- data/lib/runt19.rb +1 -0
- data/runt19.gemspec +17 -0
- data/setup.rb +1331 -0
- data/site/blue-robot3.css +132 -0
- data/site/dcl-small.gif +0 -0
- data/site/index.html +72 -0
- data/site/logohover.png +0 -0
- data/site/runt-logo.gif +0 -0
- data/site/runt-logo.psd +0 -0
- data/test/aftertetest.rb +31 -0
- data/test/baseexpressiontest.rb +110 -0
- data/test/beforetetest.rb +31 -0
- data/test/collectiontest.rb +63 -0
- data/test/combinedexpressionstest.rb +158 -0
- data/test/daterangetest.rb +89 -0
- data/test/dayintervaltetest.rb +37 -0
- data/test/difftest.rb +37 -0
- data/test/dimonthtest.rb +59 -0
- data/test/diweektest.rb +32 -0
- data/test/dprecisiontest.rb +58 -0
- data/test/everytetest.rb +36 -0
- data/test/expressionbuildertest.rb +64 -0
- data/test/icalendartest.rb +1104 -0
- data/test/intersecttest.rb +34 -0
- data/test/pdatetest.rb +147 -0
- data/test/redaytest.rb +40 -0
- data/test/remonthtest.rb +37 -0
- data/test/reweektest.rb +51 -0
- data/test/reyeartest.rb +99 -0
- data/test/rspectest.rb +25 -0
- data/test/runttest.rb +98 -0
- data/test/scheduletest.rb +148 -0
- data/test/spectest.rb +36 -0
- data/test/sugartest.rb +104 -0
- data/test/temporalexpressiontest.rb +76 -0
- data/test/uniontest.rb +36 -0
- data/test/wimonthtest.rb +54 -0
- data/test/yeartetest.rb +22 -0
- metadata +137 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module Runt
|
4
|
+
|
5
|
+
|
6
|
+
# Implementation of a <tt>pattern</tt>[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
|
+
# Return true or false depend on if the supplied event is scheduled to occur on the
|
35
|
+
# given date.
|
36
|
+
def include?(event, date)
|
37
|
+
return false unless @elems.include?(event)
|
38
|
+
return 0<(self.select{|ev,xpr| ev.eql?(event)&&xpr.include?(date);}).size
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Returns all Events whose Temporal Expression includes the given date/expression
|
43
|
+
#
|
44
|
+
def events(date)
|
45
|
+
self.select{|ev,xpr| xpr.include?(date);}
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Selects events using the user supplied block/Proc. The Proc must accept
|
50
|
+
# two parameters: an Event and a TemporalExpression. It will be called
|
51
|
+
# with each existing Event-expression pair at which point it can choose
|
52
|
+
# to include the Event in the final result by returning true or to filter
|
53
|
+
# it by returning false.
|
54
|
+
#
|
55
|
+
def select(&block)
|
56
|
+
result=[]
|
57
|
+
@elems.each_pair{|event,xpr| result.push(event) if block.call(event,xpr);}
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Call the supplied block/Proc with the currently configured
|
63
|
+
# TemporalExpression associated with the supplied Event.
|
64
|
+
#
|
65
|
+
def update(event,&block)
|
66
|
+
block.call(@elems[event])
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
class Event
|
72
|
+
|
73
|
+
attr_reader :id
|
74
|
+
|
75
|
+
def initialize(id)
|
76
|
+
raise Exception, "id argument cannot be nil" unless !id.nil?
|
77
|
+
@id=id
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_s; @id.to_s end
|
81
|
+
|
82
|
+
def == (other)
|
83
|
+
return true if other.kind_of?(Event) && @id==other.id
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
data/lib/runt/sugar.rb
ADDED
@@ -0,0 +1,171 @@
|
|
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_<start hour>_<start minute>_to_<end hour>_<end minute>
|
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_<start day>_to_<end day>
|
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_<start numeric ordinal>_to_<end numeric ordinal>
|
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_<start month>_<start day>_to_<end month>_<end day>()
|
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.<day name>()
|
89
|
+
#
|
90
|
+
# Example:
|
91
|
+
#
|
92
|
+
# self.friday()
|
93
|
+
#
|
94
|
+
# is equivilant to
|
95
|
+
#
|
96
|
+
# DIWeek.new(Friday)
|
97
|
+
#
|
98
|
+
# === DIMonth
|
99
|
+
#
|
100
|
+
# self.<lowercase ordinal>_<day name>()
|
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
|
+
|
113
|
+
require 'runt'
|
114
|
+
|
115
|
+
module Runt
|
116
|
+
MONTHS = '(january|february|march|april|may|june|july|august|september|october|november|december)'
|
117
|
+
DAYS = '(sunday|monday|tuesday|wednesday|thursday|friday|saturday)'
|
118
|
+
WEEK_OF_MONTH_ORDINALS = '(first|second|third|fourth|last|second_to_last)'
|
119
|
+
ORDINAL_SUFFIX = '(?:st|nd|rd|th)'
|
120
|
+
ORDINAL_ABBR = '(st|nd|rd|th)'
|
121
|
+
class << self
|
122
|
+
def const(string)
|
123
|
+
self.const_get(string.capitalize)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def method_missing(name, *args, &block)
|
128
|
+
result = self.build(name, *args, &block)
|
129
|
+
return result unless result.nil?
|
130
|
+
super
|
131
|
+
end
|
132
|
+
|
133
|
+
def build(name, *args, &block)
|
134
|
+
case name.to_s
|
135
|
+
when /^daily_(\d{1,2})_(\d{2})([ap]m)_to_(\d{1,2})_(\d{2})([ap]m)$/
|
136
|
+
# REDay
|
137
|
+
st_hr, st_min, st_m, end_hr, end_min, end_m = $1, $2, $3, $4, $5, $6
|
138
|
+
args = parse_time(st_hr, st_min, st_m)
|
139
|
+
args.concat(parse_time(end_hr, end_min, end_m))
|
140
|
+
return REDay.new(*args)
|
141
|
+
when Regexp.new('^weekly_' + DAYS + '_to_' + DAYS + '$')
|
142
|
+
# REWeek
|
143
|
+
st_day, end_day = $1, $2
|
144
|
+
return REWeek.new(Runt.const(st_day), Runt.const(end_day))
|
145
|
+
when Regexp.new('^monthly_(\d{1,2})' + ORDINAL_SUFFIX + '_to_(\d{1,2})' \
|
146
|
+
+ ORDINAL_SUFFIX + '$')
|
147
|
+
# REMonth
|
148
|
+
st_day, end_day = $1, $2
|
149
|
+
return REMonth.new(st_day, end_day)
|
150
|
+
when Regexp.new('^yearly_' + MONTHS + '_(\d{1,2})_to_' + MONTHS + '_(\d{1,2})$')
|
151
|
+
# REYear
|
152
|
+
st_mon, st_day, end_mon, end_day = $1, $2, $3, $4
|
153
|
+
return REYear.new(Runt.const(st_mon), st_day, Runt.const(end_mon), end_day)
|
154
|
+
when Regexp.new('^' + DAYS + '$')
|
155
|
+
# DIWeek
|
156
|
+
return DIWeek.new(Runt.const(name.to_s))
|
157
|
+
when Regexp.new(WEEK_OF_MONTH_ORDINALS + '_' + DAYS)
|
158
|
+
# DIMonth
|
159
|
+
ordinal, day = $1, $2
|
160
|
+
return DIMonth.new(Runt.const(ordinal), Runt.const(day))
|
161
|
+
else
|
162
|
+
# You're hosed
|
163
|
+
nil
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def parse_time(hour, minute, ampm)
|
168
|
+
hour = hour.to_i + 12 if ampm =~ /pm/
|
169
|
+
[hour.to_i, minute.to_i]
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,795 @@
|
|
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
|
+
# <tt>pattern</tt>[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 <tt>Composite(GoF)</tt> pattern.
|
84
|
+
class Collection
|
85
|
+
|
86
|
+
include TExpr
|
87
|
+
|
88
|
+
attr_reader :expressions
|
89
|
+
|
90
|
+
def initialize
|
91
|
+
@expressions = Array.new
|
92
|
+
end
|
93
|
+
|
94
|
+
def add(anExpression)
|
95
|
+
@expressions.push anExpression
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
# Will return true if the supplied object overlaps with the range used to
|
100
|
+
# create this instance
|
101
|
+
def overlap?(date_expr)
|
102
|
+
@expressions.each do | interval |
|
103
|
+
return true if date_expr.overlap?(interval)
|
104
|
+
end
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_s
|
109
|
+
if !@expressions.empty? && block_given?
|
110
|
+
first_expr, next_exprs = yield
|
111
|
+
result = ''
|
112
|
+
@expressions.map do |expr|
|
113
|
+
if @expressions.first===expr
|
114
|
+
result = first_expr + expr.to_s
|
115
|
+
else
|
116
|
+
result = result + next_exprs + expr.to_s
|
117
|
+
end
|
118
|
+
end
|
119
|
+
result
|
120
|
+
else
|
121
|
+
'empty'
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def display
|
126
|
+
puts "I am a #{self.class} containing:"
|
127
|
+
@expressions.each do |ex|
|
128
|
+
pp "#{ex.class}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
# Composite TExpr that will be true if <b>any</b> of it's
|
136
|
+
# component expressions are true.
|
137
|
+
class Union < Collection
|
138
|
+
|
139
|
+
def include?(aDate)
|
140
|
+
@expressions.each do |expr|
|
141
|
+
return true if expr.include?(aDate)
|
142
|
+
end
|
143
|
+
false
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_s
|
147
|
+
super {['every ',' or ']}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Composite TExpr that will be true only if <b>all</b> it's
|
152
|
+
# component expressions are true.
|
153
|
+
class Intersect < Collection
|
154
|
+
|
155
|
+
def include?(aDate)
|
156
|
+
result = false
|
157
|
+
@expressions.each do |expr|
|
158
|
+
return false unless (result = expr.include?(aDate))
|
159
|
+
end
|
160
|
+
result
|
161
|
+
end
|
162
|
+
|
163
|
+
def to_s
|
164
|
+
super {['every ', ' and ']}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# TExpr that will be true only if the first of
|
169
|
+
# its two contained expressions is true and the second is false.
|
170
|
+
class Diff
|
171
|
+
|
172
|
+
include TExpr
|
173
|
+
|
174
|
+
attr_reader :expr1, :expr2
|
175
|
+
|
176
|
+
def initialize(expr1, expr2)
|
177
|
+
@expr1 = expr1
|
178
|
+
@expr2 = expr2
|
179
|
+
end
|
180
|
+
|
181
|
+
def include?(aDate)
|
182
|
+
return false unless (@expr1.include?(aDate) && !@expr2.include?(aDate))
|
183
|
+
true
|
184
|
+
end
|
185
|
+
|
186
|
+
def to_s
|
187
|
+
@expr1.to_s + ' except for ' + @expr2.to_s
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# TExpr that provides for inclusion of an arbitrary date.
|
192
|
+
class Spec
|
193
|
+
|
194
|
+
include TExpr
|
195
|
+
|
196
|
+
attr_reader :date_expr
|
197
|
+
|
198
|
+
def initialize(date_expr)
|
199
|
+
@date_expr = date_expr
|
200
|
+
end
|
201
|
+
|
202
|
+
# Will return true if the supplied object is == to that which was used to
|
203
|
+
# create this instance
|
204
|
+
def include?(date_expr)
|
205
|
+
return date_expr.include?(@date_expr) if date_expr.respond_to?(:include?)
|
206
|
+
return true if @date_expr == date_expr
|
207
|
+
false
|
208
|
+
end
|
209
|
+
|
210
|
+
def to_s
|
211
|
+
@date_expr.to_s
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
# TExpr that provides a thin wrapper around built-in Ruby <tt>Range</tt> functionality
|
217
|
+
# facilitating inclusion of an arbitrary range in a temporal expression.
|
218
|
+
#
|
219
|
+
# See also: Range
|
220
|
+
class RSpec < Spec
|
221
|
+
|
222
|
+
## Will return true if the supplied object is included in the range used to
|
223
|
+
## create this instance
|
224
|
+
def include?(date_expr)
|
225
|
+
return @date_expr.include?(date_expr)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Will return true if the supplied object overlaps with the range used to
|
229
|
+
# create this instance
|
230
|
+
def overlap?(date_expr)
|
231
|
+
@date_expr.each do | interval |
|
232
|
+
return true if date_expr.include?(interval)
|
233
|
+
end
|
234
|
+
false
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
239
|
+
#######################################################################
|
240
|
+
# Utility methods common to some expressions
|
241
|
+
|
242
|
+
module TExprUtils
|
243
|
+
def week_in_month(day_in_month)
|
244
|
+
((day_in_month - 1) / 7) + 1
|
245
|
+
end
|
246
|
+
|
247
|
+
def days_left_in_month(date)
|
248
|
+
return max_day_of_month(date) - date.day
|
249
|
+
end
|
250
|
+
|
251
|
+
def max_day_of_month(date)
|
252
|
+
# Contributed by Justin Cunningham who took it verbatim from the Rails
|
253
|
+
# ActiveSupport::CoreExtensions::Time::Calculations::ClassMethods module
|
254
|
+
# days_in_month method.
|
255
|
+
month = date.month
|
256
|
+
year = date.year
|
257
|
+
if month == 2
|
258
|
+
!year.nil? &&
|
259
|
+
(year % 4 == 0) &&
|
260
|
+
((year % 100 != 0) ||
|
261
|
+
(year % 400 == 0)) ? 29 : 28
|
262
|
+
elsif month <= 7
|
263
|
+
month % 2 == 0 ? 30 : 31
|
264
|
+
else
|
265
|
+
month % 2 == 0 ? 31 : 30
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def week_matches?(index,date)
|
270
|
+
if(index > 0)
|
271
|
+
return week_from_start_matches?(index,date)
|
272
|
+
else
|
273
|
+
return week_from_end_matches?(index,date)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def week_from_start_matches?(index,date)
|
278
|
+
week_in_month(date.day)==index
|
279
|
+
end
|
280
|
+
|
281
|
+
def week_from_end_matches?(index,date)
|
282
|
+
n = days_left_in_month(date) + 1
|
283
|
+
week_in_month(n)==index.abs
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|
287
|
+
|
288
|
+
# TExpr that provides support for building a temporal
|
289
|
+
# expression using the form:
|
290
|
+
#
|
291
|
+
# DIMonth.new(1,0)
|
292
|
+
#
|
293
|
+
# where the first argument is the week of the month and the second
|
294
|
+
# argument is the wday of the week as defined by the 'wday' method
|
295
|
+
# in the standard library class Date.
|
296
|
+
#
|
297
|
+
# A negative value for the week of the month argument will count
|
298
|
+
# backwards from the end of the month. So, to match the last Saturday
|
299
|
+
# of the month
|
300
|
+
#
|
301
|
+
# DIMonth.new(-1,6)
|
302
|
+
#
|
303
|
+
# Using constants defined in the base Runt module, you can re-write
|
304
|
+
# the first example above as:
|
305
|
+
#
|
306
|
+
# DIMonth.new(First,Sunday)
|
307
|
+
#
|
308
|
+
# and the second as:
|
309
|
+
#
|
310
|
+
# DIMonth.new(Last,Saturday)
|
311
|
+
#
|
312
|
+
# See also: Date, Runt
|
313
|
+
class DIMonth
|
314
|
+
|
315
|
+
include TExpr
|
316
|
+
include TExprUtils
|
317
|
+
|
318
|
+
def initialize(week_of_month_index,day_index)
|
319
|
+
@day_index = day_index
|
320
|
+
@week_of_month_index = week_of_month_index
|
321
|
+
end
|
322
|
+
|
323
|
+
def include?(date)
|
324
|
+
( day_matches?(date) ) && ( week_matches?(@week_of_month_index,date) )
|
325
|
+
end
|
326
|
+
|
327
|
+
def to_s
|
328
|
+
"#{Runt.ordinalize(@week_of_month_index)} #{Runt.day_name(@day_index)} of the month"
|
329
|
+
end
|
330
|
+
|
331
|
+
private
|
332
|
+
def day_matches?(date)
|
333
|
+
@day_index == date.wday
|
334
|
+
end
|
335
|
+
|
336
|
+
end
|
337
|
+
|
338
|
+
# TExpr that matches days of the week where the first argument
|
339
|
+
# is an integer denoting the ordinal day of the week. Valid values are 0..6 where
|
340
|
+
# 0 == Sunday and 6==Saturday
|
341
|
+
#
|
342
|
+
# For example:
|
343
|
+
#
|
344
|
+
# DIWeek.new(0)
|
345
|
+
#
|
346
|
+
# Using constants defined in the base Runt module, you can re-write
|
347
|
+
# the first example above as:
|
348
|
+
#
|
349
|
+
# DIWeek.new(Sunday)
|
350
|
+
#
|
351
|
+
# See also: Date, Runt
|
352
|
+
class DIWeek
|
353
|
+
|
354
|
+
include TExpr
|
355
|
+
|
356
|
+
VALID_RANGE = 0..6
|
357
|
+
|
358
|
+
def initialize(ordinal_weekday)
|
359
|
+
unless VALID_RANGE.include?(ordinal_weekday)
|
360
|
+
raise ArgumentError, 'invalid ordinal day of week'
|
361
|
+
end
|
362
|
+
@ordinal_weekday = ordinal_weekday
|
363
|
+
end
|
364
|
+
|
365
|
+
def include?(date)
|
366
|
+
@ordinal_weekday == date.wday
|
367
|
+
end
|
368
|
+
|
369
|
+
def to_s
|
370
|
+
"#{Runt.day_name(@ordinal_weekday)}"
|
371
|
+
end
|
372
|
+
|
373
|
+
end
|
374
|
+
|
375
|
+
# TExpr that matches days of the week within one
|
376
|
+
# week only.
|
377
|
+
#
|
378
|
+
# If start and end day are equal, the entire week will match true.
|
379
|
+
#
|
380
|
+
# See also: Date
|
381
|
+
class REWeek
|
382
|
+
|
383
|
+
include TExpr
|
384
|
+
|
385
|
+
VALID_RANGE = 0..6
|
386
|
+
|
387
|
+
# Creates a REWeek using the supplied start
|
388
|
+
# day(range = 0..6, where 0=>Sunday) and an optional end
|
389
|
+
# day. If an end day is not supplied, the maximum value
|
390
|
+
# (6 => Saturday) is assumed.
|
391
|
+
def initialize(start_day,end_day=6)
|
392
|
+
validate(start_day,end_day)
|
393
|
+
@start_day = start_day
|
394
|
+
@end_day = end_day
|
395
|
+
end
|
396
|
+
|
397
|
+
def include?(date)
|
398
|
+
return true if all_week?
|
399
|
+
if @start_day < @end_day
|
400
|
+
@start_day<=date.wday && @end_day>=date.wday
|
401
|
+
else
|
402
|
+
(@start_day<=date.wday && 6 >=date.wday) || (0 <=date.wday && @end_day >=date.wday)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def to_s
|
407
|
+
return "all week" if all_week?
|
408
|
+
"#{Runt.day_name(@start_day)} through #{Runt.day_name(@end_day)}"
|
409
|
+
end
|
410
|
+
|
411
|
+
private
|
412
|
+
|
413
|
+
def all_week?
|
414
|
+
return true if @start_day==@end_day
|
415
|
+
end
|
416
|
+
|
417
|
+
def validate(start_day,end_day)
|
418
|
+
unless VALID_RANGE.include?(start_day)&&VALID_RANGE.include?(end_day)
|
419
|
+
raise ArgumentError, 'start and end day arguments must be in the range #{VALID_RANGE.to_s}.'
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
#
|
425
|
+
# TExpr that matches date ranges within a single year. Assumes that the start
|
426
|
+
# and end parameters occur within the same year.
|
427
|
+
#
|
428
|
+
#
|
429
|
+
class REYear
|
430
|
+
|
431
|
+
# Sentinel value used to denote that no specific day was given to create
|
432
|
+
# the expression.
|
433
|
+
NO_DAY = 0
|
434
|
+
|
435
|
+
include TExpr
|
436
|
+
|
437
|
+
attr_accessor :start_month, :start_day, :end_month, :end_day
|
438
|
+
|
439
|
+
#
|
440
|
+
# == Synopsis
|
441
|
+
#
|
442
|
+
# REYear.new(start_month [, (start_day | end_month), ...]
|
443
|
+
#
|
444
|
+
# == Args
|
445
|
+
#
|
446
|
+
# One or two arguments given::
|
447
|
+
#
|
448
|
+
# +start_month+::
|
449
|
+
# Start month. Valid values are 1..12. When no other parameters are given
|
450
|
+
# this value will be used for the end month as well. Matches the entire
|
451
|
+
# month through the ending month.
|
452
|
+
# +end_month+::
|
453
|
+
# End month. Valid values are 1..12. When given in two argument form
|
454
|
+
# will match through the entire month.
|
455
|
+
#
|
456
|
+
# Three or four arguments given::
|
457
|
+
#
|
458
|
+
# +start_month+::
|
459
|
+
# Start month. Valid values are 1..12.
|
460
|
+
# +start_day+::
|
461
|
+
# Start day. Valid values are 1..31, depending on the month.
|
462
|
+
# +end_month+::
|
463
|
+
# End month. Valid values are 1..12. If a fourth argument is not given,
|
464
|
+
# this value will cover through the entire month.
|
465
|
+
# +end_day+::
|
466
|
+
# End day. Valid values are 1..31, depending on the month.
|
467
|
+
#
|
468
|
+
# == Description
|
469
|
+
#
|
470
|
+
# Create a new REYear expression expressing a range of months or days
|
471
|
+
# within months within a year.
|
472
|
+
#
|
473
|
+
# == Usage
|
474
|
+
#
|
475
|
+
# # Creates the range March 12th through May 23rd
|
476
|
+
# expr = REYear.new(3,12,5,23)
|
477
|
+
#
|
478
|
+
# # Creates the range March 1st through May 31st
|
479
|
+
# expr = REYear.new(3,5)
|
480
|
+
#
|
481
|
+
# # Creates the range March 12th through May 31st
|
482
|
+
# expr = REYear.new(3,12,5)
|
483
|
+
#
|
484
|
+
# # Creates the range March 1st through March 30th
|
485
|
+
# expr = REYear.new(3)
|
486
|
+
#
|
487
|
+
def initialize(start_month, *args)
|
488
|
+
@start_month = start_month
|
489
|
+
if (args.nil? || args.size == NO_DAY) then
|
490
|
+
# One argument given
|
491
|
+
@end_month = start_month
|
492
|
+
@start_day = NO_DAY
|
493
|
+
@end_day = NO_DAY
|
494
|
+
else
|
495
|
+
case args.size
|
496
|
+
when 1
|
497
|
+
@end_month = args[0]
|
498
|
+
@start_day = NO_DAY
|
499
|
+
@end_day = NO_DAY
|
500
|
+
when 2
|
501
|
+
@start_day = args[0]
|
502
|
+
@end_month = args[1]
|
503
|
+
@end_day = NO_DAY
|
504
|
+
when 3
|
505
|
+
@start_day = args[0]
|
506
|
+
@end_month = args[1]
|
507
|
+
@end_day = args[2]
|
508
|
+
else
|
509
|
+
raise "Invalid number of var args: 1 or 3 expected, #{args.size} given"
|
510
|
+
end
|
511
|
+
end
|
512
|
+
@same_month_dates_provided = (@start_month == @end_month) && (@start_day!=NO_DAY && @end_day != NO_DAY)
|
513
|
+
end
|
514
|
+
|
515
|
+
def include?(date)
|
516
|
+
|
517
|
+
return same_start_month_include_day?(date) \
|
518
|
+
&& same_end_month_include_day?(date) if @same_month_dates_provided
|
519
|
+
|
520
|
+
is_between_months?(date) ||
|
521
|
+
(same_start_month_include_day?(date) ||
|
522
|
+
same_end_month_include_day?(date))
|
523
|
+
end
|
524
|
+
|
525
|
+
def save
|
526
|
+
"Runt::REYear.new(#{@start_month}, #{@start_day}, #{@end_month}, #{@end_day})"
|
527
|
+
end
|
528
|
+
|
529
|
+
def to_s
|
530
|
+
"#{Runt.month_name(@start_month)} #{Runt.ordinalize(@start_day)} " +
|
531
|
+
"through #{Runt.month_name(@end_month)} #{Runt.ordinalize(@end_day)}"
|
532
|
+
end
|
533
|
+
|
534
|
+
private
|
535
|
+
def is_between_months?(date)
|
536
|
+
(date.mon > @start_month) && (date.mon < @end_month)
|
537
|
+
end
|
538
|
+
|
539
|
+
def same_end_month_include_day?(date)
|
540
|
+
return false unless (date.mon == @end_month)
|
541
|
+
(@end_day == NO_DAY) || (date.day <= @end_day)
|
542
|
+
end
|
543
|
+
|
544
|
+
def same_start_month_include_day?(date)
|
545
|
+
return false unless (date.mon == @start_month)
|
546
|
+
(@start_day == NO_DAY) || (date.day >= @start_day)
|
547
|
+
end
|
548
|
+
|
549
|
+
end
|
550
|
+
|
551
|
+
# TExpr that matches periods of the day with minute
|
552
|
+
# precision. If the start hour is greater than the end hour, than end hour
|
553
|
+
# is assumed to be on the following day.
|
554
|
+
#
|
555
|
+
# NOTE: By default, this class will match any date expression whose
|
556
|
+
# precision is less than or equal to DPrecision::DAY. To override
|
557
|
+
# this behavior, pass the optional fifth constructor argument the
|
558
|
+
# value: false.
|
559
|
+
#
|
560
|
+
# See also: Date
|
561
|
+
class REDay
|
562
|
+
|
563
|
+
include TExpr
|
564
|
+
|
565
|
+
CURRENT=28
|
566
|
+
NEXT=29
|
567
|
+
ANY_DATE=PDate.day(2002,8,CURRENT)
|
568
|
+
|
569
|
+
def initialize(start_hour, start_minute, end_hour, end_minute, less_precise_match=true)
|
570
|
+
|
571
|
+
start_time = PDate.min(ANY_DATE.year,ANY_DATE.month,
|
572
|
+
ANY_DATE.day,start_hour,start_minute)
|
573
|
+
|
574
|
+
if(@spans_midnight = spans_midnight?(start_hour, end_hour)) then
|
575
|
+
end_time = get_next(end_hour,end_minute)
|
576
|
+
else
|
577
|
+
end_time = get_current(end_hour,end_minute)
|
578
|
+
end
|
579
|
+
|
580
|
+
@range = start_time..end_time
|
581
|
+
@less_precise_match = less_precise_match
|
582
|
+
end
|
583
|
+
|
584
|
+
def include?(date)
|
585
|
+
#
|
586
|
+
# If @less_precise_match == true and the precision of the argument
|
587
|
+
# is day or greater, then the result is always true
|
588
|
+
return true if @less_precise_match && date.date_precision <= DPrecision::DAY
|
589
|
+
if(@spans_midnight&&date.hour<12) then
|
590
|
+
#Assume next day
|
591
|
+
n = get_next(date.hour,date.min)
|
592
|
+
return false unless @range.begin <= n
|
593
|
+
return false unless @range.end >= n
|
594
|
+
true
|
595
|
+
end
|
596
|
+
|
597
|
+
#Same day
|
598
|
+
c = get_current(date.hour,date.min)
|
599
|
+
return false unless @range.begin <= c
|
600
|
+
return false unless @range.end >= c
|
601
|
+
true
|
602
|
+
end
|
603
|
+
|
604
|
+
def to_s
|
605
|
+
"from #{Runt.format_time(@range.begin)} to #{Runt.format_time(@range.end)} daily"
|
606
|
+
end
|
607
|
+
|
608
|
+
private
|
609
|
+
def spans_midnight?(start_hour, end_hour)
|
610
|
+
return end_hour < start_hour
|
611
|
+
end
|
612
|
+
|
613
|
+
def get_current(hour,minute)
|
614
|
+
PDate.min(ANY_DATE.year,ANY_DATE.month,CURRENT,hour,minute)
|
615
|
+
end
|
616
|
+
|
617
|
+
def get_next(hour,minute)
|
618
|
+
PDate.min(ANY_DATE.year,ANY_DATE.month,NEXT,hour,minute)
|
619
|
+
end
|
620
|
+
|
621
|
+
end
|
622
|
+
|
623
|
+
# TExpr that matches the week in a month. For example:
|
624
|
+
#
|
625
|
+
# WIMonth.new(1)
|
626
|
+
#
|
627
|
+
# See also: Date
|
628
|
+
# FIXME .dates mixin seems functionally broken
|
629
|
+
class WIMonth
|
630
|
+
|
631
|
+
include TExpr
|
632
|
+
include TExprUtils
|
633
|
+
|
634
|
+
VALID_RANGE = -2..5
|
635
|
+
|
636
|
+
def initialize(ordinal)
|
637
|
+
unless VALID_RANGE.include?(ordinal)
|
638
|
+
raise ArgumentError, 'invalid ordinal week of month'
|
639
|
+
end
|
640
|
+
@ordinal = ordinal
|
641
|
+
end
|
642
|
+
|
643
|
+
def include?(date)
|
644
|
+
week_matches?(@ordinal,date)
|
645
|
+
end
|
646
|
+
|
647
|
+
def to_s
|
648
|
+
"#{Runt.ordinalize(@ordinal)} week of any month"
|
649
|
+
end
|
650
|
+
|
651
|
+
end
|
652
|
+
|
653
|
+
# TExpr that matches a range of dates within a month. For example:
|
654
|
+
#
|
655
|
+
# REMonth.(12,28)
|
656
|
+
#
|
657
|
+
# matches from the 12th thru the 28th of any month. If end_day==0
|
658
|
+
# or is not given, start_day will define the range with that single day.
|
659
|
+
#
|
660
|
+
# See also: Date
|
661
|
+
class REMonth
|
662
|
+
|
663
|
+
include TExpr
|
664
|
+
|
665
|
+
def initialize(start_day, end_day=0)
|
666
|
+
end_day=start_day if end_day==0
|
667
|
+
@range = start_day..end_day
|
668
|
+
end
|
669
|
+
|
670
|
+
def include?(date)
|
671
|
+
@range.include? date.mday
|
672
|
+
end
|
673
|
+
|
674
|
+
def to_s
|
675
|
+
"from the #{Runt.ordinalize(@range.begin)} to the #{Runt.ordinalize(@range.end)} monthly"
|
676
|
+
end
|
677
|
+
|
678
|
+
end
|
679
|
+
|
680
|
+
#
|
681
|
+
# Using the precision from the supplied start argument and the its date value,
|
682
|
+
# matches every n number of time units thereafter.
|
683
|
+
#
|
684
|
+
class EveryTE
|
685
|
+
|
686
|
+
include TExpr
|
687
|
+
|
688
|
+
def initialize(start,n,precision=nil)
|
689
|
+
@start=start
|
690
|
+
@interval=n
|
691
|
+
# Use the precision of the start date by default
|
692
|
+
@precision=precision || @start.date_precision
|
693
|
+
end
|
694
|
+
|
695
|
+
def include?(date)
|
696
|
+
i=DPrecision.to_p(@start,@precision)
|
697
|
+
d=DPrecision.to_p(date,@precision)
|
698
|
+
while i<=d
|
699
|
+
return true if i.eql?(d)
|
700
|
+
i=i+@interval
|
701
|
+
end
|
702
|
+
false
|
703
|
+
end
|
704
|
+
|
705
|
+
def to_s
|
706
|
+
"every #{@interval} #{@precision.label.downcase}s starting #{Runt.format_date(@start)}"
|
707
|
+
end
|
708
|
+
|
709
|
+
end
|
710
|
+
|
711
|
+
# Using day precision dates, matches every n number of days after a given
|
712
|
+
# base date. All date arguments are converted to DPrecision::DAY precision.
|
713
|
+
#
|
714
|
+
# Contributed by Ira Burton
|
715
|
+
class DayIntervalTE
|
716
|
+
|
717
|
+
include TExpr
|
718
|
+
|
719
|
+
def initialize(base_date,n)
|
720
|
+
@base_date = DPrecision.to_p(base_date,DPrecision::DAY)
|
721
|
+
@interval = n
|
722
|
+
end
|
723
|
+
|
724
|
+
def include?(date)
|
725
|
+
return ((DPrecision.to_p(date,DPrecision::DAY) - @base_date).to_i % @interval == 0)
|
726
|
+
end
|
727
|
+
|
728
|
+
def to_s
|
729
|
+
"every #{Runt.ordinalize(@interval)} day after #{Runt.format_date(@base_date)}"
|
730
|
+
end
|
731
|
+
|
732
|
+
end
|
733
|
+
|
734
|
+
# Simple expression which returns true if the supplied arguments
|
735
|
+
# occur within the given year.
|
736
|
+
#
|
737
|
+
class YearTE
|
738
|
+
|
739
|
+
include TExpr
|
740
|
+
|
741
|
+
def initialize(year)
|
742
|
+
@year = year
|
743
|
+
end
|
744
|
+
|
745
|
+
def include?(date)
|
746
|
+
return date.year == @year
|
747
|
+
end
|
748
|
+
|
749
|
+
def to_s
|
750
|
+
"during the year #{@year}"
|
751
|
+
end
|
752
|
+
|
753
|
+
end
|
754
|
+
|
755
|
+
# Matches dates that occur before a given date.
|
756
|
+
class BeforeTE
|
757
|
+
|
758
|
+
include TExpr
|
759
|
+
|
760
|
+
def initialize(date, inclusive=false)
|
761
|
+
@date = date
|
762
|
+
@inclusive = inclusive
|
763
|
+
end
|
764
|
+
|
765
|
+
def include?(date)
|
766
|
+
return (date < @date) || (@inclusive && @date == date)
|
767
|
+
end
|
768
|
+
|
769
|
+
def to_s
|
770
|
+
"before #{Runt.format_date(@date)}"
|
771
|
+
end
|
772
|
+
|
773
|
+
end
|
774
|
+
|
775
|
+
# Matches dates that occur after a given date.
|
776
|
+
class AfterTE
|
777
|
+
|
778
|
+
include TExpr
|
779
|
+
|
780
|
+
def initialize(date, inclusive=false)
|
781
|
+
@date = date
|
782
|
+
@inclusive = inclusive
|
783
|
+
end
|
784
|
+
|
785
|
+
def include?(date)
|
786
|
+
return (date > @date) || (@inclusive && @date == date)
|
787
|
+
end
|
788
|
+
|
789
|
+
def to_s
|
790
|
+
"after #{Runt.format_date(@date)}"
|
791
|
+
end
|
792
|
+
|
793
|
+
end
|
794
|
+
|
795
|
+
end
|