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 })
  7 
  8 # Now select a playlist.  Shuffle the tracks and select songs until we've got a CD's worth...
  9 
 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}
 16 
 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
 28 
 29 } until ($mins -lt 80)		# If the playlist is too long, shuffle the tracks and try again
 30 
 31 
 32 $playlist|sort artist|ft -auto		# Display the list of songs in the playlist
 33 
 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.

Scripting Games, Advanced Event 7, Play Ball

Work out a rota for games between 6 teams; randomise the list to avoid one team playing too many consecutive games.

  1 $games=@()
  2 $teams=‘A’,‘B’,‘C’,‘D’,‘E’,‘F’
  3
  4 for ($i=0; $i -lt $teams.length; $i++) {
  5     $iTeam=$teams[$i]
  6     for ($j=$i+1; $j -lt $teams.length; $j++) {
  7         $jTeam=$teams[$j]
  8         # Following line distributes home and away games :-)
  9         if (($i+$j)%2) {$games+="$iTeam vs. $jTeam"} else {$games+="$jTeam vs. $iTeam"}
10     }
11 }
12
13 # Mix the games up by repeatedly swapping array elements with other random elements
14 $element= New-Object System.Random
15
16 for ($i=$games.length-1; $i -ge 0; $i–) {
17     $index=$element.next(0,$i)
18     # Swapping element $i with $index…
19     $x=$games[$i]; $games[$i]=$games[$index]; $games[$index]=$x
20 }
21
22 $games
23

So, the script is in two sections: the first part (lines 4-11) works out the list of possible unique games between the teams (and saves them in $games).  By taking even- and odd-numbered fixtures (line 9) we distribute home and away games (not required, but seemed fair:-)

Lines 14-20 use an algorithm known as a Fisher-Yates shuffle to mix up the games in a random way by swapping (in place) elements of the array with random other elements.  Item one is swapped with an item randomly selected between 1..n.  Item 1 is, in effect, now an element randomly selected from the whole array.  Item 1 is now left alone and item 2 is randomly selected from the remaining items (2..n).  And so on…  (Note: in this script, for some reason, I started at the last element and worked backwards – not sure why but normality is returned the next time this algorithm appears!)

The resulting fixtures are displayed (line 20) and that’s it.

Scripting Games, Advanced Event 6, Prime Time

When I first saw this one on the list I grinned a bit because one of the first real scripts I wrote in PowerShell was a translation of a procedure to calculate prime factors. I say "real" script here in the sense of "More than a couple of lines long" rather than "Of use in a real world situation"!

But this was actually a different problem – in fact just a simple prime sieve.

Here it is:

  1 param ([int]$max=200)	# Find primes up to $max
  2 
  3 $sieve=,1*($max+1)	# Initialise sieve, assume all primes to start
  4 
  5 for ($i=2; $i -le $max; $i++) {
  6 	# Find next prime entry in sieve
  7 	if ($sieve[$i]) {
  8 		$i
  9 		# Blank out higher multiples of current prime in sieve
 10 		for ($j=$i*$i; $j -le $max; $j+=$i) {$sieve[$j]=0}
 11 	}
 12 }

The "sieve" in a prime sieve algorithm is just a list of numbers that are either possible primes, or which have been shown to have factors.  A couple of things I like about PowerShell (among many things!): initialising an array of number from 1 to 12 (or whatever) is just $a=1..12; Initialising an array of a single item and giving it a value of one is: $a=,1; But if you apply the multiplication operator to an array PowerShell will duplicate the array by the number of times specified.  To create a 200 element array of integers with all array entries set to 1, use: $a=,1*200.  Nice!  This is used at line 3 to create the sieve.  The "1" in this case is just a boolean value to indicate that the number at array position [x] is a prime.

{Note: I could’ve used $true and $false rather than 0 and 1 in the sieve, but old habits die hard, sorry!}

We then start at 2 and count up to $max looking for the next prime – in other words, where sieve[i] is true (line 7).  Initially the whole sieve is indicating that every number is a prime (because no factors have been found yet), so $sieve[2] is true.

Every time a match is found, the script displays the match (line 8) and then does the sieving thing.  Multiples of the current prime (in the first case 2, 4, 6, 8, …..) are marked in the sieve by looping up to $max, in increments of the current prime, setting the corresponding sieve elements to false (0) along the way (line 10).  There’s a small optimisation here; elements in the sieve less than the square of the current prime can be skipped because they have already been set by previous factors – so we start setting entries at ($i*$i).

/\/\o\/\/ mentioned performance in his post; I don’t think it was me on the #powershell IRC channel (I can’t find a good IRC client – can anyone recommend one??)  But here’s a sample run on the first 100,000 primes (faster than a brute-force attack to 200!):

