[QUIZ] DayRange (#92)

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 Bryan Donovan

If you've ever created a web application that deals with scheduling recurring
events, you may have found yourself creating a method to convert a list of days
into a more human-readable string.

For example, suppose a musician plays at a certain venue on Monday, Tuesday,
Wednesday, and Saturday. You could pass a list of associated day numbers to your
object or method, which might return "Mon-Wed, Sat".

The purpose of this quiz is to find the best "Ruby way" to generate this
sentence-like string.

Basically, the rules are:

  * The class's constructor should accept a list of arguments that can be day
    numbers (see day number hash below), day abbreviations ('Mon', 'Tue', etc.),
    or the full names of the days ('Monday', 'Tuesday', etc.).
  * If an invalid day id is included in the argument list, the constructor
    should raise an ArgumentError.
  * The days should be sorted starting with Monday.
  * Three or more consecutive days should be represented by listing the first
    day followed by a hyphen (-), followed by the last day of the range.
  * Individual days and the above day ranges should be separated by commas.
  * The class should number days (accepting Integers or Strings) as follows:
      1: Mon
      2: Tue
      3: Wed
      4: Thu
      5: Fri
      6: Sat
      7: Sun
  * The class needs a method named #to_s that returns the day range string.
    Here are some example lists of days and their expected returned strings:
      1,2,3,4,5,6,7: Mon-Sun
      1,2,3,6,7: Mon-Wed, Sat, Sun
      1,3,4,5,6: Mon, Wed-Sat
      2,3,4,6,7: Tue-Thu, Sat, Sun
      1,3,4,6,7: Mon, Wed, Thu, Sat, Sun
      7: Sun
      1,7: Mon, Sun
      1,8: ArgumentError

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.

Request for clarification.

Are you asking us to define a class or a top-level conversion method?

If it's a class you want, beside initialize, what methods are you asking for?

My guess is that you expect a 'to_s' that returns the human-readable form and a 'to_a' that returns an array of day numbers. Is this correct? Also, should 'initialize' accept mixed argument sequences? For example, which of following do you consider valid argument lists for 'initialize'?

('Mon-Wednesday', 5)
('1-Wed', 5)
([1, 2, 3], 'Fri')
(1, 2, 3, 'Fri')
('1-3', 'Fri')

Regards, Morton

···

On Aug 25, 2006, at 9:01 AM, Ruby Quiz wrote:

by Bryan Donovan

If you've ever created a web application that deals with scheduling recurring
events, you may have found yourself creating a method to convert a list of days
into a more human-readable string.

For example, suppose a musician plays at a certain venue on Monday, Tuesday,
Wednesday, and Saturday. You could pass a list of associated day numbers to your
object or method, which might return "Mon-Wed, Sat".

The purpose of this quiz is to find the best "Ruby way" to generate this
sentence-like string.

Basically, the rules are:

  * The class's constructor should accept a list of arguments that can be day
    numbers (see day number hash below), day abbreviations ('Mon', 'Tue', etc.),
    or the full names of the days ('Monday', 'Tuesday', etc.).
  * If an invalid day id is included in the argument list, the constructor
    should raise an ArgumentError.
  * The days should be sorted starting with Monday.
  * Three or more consecutive days should be represented by listing the first
    day followed by a hyphen (-), followed by the last day of the range.
  * Individual days and the above day ranges should be separated by commas.
  * The class should number days (accepting Integers or Strings) as follows:
      1: Mon
      2: Tue
      3: Wed
      4: Thu
      5: Fri
      6: Sat
      7: Sun
  * The class needs a method named #to_s that returns the day range string.
    Here are some example lists of days and their expected returned strings:
      1,2,3,4,5,6,7: Mon-Sun
      1,2,3,6,7: Mon-Wed, Sat, Sun
      1,3,4,5,6: Mon, Wed-Sat
      2,3,4,6,7: Tue-Thu, Sat, Sun
      1,3,4,6,7: Mon, Wed, Thu, Sat, Sun
      7: Sun
      1,7: Mon, Sun
      1,8: ArgumentError

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.

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 Bryan Donovan

If you've ever created a web application that deals with scheduling recurring
events, you may have found yourself creating a method to convert a list of days
into a more human-readable string.

For example, suppose a musician plays at a certain venue on Monday, Tuesday,
Wednesday, and Saturday. You could pass a list of associated day numbers to your
object or method, which might return "Mon-Wed, Sat".

The purpose of this quiz is to find the best "Ruby way" to generate this
sentence-like string.

Basically, the rules are:

  * The class's constructor should accept a list of arguments that can be day
    numbers (see day number hash below), day abbreviations ('Mon', 'Tue', etc.),
    or the full names of the days ('Monday', 'Tuesday', etc.).
  * If an invalid day id is included in the argument list, the constructor
    should raise an ArgumentError.
  * The days should be sorted starting with Monday.
  * Three or more consecutive days should be represented by listing the first
    day followed by a hyphen (-), followed by the last day of the range.
  * Individual days and the above day ranges should be separated by commas.
  * The class should number days (accepting Integers or Strings) as follows:
      1: Mon
      2: Tue
      3: Wed
      4: Thu
      5: Fri
      6: Sat
      7: Sun
  * The class needs a method named #to_s that returns the day range string.
    Here are some example lists of days and their expected returned strings:
      1,2,3,4,5,6,7: Mon-Sun
      1,2,3,6,7: Mon-Wed, Sat, Sun
      1,3,4,5,6: Mon, Wed-Sat
      2,3,4,6,7: Tue-Thu, Sat, Sun
      1,3,4,6,7: Mon, Wed, Thu, Sat, Sun
      7: Sun
      1,7: Mon, Sun
      1,8: ArgumentError

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.

What about the handling of day ranges that wrap, such as 1, 5, 6, and 7? Do you want Monday, Friday-Sunday, or the more logical Friday-Monday?

This is one of the problems that looks easier than it is
(at least to me). My solution handles input in form of day
abbreviations, full day names and integers as described in
the quiz. It raises an ArgumentError for all other inputs.

Further more my solution wraps around:

puts DayRange.new('Monday', 'Sun', 5, 2, 6)

results in "Fri-Tue" instead of "Mon, Tue, Fri-Sun"
(this can be easily switched of by deleting the last two
lines of the initialize method).

I also included Enumerable and provided an each method, but
i'm not sure if this is really helpful because each iterates
over each day not each range (would that be more helpful?).

Well, here it is:

···

--------------------------------------------------------------
require 'date'

class DayRange
  include Enumerable
  def initialize *days
    @days = []
    days.map do |d|
      day = Date::DAYNAMES.index(d) || Date::ABBR_DAYNAMES.index(d)
      raise ArgumentError, d.to_s unless day || (1..7).include?(d.to_i)
      day ? day.nonzero? || 7 : d.to_i
    end.uniq.sort.each do |d|
      next @days << [d] if @days.empty? || d != @days.last.last + 1
      @days.last << d
    end
    p @days
    return unless @days.first.first == 1 && @days.last.last == 7
    @days.last.concat(@days.shift) if @days.size > 1
  end

  def each
    @days.flatten.each{|d| yield d}
  end

  def to_s
    @days.map do |r|
      first = Date::ABBR_DAYNAMES[r.first % 7]
      last = Date::ABBR_DAYNAMES[r.last % 7]
      next "#{first}, #{last}" if r.size == 2
      r.size > 2 ? "#{first}-#{last}" : first
    end * ', '
  end
end

puts DayRange.new(1, 2, 3, 4, 5, 6, 7) #=> Mon-Sun
puts DayRange.new('Monday', 'Sun', 5, 2, 6) #=> Fri-Tue
puts DayRange.new(2, 6, 'Friday', 'Sun') #=> Tue, Fri-Sun

dr = DayRange.new(2, 6, 'Friday', 'Sun')
puts dr.map{|d| Date::DAYNAMES[d % 7]} * ', '
#=> Tuesday, Friday, Saturday, Sunday
-----------------------------------------------------------------

cheers

Simon

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

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

···

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.

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

Here is my solution. It took me a few tries to find an elegant
solution for the to_s function, but i particularly like this one :slight_smile:

require 'facets/core/enumerable/map_with_index'
class DayRange
  DAY_NAMES = {'Monday'=>1,'Tuesday'=>2,'Wednesday'=>3,'Thursday'=>4,'Friday'=>5,'Saturday'=>6,'Sunday'=>7}
  DAY_ABBR = {'Mon'=>1 ,'Tue'=>2 ,'Wed'=>3 ,'Thu'=>4
,'Fri'=>5 ,'Sat'=>6 ,'Sun'=>7 }
  def initialize(*days)
    @days = days.map{|d| DAY_NAMES[d] || DAY_ABBR[d] || d.to_i }.uniq.sort
    raise ArgumentError, 'Invalid day' unless @days.all?{|d| (1..7).include? d }
  end

  def to_s
    @days.map_with_index{|d,ix| DAY_ABBR.invert[d] unless
@days[ix+1]==@days[ix-1]+2 }.join(', ').gsub(/(, ){2,}/,'-')
  end
end

if __FILE__==$0

require 'test/unit'
class DayRangeTests < Test::Unit::TestCase
  def test_init
    assert_raise(ArgumentError) { DayRange.new(1,2,3,5,8) }
    assert_raise(ArgumentError) { DayRange.new(1,'Mon','foo') }
    assert_nothing_raised {
      DayRange.new()
      DayRange.new(1,1,2,'Wed','Monday','Fri','Tue',3,4,'Friday',5)
    }
  end

  def test_to_s
    assert_equal 'Mon', DayRange.new('Mon').to_s
    assert_equal 'Mon-Wed', DayRange.new(1,1,2,'Wed').to_s
    assert_equal 'Mon-Sun', DayRange.new(*1..7).to_s
    assert_equal 'Mon-Wed, Fri-Sun',
DayRange.new('Monday',2,3,'Sunday',6,6,5).to_s
    assert_equal 'Mon, Wed, Fri, Sun',
DayRange.new('Mon','Sun','Wed','Sunday',7,5).to_s
  end
end

end

Hello quizzers,

My solution ended up being something of an ode to the Enumerable#inject method
(so elegant!), and a chance to implement the flyweight design pattern (for the
Day class). I also named my class 'DaySet', which seemed more accurate to me
than 'DayRange', and hopefully won't invalidate my solution ;-).

The appended test suite made the file a touch long, so I've included it as an
attachment.

-Marshall

day_set.rb (4.15 KB)

Here's my short solution.
It uses the values from Time.strftime to generate abbreviations.
t handles input as a list of numbers, or comma separated strings, or a
mixture of both.
For example
p DayRange.new(1,"Tuesday","Wed",4,"Sat,Sun").to_s #=>"Mon-Thu, Sat, Sun"

···

On 8/25/06, Ruby Quiz <james@grayproductions.net> wrote:

The purpose of this quiz is to find the best "Ruby way" to generate this
sentence-like string.

-------
class DayRange
  def initialize *l
    dnames = (1..7).map{|i|Regexp.new(dayname(i).slice(0,3),Regexp::IGNORECASE)}<</.*/

    l=l.map{|v|v.respond_to?(:split) ? v.split(',') : v}.flatten
    @range=l.map{|v|
      (n=v.to_i)>0 ? n : (1..8).find{|i|dnames[i-1]=~v.to_s}}.sort
    raise "ArgumentError" if @range[-1]>7
  end
  def dayname n
    Time.gm(1,1,n).strftime("%a")
  end
  def to_s
    l=9
    s = @range.map{|e|"#{"-" if e==l+1}#{dayname(l=e)}"}.join(', ')
    s.gsub(/(, -\w+)+, -/,'-').gsub(/ -/,' ')
  end
end

In article <20060825130122.XQZJ22014.centrmmtao02.cox.net@eastrmimpo01.cox.net>,
  Ruby Quiz <james@grayproductions.net> writes:

one remark:
I don't like the suggested interface (though I used and didn't work out an
alternative). It does not seem reasonable to instanciate an object providing
all the information in the constructor and the only thing one can do with
that object is call one method once (multiple calls to to_s should be avoided
for performance).
Of course one could extend that api providing additional means that justify
the use of a class, but as is, it does not make sense to me. A simple function
(probably packed into a module for namespace reasons) would do the job just
as good. Or do I miss something?
I just added this remark since the quiz explicitly asks for »the best "Ruby
way"«.

