# [QUIZ] Time Window (#144)

The three rules of Ruby Quiz:

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 Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
if you can.

···

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

by Brian Candler

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:

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

The three rules of Ruby Quiz:

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 Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
original quiz message, if you can.

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

···

On Fri, 19 Oct 2007 21:14:00 +0900, Ruby Quiz wrote:
=-=-=-=-=

by Brian Candler

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:

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

--
Ken Bloom. PhD candidate. Linguistic Cognition Laboratory.
Department of Computer Science. Illinois Institute of Technology.
http://www.iit.edu/~kbloom1/

Hello,

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

# Class to store a single time range defined by a start/end
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

end

# Represents a single time period for particular days and times
# A time window may contain several of these frames
class TimeFrame
# Days - Bitmask of 7 fields (Sun @ 0, Mon @ 1, Tues @ 2, etc)
# 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

# Set days in given range in the input array
# Inputs: days - List of days in the time window
# 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

···

On 10/19/07, Ruby Quiz <james@grayproductions.net> wrote:

The three rules of Ruby Quiz:

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 Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
message,
if you can.

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

by Brian Candler

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:

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

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

====

# This is a solution to Ruby Quiz #144 (see http://www.rubyquiz.com/)
# by LearnRuby.com and released under the Creative Commons
can
# also be found at:
# http://learnruby.com/examples/ruby-quiz-144.shtml

# A TimeWindow is a specification for a time window. It is specified
# by a string, and an instance of Time can be checked to see if it's
# included in the window. The specification string is is best
# documented by quoting the Ruby Quiz #144 description:
#
# 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
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

# This is an abstract base class for time specifiers, such as a day
# 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

# 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

# Creates a TimeWindow by parsing a string specifying some
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

# Returns true if the TimeWindow includes the passed in time, false
# 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()
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

#!/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
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 Fri, 19 Oct 2007 21:14:00 +0900, Ruby Quiz wrote:

The three rules of Ruby Quiz:

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 Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
original quiz message, if you can.

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

by Brian Candler

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:

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

--
Ken Bloom. PhD candidate. Linguistic Cognition Laboratory.
Department of Computer Science. Illinois Institute of Technology.
http://www.iit.edu/~kbloom1/

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

require 'date'

class TimeWindow

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)

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 Oct 19, 7:14 am, Ruby Quiz <ja...@grayproductions.net> wrote:

The three rules of Ruby Quiz:

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 Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
if you can.

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

by Brian Candler

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:

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

--
-yossef

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

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/19/07, Ruby Quiz <james@grayproductions.net> wrote:

by Brian Candler

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:

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

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()
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

···

On 10/21/07, Gordon Thiesfeld <gthiesfeld@gmail.com> wrote:

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