2008 Winter Scripting Games

Solution to Advanced Perl Event 8: Making Beautiful Music

Event 8 Solution


Perl solution to Event 8 in the 2008 Winter Scripting Games.

Solutions are also available for VBScript and Windows PowerShell.

*

Making Beautiful Music


Event 8 proved to be somewhat dangerous for one of the Scripting Guys: as soon as he had a script that could generate random playlists he couldn’t resist taking a much larger selection of songs and randomly generating playlist after playlist, just to see what songs would end up on his hypothetical CD, and in which order. If you’re wondering why it seemed to take so long for the Scripting Guys to post the Scripting Games program or to publish information about the Sudden Death Challenge, well, now you know.

In other words, this was kind of a fun event. And because the Scripting Guys are all about sharing the fun, let’s take a look at their solution to Event 8 in the Advanced Division:

%dictionary = ();
@arrDataList = ();
$intTotalTime = 0;

open (SongList, "C:\\Scripts\\SongList.csv");
@arrSongList = <SongList>;
close (SongList);

$low = 0;
$high = @arrSongList;

do
    {
        $b = int(rand($high) + $low);
        $blnCheck = exists($dictionary{$b});

        if ($blnCheck == 0)
            {
                $dictionary{$b} = $b;
                $strSong = @arrSongList[$b];
                @arrSongInfo = split(',',$strSong);
                $strArtist = @arrSongInfo[0];
                $strTitle = @arrSongInfo[1];
                $strTime = @arrSongInfo[2];
                @arrTime = split(':', $strTime);
                $intMinutes = @arrTime[0];
                $intSeconds = @arrTime[1];
                $intTime = (60 * $intMinutes) + ($intSeconds);

                if (($intTotalTime + $intTime) > 4800)
                    {}
                else
                    {
                        $blnCheck = exists($dictionary{$strArtist});
                        if ($blnCheck == 0)
                            {
                                $dictionary{$strArtist} = 1;
                            }
                         if ($blnCheck == 1)
                            {
                                $songTotal = $dictionary{$strArtist};
                                if ($songTotal > 2)
                                    {}
                                else
                                    {
                                        $dictionary{$strArtist} = $songTotal + 1;
                                        $newItem = $strArtist . "     " . $strTitle . "     " . $arrSongInfo[2];
                                        chomp($newItem);
                                        push(@arrDataList, $newItem);
                                        $intTotalTime = $intTotalTime + $intTime;
                                    }
                            }
                    }
             }
    if ($intTotalTime > 4500)
        {$x = 1;}
} 

until $x == 1;

@arrDataList = sort(@arrDataList);

for $song (@arrDataList) {
        print "$song \n";
    }

print "\n";

$intTotalSeconds = $intTotalTime % 60;

if ($intTotalSeconds < 10)
    {
        $zero = "O";
        $intTotalSeconds = $zero . $intTotalSeconds;
    }     

$intTotalMinutes = int($intTotalTime / 60);

$strTotalTime = "Total music time: " . $intTotalMinutes . ":" . $intTotalSeconds;
print $strTotalTime;

Let’s see if we can figure out how we managed to create a playlist for a CD. To begin with, we do some housekeeping chores; we set up an empty hash table (%dictionary) and an empty array (@arrDataList); and assign the value 0 to a counter variable named $intTotalTime:

%dictionary = ();
@arrDataList = ();
$intTotalTime = 0;

After that, we use this block of code to open the text file C:\Scripts\SongList.csv, read the contents of that file into an array named @arrSongList, and then close the file:

open (SongList, "C:\\Scripts\\SongList.csv");
@arrSongList = <SongList>;
close (SongList);

For those of you who have only a nodding acquaintance with Perl, in the first line we use the open function to open the file SongList.csv, giving this file the reference SongList. As you might have noticed, we also “escaped” each \ in the file path:

C:\\Scripts\\SongList.csv

Why did we do that? Well, as it turns out, the \ is a reserved character in Perl; consequently, we need to escape every instance of the character, which simply means prefacing it with a second \. In line 2, we use standard Perl syntax to read the contents of the file, storing each line in the file as a separate item in the array @arrSongList. And then, in line 3, we use the close function to close the file.

That brings us to these two lines of code:

$low = 0;
$high = @arrSongList;

