Scripting Games, Advanced Event 8, Making Music

A list of songs are provided in a CSV file.  Produce a CD’s worth, i.e. between 75-80 minutes worth of music (selecting a maximum of two tracks from any one artitst).

  1 # The input CSV file doesn't have headers so can't use import-csv. Create custom objects instead...
  2 $s=$(foreach ($line in Get-Content 'c:\scripts\songlist.csv') {
  3 	$song=[object]|select Artist, Title, Time
  4 	$song.artist, $song.title, $song.time = $line.Split(',')
  5 	$song
  6 })
  8 # Now select a playlist.  Shuffle the tracks and select songs until we've got a CD's worth...
 10 do {
 11 	# Randomise the songlist to make the resulting CD more interesting!
 12 	# This is the Fisher-Yates shuffle again...
 13 	$c=$s.count-1
 14 	$r=New-Object system.random
 15 	0..$c|%{$j=$r.Next($_,$c); $x=$s[$_]; $s[$_]=$s[$j]; $s[$j]=$x}
 17 	# Pick songs from the list until we have > 75 minutes worth...
 18 	$CDtime=[timespan]0;
 19 	$playlist=@()
 20 	for ($i=0; ($CDtime -lt [timespan]"01:15:00") -and ($i -le $c); $i++) {
 21 		# Only add this track if we don't have two songs by this artist already
 22 		if (($playlist|?{$_.artist -eq $s[$i].artist}).count -lt 2) {
 23 			$playlist+=$s[$i]
 24 			$CDtime+=[timespan]::Parse("00:$($s[$i].time)")
 25 		}
 26 	}
 27 	$mins=[int]$CDtime.totalminutes
 29 } until ($mins -lt 80)		# If the playlist is too long, shuffle the tracks and try again
 32 $playlist|sort artist|ft -auto		# Display the list of songs in the playlist
 34 "Total music time: {0}:{1,2:00}" -f $mins, $($CDtime.seconds)

We’d normally use import-csv to read a CSV file, but this isn’t very useful here because the Scripting Guys forgot to add a header line to their music list!  Instead we read the CSV using Get-Content and create custom objects for each track (lines 2-6) – storing the resulting array of songs in $s.

Songs need to be added to a CD until the resulting list is greater than 75 minutes long.  But – the list has to fit on to the CD, so it has to be less than 80 minutes long.

There were two ways to go here.  I could’ve used a back-tracking technique (no pun intended!) – adding songs until >75 minutes worth, then if the result is >80 minutes long, remove a track and try others.  If none of the combinations work, remove two tracks and try others – etc etc.  But this seemed like overkill, so I decided to simply randomise the list of songs and select greater than 75 minutes worth.  If the result won’t fit (>80 minutes) then I throw away the entire list, randomise again and pick a new list.  The do..until loop in lines 10 and 29 keeps trying until the desired result is obtained.  For lists of this length this is good enough IMO – I never saw this take more than two attempts to generate a suitable list of tracks.

To randomise the list of songs we use the Fisher-Yates shuffle again (as seen in event 7).  This time the algorithm (lines 13-15) is written as a pipeline and counts from the beginning of the array.

To add up the minutes and seconds for each track we use the PowerShell [timespan] type in $CDtime (line 18).   The loop in line 20 iterates through the array of songs until the total timespan is greater than 75 minutes (while $CDtime -lt [timespan]"01:15:00").

The check at line 22 ensures that no more than two tracks by a single artist make it onto the CD.  If there’s less than two songs by the current artist the track is added and the total time updated accordingly.  The resulting playlist is displayed, sorted by artist, along with the final total track time.


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s