My solutions takes a list of days which may be grouped to a list of arrays.
Duplicate entries are removed. The to_s method provides an optional parameter
to output full day names.

#! /usr/bin/ruby

class Array
  # split array into array of contiguous slices
  # a slice is contiguous if each item value is the successor of the
  # value of the previous item
  def split_contiguous()
    self.inject( [ [] ] ) do | list, item |
      list[-1].empty? || list[-1][-1].succ == item ?
        list[-1] << item : list << [ item ]
      list
    end
  end
end

class DayRange
  # define weekday names as constants
  @@WEEKDAY = [ nil, 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ]
  @@FULLWEEKDAY = [ nil, 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ]

  # prepare for fast weekday to day of week resolution
  @@DAYOFWEEK = {}
  @@WEEKDAY[1,7].each_with_index { | day,idx | @@DAYOFWEEK[day] = idx + 1 }
  @@FULLWEEKDAY[1,7].each_with_index { | day,idx | @@DAYOFWEEK[day] = idx + 1 }

  # take a list of objects or arrays of objects and convert them to an
  # unique sorted array of day of week numbers
  def initialize( *days )
    @days = days.flatten.collect do | day0 |
      day = @@DAYOFWEEK[day0] || day0.to_i # allow for non integer input
      raise ArgumentError.new(day0.inspect) if day < 1 or day > 7 # check input
      day
    end.sort.uniq
  end

  # provide a list of weekdays or weekday ranges
  def day_range( full = false )
    weekday = full ? @@FULLWEEKDAY : @@WEEKDAY
    @days.split_contiguous.inject( [] ) do | list, range |
      list << ( range.size <= 2 ? weekday[range[0]] :
     weekday[range[0]] + '-' + weekday[range[-1]] )
      list << weekday[range[1]] if range.size == 2
      list
    end
  end

  def to_s( full = false )
    day_range(full).join(', ')
  end
