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
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 day_map 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/