Time Window (#144)

The three rules of Ruby Q.:

1. Please do not post any solutions or spoiler discussion for this quiz
until
48 hours have passed from the time on this message.

2. Support Ruby Q. by submitting ideas as often as you can:

http://www.rubyquiz.com/

1. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby T. follow the discussion. Please reply to the original quiz
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Brian C.

Write a Ruby class which can tell you whether the current time (or any
given
time) is within a particular “time window”. Time windows are defined by
strings
in the following format:

times

only

Sat 0700-0800; Sun 0800-0900 # 0700-0800 on Sat, plus 0800-0900

on Sun

Time ranges should exclude the upper bound, i.e. 0700-0900 is 07:00:00
to
08:59:59. An empty time window means “all times everyday”. Here are some
test
cases to make it clearer:

class TestTimeWindow < Test::Unit::TestCase
def test_window_1
w = TimeWindow.new(“Sat-Sun; Mon Wed 0700-0900; Thu 0700-0900
1000-1200”)

``````  assert ! w.include?(Time.mktime(2007,9,25,8,0,0))   # Tue
assert   w.include?(Time.mktime(2007,9,26,8,0,0))   # Wed
assert ! w.include?(Time.mktime(2007,9,26,11,0,0))
assert ! w.include?(Time.mktime(2007,9,27,6,59,59)) # Thu
assert   w.include?(Time.mktime(2007,9,27,7,0,0))
assert   w.include?(Time.mktime(2007,9,27,8,59,59))
assert ! w.include?(Time.mktime(2007,9,27,9,0,0))
assert   w.include?(Time.mktime(2007,9,27,11,0,0))
assert   w.include?(Time.mktime(2007,9,29,11,0,0))  # Sat
assert   w.include?(Time.mktime(2007,9,29,0,0,0))
assert   w.include?(Time.mktime(2007,9,29,23,59,59))
end

def test_window_2
w = TimeWindow.new("Fri-Mon")
assert ! w.include?(Time.mktime(2007,9,27)) # Thu
assert   w.include?(Time.mktime(2007,9,28))
assert   w.include?(Time.mktime(2007,9,29))
assert   w.include?(Time.mktime(2007,9,30))
assert   w.include?(Time.mktime(2007,10,1))
assert ! w.include?(Time.mktime(2007,10,2)) # Tue
end

def test_window_nil
w = RDS::TimeWindow.new("")
assert w.include?(Time.mktime(2007,9,25,1,2,3))     # all times
end
``````

end

On Fri, 19 Oct 2007 21:14:00 +0900, Ruby Q. wrote:

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone on Ruby T. follow the discussion. Please reply to the
original quiz message, if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=-=-=-=-=

by Brian C.

Write a Ruby class which can tell you whether the current time (or any
given time) is within a particular “time window”. Time windows are
defined by strings in the following format:

0700-0900 # every day between these

times #

Sat Sun # all day Sat and Sun, no other
times #
Sat Sun 0700-0900 # 0700-0900 on Sat and Sun only

Mon-Fri 0700-0900 # 0700-0900 on Monday to Friday
only #
Mon-Fri 0700-0900; Sat Sun # ditto plus all day Sat and Sun

Fri-Mon 0700-0900 # 0700-0900 on Fri Sat Sun Mon #
Sat
0700-0800; Sun 0800-0900 # 0700-0800 on Sat, plus 0800-0900 on
Sun

Time ranges should exclude the upper bound, i.e. 0700-0900 is 07:00:00
to 08:59:59. An empty time window means “all times everyday”. Here are
some test cases to make it clearer:

I have rewritten the test cases to give more informative messages:

class TestTimeWindow < Test::Unit::TestCase
def test_window_1
s = “Sat-Sun; Mon Wed 0700-0900; Thu 0700-0900 1000-1200”
w = TimeWindow.new(s)

``````  assert ! w.include?(Time.mktime(2007,9,25,8,0,0)),   "#{s.inspect}
``````

should not include Tue 8am"
assert w.include?(Time.mktime(2007,9,26,8,0,0)), “#{s.inspect}
should include Wed 8am”
assert ! w.include?(Time.mktime(2007,9,26,11,0,0)), “#{s.inspect}
should not include Wed 11am”
assert ! w.include?(Time.mktime(2007,9,27,6,59,59)), “#{s.inspect}
should not include Thurs 6:59am”
assert w.include?(Time.mktime(2007,9,27,7,0,0)), “#{s.inspect}
should include Thurs 7am”
assert w.include?(Time.mktime(2007,9,27,8,59,59)), “#{s.inspect}
should include Thurs 8:59am”
assert ! w.include?(Time.mktime(2007,9,27,9,0,0)), “#{s.inspect}
should not include Thurs 9am”
assert w.include?(Time.mktime(2007,9,27,11,0,0)), “#{s.inspect}
should include Thurs 11am”
assert w.include?(Time.mktime(2007,9,29,11,0,0)), “#{s.inspect}
should include Sat 11am”
assert w.include?(Time.mktime(2007,9,29,0,0,0)), “#{s.inspect}
should include Sat midnight”
assert w.include?(Time.mktime(2007,9,29,23,59,59)),
“#{s.inspect} should include
Saturday one minute before midnight”
end

``````def test_window_2
s = "Fri-Mon"
w = TimeWindow.new(s)
assert ! w.include?(Time.mktime(2007,9,27)), "#{s.inspect} should
``````

not include Thurs"
assert w.include?(Time.mktime(2007,9,28)), “#{s.inspect} should
include Fri”
assert w.include?(Time.mktime(2007,9,29)), “#{s.inspect} should
include Sat”
assert w.include?(Time.mktime(2007,9,30)), “#{s.inspect} should
include Sun”
assert w.include?(Time.mktime(2007,10,1)), “#{s.inspect} should
include Mon”
assert ! w.include?(Time.mktime(2007,10,2)), “#{s.inspect} should
not include Tues”
end

``````def test_window_nil
w = TimeWindow.new("")
assert w.include?(Time.mktime(2007,9,25,1,2,3)),"Empty string
``````

should include all times"
end
end

Hello,

Here is my solution to the quiz. First I used smaller classes to
simplify
TimeWindow:

class TimeRange

Each Input in form of “HHMM”

def initialize(start_str, end_str)
@start = start_str.to_i
@end = end_str.to_i
end

attr_reader :start, :end
end

class TimeFrame

Time range - List of start/end time ranges defining the time frame

def initialize(days, time_ranges)
@days = days
@time_ranges = time_ranges
end

Does the given Time match this Time Frame?

def include?(time)
if @days[time.wday]
# If no times then days matching is good enough
return true if @time_ranges.size == 0

``````  # Check time range(s)
for time_range in @time_ranges
time_n = time.hour * 100 + time.min
return true if time_n >= time_range.start and
time_n <  time_range.end
end
end

false
``````

end
end

The main class then simply parses the time window string at startup,
saving
each individual time window to memory. Then the include? method iterates
through those time windows to determine if a particular time is in the
window:

Defines a time window spanning multiple days and time ranges

class TimeWindow
Days = [“Sun”, “Mon”, “Tues”, “Wed”, “Thu”, “Fri”, “Sat”]

Constructor accepting a string as defined in ruby quiz description

def initialize(time_window)
@timeframes = []

``````for group in time_window.split(";")
days, times = Array.new(7, false), []

for item in group.split(" ")
# Range of values?
if item.include?("-")
# Yes, Figure out if range is days or times
range = item.split("-")

if Days.include?(range[0])
set_day_range(days, range[0], range[1])
else
times << TimeRange.new(range[0], range[1])
end
else
days[Days.index(item)] = true if Days.include?(item)
end
end

@timeframes << TimeFrame.new(days, times)
end
``````

end

start_day, end_day - Day range to add to the window

def set_day_range(days, start_day, end_day)
pos = Days.index(start_day)
while pos != (Days.index(end_day) + 1) % 7
days[pos] = true
pos = (pos + 1) % 7
end
end

Does the given Time match this time window?

def include?(time)
for time_frame in @timeframes
return true if time_frame.include?(time)
end

``````return (@timeframes.size == 0) # Empty time string matches all times
``````

end

private :set_day_range
end

Fortunately it passes all of the tests
A pastie is available here: http://pastie.caboo.se/109346
Thanks,

Justin

Below you’ll find my solution to the quiz. I approached it a
relatively standard object-oriented fashion. For example,
TimeSpecifier acts as an abstract base class for Day and HourMinute to
bring together their commonalities. And I did use some of that good
ol’ Ruby duck typing so that a TimeRange can be used as a
TimeSpecifier.

Eric

Are you interested in on-site Ruby training that uses well-designed,
real-world, hands-on exercises? http://LearnRuby.com

====

can

times

on Sun
class TimeWindow

Represents a time range defined by a start and end TimeSpecifier.

class TimeRange
def initialize(start_t, end_t,
include_end, allow_reverse_range = false)
raise “mismatched time specifiers in range (%s and %s)” %
[start_t, end_t] unless
start_t.class == end_t.class
raise “reverse range not allowed "%s-%s"” % [start_t, end_t]
if
start_t >= end_t && !allow_reverse_range
@start_t, @end_t, @include_end = start_t, end_t, include_end
end

``````# Equality is defined as a TimeSpecifier on the RHS being in the
# this range.
def ==(time_spec)
# do either a < or a <= when comparing the end of the range
# depending on value of @include_end
end_comparison = @include_end ? :<= : :<

# NOTE: the call to the send method below is used to call the
# method in end_comparison
if @start_t < @end_t
time_spec >= @start_t && time_spec.send(end_comparison,
``````

@end_t)
else # a reverse range, such as “Fri-Mon”, needs an ||
time_spec >= @start_t || time_spec.send(end_comparison,
@end_t)
end
end

``````def to_s
"%s-%s" % [@start_t, @end_t]
end
``````

end

of the week or a time of day.

class TimeSpecifier
include Comparable

``````def <=>(other)
raise "incompatible comparison (%s and %s)" % [self, other]
``````

unless
self.class == other.class
@specifier <=> other.specifier
end

``````protected

attr_reader :specifier

# Given an "item" regular expression returns a hash of two regular
# expressions.  One matches an individual item and the other a
# range of items.  Both returned regular expressions use parens,
# so the individual items can be extraced from a match.
def self.build_regexps(regexp)
individual_re = Regexp.new "^(%s)" % regexp
range_re = Regexp.new "^(%s)\-(%s)" % [regexp, regexp]
{ :individual => individual_re, :range => range_re }
end

# Attempts to match str with the two regexps passed in.  regexps
# is a hash that contains two regular expressions, one that
# matches a single TimeSpecifier and one that matches a range of
# TimeSpecifiers.  If there's a match then it returns either an
# instance of klass or an instance of a TimeRange of klass (and
# str is destructively modified to remove the matched text from
# its beginning).  If there isn't a match, then nil is returned.
# include_end determines whether the end specification of the
# range is included in the range (e.g., if the specifier is
# "Mon-Fri" whether or not Fri is included).  allow_reverse_range
# determines whether a range in which the start is after the end
# is allowed, as in "Fri-Mon"; this might be alright for days of
# the week but not for times.
def self.super_parse(str, klass, regexps,
include_end, allow_reverse_range)
# first try a range match
if match_data = regexps[:range].match(str)
consume_front(str, match_data[0].size)
TimeRange.new(klass.new_from_str(match_data[1]),
klass.new_from_str(match_data[2]),
include_end,
allow_reverse_range)
# second try individual match
elsif match_data = regexps[:individual].match(str)
consume_front(str, match_data[0].size)
klass.new_from_str(match_data[1])
else
nil
end
end

# Consumes size characters from the front of str along with any
# remaining whitespace at the front.  This modifies the actual
# string.
def self.consume_front(str, size)
str[0..size] = ''
str.lstrip!
end
``````

end

Time specifier for a day of the week.

class Day < TimeSpecifier
Days = %w(Sun Mon Tue Wed Thu Fri Sat)
@@regexps = TimeSpecifier.build_regexps(/[A-Za-z]{3}/)

``````def initialize(day)
raise "illegal day \"#{day}\"" unless (0...Days.size) === day
@specifier = day
end

def to_s
Days[@specifier]
end

def self.new_from_str(str)
day = Days.index(str)
raise "illegal day \"#{day_str}\"" if day.nil?
new(day)
end

def self.parse(str)
super_parse(str, Day, @@regexps, true, true)
end
``````

end

Time specifier for a specific time of the day (i.e., hour and

minute).
class HourMinute < TimeSpecifier
@@regexps = TimeSpecifier.build_regexps(/\d{4}/)

``````def initialize(hour_minute)
hour = hour_minute / 100
minute = hour_minute % 100
raise "illegal time \"#{hour_minute}\"" unless
(0..23) === hour && (0..59) === minute
@specifier = hour_minute
end

def to_s
"%04d" % @specifier
end

def self.new_from_str(str)
new str.to_i
end

def self.parse(str)
super_parse(str, HourMinute, @@regexps, false, false)
end
``````

end

combination

of day and hour-minutes, possibly in ranges.

def initialize(str)
# time_frame is a Day, HourMinute, or TimeRangeof either; it is
# set here so when it’s sent inside the block, it won’t be scoped
# to the block
time_frame = nil

``````@periods = []
str.split(/ *; */).each do |period_str|
# frame set is a hash where the keys are either the class Day or
# HourMinute and the associated values are all time specifiers
# for that class.  The default value is the empty array.
period = Hash.new { |h, k| h[k] = [] }

# process each time specifier in period_str by sequentially
# processing andconsuming the beginning of the string
until period_str.empty?
# set frame_type and time_frame based on the first matching
# parse
frame_type = [Day, HourMinute].find { |specifier|
time_frame = specifier.parse(period_str)
}
raise "illegal window specifier \"#{period_str}\"." if
time_frame.nil?

period[frame_type] << time_frame
end

@periods << period
end
``````

end

otherwise.

def include?(time)
d = Day.new(time.wday)
hm = HourMinute.new(time.hour * 100 + time.min)

``````# see if any one period matches the time or if there are no
``````

periods
@periods.empty? || @periods.any? { |period|
# a period matches if either there is no day specification or
# one day specification matches, and if either there is no
# hour-minute specification or one such specification matches
(period[Day].empty? ||
period[Day].any? { |day_period| day_period == d }) &&
(period[HourMinute].empty? ||
period[HourMinute].any? { |hm_period| hm_period == hm })
}
end

def to_s
@periods.map { |period|
(period[Day] + period[HourMinute]).map { |time_spec|
time_spec.to_s
}.join(’ ‘)
}.join(’ ; ')
end
end

Here’s my solutions. I used Runt for the heavy lifting. I just had
to parse the string and create Runt temporal expressions.

require ‘runt’

#adds ability to check Runt expressions against Time objects
class Time
include Runt::DPrecision

attr_accessor :date_precision

def date_precision
return @date_precision unless @date_precision.nil?
return Runt::DPrecision::DEFAULT
end
end

module Runt

#extends REWeek to allow for spanning across weeks
class REWeek

``````def initialize(start_day,end_day=6)
@start_day = start_day
@end_day = end_day
end

def include?(date)
return true if  @start_day==@end_day
if @start_day < @end_day
@start_day<=date.wday && @end_day>=date.wday
else
(@start_day<=date.wday && 6 >=date.wday) ||
(0 <=date.wday && @end_day >=date.wday)
end
end
``````

end

class StringParser < Runt::Intersect

``````def initialize(string)
super()
add parsed(string)
end

#recursive method to parse input string
def parse(token)
case token
when ""
REWeek.new(0)
when /^(.+);(.+)/ # split at semicolons
parse(\$1) | parse(\$2)
when /(\D+) (\d.+)/ # split days and times
parse(\$1) & parse(\$2)
when /(\D+) (\D+)/, /(\d+-\d+) (\d+-\d+)/ # split at spaces
parse(\$1) | parse(\$2)
when /([A-Z][a-z][a-z])-([A-Z][a-z][a-z])/ # create range of days
REWeek.new(Runt.const_get(\$1), Runt.const_get(\$2))
when /([A-Z][a-z][a-z])/ # create single day
DIWeek.new(Runt.const_get(\$1))
when /(\d\d)(\d\d)-(\d\d)(\d\d)/ #create time range
start = Time.mktime(2000,1,1,\$1.to_i,\$2.to_i)
# 0600-0900 should work like 0600-0859,
stop = Time.mktime(2000,1,1,\$3.to_i,\$4.to_i) - 60
REDay.new(start.hour, start.min, stop.hour, stop.min)
end
end
alias :parsed :parse
``````

end

end

class TimeWindow < Runt::StringParser
end

Another solution for Time Window quiz:

I considered only when input has day ranges in ascending order (“Mon
Fri-Sun” or “Fri-Sun Mon”, but not “Fri-Mon”) and the first day of the
week is Monday.

class TimeWindow

Days = { “Mon” => 0, “Tue” => 1, “Wed” => 2, “Thu” => 3, “Fri” => 4,
“Sat” => 5, “Sun” => 6}

def initialize (window)
@window = window
@ranges = []
parse_window
end

def include? (time)
hour = time.strftime("%H%M").to_i
day = time.strftime("%w").to_i
req = (day-1)*10000+hour
puts “#{req}”
result = false
@ranges.each{ |range|
if range[0] <= req && req < range[1]
result = true
end
}
result
end

private

#Parse the input
def parse_window
regex = /((?:Mon[ -]?|Tue[ -]?|Wed[ -]?|Thu[ -]?|Fri[ -]?|Sat[
-]?|Sun[ -]?)+)?((?:[012]\d[0-6]\d-[012]\d[0-6]\d[ ]?)+)?/
@window.split(";").each { |window|
window.strip!
match = regex.match(window)

``````  # it has days
if match[1]
days = parse_days match[1]
else
days = [[0,6]]       # everyday
end

# it has hours
if match[2]
time = parse_time match[2]
else
time = [[0,2400]]   # all day
end

days.each {|dr|
time.each {|tr|
@ranges << [dr[0]*10000+tr[0], dr[1]*10000+tr[1]]
}
}
}
``````

end

def parse_days (days)
result = []
days.scan(/(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun)-(Mon|Tue|Wed|Thu|Fri|Sat|Sun)|(Mon|Tue|Wed|Thu|Fri|Sat|Sun))/)
{
if \$3 # it’s just one day
result << [Days[\$3],Days[\$3]]
else # it’s a range
result << [Days[\$1],Days[\$2]]
end
}
result
end

def parse_time (time)
result = []
time.scan(/([012]\d[0-6]\d)-([012]\d[0-6]\d)/) {
result << [\$1.to_i, \$2.to_i]
}
result
end
end

On Fri, 19 Oct 2007 21:14:00 +0900, Ruby Q. wrote:

defined by strings in the following format:
to 08:59:59. An empty time window means “all times everyday”. Here are
w.include?(Time.mktime(2007,9,27,6,59,59)) # Thu assert
w = TimeWindow.new(“Fri-Mon”)
assert w.include?(Time.mktime(2007,9,25,1,2,3)) # all times
end
end

#!/usr/bin/env ruby

class TimeWindow
DAYNAMES=%w[Sun Mon Tue Wed Thu Fri Sat]
DAYNAME=%r{Sun|Mon|Tue|Wed|Thu|Fri|Sat}
TIME=%r{[0-9]+}

def initialize string
string = " " if string == “” #make an empty string match
everythingworking around the way clauses are split
#splitting an empty string gives an empty array (i.e. no clauses)
#splitting a " " gives a single clause with no day names (so all are
used) and no times (so all are used)
@myarray=Array.new(7){[]}