In order to choose the songs for our playlist, we’re going to randomly select items from the array @arrSongList. In order to randomly select songs, we need to generate random numbers. And to generate random numbers we need to specify a range for those numbers. In this case the low end of our range is 0 (representing the index number of the first item in the array), and the high end of the range is the total number of items in that array. (In Perl, if you assign an array to a variable, that variable will end up containing the total number of items in the array.)

Now we’re ready to get down to business. To do that, we first set up a do loop designed to run until the variable $x is equal to 1 (until $x == 1). Inside that loop, we use this line to randomly select a number corresponding to the index numbers in our array:

$b = int(rand($high) + $low);

Once we have this number, we then check to see if this particular item (song) is already in our hash table:

$blnCheck = exists($dictionary{$b});

The exists function returns a Boolean value: True if the item can be found in the hash table, False if it can’t. In our next line of code, we check to see if the return value from the exists method if False (0):

if ($blnCheck == 0)

Let’s suppose that the item does exist in the hash table; that means that the song is already on our playlist. Therefore, we go back to the top of the loop and generate a new random number. What if the item doesn’t exist in the hash table? Well, in that case, we have a little more work to do.

To begin with, we add the song to the hash table; that’s what this line of code is for:

$dictionary{$b} = $b;

Next we grab the information for the specified song (based on the random number $b) from the array @arrSongList:

$strSong = @arrSongList[$b];

That’s going to result in $strSong being equal to something like this:

The Kinks,Sunny Afternoon,3:36

That’s good information, albeit not in that format. Therefore, we next use the split function to create a mini-array named @arrSongInfo, splitting the information in this string on the comma:

@arrSongInfo = split(',',$strSong);

That’s going to make @arrSongInfo equal to this:

The Kinks
Sunny Afternoon
3:36

Now we have something we can work with. For starters, we grab the value of the first item in the array (the artist name), and store it in a variable named $strArtist:

$strArtist = @arrSongInfo[0];

We then grab item 1 in the array (the song title) and store that value in a variable named $strTitle, then store the value of item 2 in the array (the total running time of the song) in a variable named $strTime:

$strTitle = @arrSongInfo[1];
$strTime = @arrSongInfo[2];

That’s pretty good, but not quite what we need. Why not? Well, as you know, the running time of each song is stored in the format minutes:seconds; i.e., 3:36. That’s fine, except adding time values like 3:36, 4:54, 2:21, and 4:58 is … well, let’s just say that it’s not a walk in the park. That’s a problem this block of code addresses:

@arrTime = split(':', $strTime);
$intMinutes = @arrTime[0];
$intSeconds = @arrTime[1];
$intTime = (60 * $intMinutes) + ($intSeconds);

In the first line, we’re again using the split function, this time to convert our time value into an array named @arrTime, an array consisting of the following two items:

3
36

We assign item 0 in this array to a variable named $intMinutes and item 2 in the array to a variable named $intSeconds; we then use this line of code to multiply $intMinutes by 60, then add that value to $intSeconds:

$intTime = (60 * $intMinutes) + ($intSeconds);

What’s the point of all that? Well, that gives us a total running time in seconds (in this case, 216 seconds). By converting all our running times to seconds, we’ll end up adding values like 216, 294, 141, and 298; adding values like these is a walk in the park, at least as far as Perl is concerned.

As the event instructions noted, our CD must contain at least 75 minutes (4500 seconds) of music, but no more than 80 minutes (4800 seconds). Therefore, before we do anything else we need to make sure that adding this song to our playlist won’t put us over the 4800-second limit. To do that, we add the running time of the song to the counter variable $intTotalTime:

if (($intTotalTime + $intTime) > 4800)

If the total running time exceeds 4800 seconds, then we go back to the top of the loop and try again. If it doesn’t, we use this line of code to check and see if this particular artist is already in the hash table:

$blnCheck = exists($dictionary{$strArtist});

Does that really matter? Yes; as you doubtless recall, our final playlist can contain a maximum of only two songs by a given artist. If $blnCheck comes back False (meaning that the artist isn’t in the hash table yet) we use this line of code to add the artist, using the artist name as the hash table key and 1 as the hash table value:

$dictionary{$strArtist} = 1;

If the artist does exist then we use this line of code to retrieve the hash table value for that artist:

$songTotal = $dictionary{$strArtist};

