[QUIZ] DayRange (#92)

B

Boris Prinz

Hi,

my solution is definitely over-engineered :)

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
elsif Date::DAYS.has_key?(s)
@daynum = Date::DAYS
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 > a[i-1] + 1
self << DayRange.new(first, a[i-1])
first = a
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
 
A

Adam Shelly

tOn 8/27/06 said:
my solution is definitely over-engineered :)
On the opposite end, I've been thinking about codegolf too much.
Here's an update to my under-engineered solution, which meets all the
requirements in 3 statements. It could be even shorter, but I wanted
to keep some possibility of someone else reading it.

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

p DayRange.new( 1,3,4,5,6).to_s
p DayRange.new("Tuesday,Wednesday,Sunday").to_s
p DayRange.new(1,"tuesday,wed,5","6,7").to_s
 
J

James Edward Gray II

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

Well, if you allow the custom day names and control that with an
argument passed to to_s() calling it multiple times seems
reasonable. Here's my own offering allowing that:

class DayRange
SHORT_NAMES = %w[Mon Tue Wed Thu Fri Sat Sun].freeze
LONG_NAMES = %w[ Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday ].freeze

def initialize(*days)
@days = days.map do |d|
ds = d.to_s.downcase.capitalize
SHORT_NAMES.index(ds) || LONG_NAMES.index(ds) || d - 1
end rescue raise(ArgumentError, "Unrecognized number format.")
unless @days.all? { |d| d.between?(0, 6) }
raise ArgumentError, "Days must be between 1 and 7."
end
raise ArgumentError, "Duplicate days given." unless @days ==
@days.uniq
end

def to_s(names = SHORT_NAMES)
raise ArgumentError, "Please pass seven day names." unless
names.size == 7

@days.inject(Array.new) do |groups, day|
if groups.empty? or groups.last.last.succ != day
groups << [day]
else
groups.last << day
groups
end
end.map { |g| g.size > 2 ? "#{g.first}-#{g.last}" : g.join(", ") }.
join(", ").gsub(/\d/) { |i| names[i.to_i] }
end
end

__END__

James Edward Gray II
 
K

Ken Bloom

My second solution builds off of seeing one line of Morus' solution:

Morus Walter said:
self.inject( [ [] ] ) do | list, item |
(which helped, because I rarely remember inject)

and a new solution was born:

class DayRange
NAMEMAP={"Mon"=>1, "Tue"=>2, "Wed"=>3, "Thu"=>4, "Fri"=>5, "Sat"=>6,
"Sun"=>7, "Thurs"=>4, "Monday"=>1, "Tuesday"=>2, "Wednesday"=>3,
"Thursday"=>4, "Friday"=>5, "Saturday"=>6, "Sunday"=>7}