``````#different clauses are split by semicolons
string.split(/\s*;\s*/).each do |clause|

#find the days that this clause applies to
curdays=[]
clause.scan(/(#{DAYNAME})(?:(?=\s)|\$)|(#{DAYNAME})-(#{DAYNAME})/)
``````

do |single,start,finish|
single &&= DAYNAMES.index(single)
start &&= DAYNAMES.index(start)
finish &&= DAYNAMES.index(finish)
curdays << single if single
if start and finish
(start…finish).each{|x| curdays << x} if start<finish
(start…6).each{|x| curdays << x} if finish<start
(0…finish).each{|x| curdays << x} if finish<start
end
end

``````  #all days if no day names were given
curdays=(0..6).to_a if curdays==[]

#find the times that this clause applies to
found=false
clause.scan(/(#{TIME})-(#{TIME})/) do |start,finish|
found=true
curdays.each do |day|
@myarray[day] << [start,finish]
end
end

#all times if none were given
if not found
curdays.each {|day| @myarray[day] << ["0000","2400"]}
end
end
``````

end

def include? time
matchday=time.wday
matchtime="%02d%02d" % [time.hour,time.min]
@myarray[matchday].any?{|start,finish| start<=matchtime &&
matchtime<finish}
end

alias_method :===, :include?

end

On Oct 19, 7:14 am, Ruby Q. [email protected] wrote:

in the following format:
08:59:59. An empty time window means “all times everyday”. Here are some test
assert w.include?(Time.mktime(2007,9,27,7,0,0))
assert ! w.include?(Time.mktime(2007,9,27)) # Thu
end
end

Like Gordon, I used Runt a bit for my solution. Unlike Gordon, I
didn’t use Runt directly. I remembered seeing it some time ago and
used what I could recall of the general ideas of implementation to
roll my own (probably not as well as Runt itself). And I believe the
naming of “Unbound Time” comes from Martin F…

require ‘date’

class TimeWindow
attr_reader :intervals

def initialize(string)
@intervals = []

``````parse(string)
``````

end

def include?(time)
intervals.any? { |int| int.include?(time) }
end

private

attr_writer :intervals

def parse(string)
parts = string.split(‘;’)
parts = [‘’] if parts.empty?
@intervals = parts.collect { |str| TimeInterval.new(str) }
end

end

class TimeInterval
DAYS = %w(Sun Mon Tue Wed Thu Fri Sat)

UnboundTime = Struct.new(:hour, :minute) do
include Comparable

``````def <=>(time)
raise TypeError, "I need a real Time object for comparison"
``````

unless time.is_a?(Time)

``````  comp_date  = Date.new(time.year, time.month, time.mday)
comp_date += 1 if hour == 24

Time.mktime(comp_date.year, comp_date.month, comp_date.day, hour
``````

% 24, minute, 0) <=> time
end
end

UnboundTimeRange = Struct.new(:start, :end)

attr_reader :days, :times

def initialize(string)
@days = []
@times = []

``````parse(string)
``````

end

def include?(time)
day_ok?(time) and time_ok?(time)
end

private

attr_writer :days, :times

def parse(string)
unless string.empty?
string.strip.split(’ ').each do |segment|
if md = segment.match(/^(\d{4})-(\d{4})\$/)
self.times +=
[ UnboundTimeRange.new(UnboundTime.new(*md[1].unpack(‘A2A2’).collect
{ |elem| elem.to_i }), UnboundTime.new(*md[2].unpack(‘A2A2’).collect
{ |elem| elem.to_i })) ]
elsif md = segment.match(/^(\w+)(-(\w+))?\$/)
if md[2]
start_day = DAYS.index(md[1])
end_day = DAYS.index(md[3])

``````        if start_day <= end_day
self.days += (start_day .. end_day).to_a
else
self.days += (start_day .. DAYS.length).to_a + (0 ..
``````

end_day).to_a
end
else
self.days += [DAYS.index(md[1])]
end
else
raise ArgumentError, “Segment #{segment} of time window
incomprehensible”
end
end
end

``````self.days  = 0..DAYS.length if days.empty?
self.times = [ UnboundTimeRange.new(UnboundTime.new(0, 0),
``````

UnboundTime.new(24, 0)) ] if times.empty?
end

def day_ok?(time)
days.any? { |d| d == time.wday }
end

def time_ok?(time)
times.any? { |t| t.start <= time and t.end > time }
end
end

All tests pass, which at the moment is good enough for me.

On 10/19/07, Ruby Q. [email protected] wrote:

``````    #    Mon-Fri 0700-0900; Sat Sun    # ditto plus all day Sat and Sun
#    Fri-Mon 0700-0900             # 0700-0900 on Fri Sat Sun Mon
#    Sat 0700-0800; Sun 0800-0900  # 0700-0800 on Sat, plus 0800-0900 on Sun
``````

Time ranges should exclude the upper bound, i.e. 0700-0900 is 07:00:00 to
08:59:59. An empty time window means “all times everyday”. Here are some test
cases to make it clearer:

Hi,

This is my solution: nothing spectacular or too clever. The idea was
to convert every part of the window (everything between “;”) into a
class that knows how to parse the ranges. That class (TimeRange)
converts the part into an array of day_of_week ranges and an array of
hour ranges. To include a time, this window needs to match at least
one day_of_week and at least one hour range. The time window, then,
has an array of those TimeRange objects, and tries to find at least
one that matches. One interesting thing is that I convert every time
definition into a range, even the ones with just one element, so I can
use Range#include? across all time ranges.

require ‘time’

class TimeRange
def initialize(s)
@day_of_week = []
@hour = []
s.strip.split(" ").each do |range|
if (match = range.match(/(\d{4})-(\d{4})/))
@hour << (match[1].to_i…match[2].to_i)
elsif (match = range.match(/([a-zA-Z]{3})-([a-zA-Z]{3})/))
first = Time::RFC2822_DAY_NAME.index(match[1])
second = Time::RFC2822_DAY_NAME.index(match[2])
if (first < second)
@day_of_week << (first…second)
else
@day_of_week << (first…(Time::RFC2822_DAY_NAME.size-1))
@day_of_week << (0…second)
end
else
@day_of_week <<
(Time::RFC2822_DAY_NAME.index(range)…Time::RFC2822_DAY_NAME.index(range))
end
end
end

def include?(time)
dow = time.wday
hour = time.strftime(“%H%M”).to_i
any?(@day_of_week, dow) and any?(@hour, hour)
end

def any?(enum, value)
return true if enum.empty?
enum.any?{|x| x.include?(value)}
end
end

class TimeWindow
def initialize(s)
@ranges = []
s.split(“;”).each do |part|
@ranges << TimeRange.new(part)
end
end

def include?(time)
return true if @ranges.empty?
@ranges.any? {|x| x.include?(time)}
end
end

Kind regards,

Jesus.

On 10/21/07, Gordon T. [email protected] wrote:

Here’s my solutions. I used Runt for the heavy lifting. I just had
to parse the string and create Runt temporal expressions.

There was a bug in my code. I shouldn’t subtract a minute from the
end of minute ranges, just a second. Here’s the fixed code.

#time_window.rb
require ‘runt_ext’

module Runt

#extends REWeek to allow for spanning across weeks
class REWeek

``````def initialize(start_day,end_day=6)
@start_day = start_day
@end_day = end_day
end

def include?(date)
return true if  @start_day==@end_day
if @start_day < @end_day
@start_day<=date.wday && @end_day>=date.wday
else
(@start_day<=date.wday && 6 >=date.wday) || (0 <=date.wday &&
``````

@end_day >=date.wday)
end
end

end

class StringParser < Runt::Intersect

``````def initialize(string)
super()
add parsed(string)
end

#recursive method to parse input string
def parse(token)
case token
when ""
REWeek.new(0)
when /^(.+);(.+)/ # split at semicolons
parse(\$1) | parse(\$2)
when /(\D+) (\d.+)/ # split days and times
parse(\$1) & parse(\$2)
when /(\D+) (\D+)/, /(\d+-\d+) (\d+-\d+)/ # split at spaces
parse(\$1) | parse(\$2)
when /([A-Z][a-z][a-z])-([A-Z][a-z][a-z])/ # create range of days
REWeek.new(Runt.const_get(\$1), Runt.const_get(\$2))
when /([A-Z][a-z][a-z])/ # create single day
DIWeek.new(Runt.const_get(\$1))
when /(\d\d)(\d\d)-(\d\d)(\d\d)/ #create time range
start = Time.mktime(2000,1,1,\$1.to_i,\$2.to_i)
# 0600-0900 should work like 0600-0859,
stop = Time.mktime(2000,1,1,\$3.to_i,\$4.to_i) - 1
REDay.new(start.hour, start.min, stop.hour, stop.min)
end
end
alias :parsed :parse
``````

end

end

class TimeWindow < Runt::StringParser
end

Here is my solution to the time window quiz. Range.create_from_string
is the workhorse method and it would be nice if that was refactored
into smaller pieces.

class TimeWindow
def initialize(definition_string)
@ranges = []
definition_string.split(/;/).each do |part|
@ranges << Range.create_from_string(part.strip)
end
@ranges << Range.create_from_string(’’) if @ranges.empty?
end

def include?(time)
@ranges.any? {|r| r.include?(time)}
end

class Range < Struct.new(:day_parts, :time_parts)
DAYS = %w{Sun Mon Tue Wed Thu Fri Sat}

`````` def self.create_from_string(str)
time_parts = []
day_parts = []
str.split(/ /).each do |token|
token.strip!
if DAYS.include?(token)
day_parts << token
elsif token =~ /^(\w{3})-(\w{3})\$/
start_day, end_day = \$1, \$2
start_found = false
(DAYS * 2).each do |d|
start_found = true if d == start_day
day_parts << d if start_found
break if d == end_day && start_found
end
elsif token =~ /^(\d{4})-(\d{4})\$/
time_parts << ((\$1.to_i)..(\$2.to_i - 1))
else
raise "Unrecognized token: #{token}"
end
end
time_parts << (0..2399) if time_parts.empty?
day_parts = DAYS.clone if day_parts.empty?
self.new(day_parts, time_parts)
end

def include?(time)
matches_day?(time) && matches_minute?(time)
end

def matches_day?(time)
day = time.strftime('%a')
self.day_parts.include?(day)
end

def matches_minute?(time)
minute = time.strftime('%H%M').to_i
self.time_parts.any?  {|tp| tp.include?(minute) }
end
``````

end
end