[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

[QUIZ] Barrel of Monkeys (#30

James Gray

4/29/2005 12:43:00 PM

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rub...

3. Enjoy!

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Gavin Kistner

Last week one of the local radio stations was having a "Barrel of Monkey"
afternoon. While a song was playing, listeners would call in and suggest the
next song, which had to begin with the same letter as the current song ended in.

So, for example, a sample (eclectic) Barrel of Monkeys playlist might be:

Peace Train
No More "I Love You's"
Super Trooper
Rock Me, Amadeus
Song of the South
Hooked on a Feeling
Go Tell it on the Mountain
...
(See how each song name begins with the last letter of the song before it?)

Just creating ANY playlist would be too easy, however. We need a Worthy Problem
to solve.

1) Given any starting and ending song, create a playlist that connects the
two songs.

2) For extra credit, try to create a playlist of a specific duration (to
fill a particular time slot on the radio).

3) For more extra credit, try to find the shortest playlist that links the
songs. (Either in terms of number of songs, or total play time.)

You can find an XML file with over 5,000 song names and play time at
http://rubyquiz.com/SongLibr... (100k compressed). The song durations are
in milliseconds.

Finally, because this problem may be enough fun without having to discover
trouble yourself, I offer a few (spoiler?) things to think about below. (Far
below, in case you want to skip them.)

THE END OF THIS QUIZ MENTIONS A FEW PITFALLS THAT I THOUGHT OF WHILE WRITING IT
UP

IT'S UP TO YOU TO DECIDE IF YOU WANT TO READ THEM

I DON'T HAVE ANY DOMAIN KNOWLEDGE IN THIS PROBLEM AREA
AND I HAVEN'T TRIED TO SOLVE THE PROBLEM MYSELF
SO THESE AREN'T SUBTLE NUANCES THAT WILL GREATLY HELP YOU

DO NOT READ ANY FURTHER IF YOU DO NOT WANT TO READ THEM

THERE IS NOTHING MORE OF ANY INTEREST IN THIS QUIZ AFTER THIS TEXT OTHER THAN
THE PITFALLS

SO YOU DON'T NEED TO KEEP READING IF YOU WANT TO THINK ABOUT THE PROBLEM IN A
VIRGIN STATE

I THINK THIS MAY BE ENOUGH OF A WARNING

* What do you do with songs with names like "'74-'75" or "Seventy Times 7"
or "=:0 :("?

* How about a song named "Candy Everybody Wants (unplugged)" or "Voulez-Vous
[Extended Remix, 1979 US Promo]" or "Speed Racer - Hardcore Mix" or
"Breathe Remix Feat Sean Paul"?

* What do you do if there IS no way to connect two songs? (And how do you
know for sure?)


17 Answers

Dave Burt

4/30/2005 12:03:00 AM

0

> 1) Given any starting and ending song, create a playlist that connects the
> two songs.
>
> 2) For extra credit, try to create a playlist of a specific duration (to
> fill a particular time slot on the radio).
>
> 3) For more extra credit, try to find the shortest playlist that links the
> songs. (Either in terms of number of songs, or total play time.)

How about extra credit for finding the longest possible no-repeat playlist?

Cheers,
Dave


Gavin Kistner

4/30/2005 1:22:00 AM

0

On Apr 29, 2005, at 6:04 PM, Dave Burt wrote:
> How about extra credit for finding the longest possible no-repeat
> playlist?

Ooh, that would be another cool flavor, aye.

:)



Bill Guindon

4/30/2005 1:34:00 AM

0

On 4/29/05, Ruby Quiz <james@grayproductions.net> wrote:
>
> You can find an XML file with over 5,000 song names and play time at
> http://rubyquiz.com/SongLibr... (100k compressed). The song durations are
> in milliseconds.

Any suggestions for an easy to use XML library? Or for that matter,
is it ok to convert it to another format? Personally, I'd prefer CSV
or YAML (hate the extra baggage that comes with XML).

