# DayRange (#92)

**URL:** <https://www.ruby-forum.com/t/dayrange-92/67092>\
**Category:** Ruby\
**Created:** [August 31, 2006, 3:45pm UTC](https://www.ruby-forum.com/t/dayrange-92/67092 "2006-08-31T15:45:57Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [August 31, 2006, 3:45pm UTC](https://www.ruby-forum.com/t/dayrange-92/67092/1 "2006-08-31T15:45:57Z")

</div>

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…

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [September 1, 2006, 12:33am UTC](https://www.ruby-forum.com/t/dayrange-92/67092/2 "2006-09-01T00:33:22Z")

</div>

Ruby Q. wrote:

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

Thanks for the tip with #instance\_variable\_get, I’m just getting into  
meta-programming. Exciting stuff 🙂

Robin
