[QUIZ] Time Window (#144)

Discussion in 'Ruby' started by Ruby Quiz, Oct 19, 2007.

1. Ruby QuizGuest

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
on Ruby Talk follow the discussion. Please reply to the 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

Ruby Quiz, Oct 19, 2007

2. Ken BloomGuest

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
> everyone on Ruby Talk follow the discussion. Please reply to the
> 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:
>

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

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

Ken Bloom, Oct 19, 2007

3. Eric I.Guest

Solution to Time Window (#144)

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
# Attribution-Share Alike 3.0 United States License. This source code
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

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

# 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

Eric I., Oct 21, 2007
4. Gordon ThiesfeldGuest

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:Precision

attr_accessor :date_precision

def date_precision
return @date_precision unless @date_precision.nil?
return Runt:Precision:EFAULT
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 arsed arse

end

end

class TimeWindow < Runt::StringParser
end

Gordon Thiesfeld, Oct 21, 2007
5. JuangerGuest

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

Juanger, Oct 22, 2007
6. Ken BloomGuest

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
> everyone on Ruby Talk follow the discussion. Please reply to the
> 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

#!/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

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

Ken Bloom, Oct 22, 2007
7. Yossef MendelssohnGuest

Re: Time Window (#144)

On Oct 19, 7:14 am, 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 everyone
> on Ruby Talk follow the discussion. Please reply to the 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

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
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.newhour, :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.newstart, :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.

--
-yossef

Yossef Mendelssohn, Oct 22, 2007
8. Philip GattGuest

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

Philip Gatt, Oct 22, 2007
9. Jesús Gabriel y GalánGuest

On 10/19/07, 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:

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.

Jesús Gabriel y Galán, Oct 22, 2007
10. Gordon ThiesfeldGuest

On 10/21/07, Gordon Thiesfeld <> 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 arsed arse

end

end

class TimeWindow < Runt::StringParser
end

Gordon Thiesfeld, Oct 22, 2007

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.