--
Bill Guindon (aka aGorilla)



Gavin Kistner

4/30/2005 1:39:00 AM

0

On Apr 29, 2005, at 7:33 PM, Bill Guindon wrote:
> On 4/29/05, Ruby Quiz <james@grayproductions.net> wrote:
>>
>> You can find an XML file with over 5,000 song names and play time at
>> http://rubyquiz.com/SongLibr... (100k compressed). The song
>> durations are
>> in milliseconds.
>
> Any suggestions for an easy to use XML library? Or for that matter,
> is it ok to convert it to another format? Personally, I'd prefer CSV
> or YAML (hate the extra baggage that comes with XML).

Certainly you can move the library to any format you want, or come up
with your own populated playlist format.

I use REXML for XML.

http://www.germane-software.com/software/rexml/docs/tut...
http://www.ruby-doc.org/stdlib/libdoc/rexml/rdoc/...



Ryan Leavengood

4/30/2005 2:25:00 AM

0

Gavin Kistner wrote:
>
> Certainly you can move the library to any format you want, or come up
> with your own populated playlist format.
>
> I use REXML for XML.

I decided to use REXML as I started working on this quiz, and I must say
it is pretty sweet. It took only a couple lines of code to parse out the
songs from the given file (I develop iteratively, and that was my first
iteration :)

Ryan


NAKAMURA, Hiroshi

4/30/2005 2:28:00 AM

0

Hi all,

Sorry for OT & advertising.

Bill Guindon wrote:
> On 4/29/05, Ruby Quiz <james@grayproductions.net> wrote:
>
>>You can find an XML file with over 5,000 song names and play time at
>>http://rubyquiz.com/SongLibr... (100k compressed). The song durations are
>>in milliseconds.
>
> Any suggestions for an easy to use XML library? Or for that matter,
> is it ok to convert it to another format? Personally, I'd prefer CSV
> or YAML (hate the extra baggage that comes with XML).

With the latest snapshot of soap4r, you can parse that like this;