PS > (Measure-Command {./primes.ps1 100000}).tostring()
00:00:02.7081285
PS >

And on a cool million:-)

PS > (Measure-Command {./primes.ps1 1000000}).tostring()
00:01:01.5248231
PS >

So, this was about prime numbers – I’ll post my prime factor script (a translation from Rexx, S/370 Assembler, C and Basic in reverse chronological order:-) in a future blog post.

Scripting Games, Advanced Event 5, Strong Password

Check a given password against various specified criteria and rate the password accordingly.  This was mostly an exercise in pattern matching.  I chose to use a Switch block (actually three switch blocks, just because I could:-)

  1 param ($p=$(Read-Host "Enter Password to check"))
  2 $Score=13; $dictionary='c:\scripts\wordlist.txt'
  3 
  4 switch ($p) {
  5 	{Select-String "^$_$" -Quiet -Path $dictionary}
  6 		{"Password is in dictionary"; $Score--}
  7 	{Select-String "^$($_.substring(0,$_.length-1))$" -Quiet -Path $dictionary}
  8 		{"Password minus last character is in dictionary"; $Score--}
  9 	{Select-String "^$($_.substring(1))$" -Quiet -Path $dictionary}
 10 		{"Password minus first character is in dictionary"; $Score--}
 11 	{($_ -match '0') -and (Select-String "^$($_.replace('0','o'))$" -Quiet -Path $dictionary)}
 12 		{"Password (replacing zero for O) is in dictionary"; $Score--}
 13 	{($_ -match '1') -and (Select-String "^$($_.replace('1','l'))$" -Quiet -Path $dictionary)}
 14 		{"Password (replacing one for l) is in dictionary"; $Score--}
 15 	{($_.length -lt 10) -or ($_.length -gt 20)}
 16 		{"Password must be between 10 and 20 characters long"; $Score--}
 17 	{!($_ -match '[0-9]')}
 18 		{"Password must include at least one digit"; $Score--}
 19 	{!($_ -cmatch '[A-Z]')}
 20 		{"Password must include at least one uppercase letter"; $Score--}
 21 	{!($_ -cmatch '[a-z]')}
 22 		{"Password must include at least one lowercase letter"; $Score--}
 23 	{!($_ -match '[^a-z0-9]')}
 24 		{"Password must include at least one symbol"; $Score--}
 25 }
 26 
 27 switch -case -regex ($p) {
 28 	'[a-z]{4,}'		{"Password includes 4 or more consecutive lowercase characters"; $Score--}
 29 	'[A-Z]{4,}'		{"Password includes 4 or more consecutive uppercase characters"; $Score--}
 30 	'(.).*\1'		{"Password includes duplicate characters"; $Score--}
 31 }
 32 
 33 $Strength=$(switch ($score) {
 34 	{$_ -le 6}	{"weak"}
 35 	{$_ -ge 11}	{"strong"}
 36 	default		{"moderately-strong"}
 37 });"Password score $score, indicating a $strength password"
 

First thing is to read the word list file.  Since we’re going to be using this more than once we cache the content (in $dictionary).  The first five tests are handled by select-string.  Next is a simple length check.  Then some regex checking. 

The second switch block does some further regex pattern matching, including a simple regex to check for duplicate characters:

(.).*\1

Although this looks a bit like ascii-art (or a text-message smiley of some kind!) this actually says "Match any single character and remember what is was ‘(.)’; then match zero or more further characters ‘.*’; then match another occurrence of whatever character we matched initially ‘\1’".

This will scan along the string starting with the first character and walk up along the string checking and matching each character in turn until a character is found that occurs at least twice.  At this point, if there are duplicate characters, the regex succeeds.  Otherwise the end of the string is reached without a match and the regex fails.

The final switch block displays the result.

Scripting Games, Beginner Event 6, Coffee Break

Read a file containing coffee orders for a bunch of offices and add up the total number of coffees of each type.

Lines in the order file corresponding to the office number ("Office 100" etc) are dropped from the pipeline, other entries from the file are split into coffee type and number and used to populate a hash table.  Line 6 displays the resulting hash table.

  1 $Orders=@{}
  2 Get-Content 'c:\scripts\coffee.txt'|?{!($_ -match '^Office')}|%{
  3 	$coffee, $number = $_.split()
  4 	$Orders[$coffee]+=[int]$number
  5 } 
  6 $Orders.psbase.keys|%{"$($_): $($Orders[$_])"}

Scripting Games, Beginner Event 5, What’s the Difference