REVERSENAMEMAP=[nil, "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]

def initialize(*args)
#parse arguments into Integers from 1 to 7
args.collect!{|x| NAMEMAP[x] || x}
args.sort!.uniq!
raise ArgumentError if args.any? do |x|
not x.is_a?(Fixnum) or not (1..7).include? x
end

#turn everything into ranges
@ranges=args.inject([]) do |a,v|
if a[-1]==nil or a[-1].last != v-1
a << (v..v)
else
#extend the existing range to include the new element
a[-1]=((a[-1].first)..v)
end
a
end

#this code can be included if you would like wrap-around ranges
#note that it constructs an ranges (with last<first) which doesn't
#actually work with some ruby features. Hence, I don't use those
#features which it breaks.

#if @ranges[-1].last==7 and @ranges[0].first==1
# v=((@ranges[-1].first)..(@ranges[0].last))
# @ranges.delete_at(-1)
# @ranges.delete_at(0)
# @ranges << v
#end
end

def to_s
#determine how to print each range based on the length of the range
@ranges.collect do |r|
if r.first==r.last
REVERSENAMEMAP[r]
elsif r.first==r.last-1
"#{REVERSENAMEMAP[r.first]}, #{REVERSENAMEMAP[r.last]}"
else
"#{REVERSENAMEMAP[r.first]}-#{REVERSENAMEMAP[r.last]}"
end
end.join(", ")
end
end

puts DayRange.new(1,2,3,4,5,6,7).to_s
puts DayRange.new(1,2,3,6,7).to_s
puts DayRange.new(1,3,4,5,6).to_s
puts DayRange.new(2,3,4,6,7).to_s
puts DayRange.new(1,3,4,6,7).to_s
puts DayRange.new(7).to_s
puts DayRange.new(1,7).to_s
puts DayRange.new(1,8).to_s rescue puts "ArgumentError"
 
D

darren kirby

--nextPart1831175.q4aNMeT9Dg
Content-Type: text/plain;
charset="iso-8859-6"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

Does anybody have some idea why my posts are getting garbage chars ('=3D20'=
=20
and '=3D3D') added on the archive site:

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/211029

It appears to affect only me, and other archives of ruby-talk such as this=
=20
one:

http://aspn.activestate.com/ASPN/Mail/Message/ruby-talk/3250521

do not have the same issue. I would really like to fix this, but I have no=
=20
clue what may be causing it. Any suggestions are welcome...

Thanks,
=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

--nextPart1831175.q4aNMeT9Dg
Content-Type: application/pgp-signature

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

iD8DBQBE814FwPD5Cr/3CJgRAvujAJ92V14NWgPWZmsyY5Q/XiAf1AGdgQCg1pb/
VXrFUex+D/IL6gfto7mZq3I=
=pPnj
-----END PGP SIGNATURE-----

--nextPart1831175.q4aNMeT9Dg--
 
L

Lyle Johnson

Does anybody have some idea why my posts are getting garbage chars ('=20'
and '=3D') added on the archive site:

Someone smarter than me will have to fill in the blanks, but I'm
pretty sure it has to do with sending e-mails to the list as HTML
instead of plain text e-mails.
 
D

darren kirby

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

quoth the Lyle Johnson:
Someone smarter than me will have to fill in the blanks, but I'm
pretty sure it has to do with sending e-mails to the list as HTML
instead of plain text e-mails.

But I _never_ send HTML mails...ever...

=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

--nextPart2117484.EojdVfJgFO
Content-Type: application/pgp-signature

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

iD8DBQBE86h3wPD5Cr/3CJgRAtuuAJ9zcNBC+/QzTLLY0+mDiu7uCqm1MQCghY9O
XZ6v/Y6Hbd6n8C31HF+Xzcg=
=9j7H
-----END PGP SIGNATURE-----

--nextPart2117484.EojdVfJgFO--
 
D

darren kirby

quoth the darren kirby:
quoth the Lyle Johnson:

But I _never_ send HTML mails...ever...

-d

Sorry to reply to self.

I had a look at my own sent messages using 'view source' in Kmail. The extra
chars, whatever they are there too. It got me thinking...I turned off signing
my messages, but it seems to have done nothing. Still there. Nothing in my
Kmail settings seems relevant. Could it be the character encoding?

My messages aren't appearing as HTML to you all are they? My settings say
not...

Sorry again for OT chatter...

Thanks,
-d
 
C

Calamitas

Does anybody have some idea why my posts are getting garbage chars ('=20'
and '=3D') added on the archive site:

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/211029

It appears to affect only me, and other archives of ruby-talk such as this
one:

http://aspn.activestate.com/ASPN/Mail/Message/ruby-talk/3250521

do not have the same issue. I would really like to fix this, but I have no
clue what may be causing it. Any suggestions are welcome...

Apparently, Kmail sends messages encoded (inappropriately, it seems)
as quoted-printable. This means that certain characters (unprintable
ones, and ones that have the high bit set to 1) are encoded by an
equals sign (=) followed by two hexadecimal digits. I'm not a Kmail
user, so I can't give you a sure way to fix it, but a quick google
search tells me that it may help to tell Kmail to allow 8-bit encoding
for your messages. This may cause problems of its own as some mail
software may have problems with 8-bit characters, but that should be
OK as long as you stick to ASCII which is 7-bit. It may also help if
you could set your char-set to US-ASCII instead of iso-8859-6.

HTH

Peter
 
D

David Vallner

darren said:
My messages aren't appearing as HTML to you all are they? My settings say
not...

Nope. But they are encoded in iso-8859-6, which I haven't covered in
Thunderbird font settings and the default Courier New ticks me off. What
language set is that anyway? (I should send a hint to Mozilla people
that font substitution is a Good Thing, and that some people just want
to use one font for everything without clicking through all 20 language
groups, thankyouverymuch, since they can't read one bit Chinese or
Indian anyway.)

David Vallner
 
D

darren kirby

quoth the David Vallner:
Nope. But they are encoded in iso-8859-6, which I haven't covered in
Thunderbird font settings and the default Courier New ticks me off. What
language set is that anyway? (I should send a hint to Mozilla people
that font substitution is a Good Thing, and that some people just want
to use one font for everything without clicking through all 20 language
groups, thankyouverymuch, since they can't read one bit Chinese or
Indian anyway.)

Hmm. In my Kmail settings I have this:

"This list is checked for every outgoing message from the top to the bottom
for a charset that contains all required characters"
us-ascii
iso-8859-1
utf-8

So where the heck is iso-8859-6 coming from. I have explicitly sent this as
us-ascii, and if it seems to work I will send my ruby-quiz solutions the same
way.
David Vallner

Thanks for the hints guys,
-d
 
M

Morton Goldberg

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

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
 
M

Morton Goldberg

I can't see how setup and teardown would help in this case. They are
run as before and after methods for each test, not as initializer and
finalizer for the whole test case. Also, recall that I don't want to
create any test objects outside of test_valid_args. I only want to
use objects that have already been validated by this test in the
other tests. To me, that means test_valid_args MUST run first.

However, being new to Test::Unit, I may well be missing something.
Could you post an example showing how setup and teardown could be
used to avoid the trap I fell into?

Regards, Morton
 
J

James Edward Gray II

Could you post an example showing how setup and teardown could be
used to avoid the trap I fell into?

Here's my modification to your tests to get them working with setup():

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
]