0% irb
irb(main):001:0> require 'zlib'
=> true
irb(main):002:0> gz =
Zlib::GzipReader.new(File.open("SongLibrary.xml.gz"))
=> #<Zlib::GzipReader:0x101d5458>
irb(main):003:0> require 'xsd/mapping'
=> true
irb(main):004:0> library = XSD::Mapping.xml2obj(gz.read); nil
=> nil
irb(main):005:0> library.Artist.size
=> 1004
irb(main):006:0> library.Artist.find { |artist| artist.xmlattr_name =
"ABBA" }.Song.collect { |song| song.xmlattr_name }
=> ["Caught Up In You", "Fantasy Girl", "Hold On Loosely", "Second
Chance", "Teacher, Teacher", "Back Where You Belong", "The Sound of Your
Voice"]

xml2obj generates singleton object for each XML element (It means you
can't Marshal.dump it. ).

Regards,
// NaHi


Bill Guindon

4/30/2005 3:22:00 AM

0

On 4/29/05, NAKAMURA, Hiroshi <nakahiro@sarion.co.jp> wrote:
> Hi all,
>
> Sorry for OT & advertising.
>
> Bill Guindon wrote:
> > On 4/29/05, Ruby Quiz <james@grayproductions.net> wrote:
> >
> >>You can find an XML file with over 5,000 song names and play time at
> >>http://rubyquiz.com/SongLibr... (100k compressed). The song durations are
> >>in milliseconds.
> >
> > Any suggestions for an easy to use XML library? Or for that matter,
> > is it ok to convert it to another format? Personally, I'd prefer CSV
> > or YAML (hate the extra baggage that comes with XML).
>
> With the latest snapshot of soap4r, you can parse that like this;
>
> 0% irb
> irb(main):001:0> require 'zlib'
> => true
> irb(main):002:0> gz =
> Zlib::GzipReader.new(File.open("SongLibrary.xml.gz"))
> => #<Zlib::GzipReader:0x101d5458>
> irb(main):003:0> require 'xsd/mapping'
> => true
> irb(main):004:0> library = XSD::Mapping.xml2obj(gz.read); nil
> => nil
> irb(main):005:0> library.Artist.size
> => 1004
> irb(main):006:0> library.Artist.find { |artist| artist.xmlattr_name =
> "ABBA" }.Song.collect { |song| song.xmlattr_name }
> => ["Caught Up In You", "Fantasy Girl", "Hold On Loosely", "Second
> Chance", "Teacher, Teacher", "Back Where You Belong", "The Sound of Your
> Voice"]
>
> xml2obj generates singleton object for each XML element (It means you
> can't Marshal.dump it. ).
>
> Regards,
> // NaHi
>
>

Some very cool stuff. Glad I asked. I'll poke into it, and REXML as
Gavin and Ryan suggested.

Thanks much.

--
Bill Guindon (aka aGorilla)



Ezra Zygmuntowicz

5/1/2005 1:29:00 AM

0


On Apr 29, 2005, at 7:27 PM, NAKAMURA, Hiroshi wrote:

> Hi all,
>
> Sorry for OT & advertising.
>
> Bill Guindon wrote:
>
>> On 4/29/05, Ruby Quiz <james@grayproductions.net> wrote:
>>
>>
>>> You can find an XML file with over 5,000 song names and play time at
>>> http://rubyquiz.com/SongLibr... (100k compressed). The
>>> song durations are
>>> in milliseconds.
>>>
>>
>> Any suggestions for an easy to use XML library? Or for that matter,
>> is it ok to convert it to another format? Personally, I'd prefer CSV
>> or YAML (hate the extra baggage that comes with XML).
>>
>
> With the latest snapshot of soap4r, you can parse that like this;
>
> 0% irb
> irb(main):001:0> require 'zlib'
> => true
> irb(main):002:0> gz =
> Zlib::GzipReader.new(File.open("SongLibrary.xml.gz"))
> => #<Zlib::GzipReader:0x101d5458>
> irb(main):003:0> require 'xsd/mapping'
> => true
> irb(main):004:0> library = XSD::Mapping.xml2obj(gz.read); nil
> => nil
> irb(main):005:0> library.Artist.size
> => 1004
> irb(main):006:0> library.Artist.find { |artist|
> artist.xmlattr_name =
> "ABBA" }.Song.collect { |song| song.xmlattr_name }
> => ["Caught Up In You", "Fantasy Girl", "Hold On Loosely", "Second
> Chance", "Teacher, Teacher", "Back Where You Belong", "The Sound of
> Your
> Voice"]
>
> xml2obj generates singleton object for each XML element (It means you
> can't Marshal.dump it. ).
>
> Regards,
> // NaHi
>

Hey I am t6rying to play with this xml2object method but when I do a
require 'xsd/mapping'
it fails. What library is xsd/mapping ? Where do I get it or is it
supposed top be in the core?

Thanks-
-Ezra

Gavin Kistner

5/2/2005 1:55:00 AM

0

Following is my solution to the Barrel of Monkeys quiz. I got the
'extra credit' for short playlists, but didn't have time to try out
the only solution I came up with for the 'specific duration' extra
credit.

(There is a bug exposed by the test case, where (for reasons I
haven't figured out) it doesn't always find the absolutely shortest
playlist.)

I'll discuss some of the lessons I learned when I write up the
Summary on Wednesday.


A sample of the output:

[Sliver:~/Desktop/Barrel of Monkeys] gkistner$ ruby barrel.rb
Stopwatch started at Sun May 01 19:20:21 MDT 2005
+0.1s to load marshalled library from file

Looking for a path between 'Moab' and 'I'm On My Way'
#0 - Jamie Janover :: Moab :: 9:05
#1 - Shamwari :: Babmudiki :: 5:26
#2 - The Proclaimers :: I'm On My Way :: 3:44
+0.1s to create playlist

Looking for a path between 'My Girl' and 'I'm A Rainbow Too'
#0 - The Mamas & The Papas :: My Girl :: 3:30
#1 - Creedence Clearwater Revival :: Lodi :: 3:11
#2 - Bob Marley & Fatboy Slim :: I'm A Rainbow Too :: 8:00
+0.1s to create playlist

Looking for a path between 'Sand in My Shoes' and 'Bug City'
#0 - Dido :: Sand in My Shoes :: 5:00
#1 - Isao Tomita :: Syncopated Clock :: 2:31
#2 - Fats Waller :: Keepin' Out of Mischief Now :: 3:16
#3 - The Offspring :: Why Don't You Get A Job? :: 2:52
#4 - The Presidents of the United States of America :: Bug City :: 3:05
+0.0s to create playlist


What I found surprising is how often (given the supplied library)
there is a single 3-song playlist that connects just about any two
end songs.


There are six files:
barrel.rb -- loads the SongLibrary.xml file and picks two songs at
random and tries to find a playlist.
barrel_of_classes.rb -- all the classes used in the solution
barrel_of_tests.rb -- some limited unit tests (which helped my find
some bugs, yay testing!)
stopwatch.rb -- an unrelated class just for my own trivial timing
tests
arabicnumerals.rb -- (not included here) The solution to quiz number
25 by Eliah Hecht





==================================================
barrel.rb
==================================================
require 'rexml/document'
require 'rexml/xpath'
require 'barrel_of_classes'
require 'stopwatch'

$library_file = 'library.marshal'

Stopwatch.start

if File.exist?( $library_file )
library = Marshal.load( File.new( $library_file, 'r+' ) )
Stopwatch.mark( 'load marshalled library from file')
else
include REXML

xml = Document.new( File.new( 'SongLibrary.xml' ) )
Stopwatch.mark( 'load XML')

song_nodes = XPath.match( xml, '//Song' )
Stopwatch.mark( 'find songs in xml' )

library = SongLibrary.new( song_nodes.inject( [] ) do |lib,
song_node|
lib << Song.new( song_node.attributes['name'],
song_node.attributes['duration'].to_i, song_node.parent.attributes
['name'] )
end )

Stopwatch.mark( 'fill library' )


# Get rid of songs with useless names
library.songs.delete_if{ |song| song.clean_name.length < 2 }
puts "Deleted #{song_nodes.length - library.songs.length} songs"
Stopwatch.mark( 'clean library' )

# Save the library just to save time for future runs.
Marshal.dump( library, File.new( $library_file, 'w+' ) )
Stopwatch.mark( 'save library to file' )
end

all_songs = library.songs

100.times{
start_index = rand( library.songs.length )
end_index = rand( library.songs.length )

start_song = all_songs[ start_index ]
end_song = all_songs[ end_index ]

puts "\nLooking for a path between '#{start_song.name}' and '#
{end_song.name}'"

pl = Playlist::BarrelOfMonkeys.new( library.songs, start_song,
end_song )
puts pl
Stopwatch.mark( 'create playlist' )
}

Stopwatch.stop





==================================================
barrel_of_classes.rb
==================================================
require 'arabicnumerals'

class Song
attr_reader :artist, :name, :duration

# The song name made turned into only [a-z ], with no leading or
trailing spaces
attr_reader :clean_name

# The first and last letters of the song name (after 'cleaning')
attr_reader :first, :last

def initialize( name, duration=0, artist='' )
@artist = artist
@duration = duration
@name = name
@clean_name = name.downcase

# "forever young (dune remix)" => "forever young"
@clean_name.gsub!( /\s*\([^)]*mix[^)]*\)/, '' )

# "voulez-vous [extended remix, 1979 us promo]" => "voulez-
vous"
@clean_name.gsub!( /\s*\[[^\]]*mix[^\]]*\]/, '' )

# "hava nagila (live)" => "hava nagila"
@clean_name.gsub!( /\s*\([^)]*\blive\b[^)]*\)/, '' )

# "everything in its own time [live]" => "everything in
its own time"
@clean_name.gsub!( /\s*\[[^\]]*\blive\b[^\]]*\]/, '' )