end

puts DayRange.new( 1,'Tue',3,4,5,6,7 ).to_s(true)
puts DayRange.new(1,2,3,4,5,6,7)
puts DayRange.new(1,2,3,6,7)
puts DayRange.new(1,3,4,5,6)
puts DayRange.new(2,3,4,6,7)
puts DayRange.new(1,3,4,6,7)
puts DayRange.new(7)
puts DayRange.new(1,7)
puts DayRange.new(1,8)

Hi,

Here's my solution (tests included). It's a little long, but I wanted it to have these features:

* Input can consist of numbers and day name abbreviations of any length
* It is easy to make a localised version (a German one is included)
* Length of abbreviated output names can be specified

Cheers,
   Robin

···

----

require 'abbrev'
require 'test/unit'

class DayRange

   def self.use_day_names(week, abbrev_length=3)
     @day_numbers = {}
     @day_abbrevs = {}
     week.abbrev.each do |abbr, day|
       num = week.index(day) + 1
       @day_numbers[abbr] = num
       if abbr.length == abbrev_length
         @day_abbrevs[num] = abbr
       end
     end
   end

   use_day_names \
     %w(Monday Tuesday Wednesday Thursday Friday Saturday Sunday)

   def day_numbers; self.class.class_eval{ @day_numbers } end
   def day_abbrevs; self.class.class_eval{ @day_abbrevs } end

   attr_reader :days

   def initialize(days)
     @days = days.collect{ |d| day_numbers[d] or d }
     if not (@days - day_abbrevs.keys).empty?
       raise ArgumentError
     end
   end

   def to_s
     ranges = []
     number_ranges.each do |range|
       case range[1] - range[0]
       when 0; ranges << day_abbrevs[range[0]]
       when 1; ranges.concat day_abbrevs.values_at(*range)
       else ranges << day_abbrevs.values_at(*range).join('-')
       end
     end
     ranges.join(', ')
   end

   def number_ranges
     @days.inject([]) do |l, d|
       if l.last and l.last[1] + 1 == d
         l.last[1] = d
       else
         l << [d, d]
       end
       l
     end
   end

