
About the Author Marco is currently an IT system analyst for one of Canada's leading telecommunications companies and has been working in the IT industry for over 10 years. Marco recently received a Microsoft Most Valuable Professional award for his contributions to the Windows PowerShell community. His personal blog is at http://marcoshaw.blogspot.com, where he covers all kinds of PowerShell-related topics. He is also a contributor and moderator of the new PowerShell Community site at http://www.powershellcommunity.org. |
Here are the highlights of my thought process and the actual act of scripting, with the good and the bad in there.
First off, the easy part was just reading the event details. From there, I set off thinking a bit about what I would need. I saw that we had to deal with times, so I thought that New-Timespan would likely be useful. I saw that the song list was provided as a .CSV file, so I would probably be using Import-CSV, too.
I next went to check the CSV file and noticed there wasn’t a header row. Because of that, I decided I would likely use Get-Content to read in the data.
I wanted to just do some interactive commands from a PowerShell console to see how I would deal with the time. I started off by over-complicating things on how I would extract the minutes and seconds, then finally ended up with this :
PSH> gc songlist.csv|%{$secs=(new-timespan -minute $_.split(":")[1] -seconds `
$_.split(":")[1]).totalseconds;$_.split(",")[0]+","+$_.split(",")[1]+","+$secs}
Something that will come to bite me back later: I used split above, but I’m getting the same element “[1]” twice! I caught, this later on.
From here, I started using v2’s new Graphical PowerShell editor to get the job done.
I started working on my logic, then realized that if a song was over 5 minutes that could complicate things. Therefore, I tried this next command:
$listing=gc songlist.csv"|%{
$secs=(new-timespan -minute $_.split(":")[1] -seconds $_.split(":")[1]).totalseconds
$_.split(",")[0]+","+$_.split(",")[1]+","+$secs
}
# Check if there's a song over 5 minutes as this will complicate the logic.
$listing|%{
if($_.split(",")[2] -gt 5*60){
return "There's one song over 5 minutes. Your logic might fail!"
}
}
The result is that there are songs over 5 minutes. OK, my logic will have to be more complicated, but I'll leave that for later.
After that I hacked something up. Finally, I think I have some working code. My code is built on the following pseudo-code (incomplete). The script:
1. | Remembers the current artist, so that we make sure we don’t have the same artist more than twice. |
2. | Goes to the next artist if the artist has been seen more than once. |
I’m thinking to myself, it can’t be this easy. Then I’m struck by lightning when I actually run it. What do I get? *NOTHING*. Oh well, back to the drawing board.
Speaking of drawing board, I probably should have drawn a diagram of my script before I started, but I didn’t. So shoot me…
I added a simple loop at the beginning of my script to tell me whether *something* was happening:
$i=0
#step 1
$listing|%{
$i
$i++
...
My output was 0 to 5, so something is working, but not completely. I looked at the first 7 entries of the input file:
PSH> gc songlist.csv|select -first 7 Alice Cooper,I'm Eighteen,2:58 Alice Cooper,Be My Lover,3:22 Alice Cooper,School's Out,3:30 The Animals,House of the Rising Sun,4:31 Badfinger,No Matter What,2:59 Badfinger,Day After Day,3:11 Badfinger,Come and Get It,2:22
Hmmm... Alice Cooper and Badfinger repeat 3 times here, at least. Maybe my logic for counting duplicates isn't working. I wasn’t sure what the problem was at first glance, so I started outputting some of the variables.
As it turned out, I had to remove an entire else section that was not doing what I had thought it would. I’m getting some output now.
Now, I need to go back and put in checks to make sure the total song time is greater than 75 minutes and less than 80 minutes. I'll have to do this a few times, so I'd better create a function:
function check_total {
$tmp_total=$total
if($total -lt 75*60){
$tmp_total+=$_.split(",")[2]
if($tmp_total -lt 80*60){
$total=$tmp_total
$final+=$_
}
}
else{
"Error!"
return "An error has occured!"
}
}
I plugged this in; it wasn’t working properly at all. More debugging, more echoing, then I backed up a bit. I determined I had a problem with scoping, and fixed that.
And still my results made no sense. I rechecked a few things, then I noticed the error that I mentioned earlier:
new-timespan -minute $_.split(":")[1] -seconds $_.split(":")[1]
What had I done there!? I changed this to the following:
new-timespan -minute $_.split(":")[0] -seconds $_.split(":")[1]
Finally, things were looking up. Now, I just needed to work on the formatting of the data. As an extra challenge, I decided to also output the song list to v2's Out-Gridview cmdlet. I also decided to output the songlist exactly as the event states to the console.
I could have gone with format-table or other to output to the console, but I wanted to reproduce the exact requirements as they were laid out, making sure to sort the data by artist name!
Here’s the final script:
#requires -version 2
# The above is added because we use out-gridview below.
# out-gridview comes from the v2 CTP.
# Get my data.
$listing=gc "c:\scripts\songlist.csv"|foreach-object{
$secs=(new-timespan -minute $_.split(",")[2].split(":")[0] `
-seconds $_.split(",")[2].split(":")[1]).totalseconds
$_.split(",")[0]+","+$_.split(",")[1]+","+$secs
}
# Initialize some variables.
$used=0
$artist=$null
# Define as an array.
$script:final=@()
# Setup a variable with a script scope.
$script:total=0
# We will use this a few times to check our current total.
# We need to make sure our current size is between 75MB and 80MB.
# We do a little bit of magic here.
function check_total {
$tmp_total=$script:total
if($total -lt 75*60){
$tmp_total+=$_.split(",")[2]
if($tmp_total -lt 80*60){
$script:total=$tmp_total
# We're good to go. Write this to the list.
$script:final+=$_
}
}
}
# OK the guts are here.
$listing|foreach-object{
if($artist -ne $null){
# Get the artist from the current object.
$cur_artist=$_.split(",")[0]
# If the current object's artist property matches the last
# object's same property.
if($cur_artist -eq $artist){
if($used -lt 2){
$artist=$_.split(",")[0]
check_total
$used++
}
}
# We have started using a new artist.
else{
$artist=$_.split(",")[0]
check_total
$used=1
}
}
else{
# Save the artist in a variable and write the current object
# to the final array.
$artist=$_.split(",")[0]
check_total
$used++
}
}
# Now we need to convert the seconds per song back to MINUTE:SECOND.
$final_list=$script:final|foreach-object{
$mins=((new-timespan -seconds $_.split(",")[2]).totalminutes).tostring().split(".")[0]
$secs=(new-timespan -seconds $_.split(",")[2]).seconds
if($secs -lt 10){
$secs="0"+$secs
}
$_.split(",")[0]+","+$_.split(",")[1]+","+$mins+":"+$secs
}
# We need to create some objects here so that we can use out-gridview.
$final_list| `
select-object @{e={$_.split(",")[0]};n="Artist"},@{e={$_.split(",")[1]};n="Song"},`
@{e={$_.split(",")[2]};n="Length"}|sort-object Artist|tee-object -variable data|out-gridview
# We need to format the data to output to the console.
$final_list|foreach-object{$_.split(",")[0]+"`t"+$_.split(",")[1]+"`t"+$_.split(",")[2]}|
`
sort-object
# Now we need to format the total time as MINUTE:SECOND.
$tot_minutes=((new-timespan -seconds $script:total).totalminutes).tostring().split(".")[0]
$total_secs=(new-timespan -seconds $script:total).seconds
if($total_secs -lt 10){
$total_secs="0"+$total_secs
}
""
""
"Total music time: "+$tot_minutes+":"+$total_secs