# "it's a fine day (radio edit)" => "it's a fine day"
@clean_name.gsub!( /\s*\([^)]*\bedit\b[^)]*\)/, '' )

# "pearl's girl [7" edit]" => "pearl's girl"
@clean_name.gsub!( /\s*\[[^\]]*\bedit\b[^\]]*\]/, '' )

# "can't stop raving - remix" => "can't stop raving -"
@clean_name.gsub!( /\s*remix\s*$/, '' )

# "50,000 watts" => "50000 watts"
@clean_name.gsub!( /,/, '' )

# "50000 watts" => "fifty thousand watts"
@clean_name.gsub!( /\b\d+\b/ ){ |match| match.to_i.to_en }

@clean_name.gsub!( /[^a-z ]/, '' )
@clean_name.strip!

@first = @clean_name[ 0..0 ]
@last = @clean_name [ -1..-1 ]
end

def to_s
self.artist + ' :: ' + self.name + ' :: ' +
self.duration.as_time_from_ms
end
end

class Numeric
# Treat the number as a number of milliseconds and return a
formatted version
# Produces "0:07" for 7124
# Produces "8:17" for 497000
# Produces "1:07:43" for 4063000
# Produces "59:27:44" for 214063999
# (Only handles units of time up to hours)
def as_time_from_ms
minutes = 0
seconds = (self / 1000.0).round
if seconds >= 60
minutes = seconds / 60
seconds %= 60
if minutes >= 60
hours = minutes / 60
minutes %= 60
end
end
seconds = seconds.to_s
seconds = '0' + seconds unless seconds.length > 1