end

class DayRangeGerman < DayRange
   use_day_names \
     %w(Montag Dienstag Mittwoch Donnerstag Freitag Samstag Sonntag), 2
end

class DayRangeTest < Test::Unit::TestCase

   def test_english
     tests = {
       [1,2,3,4,5,6,7] => 'Mon-Sun',
       [1,2,3,6,7] => 'Mon-Wed, Sat, Sun',
       [1,3,4,5,6] => 'Mon, Wed-Sat',
       [2,3,4,6,7] => 'Tue-Thu, Sat, Sun',
       [1,3,4,6,7] => 'Mon, Wed, Thu, Sat, Sun',
       [7] => 'Sun',
       [1,7] => 'Mon, Sun',
       %w(Mon Tue Wed) => 'Mon-Wed',
       %w(Frid Saturd Sund) => 'Fri-Sun',
       %w(Monday Wednesday Thursday Friday) => 'Mon, Wed-Fri',
       [1, 'Tuesday', 3] => 'Mon-Wed'
     }
     tests.each do |days, expected|
       assert_equal expected, DayRange.new(days).to_s
     end
   end

   def test_german
     tests = {
       [1,2,3,4,5,6,7] => 'Mo-So',
       [1,2,3,6,7] => 'Mo-Mi, Sa, So',
       [1,3,4,5,6] => 'Mo, Mi-Sa',
       [2,3,4,6,7] => 'Di-Do, Sa, So',
       [1,3,4,6,7] => 'Mo, Mi, Do, Sa, So',
       [7] => 'So',
       [1,7] => 'Mo, So',
       %w(Mo Di Mi) => 'Mo-Mi',
       %w(Freit Samst Sonnt) => 'Fr-So',
       %w(Montag Mittwoch Donnerstag Freitag) => 'Mo, Mi-Fr',
       [1, 'Dienstag', 3] => 'Mo-Mi'
     }
     tests.each do |days, expected|
       assert_equal expected, DayRangeGerman.new(days).to_s
     end
   end

   def test_translation
     eng = %w(Mon Tue Wed Fri)
     assert_equal 'Mo-Mi, Fr',
                  DayRangeGerman.new(DayRange.new(eng).days).to_s
   end

   def test_should_raise
     assert_raise ArgumentError do
       DayRange.new([1, 8])
     end
   end

end

I found this problem deceptively difficult. I had the bulk of the class
written in a few minutes, and the other 90% of the time I spent on this was
on the to_s method. I just couldn't figure it out. I finally came up with a
solution, but I do not think it is particularly nice.

The constructor can take numbers, abbreviated names (Mon, Tue etc) and also
full names. The only catch is that they must be passed in an array.

The "lang" argument allows you to return the result in a different language (I
coded for En, Fr, De, It, and Es). If you pass a non-nil value to the "form"
argument it will return full names rather than abbreviations.

If you want to change the "form" you must also pass the "lang". This is where
having named args not dependant on position would be very useful...Why are
there none?

Anyway, here is a sample irb session followed by the code:

···

###################################

days = DayRange.new([1,2,3,4,5,6,7])

...

days.to_s

=> "Mon-Sun"

days = DayRange.new(["Mon","Wed","Fri"], lang='en', form=1)

...

days.to_s

=> "Monday, Wednesday, Friday"

days = DayRange.new([1,5,6,7], lang='es')

...

days.to_s

=> "lun, vie-dom"

days = DayRange.new([1,4,5,6], lang='de', form=1)

...

days.to_s

=> "Montag, Donnerstag-Samstag"
# add another language:

class DayRange

1> def day_no
2> [['Man','Mandag'],['Tir','Tirsdag'],['Ons','Onsdag'],
['Tor','Torsdag'], ['Fre','Fredag'],['Lør','Lørdag'],['Søn','Søndag']]
2> end
1> end
=> nil

days = DayRange.new([1,2,3,4,5,6,7], lang='no', form=1)

...

days.to_str # different than 'to_s'

=> "Mandag Tirsdag Onsdag Torsdag Fredag Lørdag Søndag"
...
###################################
# Quiz 92 - Day Range