def setup
assert_nothing_raised(ArgumentError) do
@one_range = ONE_RANGE.map { |args| DayRange.new(*args) }
end
assert_nothing_raised(ArgumentError) do
@two_ranges = TWO_RANGES.map { |args| DayRange.new(*args) }
end
end

def test_valid_args
@one_range.each do |obj|
assert_expected_days([1..7], obj)
end
@two_ranges.each do |obj|
assert_expected_days([1..3, 5..7], obj)
end
end

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

def test_to_s
@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
@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

private

def assert_expected_days(expected, day_range)
assert_equal(expected, day_range.instance_variable_get:)@days))
end

end

James Edward Gray II
 
M

Morton Goldberg

Thanks for trying to help, but if I understand how setup functions,
your modification defeats my requirement that the test objects be
created once and once only. As I understand it, setup will run before
EACH test and the test objects will be created over and over again,
even for test_bad_args where they are not needed at all.

Renaming test_valid_args to test_args is not only simpler, it meets
the requirement of parsimony of test objects.

Regards, Morton

Could you post an example showing how setup and teardown could be
used to avoid the trap I fell into?

Here's my modification to your tests to get them working with setup():

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
]

def setup
assert_nothing_raised(ArgumentError) do
@one_range = ONE_RANGE.map { |args| DayRange.new(*args) }
end
assert_nothing_raised(ArgumentError) do
@two_ranges = TWO_RANGES.map { |args| DayRange.new(*args) }
end
end

def test_valid_args
@one_range.each do |obj|
assert_expected_days([1..7], obj)
end
@two_ranges.each do |obj|
assert_expected_days([1..3, 5..7], obj)
end
end

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

def test_to_s
@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
@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

private

def assert_expected_days(expected, day_range)
assert_equal(expected, day_range.instance_variable_get:)@days))
end

end

James Edward Gray II
 
J

James Edward Gray II

Thanks for trying to help, but if I understand how setup functions,
your modification defeats my requirement that the test objects be
created once and once only. As I understand it, setup will run
before EACH test and the test objects will be created over and over
again, even for test_bad_args where they are not needed at all.

My opinion is that this is a very good thing. Tests should work in
isolation as much as possible.

You are testing one small part of the whole, to verify correctness.
When you start sharing details between the tests you tie them right
back into a complete system again and that's what unit testing is
trying to avoid. Requiring that tests be run in a given order is
just too fragile.

When I do require that some tests work sequentially, I do it like this:

def test_me_first
# ...
end

def test_me_second
test_me_first

# ...
end

I only feel safe counting on the order when I get to say what the
order is, using the above.

