A couple of submitters mentioned that this problem isn’t quite as simple
as it
looks like it should be and I agree. When I initially read it, I was
convinced
I could come up with a clever iterator call that spit out the output.
People
got it down to a few lines, but it’s still just not as straightforward
as I
expected it to be.
A large number of the submitted solutions included tests this time
around. I
think that’s because the quiz did a nice job of laying down the ground
rules and
this is one of those cases where it’s very easy to quickly layout a set
of
expected behaviors.
A lot of solutions also added some additional functionality, beyond what
the
quiz called for. Many interesting additions were offered including
enumeration,
support for Date methods, mixed input, and configurable output. A lot
of good
ideas in there.
Below, I want to examine Robin S.'s solution, which did include a
neat
extra feature. Let’s begin with the tests:
require 'test/unit'
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
# ...
Here we see a set of hand-picked cases being tried for expected results.
Most
submitted tests iterated over some cases like this, since it’s a pretty
easy way
to spot check basic functionality.
Do note the final test case handling mixed input. Robin’s code supports
that,
as many others did.
Here are some tests for Robin’s extra feature, language translation:
# ...
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
# ...
This time the spot checking is done in German, the other language
included in
this solution. You can also see support for translating between
languages, in
the second test here.
One last test:
# ...
def test_should_raise
assert_raise ArgumentError do
DayRange.new([1, 8])
end
end
end
This time the test ensures that the code does not accept invalid
arguments.
Some people chose to spot check several edge cases here as well.
OK, let’s get to the solution:
require 'abbrev'
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
# ...
The main work horse here is DayRange::use_day_names, which you can see
used just
below the definition. This associates seven names with the day indices
the
program uses to work.
Array#abbrev is used here so the code can create a lookup table for all
possible
abbreviations to the actual numbers. Another lookup table is populated
for the
code to use in output Strings and this one accepts a target abbreviation
size.
The two instance methods below provide access to the lookup tables.
Note that
these two methods could use Object#instance_variable_get as opposed to
Module#class_eval if desired.
Next chunk of code, coming right up:
# ...
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
# ...
Nothing too tricky here. DayRange#initialize handles the mixed input by
trying
to find it in the lookup table or defaulting to what was passed.
There’s also a
check in here to make sure we end up with only days we have a name for.
This
handles bounds checking of the input.
Other solutions varied the initialization process a bit. I particularly
liked
how Marshall T. Vandergrift allowed for multiple arguments, a single
Array, or
even Ranges to be passed with some nice Array#flatten work.
Alright, let’s get back to Robin’s solution:
# ...
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
This is the heart of the String building process and many solutions
landed on
code similar to this. DayRange#number_ranges starts the process by
building an
Array of Arrays with the days divided into groups of start and end days.
Days
that run in succession are grouped together and lone days appear as both
the
start and end. For example, the days 1, 2, 3, and 6 would be divided
into [[1, 3], [6, 6]]
.
DayRange#to_s takes the Array from that process and turns it into the
output
String. It just separates the groups by the number of members they
have. One
and two day groups just have their days added to an Array used to build
up the
output. Longer groups are turned into strings with a hyphen between the
first
and last entries. Finally, the Array is joined with commas creating the
desired
String result.
Ready to see how much extra work it was to translate this class to
German?
class DayRangeGerman < DayRange
use_day_names \
%w(Montag Dienstag Mittwoch Donnerstag Freitag Samstag Sonntag), 2
end
The only required step is to supply the day names and abbreviation
level, as you
can see. It wouldn’t be much work to add a whole slew of supported
languages to
this solution. Very nice.
My thanks to the many people who came out of the woodwork to show just
how
creative you can be. You all made such cool solutions and I hope others
will
take some time to browse through them.
Tomorrow, we will attempt to psychoanalyze Ruby’s Integer class…