DayRange (#92)

The three rules of Ruby Q.:

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

http://www.rubyquiz.com/

  1. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby T. follow the discussion. Please reply to the original quiz
message,
if you can.

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

by Bryan D.

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

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 G. schrieb:

On Aug 26, 2006, at 10:02 AM, Robert R. 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.

There’s no right or wrong answers here. I see you introduces a
feature not called for in the quiz and I like it. Clever.

James Edward G. II

Ruby Q. wrote:

into a more human-readable string.

  • The class’s constructor should accept a list of arguments that can be day
    2: Tue
    2,3,4,6,7: Tue-Thu, Sat, Sun

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?

It certainly looks good to me. I believe the quiz master has
indicated we have considerable latitude in what we admit as a valid
argument sequence for DayRange#initialize.

Allowing a set of day names to be specified when an instance of
DayRange is created is a neat idea. I didn’t think of that. Also, I
decided not to allow lists such as [1,3,4,5,6,7] although my
DayRange#initialize is pretty permissive in other ways. The closest I
can get to what you show is:

days = DayRange.new(1,3,4,5,6,7) puts days puts days.to_s(true) Mon, Wed-Sun Monday, Wednesday-Sunday

As you can see, I do allow optional long-form English names for days
to be returned from to_s. But your idea of allowing the programmer to
specify the names at instance creation time is better.

Regards, Morton

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?

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

James Edward G. II

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

On Aug 25, 2006, at 9:01 AM, Ruby Q. 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 :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.

#! /usr/bin/ruby -w # Author: Morton G. # # Date: August 27, 2006 # # Ruby Q. #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 [email protected]
      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

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]

Regards, Morton

Here’s my solution with unit tests (I’m trying to get into the habit).

I made Day a subclass of Date, but I ended up overriding most of the
methods I needed, so I don’t know if it bought me much. It also handles
wrapping, but can be disabled by commenting out the Day#succ method.

Thanks,

Gordon

day_range.rb

class Array

def collapse_ranges(options = {})
range = []
return_array = []
self.each_with_index do |item, i|
range.push(item)
# if this is the last item
# - or -
# there is another item after this one
if item == self.last || self[i + 1]
# if this is the last item
# - or -
# the next item is not the item after the current one
if item == self.last|| item.succ != self[i + 1]
# if there is a range of 3 items or more
if range.length >= 3
return_array.push(range.first…range.last)
# else empty the range individually
else
return_array.concat range
end
# clear out the range
range.clear
end
end
end

    return return_array
end

def to_s
self.map { |i| i.to_s }.join(’, ')
end
end

class Range
def to_s
“#{first}-#{last}”
end
end

require ‘date’

class Day < Date

Days = [nil, “Monday”, “Tuesday”, “Wednesday”, “Thursday”, “Friday”,
“Saturday”, “Sunday”]
Abbr = [nil, “Mon”, “Tue”, “Wed”, “Thu”, “Fri”, “Sat”, “Sun”]

def self.commercial(int)
day = send(“from_#{int.class}”.downcase.intern, int)
super(1984,1,day)
end

def succ
if cwday == 7
Day.commercial(1)
else
super
end
end

def to_s
Days[cwday]
end

def to_abbr
Abbr[cwday]
end

alias_method :to_s, :to_abbr

def self.from_string(string)
# If string isn’t in Days or Abbr, return string and let
Date#commercial raise ArgumentError
Days.index(string.capitalize) || Abbr.index(string.capitalize) ||
string.capitalize
end

def self.from_fixnum(int)
# Date#commercial allows integers over 7, so raise ArgumentErrror
here
if (1…7).include? int
int
else
raise ArgumentError
end
end

end

class DayRange

def initialize(array)
@array = array.map{|i| Day.commercial(i) }.collapse_ranges
end

def to_s
@array.to_s
end

end

test_day_range.rb

require ‘test/unit’
require ‘lib/days.rb’

class TestArray < Test::Unit::TestCase
def test_collapse_ranges
assert_equal( [(1…4),6], [1,2,3,4,6].collapse_ranges)
end

def test_to_s
assert_equal([1,2,3].to_s, ‘1, 2, 3’)
end
end

class TestRange < Test::Unit::TestCase
def test_to_s
assert_equal((1…3).to_s, ‘1-3’)
end
end

class TestDay < Test::Unit::TestCase
def setup
@day = Day.commercial(6)
@next_day = Day.commercial(‘Sun’)
end

def test_error
assert_raise(ArgumentError){ Day.commercial(‘not’) }
assert_raise(ArgumentError){ Day.commercial(8) }
end

def test_succ
assert_equal(@day.succ.cwday,7)
end

def test_spaceship
assert(@day < @next_day)
end

def test_to_s
assert_equal(‘Sat’, @day.to_s)
end
end

class TestDayRange< Test::Unit::TestCase
def test_to_s
[
[[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”] ,
[[‘Tue’,‘Wed’,‘Thu’,‘Fri’],“Tue-Fri”],
[[‘Wednesday’,‘Thursday’,‘Friday’,‘Saturday’],“Wed-Sat”],
[[‘tue’,‘fri’,‘sat’,‘sun’], “Tue, Fri-Sun”],
[[5,6,7,1],“Fri-Mon”],
[[1,5,6,7],“Mon, Fri-Sun”]
].each do |arr, str|
assert_equal(str, DayRange.new(arr).to_s)
end
assert_raise(ArgumentError){ DayRange.new([1,8]).to_s }
end
end

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

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

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

On 8/25/06, Ruby Q. [email protected] wrote:

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

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”


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

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

Gordon T. wrote:

Here’s my solution with unit tests (I’m trying to get into the habit).

I forgot to mention that I got the Array#collapse_ranges method here
(though I tweaked it a bit):

Just wanted to give credit where it was due.

In article
[email protected],
Ruby Q. [email protected] 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

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
Date::ABBR_DAYNAMES[@daynum % 7]
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