If this value is greater than 2 that means our playlist already has two songs by this artist; in that case, it’s back to the top of the loop to try again. If this value is not greater than 2 then we execute the following block of code:

$dictionary{$strArtist} = $songTotal + 1;
$newItem = $strArtist . "     " . $strTitle . "     " . $arrSongInfo[2];
chomp($newItem);
push(@arrDataList, $newItem);
$intTotalTime = $intTotalTime + $intTime;

In line 1 we simply increment the hash table value for this artist by 1; that helps us keep track of the number of songs by this artist on the playlist. (After adding the first song, this value will be 2. After adding the second song, the value will be 3, and we’ll no longer be able to add any songs by this artist.)

Next we use this line of code to create a string variable containing the artist name, the song title, and the song running time, separating each of these items with 5 blank spaces:

$newItem = $strArtist . "     " . $strTitle . "     " .

Note. If you haven’t worked with Perl before, the period is used for doing string concatenation, much in the same way the ampersand concatenates strings in VBScript and the plus sign combines string values in Windows PowerShell.

After using the chomp function to remove any end-of-line characters from our string value we then use this line of code to add the song information to the array @arrDataList:

push(@arrDataList, $newItem);

Finally, we add the total running time of the song (in seconds) to the counter variable $intTotalTime.

Our next task is to determine whether or not our total running time for the CD exceeds 4500 seconds; that’s what this line of code is for:

if ($intTotalTime > 4500)

If it does, then we’re done, meaning we set the value of $x to 1 and we exit the loop. If not, we go back to the top of the loop and add the next song to the playlist.

Once our playlist is complete we use the sort function to sort the list by artist name:

@arrDataList = sort(@arrDataList);

In turn, that enables us to print out song information for the list using this block of code:

for $song (@arrDataList) {
        print "$song \n";
    }

What’s that? Are we there yet? Well, almost. But we still have one more thing to do: we still have to echo back the total running time of the playlist, and in the format minutes:seconds.

Let’s assume that our total running time for the playlist, in seconds, in 4515. How do we convert 4515 seconds to minutes and seconds? Well, for starters, we do some modulus arithmetic, dividing the total running time by 60, and using the % operator to return the remainder:

$intTotalSeconds = $intTotalTime % 60;

In this case we get back 15 seconds; in other cases, however, we might get back, say, 7 seconds. Is that a problem? Well, sort of; after all, time values are typically expressed using leading zeroes for the seconds:

45:07

So yes, this is problem; fortunately, though, it’s not a big problem, in part because we can use this block of code to add a leading 0 to the seconds (assuming that the total seconds are less than 10):

if ($intTotalSeconds < 10)
    {
        $zero = "O";
        $intTotalSeconds = $zero . $intTotalSeconds;
    }

Once we have the seconds we can then use this line of code to calculate the minutes:

$intTotalMinutes = int($intTotalTime / 60);

Here we’re again dividing the total time by 60, then using the int function to drop any decimal points and take only the whole number portion of the result. For example, 4515 divided by 60 yields 75.25. The int function chops off the .25, leaving us with 75 minutes.

At that point we can display the running time using these two lines of code:

$strTotalTime = "Total music time: " . $intTotalMinutes . ":" . $intTotalSeconds;
print $strTotalTime;

And what will that give us? That will give us a playlist – and output – similar to this:

Alice Cooper     I'm Eighteen     2:58
Badfinger     Come and Get It     2:22
Badfinger     Day After Day     3:11
Credence Clearwater Revival     Bad Moon Rising     2:20
Dire Straits     Ticket to Heaven     4:25
Donovan     Lalena     2:55
George Harrison     What is Life?     4:27
John Prine     Same Thing Happened to Me     3:18
Nick Cave and the Bad Seeds     Red Right Hand     4:48
Nick Cave and the Bad Seeds     The Weeping Song     4:20
Nirvana     All Apologies     3:50
Nirvana     Heart-Shaped Box     4:41
REM     Man on the Moon     5:13
Robert Palmer     Addicted to Love     5:18
The Bangles     Manic Monday     3:06
The Beatles     Helter Skelter     4:29
The Beatles     I Saw Her Standing There     2:55
The Kinks     Apeman     3:56
The Kinks     Big Black Smoke     2:36
The Rolling Stones     Anybody Seen My Baby?     4:07

Total music time: 75:12

Wow, that is a nice playlist, isn’t it?


Top of pageTop of page