Take a date parameter in the form "March 3, 2008" and display the difference from today’s date in various contrived formats…

Although this worked OK for me apparently the Scripting Guys didn’t like it much as I got a 0 – ho hum!  Maybe it doesn’t work in US timezones – or maybe I just messed up the submission?  Or maybe it’s just wrong?!  If you can see why it might be broken please post a comment – thanks!

  1 # Collect all the supplied arguments and join them together as strings...
  2 # We're hoping to be passed "March 3, 2009" (but without the quotes, unfortunately!)
  3 $Args|%{$s=""}{$s+=[string]$_+" "}
  4 
  5 $d=Get-Date($s)	# Hopefully the resulting parameter string will turn out to be a valid date
  6 
  7 $now=Get-Date
  8 
  9 $MonthsDifference=($d.year-$now.Year)*12+$d.Month-$now.Month-1
 10 $MonthDaysDifference=($d-$now.AddMonths($MonthsDifference)).days
 11 
 12 "Days: $(($d-$now).days)"
 13 "Months: $(($d.year-$now.year)*12+$d.month-$now.month)"
 14 "Month/Days: $($MonthsDifference)/$MonthDaysDifference"

This does illustrate one of the foibles of PowerShell’s argument handling; while it is undoubtedly a Good Thing to have unified parameter parsing that’s handled for you automatically, it can be a nuisance when the specific requirements need something else.  In this case we need to take the complete parameter string and pass it to Get-Date to convert it to a date type*.  This would be fine if the whole parameter string were enclosed in quotes, but since we can’t rely on that we have to piece together the individual chunks from $args[].  In a good-old batch file we can get the full argument list in a single variable, but not in PS!

We could, instead, get the line used to invoke the script from $MyInvocation.line and split off the script name from the start of the line, but this fails if the script was called from a line like (for example):

> $a=script parm1 parm2; $a.length

In this case the information from $MyInvocation.line includes the WHOLE line (including the second statement after the ";").  Once again, we could do a bit more (potentially complex) parsing to eliminate all but the section we are interested in, but this starts to get very messy.

Thankfully this situation is improved (somewhat) with V2 (although maybe I should get onto Connect and request an $AllArgs variable:-)

 

* <rant> Oh, BTW, we could use a cast to [datetime] but in general casts to this type are best avoided (even if you do live in the US) thanks to the horribly broken format the PS team sprung on us at the very end of the V1 beta :-(   </rant>

Scripting Games, Sudden Death Challenge 5

First, some excuses!  I haven’t done much playing around with WMI so far – and was also pushed for time to get this in; it works fine but maybe a little rough around the edges!

Problem was to list properties of WMI Win32 classes with names starting from A-Z (actually from A-Y since as of today there aren’t any property names in the Win32 classes starting with Z)

  1 $w=Get-WmiObject -List|?{$_.Name -like 'Win32_*'}
  2 
  3 for ($c='A'; $c -le 'Z'; [char]$c=[int][char]$c+1) {
  4 	foreach ($name in $w) {
  5 		if ($found=$name.PSBase.properties |?{$_.name -like "$($c)*"}|Select-Object -First 1) {
  6 			"{0,-30}{1}" -f $($found.name), $($name.__CLASS)
  7 			break
  8 		}
  9 	}
 10 }

So, the script initially gets a list of all the Win32 WMI classes (in $w) and then loops through initial characters from A to Z.  For each initial character it iterates over the WMI classes checking the properties of each class for a property name beginning with the current initial letter ($name.PSBase.properties |?{$_.name -like "$($c)*"}).  When an entry is found it’s displayed and the script moves on to the next initial character.  Here’s the output on my (Vista) machine:

AdditionalDescription         Win32_JobObjectStatus
Bias                          Win32_TimeZone
ControlFlags                  Win32_SecurityDescriptor
Description                   Win32_PrivilegesStatus
ExitStatus                    Win32_ProcessStopTrace
FileName                      Win32_ModuleLoadTrace
GuidInheritedObjectType       Win32_ACE
HostingModel                  Win32_OsBaselineProvider
ImageBase                     Win32_ModuleLoadTrace
JobMemoryLimit                Win32_NamedJobObjectLimitSetting
KeepAliveInterval             Win32_NetworkAdapterConfiguration
Logfile                       Win32_NTLogEvent
MachineName                   Win32_ComputerSystemEvent
Name                          Win32_Trustee
Operation                     Win32_PrivilegesStatus
ParameterInfo                 Win32_PrivilegesStatus
Quarter                       Win32_CurrentTime
RecordNumber                  Win32_NTLogEvent
StatusCode                    Win32_PrivilegesStatus
TIME_CREATED                  Win32_Trustee
UserStackBase                 Win32_ThreadStartTrace
Version                       Win32_OsBaselineProvider
Win32ErrorCode                Win32_JobObjectStatus
XOffCharacter                 Win32_SerialPortConfiguration
YResolution                   Win32_PrinterConfiguration

Scripting Games, Advanced Event 4, Image is Everything

Display a calendar for the specified month (…tidy formatting counts!)

Years’ ago one of the of the tasks we were set at uni was to write a compiler and an intermediate-code interpreter for a subset of Pascal.  When my solution was up a running one of the test programs I complied and ran produced a calendar in a similar way (although I’ve long since lost the source, unfortunately, thanks to frequent occurrences of media obsolescence; maybe XML saved to the web will stand the test of time!)

Back then, calculating the day of the week for a given date involved using an algorithm known as Zellar’s congruence.  These days just use the .Net .DayOfWeek property:-)

  1 $month, $year=(Read-Host "Enter month/year").split('/')
  2 
  3 $days=[datetime]::DaysInMonth($year,$month)
  4 
  5 " "; get-date("$year/$month") -f "MMMM yyyy"; " "
  6 "Sun  Mon  Tue  Wed  Thu  Fri  Sat"
  7 
  8 $line="     "*[int](get-date("$year/$month/1")).dayofweek
  9 for ($i=1; $i -le $days; $i++) {
 10   $line+="{0,3:##}  " -f $i
 11   if ($line.Length -ge 35){$line; $line=""}
 12 }
 13 if ($line) {$line}

