
Windows PowerShell solution to Event 8 in the 2008 Winter Scripting Games.
Event 8 turned out to be something of an anomaly among Scripting Games events. Why? Because this is a script you might possibly use sometime. The truth is, you probably won’t ever need to generate a list of prime numbers between 1 and 200; for that matter, you’ll probably never need to reverse all the words in a text file, either. (Although who knows? After all, for all we know the daily Hey, Scripting Guy! column contains hidden messages that can be revealed only by reversing the characters in each word. Heaven knows that, read in regular fashion, the columns rarely make much sense.)
But come up with a set of songs that will fit on a CD? That, as they say, is a horse of a different color. Will you ever need to generate a set of songs to burn to a CD? Some people do that on a daily basis.
If you happen to be one of those people, the following little script might come in handy:
$a = New-Object Random
$dictionary = @{}
$dataList = @()
$intTotalTime = 0
$arrContents = Get-Content "C:\Scripts\SongList.csv"
:OuterLoop do
{
$b = $a.next(0, $arrContents.Length)
$blnCheck = $dictionary.Contains($b)
if ($blnCheck -eq $False)
{
$dictionary.Add($b, $b)
[string] $strSong = $arrContents[$b]
$arrSongInfo = $strSong.Split(",")
$strArtist = $arrSongInfo[0]
$strTitle = $arrSongInfo[1]
$arrTime = $arrSongInfo[2].Split(":")
$intTime = (60 * $arrTime[0]) + $arrTime[1]
if ($intTotalTime + $intTime -gt 4800)
{}
else
{
$blnCheck = $dictionary.Contains($strArtist)
if ($blnCheck -eq $False)
{$dictionary.Add($strArtist, 1)}
if ($blnCheck -eq $True)
{
$songTotal = $dictionary.Get_Item($strArtist)
if ($songTotal -gt 2)
{}
else
{
$dictionary.Set_Item($strArtist, $songTotal + 1)
$newItem = $strArtist + " " + $strTitle + " " + $arrSongInfo[2]
$dataList += $newItem
$intTotalTime = $intTotalTime + $intTime
}
}
}
}
if ($intTotalTime -gt 4500)
{break OuterLoop}
}
while ($x -ne 100)
$dataList | Sort-Object
Write-Host
$intTotalSeconds = $intTotalTime % 60
if ($intTotalSeconds -lt 10)
{$zero = "0"
$intTotalSeconds = [string] $zero + $intTotalSeconds
}
$intTotalMinutes = [math]::floor([int] $intTotalTime / [int] 60)
$strTotalTime = [string] "Total music time: " + $intTotalMinutes + ":" + $intTotalSeconds
Write-Host $strTotalTime
Let’s see if we can figure out how this script works. To begin with, we use the New-Object cmdlet to create an instance of the System.Random class; System.Random, as the name implies, is a .NET Framework class that enables us to generate random numbers. After we get our instance of System.Random we then create an empty hash table named $dictionary and an empty array named $dataList:
$dictionary = @{}
$dataList = @()
From there we set a counter variable named $intTotalTime to 0, then use the Get-Content cmdlet to read in information about all the available songs, a list kept in the file C:\Scripts\SongList.csv:
$arrContents = Get-Content "C:\Scripts\SongList.csv"
By default, that’s going to create an array named $arrContents, with each item in the array representing information about a particular song:
Alice Cooper,I'm Eighteen,2:58
Once we’ve done that, we’re ready to generate a playlist.
To that end, we start by setting up a do loop designed to run as long as $x does not equal 100. (Something that will never happen, seeing as how we don’t do anything to change the value of $x.) Inside this loop, we use our instance of the System.Random class and the next method to generate a random number between 0 (inclusive) and the number of items in our array of song information (exclusive):
$b = $a.next(0, $arrContents.Length)
As soon as we have that random number we use the hash table’s Contains method to determine whether or not that number appears in the hash table:
$blnCheck = $dictionary.Contains($b)
If $blnCheck returns True ($True) we simply return to the top of the loop and try again with another random number. If $blnCheck returns False, however, well ….
If $blnCheck returns False we use the Add method to add the random number to the hash table, using the number as both the hash table key and item:
$dictionary.Add($b, $b)
Let’s assume that $b (our random number) is equal to 5. With that in mind, we add a new key and item to the hash table, then use this line of code to grab array item 5 and store that value in a string variable named $strSong:
[string] $strSong = $arrContents[$b]
That’s going to make $strSong equal to this:
Badfinger,Day After Day,3:11
From there we use the split method to split $strSong into an array named $arrSongInfo; that’s going to give us a mini-array consisting of the following items:
Badfinger Day After Day 3:11
In other words, at this point we’ve simply separated the song’s artist, title, and running time, making it easy to access any one (or all) of those values.
How easy? Well, easy enough that we can use these two lines of code to assign the artist and title to the variables $strArtist and $strTitle, respectively:
$strArtist = $arrSongInfo[0] $strTitle = $arrSongInfo[1]
The running time, as you might expect, is a little trickier; that’s because running time is stored in the format minutes:seconds. Is that a problem? Well, it can be. After all, we need to keep track of the total running time of all the music on our CD; that running time can’t exceed 80 minutes (4800 seconds). Trying to add up a series of time values like 4:36 + 3:57 + 2:29 + 4:18 is anything but easy; consequently, we decided to convert the running time to seconds. How do we do that? By using these two lines of code:
$arrTime = $arrSongInfo[2].Split(":")
$intTime = (60 * $arrTime[0]) + $arrTime[1]
In the first line we’re using the split method to create another array, splitting our time (3:11) on the colon. That results in an array named $arrTime that consists of the following two items:
3 11
In line 2 we then determine the total number of seconds in the running time. Because the first item in the array $arrTime (3) represents the minutes we multiply that value by 60, then add the result to the second item in the array (11). The net result? The song Day After Day has a running time of 191 seconds.
Still with us? Good. Our next step is to make sure that adding this song to our playlist will not cause us to exceed the maximum allowed time of 80 minutes. That’s what this line of code is for:
if ($intTotalTime + $intTime -gt 4800)
As you might have guessed, the variable $intTotalTime keeps track of the total running time of our playlist. What we do here is check to see if the running time for our new song would cause the total running time to exceed 4800 seconds. If it does, we’re not going to do anything with this particular song; instead, we’ll simply go back to our do loop and try again.
But what if the new song doesn’t cause us to exceed the total running time limitation; does that mean that we can add that song to our playlist? Not yet. Before we can do that we need to check and see if the song artist is already in our hash table:
$blnCheck = $dictionary.Contains($strArtist)
If $blnCheck is False, then we use this line of code to add the artist to the hash table, using the artist name as the hash table key and the value 1 as the hash table item:
$dictionary.Add($strArtist, 1)
Ah, good question: what is the 1 for? Well, that simply indicates that this is the first song by this artist to be added to the playlist. That’s important, because the event rules state that the playlist can contain no more than 2 songs by a given artist.
If $blnCheck returns True then we use the Get_Item method to retrieve the hash table item value for the artist in question:
$songTotal = $dictionary.Get_Item($strArtist)
If this value is greater than 2 that means our playlist already contains 2 songs by this artist. Because that’s the maximum number of songs allowed per artist that means we can’t add this song to the playlist after all; therefore, we simply pop back to the top of the do loop and try again. On the other hand, if the item value is not greater than 2 we execute this block of code:
$dictionary.Set_Item($strArtist, $songTotal + 1) $newItem = $strArtist + " " + $strTitle + " " + $arrSongInfo[2] $dataList += $newItem $intTotalTime = $intTotalTime + $intTime
In the first line we’re simply incrementing the hash table value for our artist by 1. If the artist already has 1 song in the playlist this will assign the value 2 to the hash table; if, later on, we randomly select another song by this artist then we’ll know that the song we’re about to add will be the second song for this artist. After adjusting the hash table we use this line of code to combine the artist name, song title, and total running time into a single string value:
$newItem = $strArtist + " " + $strTitle + " " + $arrSongInfo[2]
Once that’s done we add this string value to our array which, needless to say, is the container we’re using to keep track of our song list:
$dataList += $newItem
Finally, we add the total running time for the new song to the variable $intTotalTime; that has the net effect of updating the total running time for our CD:
$intTotalTime = $intTotalTime + $intTime
Next we need to check to see if that total running time exceeds 75 minutes (4500 seconds):
if ($intTotalTime -gt 4500)
If it does, then we’ve added enough music to this particular CD. Consequently, we use the break command to break out of our do loop. If not, then we simply loop around and repeat the entire process all over again.
Sooner or later, of course, we will exceed the 4500 second barrier and thus break out of the do loop. When we do so, the first thing we do is pass our song list to the Sort-Object cmdlet:
$dataList | Sort-Object
It probably goes without saying that this will sort and display the playlist by artist name (because each song starts off with the artist name). That’s going to give us output similar to this:
Alice Cooper School's Out 3:30 Badfinger Carry On Til Tomorrow 4:49 Badfinger No Matter What 2:59 Dire Straits So Far Away 5:12 Dire Straits Walk of Life 4:12 Donovan Catch the Wind 5:02 Donovan Lalena 2:55 Nick Cave and the Bad Seeds Henry Lee 3:56 Nick Cave and the Bad Seeds The Weeping Song 4:20 Nirvana Heart-Shaped Box 4:41 Nirvana The Man Who Sold the World 4:20 Paul Simon Mother and Child Reunion 2:48 Robert Palmer Addicted to Love 5:18 The Beatles Help 2:18 The Beatles Rocky Raccoon 3:41 The Kinks Lola 4:05 The Kinks Village Green Preservation Society 2:49 The Rolling Stones Anybody Seen My Baby? 4:07 The Rolling Stones It's Only Rock and Roll 4:10
All that’s left now is to correctly display the total running time for our playlist. At the moment, all we have for a running time is the total number of seconds; in this case, 4512. Somehow or another, we need to turn 4512 seconds into 75 minutes and 12 seconds.
So how are we going to do that? Well, to begin with, we’re going to execute this line of code:
$intTotalSeconds = $intTotalTime % 60
What we’re doing here is using the modulus operator (%) to divide 4512 seconds by 60. The modulus operator is designed to divide 2 numbers and return the remainder. In this case, 4512 divided by 60 equals 75, remainder 12. That remainder – 12 – represents the number of seconds in our total running time.
Well, almost. Before we can call this the final, official number of seconds we need to run this block of code:
if ($intTotalSeconds -lt 10)
{$zero = "0"
$intTotalSeconds = [string] $zero + $intTotalSeconds
}
What are we doing here? Well, in the minutes:seconds format seconds are always displayed using leading zeroes; thus 76 minutes and 3 seconds is displayed like this:
76:03
With this block of code we’re checking to see if the umber of seconds in our running time is less than 10. If it is, then we simply put a leading 0 onto the front of the value, turning 3 into 03.
As for the total minutes, well, that can be (and is) calculated using this line of code:
$intTotalMinutes = [math]::floor([int] $intTotalTime / [int] 60)
This is, admittedly, a crazy-looking line of code. All we’re doing, in a somewhat-convoluted fashion, is dividing 4512 by 60. That gives us a value of 75.2. Of course, we don’t want the .2; all we want is the 75. That’s what [math]::floor is for. Here we’re using the .NET Framework’s System.Math class and the floor method to strip off the decimal points and return just the integer portion of the value. (Technically, the floor method “returns the largest integer less than or equal to the specified number.”) It’s goofy-looking, but it works.
Once we know the minutes we can create a string value that displays information about the total running time of the CD:
$strTotalTime = [string] "Total music time: " + $intTotalMinutes + ":" + $intTotalSeconds
When we echo back the value of $strTotalTime we should see something like this:
Total music time: 75:12
And now, if you’ll excuse us, we have a CD to burn.