minutes = minutes.to_s
minutes = '0' + minutes unless minutes.length > 1 or not hours
"#{hours}#{hours ? ':' : ''}#{minutes}:#{seconds}"
end
end

class Array
def random
self[ rand( self.length ) ]
end
end

class SongLibrary
attr_accessor :songs
def initialize( array_of_songs = [] )
@songs = array_of_songs
end
end

class Playlist
attr_reader :songs
def initialize( *songs )
@songs = songs
@current_song_number = 0
end

def to_s
out = ''
songs.each_with_index{ |song,i| out << "##{i} - #{song}\n" }
if songs
out
end

class BarrelOfMonkeys < Playlist
# Given an array of Song items and songs to start with and
end with
# produce a playlist where each song begins with the same
letter as
# the previous song ended with
def initialize( songs, start_song, end_song, options={} )

# Create a map to each song, by first letter and then
last letter
@song_links = {}
songs.each do |song|
first_map = @song_links[ song.first ] ||= {}
( first_map[ song.last ] ||= [] ) << song
end

# For speed, pick only one song for each unique
first_last pair
@song_links.each_pair do | first_letter,
songs_by_last_letters |
songs_by_last_letters.each_key do |last_letter|
songs_by_last_letters[ last_letter ] =
songs_by_last_letters[ last_letter ].random
end
end

# Get rid of any songs which start and end with the same
letter
@song_links.each_pair do | first_letter,
songs_by_last_letters |
songs_by_last_letters.delete( first_letter )
end

@songs = shortest_path( start_song, end_song )
unless @songs
warn "There is no path to make a Barrel of Monkeys
playlist between '#{start_song.name}' and '#{end_song.name}' with the
supplied library."
end
end

private
def shortest_path( start_song, end_song,
start_letters_seen='', d=0 )
# Bail out if a shorter solution was already found
return nil if @best_depth and ( @best_depth <= d )

#puts( ( "." * d ) + start_song.name )
path = [ start_song ]

if start_song.last == end_song.first
best_path = [ end_song ]
@best_depth = d
else
best_length = nil

