[QUIZ] DayRange (#92)

R

Ruby Quiz

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

Morton Goldberg

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
 
J

James Edward Gray II

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
 
M

Morton Goldberg

The quiz asks for a class, yes.

Thanks for the clarification. Rereading the OP more carefully, I see
this is explicitly specified in the bulleted rules. I got thrown off
course by following sentences, which precede the rules:
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".

To me, the second sentence doesn't make a lot of sense in the context
of a DayRange class. Something that takes a list of numbers and
returns a string, suggests a top-level conversion function.

Regards, Morton
 
R

Robert Retzbach

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

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

Michael W. Ryder

Ruby said:
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?
 
J

James Edward Gray II

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 Gray II
 
M

Morton Goldberg

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:

<code>
days = DayRange.new(1,3,4,5,6,7)
puts days
puts days.to_s(true)
</code>

<result>
Mon, Wed-Sun
Monday, Wednesday-Sunday
</result>

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
 
S

Simon Kröger

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
 
M

Morton Goldberg

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 (e-mail address removed)
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
 
J

James Edward Gray II

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 Gray II
 
G

Gordon Thiesfeld

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
 
S

Sander Land

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


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
 
R

Rörd Hinrichsen

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
 
A

Adam Shelly

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
 
M

Morus Walter

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)
 
R

Robin Stocker

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
 
D

darren kirby

--nextPart1398464.e2a1lfSOlH
Content-Type: text/plain;
charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

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

The constructor can take numbers, abbreviated names (Mon, Tue etc) and also=
=20
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=20
coded for En, Fr, De, It, and Es). If you pass a non-nil value to the "form=
"=20
argument it will return full names rather than abbreviations.

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

Anyway, here is a sample irb session followed by the code:
###################################
days =3D DayRange.new([1,2,3,4,5,6,7]) =2E..
days.to_s =3D> "Mon-Sun"
days =3D DayRange.new(["Mon","Wed","Fri"], lang=3D'en', form=3D1) =2E..
days.to_s
=3D> "Monday, Wednesday, Friday"
days =3D DayRange.new([1,5,6,7], lang=3D'es') =2E..
days.to_s =3D> "lun, vie-dom"
days =3D DayRange.new([1,4,5,6], lang=3D'de', form=3D1) =2E..
days.to_s
=3D> "Montag, Donnerstag-Samstag"
# add another language:
class DayRange
1> def day_no
2> [['Man','Mandag'],['Tir','Tirsdag'],['Ons','Onsdag'],
['Tor','Torsdag'], ['Fre','Fredag'],['L=F8r','L=F8rdag'],['S=F8n','S=F8ndag=
']]
2> end
1> end
=3D> nil
days =3D DayRange.new([1,2,3,4,5,6,7], lang=3D'no', form=3D1) =2E..
days.to_str # different than 'to_s'
=3D> "Mandag Tirsdag Onsdag Torsdag Fredag L=F8rdag S=F8ndag"
=2E..
###################################
# Quiz 92 - Day Range

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

def to_s
s =3D String.new

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

s +=3D "#{@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 =3D=3D 1 and offset[i+1] =3D=3D 1 # Found a range?
s +=3D "-" unless s[-1] =3D=3D 45 # "-"
next # then move along...
else
s +=3D " #{@day_str_array[v-1][@type]}" # otherwise add the name.
next
end
else
s +=3D " #{@day_str_array[@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 =3D String.new
@day_num_array.each { |n| s +=3D "#{@day_str_array[n-1][@type]} " }
s.strip!
end

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

private
def parse_args
if @days[0].class =3D=3D Fixnum
@day_num_array =3D @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 =3D 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=E9rcoles'],['jue','jueves'=
],
['vie','viernes'],['sab','s=E1bado'],['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=EC'],['mar','marted=EC'],['mer','mercoled=EC'],['gio','g=
ioved=EC'],
['ven','venerd=EC'],['sab','sabato'],['dom','domenica']]
end

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

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

--nextPart1398464.e2a1lfSOlH
Content-Type: application/pgp-signature

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.5 (GNU/Linux)

iD8DBQBE8hzlwPD5Cr/3CJgRAr1RAKCOkG20IGXhC50d8SENDdKn4FWVFACgxeF1
vctNIMLmF6xUSE9mU6560LI=
=c/sF
-----END PGP SIGNATURE-----

--nextPart1398464.e2a1lfSOlH--
 
J

Jason Merrill

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}"

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

Ask a Question

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

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,065
Latest member
OrderGreenAcreCBD

Latest Threads

Top