This does extra work, as you complained about with my implementation
of setup(). Ruby doesn't mind the exercise though and as long as she
can do it quickly, I don't either. Besides, it shoots that test
counter right on up! (Makes me feel great, "These tests are just
flying by...")
Renaming test_valid_args to test_args is not only simpler, it meets
the requirement of parsimony of test objects.

I'm very convinced this is the wrong way to go. You are relying on
an implementation detail of Test::Unit here, that could change at
anytime. That could cause your tests to start magically failing at
some point down the road.

I also don't believe you have the order correct. Your tests run as
expected on my box with no modification to the method names. I
haven't gone into the source of Test::Unit to determine why this is,
but it could be that the methods are hashed in which case you can't
count on any order at all.

James Edward Gray II
 
M

Morton Goldberg

My opinion is that this is a very good thing. Tests should work in
isolation as much as possible.

You are testing one small part of the whole, to verify
correctness. When you start sharing details between the tests you
tie them right back into a complete system again and that's what
unit testing is trying to avoid. Requiring that tests be run in a
given order is just too fragile.

I appreciate the point you are making here. My concept, and I admit
it is the concept of a complete Test::Unit newbie, was that the test
case class was the unit of test, not the individual tests defined in
the class. I guess I need to read up on the philosophy behind unit
testing. Know any good references?

Also, I still don't like the idea of creating test objects over and
over again, especially for tests that don't use them at all. Maybe my
old (and now bad) habits formed over years of working on machines
with limited resources are leading me astray, but -- at least for now
-- I can't overcome my distaste for such extravagance.
When I do require that some tests work sequentially, I do it like
this:

def test_me_first
# ...
end

def test_me_second
test_me_first

# ...
end

I only feel safe counting on the order when I get to say what the
order is, using the above.

This is good advice. I'll try to heed it.
This does extra work, as you complained about with my
implementation of setup(). Ruby doesn't mind the exercise though
and as long as she can do it quickly, I don't either. Besides, it
shoots that test counter right on up! (Makes me feel great, "These
tests are just flying by...")


I'm very convinced this is the wrong way to go. You are relying on
an implementation detail of Test::Unit here, that could change at
anytime. That could cause your tests to start magically failing at
some point down the road.

You've almost convinced me, too ;)
I also don't believe you have the order correct. Your tests run as
expected on my box with no modification to the method names. I
haven't gone into the source of Test::Unit to determine why this
is, but it could be that the methods are hashed in which case you
can't count on any order at all.

I'm not sure what you mean by not correct. My results printout speaks
for itself, doesn't it?
I agree that its being different on your box is troubling. It makes,
as you point out, relying on any particular order indefensible.

One thing is for sure -- your comments are sending me back to redo my
test case class. I'm not sure where I will take it (I'm still
resisting you approach using setup), but I will change it so it
doesn't rely on the order in which the tests are run. Oh well, I
wanted to learn about Test::Unit and I'm certainly doing that :)

Regards, Morton
 
J

James Edward Gray II

I guess I need to read up on the philosophy behind unit testing.
Know any good references?

You know, I've read a couple of books on the subject now and still
have not found one I just fell in love with. Hopefully others will
chime in with some good suggestions.
Also, I still don't like the idea of creating test objects over and
over again, especially for tests that don't use them at all. Maybe
my old (and now bad) habits formed over years of working on
machines with limited resources are leading me astray, but -- at
least for now -- I can't overcome my distaste for such extravagance.

Remember, these are unit tests, not nuclear missile guidance
systems. The code is only run when you ask it to run as a tool to
help you develop your code.

The goal is to make that testing process as painless as possible, not
to win awards for speed and low memory consumption.

This is also very common in unit testing. Rails for example reloads
database fixtures before every single test, whether the test uses
them or not. That's a substantially higher commitment than your
eight DayRange objects. ;) (To be fair though, the Rails team has
had to work hard to get that fixture load time down. Slow tests are
bad!)
I'm not sure what you mean by not correct. My results printout
speaks for itself, doesn't it?

Oops, I misunderstood them. My bad. The situation you described
*is* the same on my box. Sorry to spread confusion.

James Edward Gray II
 
R

Rick DeNatale

You know, I've read a couple of books on the subject now and still
have not found one I just fell in love with. Hopefully others will
chime in with some good suggestions.

Well, I understand that test/unit is patterned after JUnit, which in
turn came from SUnit, one idea would be to google for terms like

junit
sunit

and

test infected
which is kind of a cult term.

I guess one of my claims to fame was that I was there when Kent Beck
"test infected" Erich Gamma, which led to JUnit.
 
M

Mitchell Koch

--ZInfyf7laFu/Kiw7
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

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

Mitchell Koch

--ZInfyf7laFu/Kiw7
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="dayrange.rb"

#!/usr/bin/env ruby

class DayRange

WEEKDAYS = %w{ Monday Tuesday Wednesday Thursday Friday Saturday Sunday }
WDA = WEEKDAYS.map { |d| d[0..2] } # 3-letter abbrevs

def initialize(*args)
@days = args.map do |arg|
daynum = nil
case arg
when Integer
if 0 < arg && arg <= WEEKDAYS.size
daynum = arg
end
when String
days_arr = (arg.size == 3 ? WDA : WEEKDAYS)
if idx = days_arr.index(arg)
daynum = idx + 1
end
end
daynum or raise ArgumentError
end.uniq.sort
raise ArgumentError if @days.empty?
end

def to_s
adj = [[]]
@days.each do |day|
if adj.last.empty? or adj.last.last == day-1
adj.last << day
else
adj << [day]
end
end

adj.map do |r|
r.map!{|n| WDA[n-1]}
if r.size > 1
sep = r.size == 2 ? ", " : "-"
r.first.to_s + sep + r.last.to_s
else
r.first
end
end.join(", ")
end
end

--ZInfyf7laFu/Kiw7--
 

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,770
Messages
2,569,583
Members
45,074
Latest member
StanleyFra

Latest Threads

Top