class DayRange
  def initialize(days, lang='en', form=nil)
    form == nil ? @type = 0 : @type = 1 # Abbreviated or full name?
    @day_str_array = send "day_#{lang}" # 'lang' one of: en fr de es it
    @day_num_array = Array.new
    @days = days
    parse_args
  end

  def to_s
    s = String.new

    # Offset is the difference between numeric day values
    offset = Array.new
    f = @day_num_array[0]
    @day_num_array[1..-1].each do |n|
      offset << n - f
      f = n
    end

    s += "#{@day_str_array[@day_num_array[0]-1][@type]} "
    @day_num_array[1..-1].each_with_index do |v,i|
      if i < @day_num_array[1..-1].size
        if offset[i] == 1 and offset[i+1] == 1 # Found a range?
          s += "-" unless s[-1] == 45 # "-"
          next # then move along...
        else
          s += " #{@day_str_array[v-1][@type]}" # otherwise add the name.
          next
        end
      else
        s += " #{@day_str_array[i][@type]}"
      end
    end
    # cleanup and return string
    s.gsub!(" -","-")
    s.gsub!("- ","-")
    s.gsub!(/ {2,}/," ")
    s.gsub!(" ",", ")
    s
  end

  # Maybe you just want the day names
  def to_str
    s = String.new
    @day_num_array.each { |n| s += "#{@day_str_array[n-1][@type]} " }
    s.strip!
  end

  # Maybe you want them in an array
  def to_a
    a = Array.new
    @day_num_array.each { |n| a << @day_str_array[n-1][@type] }
    a
  end

  private
  def parse_args
    if @days[0].class == Fixnum
      @day_num_array = @days.sort!
      if @day_num_array[-1] > 7
        raise ArgumentError, "Argument out of range: #{@day_num_array[-1]}"
      end
    else
      @days.each do |d|
        if @day_str_array.flatten.include?(d)
          indice = case @day_str_array.flatten.index(d)
            when 0..1: 1
            when 2..3: 2
            when 4..5: 3
            when 6..7: 4
            when 8..9: 5
            when 10..11: 6
            when 12..13: 7
          end
          @day_num_array << indice
        else
          raise ArgumentError, "Bad argument: #{d}"
        end
      end
      @day_num_array.sort!
    end
  end

  def day_en
    [['Mon','Monday'],['Tue','Tuesday'],['Wed','Wednesday'],
['Thu','Thursday'],
     ['Fri','Friday'],['Sat','Saturday'],['Sun','Sunday']]
  end

  def day_fr
    [['lun','lundi'],['mar','mardi'],['mer','mercredi'],['jeu','jeudi'],
     ['ven','vendredi'],['sam','samedi'],['dim','dimanche']]
  end

  def day_es
    [['lun','lunes'],['mar','martes'],['mie','miércoles'],['jue','jueves'],
     ['vie','viernes'],['sab','sábado'],['dom','domingo']]
  end

  def day_de
    [['Mon','Montag'],['Die','Dienstag'],['Mit','Mittwoch'],
['Don','Donnerstag'],
     ['Fre','Freitag'],['Sam','Samstag'],['Son','Sonntag']]
  end

  def day_it
    [['lun','lunedì'],['mar','martedì'],['mer','mercoledì'],['gio','giovedì'],
     ['ven','venerdì'],['sab','sabato'],['dom','domenica']]
  end

end
##################################

-d
--
darren kirby :: Part of the problem since 1976 :: http://badcomputer.org
"...the number of UNIX installations has grown to 10, with more expected..."
- Dennis Ritchie and Ken Thompson, June 1972

Hi,

my solution is definitely over-engineered :slight_smile:

There are three classes:
WeekDay: a day of the week
DayRange: a range between two days of the week
DayRangeArray: a list of DayRanges.

The DayRangeArray constructor does the work of splitting a list
of days into DayRange instances.

Regards,
Boris

### day_range.rb
require 'date'

# A day of the week. In calculations and comparisons a WeekDay behaves
# like an integer with 1=Monday, ..., 7=Sunday.
class WeekDay
   # A WeekDay can be constructed from a number between 1 and 7 or a
   # string like 'mon' or 'monday'.
   def initialize(arg)
     case arg
     when Fixnum
       if arg < 1 or arg > 7
         raise ArgumentError.new("day number must be between 1 and 7")
       end
       @daynum = arg
     when WeekDay
       @daynum = arg.to_i
     else
       s = arg.to_s.downcase
       if Date::ABBR_DAYS.has_key?(s)
         @daynum = Date::ABBR_DAYS[s]
       elsif Date::DAYS.has_key?(s)
         @daynum = Date::DAYS[s]
       else
         raise ArgumentError.new("#{s} is not a day")
       end
       @daynum = 7 if @daynum == 0
     end
   end

   # Returns the abbreviated name of the day (e.g. 'Mon')
   def to_s
   end

   # Returns the number of the day (1=Monday, ..., 7=Sunday)
   def to_i
     @daynum
   end

   %w{== <=> + - >}.each do |meth|
     define_method meth do |other|
       self.to_i.send(meth, other.to_i)
     end
   end
end

# A Range of days between two days of the week.
class DayRange < Range
   # The first and last day of the range can be given as instances of
   # class WeekDay, numbers or strings.
   def initialize(from, to, exclusive=false)
     from_day = WeekDay.new(from)
     to_day = WeekDay.new(to)
     super(from_day, to_day, exclusive)
   end

   # Returns a string representation of the range. Two consecutive days
   # are returned as a list, e.g. 'Mon, Tue'.
   def to_s
     from = self.begin.to_s
     to = self.end.to_s

     case self.end - self.begin
     when 0 then return from
     when 1 then return from + ', ' + to
     else return from + '-' + to
     end
   end
end

# An array containing several DayRange instances.
class DayRangeArray < Array
   private
   def normalize_days days
     days.collect{|d| WeekDay.new(d)}.sort.uniq
   end

   # Given a list of days (as numbers or strings), an array of
   # DayRanges is created.
   def initialize(*days)
     return if days.size == 0

     a = normalize_days(days)
     first = a.first

     1.upto(a.size - 1) do |i|
       if a[i] > a[i-1] + 1
         self << DayRange.new(first, a[i-1])
         first = a[i]
       end
     end
     self << DayRange.new(first, a.last)
   end

   public
   # The DayRanges are separated by comma. For example:
   # DayRangeArray.new(1, 2, 3, 5).to_s # => "Mon-Wed, Fri"
   def to_s
     self.join(', ')
   end