songs_by_last_letters = @song_links
[ start_song.last ]
if songs_by_last_letters
songs_by_last_letters.each_pair do |
last_letter, song|
if start_letters_seen.include?
( song.first ) || ( start_letters_seen.include?( song.last ) &&
( song.last != end_song.first ) )
next
end
start_letters_seen += start_song.first
trial_path = shortest_path( song,
end_song, start_letters_seen, d+1 )
if trial_path && ( !best_length ||
( trial_path.length < best_length ) )
best_path = trial_path
end
end
end
end

if best_path
path << best_path
path.flatten!
else
nil
end

end
end
end





==================================================
barrel_of_tests.rb
==================================================
require 'test/unit'
require 'barrel_of_classes.rb'

class SongTest < Test::Unit::TestCase
def test_cleaning
song_name = 'Hello World'
clean_name = song_name.downcase
s1 = Song.new( song_name )
assert_equal( clean_name, s1.clean_name )

song_name = 'Hello World (remix)'
s1 = Song.new( song_name )
assert_equal( clean_name, s1.clean_name )

song_name = ' Hello World - remix'
s1 = Song.new( song_name )
assert_equal( clean_name, s1.clean_name )

song_name = ' Hello World Remix '
s1 = Song.new( song_name )
assert_equal( clean_name, s1.clean_name )

song_name = "'74 - '75"
s1 = Song.new( song_name )
assert_equal( 's', s1.first )
assert_equal( 'e', s1.last )

song_name = 'As Lovers Go [Ron Fair Remix]'
clean_name = 'as lovers go'
s1 = Song.new( song_name )
assert_equal( clean_name, s1.clean_name )
end
end

class BarrelTest < Test::Unit::TestCase
def setup
@lib = SongLibrary.new

('A'..'H').each{ |x| @lib.songs << Song.new( 'Alpha ' + x ) }
@lib.songs << Song.new( 'Beta F' )
('A'..'I').each{ |x| @lib.songs << Song.new( 'Foo ' + x ) }
@lib.songs << Song.new( 'Icarus X' )
('A'..'H').each{ |x| @lib.songs << Song.new( 'Jim ' + x ) }

@links = { }
@lib.songs.each{ |song|
link = song.first + song.last
@links[ link ] = song
}
end

def test1_valid
af = @links[ 'af' ]
fg = @links[ 'fg' ]
pl = Playlist::BarrelOfMonkeys.new( @lib.songs, af, fg )
desired_playlist = [ af, fg ]
assert_equal( desired_playlist, pl.songs )

ab = @links[ 'ab' ]
bf = @links[ 'bf' ]
fi = @links[ 'fi' ]
pl = Playlist::BarrelOfMonkeys.new( @lib.songs, ab, fi)
desired_playlist = [ ab, bf, fi ]
assert_equal( desired_playlist, pl.songs )

ix = @links[ 'ix' ]
pl = Playlist::BarrelOfMonkeys.new( @lib.songs, ab, ix )
desired_playlist << ix
assert_equal( desired_playlist, pl.songs )

aa = @links[ 'aa' ]
pl = Playlist::BarrelOfMonkeys.new( @lib.songs, aa, ix )
desired_playlist = [ aa, af, fi, ix ]
assert_equal( desired_playlist, pl.songs )
end

def test3_broken
aa = @links[ 'aa' ]
ab = @links[ 'ab' ]
jh = @links[ 'jh' ]
pl = Playlist::BarrelOfMonkeys.new( @lib.songs, aa, jh )
assert_nil( pl.songs )

pl = Playlist::BarrelOfMonkeys.new( @lib.songs, ab, jh )
assert_nil( pl.songs )
end

end






=============================================
stopwatch.rb
=============================================
module Stopwatch
class Lap
attr_reader :name, :time
def initialize( name )
@name = name
@time = Time.new
end
end

def self.start
@laps = [ ]
self.mark :start
end