All standard stuff.  Line 1 reads the month and year ("3/2008") in the format specified.  5-6 display a couple of header lines.

The only line of note is line 8.  This takes the day of the week for the 1st of the month in question (Sunday..Saturday as an integer 0..6) and uses this value to calculate in which column day 1 should appear.  A number of spaces are multiplied by the ordinal of the day of the week to give the correct offset.

Lines 9-11 then produce output lines, adding on days until the end of the month.

The formatting for a two digit integer displayed in a space 3 character wide (0,3:##) is vaguely reminiscent of COBOL (anyone else remember those PICTURE entries in the DATA DIVISION?  Shiver!)

Scripting Games, Advanced Event 3, Instant Runoff Election

An interesting problem, although the resulting script (well, my resulting script!) isn’t that thrilling…

Basic recap: votes are cast for candidates in order of preference (1st choice, 2nd choice etc).  Count all the 1st choice votes for each candidate; if any of the candidates have greater than 50% of the total 1st choice votes they win.  Otherwise, ignore votes for the candidate in last place and recount, promoting other candidates accordingly.

Here’s my entry:

  1 $results=Get-Content 'c:\scripts\votes.txt'
  2 $candidates = 4
  3 $eliminated = @{}
  4 
  5 for ($i = $candidates - 1; $i -ge 0; $i -- ) {
  6 	$votes = @{}   # Accumulated count of votes for each candidate
  7 	$votecount = 0    # Tally total number of votes cast
  8 	foreach ($vote in $results) {
  9 		$votecount ++ 
 10 		foreach ($name in $vote.split(',')) {
 11 			if ( ! $eliminated[$name]) {
 12 				$votes[$name] ++ ; break
 13 			}
 14 		}
 15 	}
 16 	$names = $votes.psbase.keys|sort -Descending {$votes[$_]}
 17 
 18 	if ($votes[$names[0]] / $votecount -gt 0.5) { 
 19 		"The winner is $($names[0]) with {0:p1} of the vote" -f ($votes[$names[0]] / $votecount)
 20 		break
 21 	}
 22 	else {
 23 		$eliminated[$names[$i]]=$TRUE   # Eliminate loser and count again...
 24 	}
 25 }

The basic vote counting part of the script is between lines 6-24.  This is broken down into two main sections: a scan of the votes collecting counts; and a decision about a winner.  Lines 8-15 actually count the votes – take the first place from each ballot paper and if the candidate has not been eliminated add a first place vote to his count (in hash $votes, indexed by candidate name).  If the candidate has been eliminated check the next candidate and so on.

Once all counted, line 16 sorts the hash table by total votes and saves the result in $names (1st place candidate in $names[0])

Line 18 checks if the candidate with the highest result actually achieved more the 50% of the vote – if so the result is announced and the script ends.  If this isn’t the case the candidate with the lowest score is added to a hash of eliminated candidates at line 23.

The process is then repeated up to "$candidate" times at line 5.  The rather odd looking sequence $i=3..0 is to enable the last place candidate to be identified (in the first round of counting the last place candidate will be in $names[3] – see line 23).

Not very elegant I’m afraid.