end

### day_range_test.rb
require 'day_range'
require 'test/unit'

class DayRangeTest < Test::Unit::TestCase
   def test_new
     dr = DayRange.new(1, 5)
     assert_equal WeekDay.new(1), dr.begin
     assert_equal WeekDay.new(5), dr.end
   end

   def test_argument_error
     assert_raise(ArgumentError) { DayRange.new(1, 8) }
     assert_raise(ArgumentError) { DayRange.new(0, 3) }
     assert_raise(ArgumentError) { DayRange.new(-1, 3) }

     assert_raise(ArgumentError) { DayRangeArray.new(1, 8) }
     assert_raise(ArgumentError) { DayRangeArray.new('funday') }
   end

   def test_to_s
     assert_equal 'Fri-Sun', DayRange.new(5, 7).to_s
     assert_equal 'Mon-Fri', DayRange.new(1, 5).to_s
     assert_equal 'Wed', DayRange.new(3, 3).to_s
     assert_equal 'Mon, Tue', DayRange.new(1, 2).to_s
   end

   def test_day_range_list
     exp = {
       [1,2,3,4,5,6,7] => 'Mon-Sun',
       [1,2,3,6,7] => 'Mon-Wed, Sat, Sun',
       [1,3,4,5,6] => 'Mon, Wed-Sat',
       [2,3,4,6,7] => 'Tue-Thu, Sat, Sun',
       [1,3,4,6,7] => 'Mon, Wed, Thu, Sat, Sun',
       [7] => 'Sun',
       [1,7] => 'Mon, Sun',
       [7,6,7,4,3] => 'Wed, Thu, Sat, Sun',
       [] => '',
       ['mon', 'Tuesday', 'WED', 5, 'saturday', 'sUnDaY'] => 'Mon-Wed, Fri-Sun'
     }

     exp.each do |list, string|
       assert_equal string, DayRangeArray.new(*list).to_s
     end
   end
end

···

Date::ABBR_DAYNAMES[@daynum % 7]

After looking through some of the other solutions to this quiz, I decided to adapt some of the good ideas I found to improve my own solution. Using Test::Unit to for testing was the first modification I wanted to make. I have never used Test::Unit up to now. I thought it was high time for me to do so.

Since changing over to Test::Unit doesn't really change my solution, I didn't plan to repost it after making the change. I changed my mind because I ran into something with Test::Unit that really surprised me. First, the modified code and then some more discussion.

<code>
require 'test/unit'

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 = %w[_ Mon Tue Wed Thu Fri Sat Sun].freeze

    LONG_NAMES = %w[_ Monday Tuesday Wednesday Thursday
                    Friday Saturday Sunday].freeze

    # 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 to 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

class TestDayRange < Test::Unit::TestCase

    # All these produce @days == [1..7].
    ONE_RANGE = [
       %w[mon tue wed thu fri sat sun],
       %w[monDay tuesday Wednesday Thursday friDAY saturday SUNDAY],
       %w[mon-fri sat-sun],
       %w[4-7 1-3],
       (1..7).to_a.reverse,
       [4, 7, 6, 5, 4, 1, 2, 1, 2, 3, 3, 7, 6, 5],
    ]

    # Both these produce @days == [1..3, 5..7].
    TWO_RANGES = [
       %w[mon-mon tue-tue wed-wed fri-sun],
       [1, 2, 'mon-wed', 'friday', 6, 7]
    ]

    INVALID_ARGS = [
       [1, 2, 'foo'],
       %w[foo-bar],
       %w[sat-mon],
       (0..7).to_a.reverse,
       (1..8).to_a
    ]

    @@one_range = []
    @@two_ranges = []

    def test_args_helper(args, expected)
       obj = nil
       assert_nothing_raised(ArgumentError) {obj = DayRange.new(*args)}
       assert_equal(expected, obj.instance_variable_get(:@days))
       obj
    end

    def test_valid_args
       ONE_RANGE.each do |args|
          @@one_range << test_args_helper(args, [1..7])
       end
       TWO_RANGES.each do |args|
          @@two_ranges << test_args_helper(args, [1..3, 5..7])
       end
       puts "test_valid_args -- #{@@one_range.size}, #{@@two_ranges.size}"
    end

    def test_bad_args
       puts "test_bad_args"
       INVALID_ARGS.each do |args|
          assert_raise(ArgumentError) {DayRange.new(*args)}
       end
    end

    def test_to_s
       puts "test_to_s -- #{@@one_range.size}, #{@@two_ranges.size}"
       @@one_range.each do |obj|
          assert_equal('Mon-Sun', obj.to_s)
       end
       @@two_ranges.each do |obj|
          assert_equal('Mon-Wed, Fri-Sun', obj.to_s)
       end
    end

    def test_to_a
       puts "test_to_a -- #{@@one_range.size}, #{@@two_ranges.size}"
       @@one_range.each do |obj|
          assert_equal((1..7).to_a, obj.to_a)
       end
       @@two_ranges.each do |obj|
          assert_equal([1, 2, 3, 5, 6, 7], obj.to_a)
       end
    end

end
</code>

<result>
Loaded suite /Users/mg/Projects/Ruby/Ruby Quiz/Quiz 92/quiz_92
Started
test_bad_args
.test_to_a -- 0, 0
.test_to_s -- 0, 0
.test_valid_args -- 6, 2
.
Finished in 0.010845 seconds.