def self.mark( lap_name )
lap = Lap.new( lap_name )
if @laps.empty?
puts "Stopwatch started at #{lap.time}"
else
last_lap = @laps.last
elapsed = lap.time - last_lap.time
puts "+#{(elapsed*10).round/10.0}s to #{lap_name}" # +
" (since #{last_lap.name})"
end
@laps << lap
end

def self.time( lap_name )
yield
self.mark lap_name
end

def self.stop
now = Time.new
elapsed = now - @laps.first.time
puts "Stopwatch stopped at #{now}; #{(elapsed*10).round/10.0}
s elapsed"
end
end


Dave Burt

5/2/2005 6:57:00 AM

0

My solution is here:
http://www.dave.burt.id.au/ruby/barrel_of_...
It requires YAMLized song library, such as that available here:
http://www.dave.burt.id.au/ruby/SongL...
And it requires my solution to last week's quiz, HighLine::Dave, available
here:
http://www.dave.burt.id.au/ruby/highli...

Get the lot here:
http://www.dave.burt.id.au/ruby/barrel_of_m...

The core of my solution is a function (BarrelOfMonkeys#playlist) that
constructs playlists from the library matching criteria passed as parameters
to the function, and also the requirement that all songs begin with the last
letter of the previous song.
The criteria this function accepts include first letter, last letter, bounds
and targets for number of songs and duration, and excluded songs.

When run as an application, my barrel_of_monkeys.rb prompts the user for
these parameters, passes them to this function and returns the result.

A weakness in my solution, that I'd remove if I had time, is that the
playlist-constructing search is depth-first, which means that a query "find
the playlist with the least number of songs" takes the same time as "find
the playlist with the most songs".

Now, for the quiz criteria:

1) My solution doesn't take starting and ending songs, it takes first and
last letters. To find a playlist to join "Peace Train" and "Go Tell it on
the Mountain", you would enter first letter => "n", last letter => "g".

Note that my solution ignores dangly bits on the end of a song's name, like
"(Hardcore remix)" or "feat. Albert", when calculating a song's first and
last letters. And a song has to have at least one actual alphabetic letter
to really be considered.

2) You can enter a target duration. You can also set bounds on the duration,
which allows a query like "as close as possible to 5 minutes, but no longer"
(max duration => 300_000, target duration => 300_000)

3) Similarly to above, you can find the shortest playlist by setting the
target duration or target songs to 0 or 1.

An example session is given below, and that's all. Thanks for another quiz.

Cheers,
Dave

Loading...done.
What letter should the first song start with? z
What letter should the last song end with? a
Minimum songs in playlist: [1]
Maximum songs in playlist (more than 3 could take a while): [5115] 3
Target number of songs: [no target] 1
Minimum duration in milliseconds: [0]
Maximum duration in milliseconds: [Infinity]
Target duration in milliseconds: [no target]
Generating playlists...done in 41.063 seconds.
Found 61 playlists:
<Playlist tracks: 2, duration: 0:9:20>
1. Banco de Gaia - Zeus No Like Techno (6:01)
2. Buena Vista Social Club - Orgullecida (3:19)
....
<Playlist tracks: 2, duration: 0:9:08>
1. The Cranberries - Zombie (5:06)
2. The Carpenters - Every Sha La La La (4:02)
Another barrel of monkeys? [yN] y
What letter should the first song start with? z
What letter should the last song end with? a
Minimum songs in playlist: [1] 3
Maximum songs in playlist (more than 3 could take a while): [5115] 3
Target number of songs: [no target]
Minimum duration in milliseconds: [0]
Maximum duration in milliseconds: [Infinity]
Target duration in milliseconds: [no target] 999999999
Generating playlists...done in 47.656 seconds.
Found 1 playlist:
<Playlist tracks: 3, duration: 7:44:06>
1. Cherry Poppin Daddies - Zoot Suit Riot (3:53)
2. Robert A. Heinlein - The Menace from Earth (452:54)
3. Afro Celt Sound System - Hypnotica (7:18)
Another barrel of monkeys? [yN]