Ackkkk!
I just noticed how my attachments came over on the rubyquiz website.
So here's the code in-line in the message
=== subranges.rb ===
module Enumerable
# Return an array containing the sub-ranges of sorted contents of the receiver
# Each element must be comparable, and must respond to succ
def subranges(min_span = 2)
range_start = range_end = nil
subranges = []
self.sort.each do |elem|
if range_start.nil?
range_start = range_end = elem
else
if range_end.succ == elem
range_end = elem
else
subrange = (range_start..range_end)
if subrange.entries.length >= min_span
subranges << subrange
else
subrange.each {|ea| subranges << (ea..ea) }
end
range_start = range_end = elem
end
end
end
unless range_start.nil?
subrange = (range_start..range_end)
if subrange.entries.length >= min_span
subranges << subrange
else
subrange.each {|ea| subranges << (ea..ea)}
end
end
subranges
end
end
=== test_day_range.rb===
require 'day_range.rb'
require 'test/unit'
class TestDayRange < Test::Unit::TestCase
EsperantoMap = {"Lundo" => 1, "Lun" => 1, "Mardo" => 2, "Mar" => 2,
"Merkredo" => 3, "Mer" => 3,
"Jhaudo" => 4, "Jha" => 4, "Vendredo" => 5, "Ven" => 5, "Sabato" =>
6, "Sab" => 6,
"Dimancho" => 7, "Dim" => 7}
EsperantoNames = ["Lun", "Mar", "Mer", "Jha", "Ven", "Sab","Dim"]
GermanMap = { "Montag" => 1, "Mon" => 1, "Dienstag" => 2, "Die" => 2,
"Mittwoch" => 3, "Mitt" => 3,
"Donnerstag" => 4, "Don" => 4, "Freitag" => 5, "Frei" => 5,
"Samstag" => 6, "Sam" => 6,
"Sonntag" => 7, "Sonn" => 7 }
GermanNames = ["Mon", "Die", "Mitt", "Don", "Frei", "Sam", "Sonn"]
def test_equal_1
dr1 = DayRange.new(1,2,4,5)
dr2 = DayRange.new(1,2,4,5)
dr3 = DayRange.new(2,5,4,1)
assert_equal(dr1,dr2)
assert_equal(dr1,dr3)
end
def test_weekstart_1
dr1 = DayRange.new('Mon', 'Tuesday', 'Thursday', 'Friday', 'Sat',
:week_start => 2)
assert_equal("Tue, Thu-Sat, Mon",dr1.to_s)
end
def test_equal_2
dr1 = DayRange.new(1,2,4,5)
dr2 = DayRange.new('Monday', 'Tuesday', 'Thursday', 'Friday')
assert_equal(dr1,dr2)
end
def test_to_s_numbers
dr = DayRange.new(1,2,4,5)
assert_equal("Mon-Tue, Thu-Fri",dr.to_s(:min_span => 2))
assert_equal("Mon, Tue, Thu, Fri",dr.to_s)
dr = DayRange.new(1,2,3, 5,6)
assert_equal("Mon-Wed, Fri-Sat",dr.to_s(:min_span => 2))
assert_equal("Mon-Wed, Fri, Sat",dr.to_s)
end
def test_to_s_names
dr = DayRange.new('Monday', 'Tuesday', 'Thursday', 'Friday')
assert_equal("Mon, Tue, Thu, Fri",dr.to_s)
assert_equal("Mon-Tue, Thu-Fri",dr.to_s(:min_span => 2))
end
def test_to_s_options
dr = DayRange.new('Monday', 'Tuesday', 'Thursday', 'Friday')
assert_equal("Lun, Mar, Jeu, Ven", dr.to_s(:language => :French))
assert_equal("Lun-Mar, Jeu-Ven", dr.to_s(:language => :French,
:min_span => 2))
assert_equal("Mercury-Venus, Mars-Saturn",
dr.to_s(:day_names => %w[Mercury Venus Earth Mars Saturn
Jupiter Uranus],
:min_span => 2)
)
dr = DayRange.new(1, 2, 3, 6, 7)
assert_equal("Mon-Wed, Sat, Sun", dr.to_s)
assert_equal("Tue, Wed, Sat-Mon", dr.to_s(:week_start => 2))
assert_equal("Wed, Sat-Tue",dr.to_s(:week_start => 3))
assert_equal("Sat-Wed", dr.to_s(:week_start => 4))
assert_equal("Sat-Wed", dr.to_s(:week_start => 5))
assert_equal("Sat-Wed", dr.to_s(:week_start => 6))
assert_equal("Sun-Wed, Sat", dr.to_s(:week_start => 7))
end
def test_translate_to_french
dr = DayRange.new(1,2,4,5)
assert_equal("Lun, Mar, Jeu, Ven",dr.to_s(:language => 'French'))
assert_equal("Lun-Mar, Jeu-Ven",dr.to_s(:language => 'French',
:min_span => 2))
end
def test_new_french
dr1F = DayRange.new(1,2,4,5, :language => 'French')
dr1 = DayRange.new(1,2,4,5)
assert_equal(dr1, dr1F)
assert_equal("Lun-Mar, Jeu-Ven",dr1F.to_s(:min_span => 2))
assert_equal("Mon-Tue, Thu-Fri",dr1F.to_s(:language => 'English',
:min_span => 2))
assert_equal("Lun, Mar, Jeu, Ven",dr1F.to_s)
assert_equal("Mon, Tue, Thu, Fri",dr1F.to_s(:language => 'English'))
end
def test_new_esperanto
dr1Esp = DayRange.new(1,2,4,5, :day_map => EsperantoMap)
dr1 = DayRange.new(1,2,4,5)
assert_equal(dr1, dr1Esp)
assert_equal("Lun-Mar, Jha-Ven", dr1Esp.to_s(:min_span => 2))
end
def test_bad_days
assert_raise(ArgumentError) {DayRange.new(1,2,8)}
assert_raise(ArgumentError) {DayRange.new(1, :day_map => {'Mon' =>
1, "Tue" => 9})}
end
def test_add_german
DayRange.remove_language(:German)
DayRange.add_language(:German, GermanMap, GermanNames)
assert_equal("Die, Don, Sam, Sonn", DayRange.new(2,4,6,7, :language
=> 'German').to_s)
assert_equal("Die, Don, Sam-Sonn", DayRange.new(2,4,6,7,
:language => 'German').to_s(:min_span => 2))
DayRange.remove_language(:German)
DayRange.add_language(:German, GermanMap)
assert_equal("Die, Don, Sam, Sonn", DayRange.new(2,4,6,7, :language
=> 'German').to_s)
assert_equal("Die, Don, Sam-Sonn", DayRange.new(2,4,6,7,
:language => 'German').to_s(:min_span => 2))
end
def test_each_name
dr = DayRange.new(1,2, 5, 6)
expected = ['Mon', 'Tue', 'Fri', 'Sat']
dr.each_name { |name| assert_equal(expected.shift, name)}
assert(expected.empty?, "Missing results #{expected.inspect}")
expected = ['Doc', 'Grumpy', 'Bashful', 'Sleepy']
dwarves = ['Doc', 'Grumpy', 'Happy', 'Sneezy','Bashful', 'Sleepy', 'Dopey']
dr.each_name(:day_names => dwarves) { |name|
assert_equal(expected.shift, name)}
assert(expected.empty?, "Missing results #{expected.inspect}")
expected = ['Lun', 'Mar', 'Ven', 'Sam']
dr.each_name(:language => 'French') { |name|
assert_equal(expected.shift, name)}
assert(expected.empty?, "Missing results #{expected.inspect}")
end
end
=== day_range.rb===
# This class was written as an answer to RubyQuiz92.
···
#
# The DayRange.new method takes one or more day specifications as
either integers or natural language
# strings representing day names. An instance will respond to to_s
with a string representing
# the list of days with consecutive days collapsed to a form like 'Mon-Fri'
#
# Several methods take "Rails-style" options, one or more associations
after any normal parameters.
# The keys of these associations can be Strings or symbols which will
be converted using to_sym.
#
# Features not called for in the quiz include:
#
# * A number for the start of the week may be specified. This will
affect the output of
# to_s. For example, :week_start => 7, indicates that the week
starts on Sunday, and
# DayRange.new('Sat', 'Sun', 'Mon', :week_start => 7).to_s =>
"Sun-Mon, Sat"
#
# * Support is provided for languages other than English, French is
built in, but additional
# languages can be added, either on the new call, or by a class
method DayRange.add_language
#
# * DayRanges are enumerable and produce the numbers of the day they
contain, in Monday-Sunday
# order.
#
# * Two Dayranges are == if they contain the same days
#
# Author: Rick DeNatale http://talklikeaduck.denhaven2.com
#
# Test cases are in the file testdayrange.rb
#
# The code which does most of the work in detecting sub-ranges is in
the file subranges.rb
# This adds a method to Enumerable which produces an array of ranges
which cover the same contents
# as the Enumeration.
require 'subranges'
#
class DayRange
include Enumerable
# StringSymHash extends Hash so that symbol and string keys are
equivalent a la Rails
# Normally I don't like implementing things like this via sub-classing but...
class StringSymHash < Hash
def [](key)
super(key.to_sym)
end
def []=(key,value)
super(key.to_sym, value)
end
def StringSymHash.[](hash)
ssh = StringSymHash.new
hash.each { |k, v| ssh[k] = v}
ssh
end
end
# maps and names for English and French
@@day_maps = StringSymHash[ :English => {
'Monday' => 1, 'Mon' =>
1, 'Tuesday' => 2, 'Tue' => 2,
'Wednesday' => 3, 'Wed' => 3,
'Thursday' => 4, 'Thu' => 4,
'Friday' => 5, 'Fri' => 5, 'Saturday'
=> 6, 'Sat' => 6,
'Sunday' => 7, 'Sun' => 7 },
:French => {
'Lundi' => 1, 'Lun' => 1,
'Mardi' => 2, 'Mar' => 2,
'Mercredi' => 3, 'Mer' => 3, 'Jeudi' =>
4, 'Jeu' => 4,
'Vendredi' => 5, 'Ven' => 5, 'Samedi'
=> 6, 'Sam' => 6,
'Dimanche' => 7, 'Dim' => 7 } ]
@@day_names = StringSymHash[
:English => [nil, 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
:French => [nil, 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'],
]
# Add a language to those supported by the DayRange class
#
# :call-seq:
# DayRange.add_language(lang_name, day_map[, day_names])
#
# The <em>lang_name_ parameter</em> is the name of the language. It
will be internally converted
# to a symbol. So, for example, if you have:
#
# DayRange.add_language('Esperanto', ...)
#
# then one could ask for a DayRange in Esperanto with:
#
# DayRange.new(1, 3, :Language => :Esperanto)
#
# The <em>day_map</em> parameter should be a Hash which maps day
names to integers in
# the range (1..7). More than one name may map to a particular day_number.
#
# The _day_names_ parameter must be duck-typeable to a 7-element
array or Strings, with the
# first element containing the name which will be used for Monday for
output (e.g. for to_s),
# and the last for Sunday.
#
# If _day_names_ is omitted, then it will be constructed by finding a
name for each day_number
# as least as short as any other name which maps to that day_number
in _day_map_
def DayRange.add_language(lang_name, day_map, day_names = nil)
#HACK - if user supplied day_names pre-pend an element so that we can use
# pseudo 1-origin indexing
day_names = day_names.dup.unshift('') if day_names
@@day_names[lang_name.to_s] = validated_day_names(day_names ||
day_names_from_day_map(day_map))
@@day_maps[lang_name.to_s] = day_map.dup
end
# Remove the language _lang_name_
# Do nothing silently if _lang_name_ is not present
#
# :call-seq:
# DayRange.remove_language(lang_name)
#
def DayRange.remove_language(lang_name)
@@day_names.delete(lang_name)
@@day_maps.delete(lang_name)
end
def DayRange.day_names_from_day_map(day_map) #:nodoc:
# set each days name to the shortest name
# in the name mapping
# puts("Debug- DayRange.day_names_from_day_map(#{day_map})")
day_names = Array.new(8)
day_map.each do |name ,number|
current_name = day_names[number] || day_names[number] = name
day_names[number] = name if name.length < current_name.length
end
validated_day_names(day_names)
end
def DayRange.validated_day_names(day_names) #:nodoc:
(1..7).each do |i|
check_arg(day_names[i], "No name for day number #{i}")
end
day_names
end
def DayRange.check_arg(assertion, msg) # :nodoc:
raise ArgumentError.new( msg) unless assertion
end
def DayRange.day_names_from_options(options, day_map) # :nodoc:
# puts "Debug- DayRange.day_names_from_options(options=#{options.inspect},"
# puts " day_map=#{day_map.inspect})"
return options[:day_names] if options.key?(:day_names)
return DayRange.day_names_from_day_map(day_map)
end
def DayRange.language_from_options(options) # :nodoc:
get_option(:language, options, :English)
end
def DayRange.get_option(option, options, default) # :nodoc:
options.key?(option) ? options[option] : default
end
# Returns a new DayRange (which contains one or more days of the week)
#
# :call-seq:
# DayRange.new(day* [, options])
#
# <em>day</em> arguments can be either numbers in the range
# (1..7) or names in the <em>day_mapping</em> (see <b>:day_mapping</b> option).
#
# Options:
#
# [*:language* => symbol] Specifies the language to be used to
# interpret the _day_s which are Strings, and for the default options
for output via DayName#to_s
# The possible values for the symbol are :English, and :French,
# additional languages can be added via the DayRange.addLanguage
# method. If this option is not specified, :English will be used.
#
# [*:day__map* => hash ] The value _hash_ should be a hash which maps the names
# of days to the number of the day, with 1 being the first
# day of the week (normally Monday), up to 7 for the last day
# of the week (normally Sunday).
# More than one name may map to the same day. If not specified,
# the day_mapping for the selected language is used.
#
# [*:day_names* => array] The value _array_ must be duck-typeable to
a 7-element array.
# The elements are the names of the days to be used by default for
# output (e.g. with DayRange#to_s. If not specified, then the
day_names for the selected
# language will be used, unless *:day_map* is specified in which case
# *:day_names* will be computed from one of the sortest names in the
map for each
# day.
#
# [*:week_start* => int] The value _int_ must be in the range (1, 7).
# It is used to shift the start of the week. For example to create a
# DayRange for a week which starts on Sunday rather than Monday,
# specify a week_start of 7. Although it is also possible to
# achieve the same effect by changing the numbers in day_mapping,
# using week_start allows the same day_mapping to be used for weeks
# starting on different days.
#
# [*:min_span* => int] The value _int_ indicates the minimum span of
days which will
# be collapsed into hyphenated form. The default is 3, as specified
by the Quiz spec
# I missed this the first time.
def initialize( *days ) #:doc:
options = extract_options_from_args!(days)
# puts "Debug: options = #{options.inspect}"
@day_map = day_map_from_options(options)
@day_map.each do |name, number|
DayRange.check_arg((1..7) === number,
"'#{number}' is not an
acceptable day for #{name.to_s}.")
end
@min_span = DayRange.get_option(:min_span, options, 3)
@language = DayRange.language_from_options(options)
@day_names = DayRange.day_names_from_options(options,@day_map)
@week_start = week_start_from_options(options, @day_map)
@day_numbers = days.map do | day |
number = @day_map[day] || day
DayRange.check_arg((1..7) === number, "'#{number.inspect}' is not
an acceptable day.")
number
end
@day_numbers.sort!
end
# Return an array of subranges of @day_numbers adjusted for the week_start
def adjusted_ranges(min_span, week_start)
(week_start == 1 ? @day_numbers : @day_numbers.map { |elem|
ws_adj(elem,week_start) }).subranges(min_span)
end
# Two DayRanges are == if they contain the same day numbers
def ==(other)
false unless other.kind_of? DayRange
self.to_a == other.to_a
end
# Call _block_ once for each day number in _day_range_ passing the
day number to the block.
# The order should be the same regardless of week start, i.e. Monday
should always come first
# then Tuesday, etc.
#
# :call-seq:
# day_range.each {|day_number| block } -> _day_range_
def each()
@day_numbers.each { | elem | yield elem }
end
# Convert _day_range_ to an array, elements will be in order so that
Monday, if it is the range
# will be first then Tuesday, etc. i.e. the effect of weekstart will
be removed
# :call-seq:
# day_range.to_a
def to_a
@day_numbers.dup
end
# Call _block_ once for each day name in _day_range_, passing that
name to the block
#
# :call-seq:
# day_range.each_name [(options)] { |day_name| block } -> _day_range_
#
# *Options*
#
# Options are specified Rails style, as one or more associations at
the end of the argument
# list.
#
# [*:language* => symbol] Specifies the language to be used for the names
# The possible values for the _symbol_ are :English, and :French,
# additional languages can be added via the DayRange.addLanguage
# method. If this option is not specified, :English will be used.
#
# [*:day_names* => array] The _array_ must be 7-element array.
# The elements are the names of the days to be used by default for
# output via to_s. If not specified, then the day_name for the selected
# language will be used.
#
def each_name(options={})
names = get_names_override(options)
to_a.each { | day_number | yield names[day_number] }
end
def get_names_override(options)
return options[:day_names].dup.unshift('') if options.key?(:day_names)
language = options[:language]
return @@day_names[language] if language
@day_names
end
# Returns a string representing the DayRange, Options can be specified.
#
# :call-seq:
# day_range.to_s [(options)]
#
# Options:
#
# [*:language* => symbol] Specifies the language to be used for output
# The possible values for _symbol_ are :English, and :French,
# additional languages can be added via the DayRange.addLanguage
# method. If this option is not specified, the language used when the DayRange
# was created will be used.
#
# [*:day_names* => array] The _array_ must be duck-typeable to a
7-element array.
# The elements are the names of the days to be used by default for
# output via to_s. If not specified, then the day_names for the selected
# language will be used.
#
# [*:min_span* => int] The value _int_ indicates the minimum span of
days which will
# be collapsed into hyphenated form. The default is 3, as specified
by the Quiz spec
#
# [*:week_start* => int] The value _int_ must be in the range (1, 7).
#
def to_s(options={})
names = get_names_override(options)
#puts "Debug: @day_names=#{@day_names.inspect}, names=#{names}"
min_span = DayRange.get_option(:min_span, options, @min_span)
week_start = DayRange.get_option(:week_start, options, @week_start)
result = ""
adjusted_ranges(min_span,week_start).map {|range|
range.first == range.last ?
"#{names[ws_unadj(range.first, week_start)]}" :
"#{names[ws_unadj(range.first,
week_start)]}-#{names[ws_unadj(range.last,week_start)]}"
}.join(", ")
end
private
# convert a number where 1 = Mon.. 7 = Sunday to the equivalent
# when the week starts on day number
def ws_adj(number, week_start)
((number - week_start) % 7) + 1
end
# convert a number back to the original form
def ws_unadj(number, week_start)
((number + week_start + 5) % 7) + 1
end
def extract_options_from_args!(args)
#puts "Debug - extract_options_from_args!(#{args.inspect})"
#puts " #{args.last.class}"
StringSymHash[args.last.kind_of?(Hash) ? args.pop : {}]
end
def day_map_from_options(options)
#puts "Debug: language=#{DayRange.language_from_options(options)}"
#puts " map=#{@@day_maps[DayRange.language_from_options(options)]}"
DayRange.get_option(:day_map, options,
@@day_maps[DayRange.language_from_options(options)])
end
def week_start_from_options(options, day_map)
week_start = DayRange.get_option(:week_start, options, 1)
week_start = day_map[week_start] || week_start
DayRange.check_arg((1..7) === week_start,":week_start must be in the
range (1..7)")
week_start
end
end
--
Rick DeNatale
My blog on Ruby
http://talklikeaduck.denhaven2.com/