On Aug 25, 2006, at 9:01 AM, Ruby Quiz wrote:
> This is not intended to be a difficult quiz, but I think the  
> solutions would be
> useful in many situations, especially in web applications. The  
> solution I have
> come up with works and is relatively fast (fast enough for my  
> purposes anyway),
> but isn't very elegant. I'm very interested in seeing how others  
> approach the
> problem.

It wasn't difficult because I could call on Array, Hash, Range,  
Regexp, and Enumerable to do the heavy lifting. It wouldn't be  
pleasant to write this in C using just the standard libraries. As for  
speed, I don't see that as much of an issue (and I didn't try to make  
my code fast) because I can't see myself using this in a situation  
where it would be evaluated at high frequency. As for elegance --  
elegance is in the eye of the beholder :)

The only bell (or is it a whistle?) I've added is a flag that  
controls whether or not day names are printed in long or short form  
by to_s. I've taken a fairly permissive approach on what arguments  
DayRange#initialize accepts. Arguments may be repeated or given in no  
particular order.

<code>
#! /usr/bin/ruby -w
# Author: Morton Goldberg
#
# Date: August 27, 2006
#
# Ruby Quiz #92 -- DayRange
class DayRange

    DAY_DIGITS = {
       'mon' => 1,
       'tue' => 2,
       'wed' => 3,
       'thu' => 4,
       'fri' => 5,
       'sat' => 6,
       'sun' => 7,
       'monday' => 1,
       'tuesday' => 2,
       'wednesday' => 3,
       'thursday' => 4,
       'friday' => 5,
       'saturday' => 6,
       'sunday' => 7,
       '1' => 1,
       '2' => 2,
       '3' => 3,
       '4' => 4,
       '5' => 5,
       '6' => 6,
       '7' => 7
    }

    SHORT_NAMES = [nil, 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

    LONG_NAMES = [ nil, 'Monday', 'Tuesday', 'Wednesday',
                  'Thursday', 'Friday', 'Saturday', 'Sunday']

    # Return day range as nicely formatted string.
    # If @long is true, day names appear in long form; otherwise, they
    # appear in short form.
    def to_s
       names = @long ? LONG_NAMES : SHORT_NAMES
       result = []
       @days.each do |d|
          case d
          when Integer
             result << names[d]
          when Range
             result << names[d.first] + "-" + names[d.last]
          end
       end
       result.join(", ")
    end

    # Return day range as array of integers.
    def to_a
       result = @days.collect do |d|
          case d
          when Integer then d
          when Range then d.to_a
          end
       end
       result.flatten
    end

    def initialize(*args)
       @days = []
       @long = false
       @args = args
       @args.each do |arg|
          case arg
          when Integer
             bad_arg if arg < 1 || arg > 7
             @days << arg
          when /^(.+)-(.+)$/
             begin
                d1 = DAY_DIGITS[$1.downcase]
                d2 = DAY_DIGITS[$2.downcase]
                bad_arg unless d1 && d2 && d1 <= d2
                d1.upto(d2) {|d| @days << d}
             rescue StandardError
                bad_arg
             end
          else
             d = DAY_DIGITS[arg.downcase]
             bad_arg unless d
             @days << d
          end
       end
       @days.uniq!
       @days.sort!
       normalize
    end

# Use this change printing behavior from short day names to long day  
names
# or vice-versa.
attr_accessor :long

private

    # Convert @days from an array of digits to normal form where runs of
    # three or more consecutive digits appear as ranges.
    def normalize
       runs = []
       first = 0
       for k in 1... / days.size
          unless @days[k] == @days[k - 1].succ
             runs << [first, k - 1] if k - first > 2
             first = k
          end
       end
       runs << [first, k] if k - first > 1
       runs.reverse_each do |r|
          @days[r[0]..r[1]] = @days[r[0]]..@days[r[1]]
       end
    end

    def bad_arg
       raise(ArgumentError,
             "Can't create a DayRange from #{@args.inspect}")
    end

end

if $0 == __FILE__
    # The following should succeed.
    days = DayRange.new("mon-wed", "thursday", 7)
    puts days
    days.long = true
    puts days
    p days.to_a
    puts

    days = DayRange.new("friday-fri", "mon-monday")
    puts days
    days.long = true
    puts days
    p days.to_a
    puts

    days = DayRange.new("mon", 7, "thu-fri")
    puts days
    days.long = true
    puts days
    p days.to_a
    puts

    days = DayRange.new("2-7")
    puts days
    days.long = true
    puts days
    p days.to_a
    puts

    days = DayRange.new(1, 2, 1, 2, 3, 3)
    puts days
    days.long = true
    puts days
    p days.to_a
    puts

    args = (1..4).to_a.reverse
    days = DayRange.new(*args)
    puts days
    days.long = true
    puts days
    p days.to_a
    puts

    # The following should fail.
    begin
       DayRange.new("foo")
    rescue StandardError=>err
       puts err.message
       puts
    end
    begin
       DayRange.new("foo-bar")
    rescue StandardError=>err
       puts err.message
       puts
    end
    begin
       DayRange.new("sat-mon")
    rescue StandardError=>err
       puts err.message
       puts
    end
    begin
       args = (0..4).to_a.reverse
       DayRange.new(*args)
    rescue StandardError=>err
       puts err.message
       puts
    end
end
</code>

<result>
Mon-Thu, Sun
Monday-Thursday, Sunday
[1, 2, 3, 4, 7]

Mon, Fri
Monday, Friday
[1, 5]

Mon, Thu, Fri, Sun
Monday, Thursday, Friday, Sunday
[1, 4, 5, 7]

Tue-Sun
Tuesday-Sunday
[2, 3, 4, 5, 6, 7]

Mon-Wed
Monday-Wednesday
[1, 2, 3]

Mon-Thu
Monday-Thursday
[1, 2, 3, 4]

Can't create a DayRange from ["foo"]

Can't create a DayRange from ["foo-bar"]

Can't create a DayRange from ["sat-mon"]

Can't create a DayRange from [4, 3, 2, 1, 0]
</result>

Regards, Morton