Time Window (#144)
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:
ruby
# 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:
ruby
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
marc
--
ms4...@sdf.lonestar.org
SDF Public Access UNIX System - http://sdf.lonestar.org
http://www.lispcast.com/index.php/2007/10/time-window-matcher/
I have the code, plus some comments on it.
ericwn...@gmail.com wrote:
> Ok, I did it. You can check it out here:
>
> http://www.lispcast.com/index.php/2007/10/time-window-matcher/
>
> I have the code, plus some comments on it.
>
You requested feedback. Code looks great. Spotted this:
(some #'identity (mapcar (lambda (tw)
(includesp tw time))
(rest tw))))
I am scratching my head, why not?:
(some (lambda (tw) (includesp tw time)) (rest tw))
Also saves you from consing up all the results. You did the same later
with (every 'identity (lambda ....
My next point would be you do not really need the result (which is a
predicate anyway, not the matched tw) so find-if does as well and does
not mislead the maintainer into thinking the result matters other than
being non-nil.
Finally:
(loop for tw in (rest tw)
thereis (includesp tw time))
Not so many parentheses. :)
hth,kt
--
http://www.theoryyalgebra.com/
"Career highlights? I had two. I got an intentional walk
from Sandy Koufax and I got out of a rundown against the Mets."."
- Bob Uecker
Ken Tilton wrote:
>
>
> ericwn...@gmail.com wrote:
>
>> Ok, I did it. You can check it out here:
>>
>> http://www.lispcast.com/index.php/2007/10/time-window-matcher/
>>
>> I have the code, plus some comments on it.
>>
>
>
> You requested feedback. Code looks great. Spotted this:
(defun hour-of (time)
(multiple-value-bind (second minute hour) (decode-universal-time time)
hour))
That would give me warnings about unused second and minute variables. I
could declare ignore or:
(nth-value 2 (decode....))
Another solution (depends only on split-sequence):
(defvar *weekdays* '(mon tue wed thu fri sat sun))
(defclass time-window ()
((weekdays :accessor weekdays :initarg :weekdays)
(from :accessor from :initarg :from)
(to :accessor to :initarg :to)))
(defmethod print-object ((window time-window) stream)
(print-unreadable-object (window stream :type t)
(with-slots (weekdays from to) window
(format stream "~A from ~A to ~A" weekdays from to))))
(defmethod includes ((windows list) (time list))
(includes windows (apply #'encode-universal-time
(reverse (subseq (append time '(0 0 0)) 0 6)))))
(defmethod includes ((windows list) (time integer))
(loop for window in windows when (includes window time) return t))
(defmethod includes ((window time-window) (time integer))
(flet ((hhmmss-as-seconds (h &optional m s)
(+ (* 3600 h) (* 60 (or m 0)) (or s 0))))
(multiple-value-bind (sec min hour date month year day)
(decode-universal-time time)
(declare (ignore date month year))
(and (member (nth day *weekdays*) (weekdays window))
(<= (apply #'hhmmss-as-seconds (from window))
(hhmmss-as-seconds hour min sec))
(< (hhmmss-as-seconds hour min sec)
(apply #'hhmmss-as-seconds (to window)))))))
(defun create-windows (string)
(loop for def in (split-sequence:split-sequence #\; string)
append (let (days time-intervals)
(loop for item in (split-sequence:split-sequence #\Space def)
unless (string= item "")
do (ecase (length item)
(3 (push (read-from-string item) days))
(7 (let ((from-day (position (read-from-string (subseq item 0
3)) *weekdays*))
(to-day (position (read-from-string (subseq item 4 7))
*weekdays*)))
(when (< to-day from-day) (incf to-day 7))
(loop for i from from-day to to-day
do (pushnew (nth (mod i 7) *weekdays*) days))))
(9 (push item time-intervals))))
(loop for time-interval in (or time-intervals '("0000-2400"))
collect (make-instance 'time-window :weekdays (or days *weekdays*)
:from (list (parse-integer (subseq time-interval 0 2))
(parse-integer (subseq time-interval 2 4)))
:to (list (parse-integer (subseq time-interval 5 7))
(parse-integer (subseq time-interval 7 9))))))))
(progn
(let ((windows (create-windows "Sat-Sun; Mon Wed 0700-0900; Thu
0700-0900 1000-1200")))
(print windows)
(print (list (not (includes windows '(2007 9 25 8 0 0)))
(includes windows '(2007 9 26 8 0 0))
(not (includes windows '(2007 9 26 11 0 0)))
(not (includes windows '(2007 9 27 6 59 59)))
(includes windows '(2007 9 27 7 0 0))
(includes windows '(2007 9 27 8 59 59))
(not (includes windows '(2007 9 27 9 0 0)))
(includes windows '(2007 9 27 11 0 0))
(includes windows '(2007 9 29 11 0 0))
(includes windows '(2007 9 29 0 0 0))
(includes windows '(2007 9 29 23 59 59)))))
(let ((windows (create-windows "Fri-Mon")))
(print windows)
(print (list (not (includes windows '(2007 9 27)))
(includes windows '(2007 9 28))
(includes windows '(2007 9 29))
(includes windows '(2007 9 30))
(includes windows '(2007 10 1))
(not (includes windows '(2007 10 2))))) )
(let ((windows (create-windows "")))
(print windows)
(print (list (includes windows '(2007 9 25 1 2 3))))))
;; (#<TIME-WINDOW (SUN SAT) from (0) to (24)>
;; #<TIME-WINDOW (WED MON) from (7 0) to (9 0)>
;; #<TIME-WINDOW (THU) from (10 0) to (12 0)>
;; #<TIME-WINDOW (THU) from (7 0) to (9 0)>)
;; (T T T T T T T T T T T)
;; (#<TIME-WINDOW (MON SUN SAT FRI) from (0) to (24)>)
;; (T T T T T T)
;; (#<TIME-WINDOW (MON TUE WED THU FRI SAT SUN) from (0) to (24)>)
;; (T)
Leandro
;(require 'split-sequence)
(defstruct timeframe
days hour-ranges)
(defparameter *weekdays* '#1=(mon tue wed thu fri sat sun . #1#))
;;; Timewindow parsing and creation
(defun get-days-range (di df)
"Returns a list containing the set of weekdays between di and df"
(loop for day in *weekdays*
with proceed
when (eq day di) do (setf proceed t)
when proceed collect day into days
when (and (eq day df) proceed) return days))
(defun day-lapse (string)
"Parses a string representing a range of weekdays 'Mon-Fri'
and returns a list with the set of weekdays into that range"
(get-days-range (read-from-string (subseq string 0 3))
(read-from-string (subseq string 4))))
(defun hour-lapse (string)
"Parses a string representing an hour range '0900-1200' and returns
a list with the limits of that range"
(list (* (parse-integer (subseq string 0 4)) 100)
(- (* (parse-integer (subseq string 5)) 100) 1)))
(defun parse-timeframe (string)
"Parses a string representing a timeframe and returns a timeframe
struct"
(if (= (length string) 0) ; all weekdays, all times
(make-timeframe)
(loop
for string-lapse in (split-sequence:split-sequence #\Space string)
for tipo = (search "-" string-lapse)
when (eq tipo 3) append (day-lapse string-lapse) into days ;weekdays range
when (eq tipo 4) collect (hour-lapse string-lapse) into hours ;hour range
when (null tipo) collect (read-from-string string-lapse) into days
;single weekday
finally (return (make-timeframe :days days :hour-ranges hours)))))
(defun create-timewindow (string)
"parses a string representing a timewindow and returns a list
of timeframe structs (a timewindow)"
(loop for string-window in (split-sequence:split-sequence #\; string)
collecting (parse-timeframe (string-trim " " string-window))))
;;; Predicates to test inclusion in timewindow
(defun in-timeframe-p (tf weekday time)
"Checks if weekday and time belong to timeframe"
(let ((hours (timeframe-hour-ranges tf))
(days (timeframe-days tf)))
(and (if days
(member weekday days)
t)
(if hours
(loop for (start end) in hours
when (<= start time end) return t)
t))))
(defun include-p (timewindow year month day hours minutes seconds)
"Checks if date and time belong to timewindow"
(assert (<= 0 seconds 59) (seconds))
(assert (<= 0 minutes 59) (minutes))
(assert (<= 0 hours 23) (hours))
(let ((time (+ (* hours 10000) (* minutes 100) seconds))
(day (nth (day-of-week day month year) *weekdays*)))
(loop for tf in timewindow
when (in-timeframe-p tf day time) return t)))
;;; Utility
;;; borrowed from cl-cookbook:
(defun day-of-week (day month year)
"Returns the day of the week as an integer.
Monday is 0."
(nth-value 6 (decode-universal-time
(encode-universal-time 0 0 0 day month year 0)
0)))
;; Usage:
(include-p (create-timewindow
"Sat-Sun; Mon Wed 0700-0900; Thu 0700-0900 1000-1200")
2007 10 25 8 0 0)