4 tests, 21 assertions, 0 failures, 0 errors
</result>

None of the assertions in test_to_s and test_to_a execute. This is because @@one_range and @@two_ranges are both empty when these tests are run. My plan of creating the test objects in test_valid_args and reusing them in test_to_s and test_to_a fails. This happens because the tests are run, not in the order in which they appear in the source code, but in the sort order of their names. How strange!

The fix is obvious and I have already applied it: rename test_valid_args to test_args so it runs first.

I've posted this because it took me longer to trace down this problem than it took me to write the TestDayRange class. I will consider it worth the effort if the posting helps one other person to avoid this trap. On the other hand, if I'm guilty of belaboring a well-known "feature" of Test::Unit, I apologize.

Regards, Morton

Here's my solution. It's pretty short.

Mitchell Koch

dayrange.rb (1006 Bytes)

Request for clarification.

Are you asking us to define a class or a top-level conversion method?

The quiz asks for a class, yes.

If it's a class you want, beside initialize, what methods are you asking for?

#initialize and #to_s plus anything else you deem cool.

Also, should 'initialize' accept mixed argument sequences?

I'll leave that to your judgement.

James Edward Gray II

···

On Aug 25, 2006, at 5:47 PM, Morton Goldberg wrote:

Thanks I was about to ask too.

For the time being I made it this way:

  dayrange = DayRange.new [1,3,4,5,6,7], %w{Mo Di Mi Do Fr Sa So}
  p dayrange.to_s #=> "Mo, Mi-So"

I think that's the way it should work. If not, please tell me.

Morton Goldberg schrieb:

···

Request for clarification.

Are you asking us to define a class or a top-level conversion method?

If it's a class you want, beside initialize, what methods are you asking
for?

My guess is that you expect a 'to_s' that returns the human-readable
form and a 'to_a' that returns an array of day numbers. Is this correct?
Also, should 'initialize' accept mixed argument sequences? For example,
which of following do you consider valid argument lists for 'initialize'?

('Mon-Wednesday', 5)
('1-Wed', 5)
([1, 2, 3], 'Fri')
(1, 2, 3, 'Fri')
('1-3', 'Fri')

Regards, Morton

On Aug 25, 2006, at 9:01 AM, Ruby Quiz wrote:

by Bryan Donovan

If you've ever created a web application that deals with scheduling
recurring
events, you may have found yourself creating a method to convert a
list of days
into a more human-readable string.

For example, suppose a musician plays at a certain venue on Monday,
Tuesday,
Wednesday, and Saturday. You could pass a list of associated day
numbers to your
object or method, which might return "Mon-Wed, Sat".

The purpose of this quiz is to find the best "Ruby way" to generate this
sentence-like string.

Basically, the rules are:

    * The class's constructor should accept a list of arguments that
can be day
      numbers (see day number hash below), day abbreviations ('Mon',
'Tue', etc.),
      or the full names of the days ('Monday', 'Tuesday', etc.).
    * If an invalid day id is included in the argument list, the
constructor
      should raise an ArgumentError.
    * The days should be sorted starting with Monday.
    * Three or more consecutive days should be represented by listing
the first
      day followed by a hyphen (-), followed by the last day of the
range.
    * Individual days and the above day ranges should be separated by
commas.
    * The class should number days (accepting Integers or Strings) as
follows:
        1: Mon
        2: Tue
        3: Wed
        4: Thu
        5: Fri
        6: Sat
        7: Sun
    * The class needs a method named #to_s that returns the day range
string.
      Here are some example lists of days and their expected returned
strings:
        1,2,3,4,5,6,7: Mon-Sun
        1,2,3,6,7: Mon-Wed, Sat, Sun
        1,3,4,5,6: Mon, Wed-Sat
        2,3,4,6,7: Tue-Thu, Sat, Sun
        1,3,4,6,7: Mon, Wed, Thu, Sat, Sun
        7: Sun
        1,7: Mon, Sun
        1,8: ArgumentError

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.

That's a great question. You decide what is best an implement that.

James Edward Gray II

···

On Aug 26, 2006, at 10:50 PM, Michael W. Ryder wrote:

What about the handling of day ranges that wrap, such as 1, 5, 6, and 7? Do you want Monday, Friday-Sunday, or the more logical Friday-Monday?

Nice quiz

Note to JEGII, much better like that :wink:

I agree with those who pointed out that altough not difficult the challange
is to do it nicely.

Make Ruby shine!! (hey I've never seen that one before ;).

While I found it too difficult (to read;) to wrap around my solution is
extremly forgiving for typos - which might be a bad thing (e.g. "Tuh" is
interpreted as "Tue", but might have been intended as "Thu", but that
"feature" can easily be switched off of course)

OTOH it accepts its output as input again, i.e. one can write

Days.new "Mo-Friday", 7 => "Mon-Fri,Sun"
Days.new "Sun,Mon-Fri" etc.etc

Cheers
Robert

days.rb (3.1 KB)

test_days.rb (2.34 KB)

Hello,

this is the first solution I submit. It doesn't do much more than what
was required (I added the opion to supply different output day names
after I saw Robert's post), but that it does, so it may still be
interesting.

Some simple usage examples:

puts DayRange.new([1,3,'Fri','Sat','Sunday'])
=> Mon, Wed, Fri-Sun

puts DayRange.new([1,3,'Fri','Sat','Sunday'], Date::DAYNAMES)
=> Monday, Wednesday, Friday-Sunday

puts DayRange.new([1,7])
=> Mon, Sun

puts DayRange.new([1,8])
=> ArgumentError: 8 is not a valid day id.

puts DayRange.new([1,'So'])
=> ArgumentError: So is not a valid day id.

Here is the code:

···

------------------------------------------------------------------------
require "date"

# class DayRange represents selected days of a week.
class DayRange

    ABBREVIATIONS = Date::ABBR_DAYNAMES

    FULL_NAMES = Date::DAYNAMES

    # Initialize a new DayRange.
    # Takes an array of day ids, which are either numbers (1-7),
    # three-letter abbreviations or full week-day names,
    # and optionally an array of output day names, starting with Sunday.
    def initialize list, names = ABBREVIATIONS
        @names = names
        @list = []
        list.each { |day|
            if day.class == Fixnum and 1 <= day and day <= 7
                @list << day
            elsif day.class == String and
                idx = ABBREVIATIONS.index(day) || FULL_NAMES.index(day)
                if idx == 0 then idx = 7 end
                @list << idx
            else
                raise ArgumentError, "#{day} is not a valid day id."
            end
        }
        @list.uniq!
        @list.sort!
    end

    # Return a string representation of the DayRange.
    # The representation is a comma-seperated list of output day names.
    # If more than two days are adjacent, they are represented by a range.
    def to_s
        list = to_a
        result = []
        while day = list.shift
            next_day = day + 1
            while list.first == next_day
                list.shift
                next_day += 1
            end
            if next_day <= day + 2
                result << @names[day % 7]
                if next_day == day + 2
                    result << @names[(day+1) % 7]
                end
            else
                result << (@names[day % 7] + "-" +
                           @names[(next_day-1) % 7])
            end
        end
        result.join ", "
    end

    # Return an array of the selected days of the week,
    # represented by numbers (1-7).
    def to_a
        @list.clone
    end

end

This solution does more or less the absolute minimum required by the
quiz, but it is fairly short and easy to follow, I believe. This is
my first time submitting a solution, and I'm fairly new to ruby.

Ideally, I'd like to make the class methods private, but I was having
some trouble getting that to work out. If I make them private as
things stand now, I get a no method error, "private method called" in
the constructor. I thought one could fix that by calling the class
methods without a reciever, but for some reason, when I call these
class methods without a reciever, I also get a no method error. To be
concrete, if I change the line

@string = DateRange.build_string(arr)

to

@string = build_string(arr),

when I run the unit tests below I get

NoMethodError: undefined method 'build_string' for :DateRange
    ./daterange.rb:21:in 'initialize'
    daterangetest.rb:7:in 'test_init'

Any advice anyone could offer about how to do this
private_class_method buisiness correctly would be greatly appreciated.

-- Jason Merrill

require 'test/unit'

class DateRange
  
  DAY_NUMBERS = {
          1 => 'Mon',
          2 => 'Tue',
          3 => 'Wed',
          4 => 'Thu',
          5 => 'Fri',
          6 => 'Sat',
          7 => 'Sun'
        }
  
  # Creates a new DateRange from a list of representations of days of the week
  def initialize(*args)
    
    # Convert the arguments to an array of day numbers
    arr = args.collect {|rep| DateRange.day_num(rep)}
    arr.uniq!
    arr.sort!
    
    @string = DateRange.build_string(arr)
    
  end
  
  # Given a sorted array of day numbers, build the string representation
  def self.build_string(arr)
    result = ''
    
    while i = arr.shift
      
      # Add the day name to the string
      result << "#{DAY_NUMBERS[i]}"
      
      # If there is a run of 3 or more consecutive days, add a '-' character,
      # and then put the last day of the run after it
      if arr[1] == i + 2
        result << '-'
        i = arr.shift while arr.first == i + 1
        result << "#{DAY_NUMBERS[i]}"
      end
      
      # Unless this is the last day
      result << ', ' if arr.first
    end
    
    result
  end
  
  # Returns the number representation of a day of the week specified by number,
  # name, or abbreviation

···

#
  # DateRange.day_num(2) => 2
  # DateRange.day_num('Fri') => 5
  # DateRange.day_num('saturday') => 6
  def self.day_num(rep)
    if (1..7).include?(rep.to_i)
      rep.to_i
    else
      result = DAY_NUMBERS.index(rep[0,3].capitalize)
      raise ArgumentError unless result
      result
    end
  end
  
  def to_s
    @string
  end
  
end

class DateRangeTest < Test::Unit::TestCase
  
  def test_init
    assert_equal('Mon-Sun', DateRange.new(1,2,3,4,5,6,7).to_s)
    assert_equal('Mon-Wed, Sat, Sun', DateRange.new(1,2,3,6,7).to_s)
    assert_equal('Mon, Wed-Sat', DateRange.new(1,3,4,5,6).to_s)
    assert_equal('Tue-Thu, Sat, Sun', DateRange.new(2,3,4,6,7).to_s)
    assert_equal('Mon, Wed, Thu, Sat, Sun', DateRange.new(1,3,4,6,7).to_s)
    assert_equal('Sun', DateRange.new(7).to_s)
    assert_equal('Mon, Sun', DateRange.new(1,7).to_s)
    assert_equal('Mon-Fri', DateRange.new(*%w(Wednesday Monday Tuesday
Thursday Friday Monday)).to_s)
    assert_raise(ArgumentError) {DateRange.new(1,8)}
  end

end