| Chris's profileChris Warwick - The Blog...PhotosBlogSkyDrive | Help |
Chris Warwick - The Blog EditionPowerShell and Assorted Ramblings and Rants |
||||
|
January 06 ISE Functions – in a ModuleThe module linked below adds three functions to the ISE editor:
To try it out just copy the folder from here: CJW-ISE-Functions.zip and place it under your Modules folder. If you don’t have a Modules folder yet, see This Post on the PowerShell Team Blog, or This Post on Thomas’s Blog. Here’s a screenshot of the “All” function:
As you’d expect, the Indent and Outdent functions do exactly what they say; default is to in/out-dent by 4 spaces. Indent is mapped to Ctrl-F12 and Outdent to Ctrl-F11. I’m never quite sure if SkyDrive is going to work, so here’s the CJW-ISE-Functions.psm1 file. This was created with the Module Module!: Cheers, Chris
1 ################################################################################ 2 3 Function Get-ISEMatchingLines { 4 <# 5 .Synopsis 6 Displays lines matching the given pattern from the currently active ISE Editor window 7 .Description 8 This is a poor-man's-version of the Xedit "ALL" command. All lines matching the given parameter 9 are displayed as output 10 11 .Parameter Pattern 12 .Example 13 Get-ISEMatchingLines "# Todo" 14 .ReturnValue 15 Output lines matching pattern 16 .Link 17 18 .Notes 19 NAME: Get-ISEMatchingLines 20 AUTHOR: Chris Warwick 21 LASTEDIT: 01/05/2009 22:30:12 22 #Requires -Version 2.0 23 #> 24 25 [CmdletBinding(SupportsShouldProcess=$False, SupportsTransactions=$False, ConfirmImpact='None')] 26 27 Param([Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)][String]$Pattern) 28 29 Process{ 30 If ($host.Name -ne 'Windows PowerShell ISE Host'){Throw 'This function only runs in PowerShell ISE'} 31 $psISE.CurrentOpenedFile.Editor.Text -split "`n"|Select-String -pattern $Pattern 32 } 33 } 34 35 ################################################################################ 36 37 Function Indent-ISESelection { 38 <# 39 .Synopsis 40 Indents the current editor selection by the specified number of characters 41 .Description 42 The editor current selection is indented by having a given number of spaces (default=4) 43 inserted before each line in the selection. Note that if the selection does not start 44 on a line-break boundary this cannot be detected and indenting will occur from the start 45 of the selection irrespective of its actual place in the line. 46 47 .Parameter Spaces 48 .Example 49 Indent-ISESelection 8 50 .ReturnValue 51 Selected lines indented 52 .Link 53 54 .Notes 55 NAME: Indent-ISESelection 56 AUTHOR: Chris Warwick 57 LASTEDIT: 01/05/2009 22:30:12 58 #Requires -Version 2.0 59 #> 60 61 [CmdletBinding(SupportsShouldProcess=$False, SupportsTransactions=$False, ConfirmImpact='None')] 62 63 Param([Parameter(Position=0, Mandatory=$false, ValueFromPipeline=$false)][Int]$indent=4) 64 65 Process{ 66 If ($host.Name -ne 'Windows PowerShell ISE Host'){Throw 'This function only runs in PowerShell ISE'} 67 $psISE.CurrentOpenedFile.Editor.InsertText( 68 ($psISE.CurrentOpenedFile.Editor.SelectedText -split "`n"|%{$_ -Replace '^', (' '*$Indent)}) -join "`n" 69 ) 70 } 71 } 72 73 ################################################################################ 74 75 Function Outdent-ISESelection { 76 <# 77 .Synopsis 78 Outdents the current editor selection by the specified number of characters 79 .Description 80 The editor current selection is outdented by having a given number of spaces (default=4) 81 removed before each line in the selection. Note that if the selection does not start 82 on a line-break boundary this cannot be detected and outdenting will occur from the start 83 of the selection irrespective of its actual place in the line. 84 85 The outdenting process will only remove whitespace characters from the start of each line. 86 If whitespace characters run out then no shifting will occur 87 88 .Parameter Spaces 89 .Example 90 Outdent-ISESelection 8 91 .ReturnValue 92 Selected lines outdented 93 .Link 94 95 .Notes 96 NAME: Outdent-ISESelection 97 AUTHOR: Chris Warwick 98 LASTEDIT: 01/05/2009 22:30:12 99 #Requires -Version 2.0 100 #> 101 102 [CmdletBinding(SupportsShouldProcess=$False, SupportsTransactions=$False, ConfirmImpact='None')] 103 104 Param([Parameter(Position=0, Mandatory=$false, ValueFromPipeline=$false)][Int]$Outdent=4) 105 106 Process{ 107 If ($host.Name -ne 'Windows PowerShell ISE Host'){Throw 'This function only runs in PowerShell ISE'} 108 $psISE.CurrentOpenedFile.Editor.InsertText( 109 ($psISE.CurrentOpenedFile.Editor.SelectedText -split "`n"|%{$_ -Replace "^\s{0,$Outdent}"}) -join "`n" 110 ) 111 } 112 } 113 114 ################################################################################ 115 116 $Null=$psISE.CustomMenu.Submenus.Add('Indent', {Indent-ISESelection}, 'Ctrl+F12') 117 $Null=$psISE.CustomMenu.Submenus.Add('Outdent', {Outdent-ISESelection}, 'Ctrl+F11') 118 119 Export-ModuleMember Get-ISEMatchingLines, Indent-ISESelection, Outdent-ISESelection 120 121 Set-Alias All Get-ISEMatchingLines -Scope Global 122 Set-Alias Indent Indent-ISESelection -Scope Global 123 Set-Alias Outdent Outdent-ISESelection -Scope Global 124 125 ################################################################################ PowerShell ISE – Yippee!No blog entries here for ever, I was thinking this blog would see no life until next year’s Scripting Games:-) But then CTP3 was released and in particular, the Integrated Scripting Environment (ISE) has got my attention – because it’s scriptable (See PowerShell ISE Can Do a Lot More Than You Think). The CTP3 Story So FarI’ve installed CTP3 on 6 machines here at home (3xVista Desktop; 1xVista Laptop; 2xServer 2008 Domain Controllers) – all working as expected; remoting running between all the machines. I’ve been trying to keep up with the blogs (there have been 33 posts to date on the PowerShell Team Blog alone since CTP3 was released!) and have been toying with Advanced Functions and Modules. All very, very cool. My PowerShell Environment (or, Why I Didn’t Use ISE)I use (and highly recommend) PowerShell Plus (now sold by Idera http://www.idera.com/Products/PowerShell/). Hopefully the rumoured update due soon will improve the CTP3 interoperability… Anyway, being a PS+ fan meant I practically ignored ISE all the way through CTP2. I fired up ISE with CTP3 and thought “same-old, same-old” until I saw Jeffery’s post – at which point I got slightly more interested. Now this is getting good! I’ve just written a module containing some ISE functions (to be posted shortly) and thought I’d provide some thoughts… Where To Next, ISE?This is a fantastic opportunity! OK, currently the ISE object model is a little, err, on the light side. But the potential is there. I come from a IBM/VM background and was a huge fan of Xedit – the VM programmable editor; there was nothing that couldn’t be done with that editor and I’ve never been 100% happy about my current-fave replacement – whatever that happens to have been (currently EditPad Pro and the PS+ Editor (yeah, I’ve tried the PowerGUI one too…)) ISE could be right up there if the object model is expanded. One of the good things in Xedit was the “All” command; this took a pattern and displayed all the lines in the current file that matched the pattern. All other lines were either hidden entirely or bunches of hidden lines were replaced with a “shadow line” (Set Shadow On/Off). Global edits to the file would then operate only on the displayed lines (Set Scope Display). This was incredibly useful (You perhaps don’t miss it until you don’t have it anymore!) – it’s been implemented in other editors (for example GVIM – and Emacs I’m sure) – if you’d like a go get the free Xedit clone by Mark Hessling (here: http://sourceforge.net/project/showfiles.php?group_id=29648) called “The Hessling Editor” or “THE” for short. I’ve written a very simple, poor-man’s “All” command for ISE (included in the module), but it’s somewhat less than it could be … hopefully that object model will improve! What about ISE? Well, check the (extremely comprehensive) Xedit command set and object model here: http://hessling-editor.sourceforge.net/doc/index.html. As a first step – let us display or hide lines in the editor window:-) Preferably, find out where Mark Hessling is and get him a job on the PowerShell Team:-)) May 04 PowerShell V2 CTP2I download the latest CTP2 build yesterday and installed it on a couple of machines - all seems to be fine, nothing appears to be broken yet! The remoting has changed significantly since the last CTP and looks like it's going to be a fantastic facility (shame it's going to be quite a long while before I can rely on V2 being available on all the production machines I come across - oh well!) Thanks to Shay and Marco on the newsgroup for pointing me in the direction of the WinRM CTP - you'll need to install this too from here before you can run the Configure-WSMan.PS1 script supplied with the CTP2 build. The WinRM CTP install requires a reboot. VersioningI now have PowerShell V1 and V2 to contend with - as well as running different hosts (PowerShell and PowerShell Plus) and the PSCX plug-in. When writing scripts that rely on a particular feature I usually try to test before-hand to make sure that feature is going to be there. I was stumped for a while trying to find out if I was running on V1 or V2 (the underlying PowerShell engine version) - Get-Host would normally do the trick, but in PS+ the Get-Host cmdlet only tells me I'm running in PSPlus - nothing about the underlying engine. PSCX uses "$PSVersionString = (Get-FileVersionInfo "$PSHome\PowerShell.exe").ProductVersion" - but that's no good if PSCX isn't loaded. Luckily, hunting around in the help files for CTP2 revealed a new $PSVersionTable hash:
Just the job:-)
Chris April 29 Spelling and ThingsMy I do usually have a PowerShell session open somewhere though (and if I don't, there's always the Sidebar PowerShell Gadget), so I thought I'd write a "Spell" command that would just check a spelling for me. Although I have an ancient (16-bit, circa Windows 3.1) software version of the Concise Oxford English Dictionary it doesn't have a COM interface - so it seemed a Web Service was the way to go. A quick search showed Yahoo! and Cdyne to be the most useful candidates (Google also provide a service, but it requires registration - not a big deal, but why do I want to manage a registration key for ever-more when I don't need to?) Writing the PowerShell script is then a complete pushover, PowerShell just makes this kind of thing so easy. [Gratuitous praise (no payment required...) It's even more of a pushover when you use PowerShell Plus (I've just download the latest version and it just keeps getting better and better - well done Karl et al!)]. The code just gets a new System.Net.WebClient object, composes the required URLs and reads back the resulting XML from Yahoo! and Cdyne. Assuming something useful was returned it's displayed accordingly. Here's some sample output: PS> spell advanceed
Here's the code (I dot-source this in my profile) - Enjoy! # Get-Spelling # Uses Yahoo! and cdyne Web Services to check spelling # Chris Warwick, April 2008 Function Get-Spelling ([String]$Word = $(Throw "Specify a word to spell-check")) { $WebClient = New-Object System.Net.WebClient #Create the Yahoo! Service Url, get the result and cast to XML $url = "http://search.yahooapis.com/WebSearchService/V1/spellingSuggestion?appid=CJWGet-Spelling&query=$word" $YahooResponse = [xml]$WebClient.DownloadString($url) #Create the cdyne Service Url, get the result and cast to XML $url = "http://ws.cdyne.com/SpellChecker/check.asmx/CheckTextBody?BodyText=$word&licensekey=" $CdyneResponse = [xml]$WebClient.DownloadString($url) If ($YahooResponse.ResultSet.Result -ne $null) { "Yahoo! Suggestions:" $YahooResponse.ResultSet.Result } else { "'$Word' - no alternative suggestions from Yahoo!" } If ($CdyneResponse -ne $null -and [int]$CdyneResponse.DocumentSummary.MisspelledWord.SuggestionCount -gt 0) { "Cdyne Suggestions:" $CdyneResponse.DocumentSummary.MisspelledWord.Suggestions[0..3] # (Show only top 4 results) } else { "'$Word' - no alternative suggestions from Cdyne" } } Set-Alias Spell Get-Spelling Set-Alias Speel Get-Spelling # :-) Infrastructure Upgrade - UpdateSo, I went for an AMD Athlon BE-2350 (45W TDP), 4GB Ram, a 750GB SATA Disk (the price break seems to be at this capacity, the 1TB disks are still a bit more expensive). All built and running Server 2008 64-bit - neat:-) Now I just need to settle on the best way to migrate my domain. I don't really want to do a straight upgrade to AD '08 as I have a lot of past development junk hanging around in the current domain - including the E2k3 schema extensions (I've now moved into the cloud and use Windows Live Domains and Windows Live Mail with the Outlook Connector - recommended). But, I would like to keep the same domain name that I've used for a while now. I could:
I'm still considering options 1, 3 and 5 - will keep you posted! Once I've decommissioned the existing domain I can free-up some hardware and continue with the on-going upgrade. Skiing - Wipe Out!If you'd like to see me get wiped out on the slopes watch this:-) Les Arc, earlier in the month. My eldest daughter is videoing us, my son (who has grown quickly over the last few months!) poses past the camera and - oops! Luckily I landed on something soft (the lunch in my rucksack!)
Enjoy! March 13 Infrastructure Upgrade!I've been running an AD domain here for a number of years (it's my job OK?!) on a couple of DCs. I have a bunch of other stuff set up too - including MOM, SQL, WSUS, ISA. For a long time I also had Exchange - that went last year when the Windows Live team released the Outlook Connector and Window Live Domains (but that's another story...) Anyway, the time has come to move Server 2008 out of it's VM dev hideaway and into production - trouble is the hardware I have is so old and cranky that I have doubts about its likely longevity. I'm also fresh out of space - what seemed an unfeasibly large (PATA) 120GB a couple of years ago is now full of the kids videos and photos. Something else too - I run my servers 24/7 and things have changed over the last few years; these days, every time I look at all the flashing lights I feel guilty about the continuous power drain. Time to upgrade. Only problem is - upgrade to what? Real-world server class hardware is a complete non-starter (power, cost). Desktops with fast processors and graphics cards are similarly power-hungry and just over-spec. But low-end, low-power systems don't fit the bill either - often the system boards have a restricted memory capability and off-the-shelf systems aren't customisable to my requirements. I considered going down the virtualisation route - buying a single, large, fast machine and running everything in VMs - but that just doesn't feel right (yet). Maybe two large, fast machines would be OK - but that starts to look expensive. So... self-build it is! Here are my list of requirements:
I've just started looking around - I think I'm going to need 4 machines total. So far I'm looking at AMD Athlon 64, 4GB, 1TB SATA. If you have any helpful thoughts please post comments. March 03 Scripting Games, Advanced Event 10, BlackjackDisclaimer: This is modified slightly over the version I submitted. I got a bit carried away with the description so this is a long post! First, a screen-shot:-) When I saw MOW's teaser a couple of weeks' back I decided a text-only version of Blackjack was going to be unacceptable! I didn't think I'd go as far as drawing cards, but thought it would be good to have a go at writing to the console buffer to display things in a nice tidy fashion: I wrote this in two stages, firstly getting the regular game to work (but using dummy functions to display the output as simple text on the console); secondly, replacing the display functions to use $Host.UI.RawUI calls, changing the program into a full-screen version. I haven't tried this before (that's the good thing about The Scripting Games!), so there are a few rough edges, noted below. Here's the code, split into sections (the full code is at the end of the post). Firstly some setup and pre-amble: 1 Clear-Host 2 $script:pos = $Host.ui.rawui.CursorPosition 3 4 $rect = New-Object System.Management.Automation.Host.Rectangle 5 $rect.Top = $pos.y 6 $rect.Right = 70 7 $rect.Left = 0 8 $rect.Bottom = $pos.y + 25 9 $script:buf=$Host.UI.RawUI.GetBufferContents($rect) 10 11 $script:tablecolour='darkgreen' 12 $script:textcolour='black' 13 14 function setTable($colour=$script:tablecolour) { 15 $c=New-Object System.Management.Automation.Host.BufferCell 16 $c.BackgroundColor=$colour 17 $c.ForegroundColor='black' 18 $c.Character=' ' 19 20 for ($i=0; $i -le $script:buf.GetLongLength(0)-1; $i++){ 21 for ($j=0; $j -le $script:buf.GetLongLength(1)-1; $j++){ 22 $script:buf[$i,$j]=$c 23 } 24 } 25 } The first section defines a playing area and a function to initialise the area. I'm sure this is unnecessary/can be done in a better way. See MOW's event 10 post for definitive information:-) The next section defines a function to write a string to the screen buffer ("writebuf", below) with optional foreground and background colours. I did this by writing individual characters into the buffer (line 5 below) although, in hindsight, I think using $Host.UI.RawUI.NewBufferCellArray would be the better way to go. "Writebuf" will potentially be called multiple times, making changes to the screen buffer until function "flush" (line 8 below) is called to display the buffer on the screen. "ClearTable" (line 10 below) is used to wipe out the previous hand's cards and scores from the screen. 1 function writebuf($x,$y,$string,$fore=$script:textcolour,$back=$script:tablecolour) { 2 $c=New-Object System.Management.Automation.Host.BufferCell 3 $c.BackgroundColor=$back 4 $c.ForegroundColor=$fore 5 $string.tochararray()|%{$c.Character=$_; $script:buf[$x,$y++]=$c} 6 } 7 8 function flush {$Host.ui.RawUI.SetBufferContents($script:pos,$script:buf)} 9 10 function clearTable { 11 7..18|%{writebuf ($_) 7 (' ').padright(25)} 12 7..18|%{writebuf ($_) 36 (' ').padright(25)} 13 writebuf 4 18 $(' '.padright(18)) 14 writebuf 4 51 $(' '.padright(18)) 15 flush 16 } 17 18 function status ($string) {writebuf 23 7 $string.padright(60); flush} 19 20 function pause ($milliseconds) {Start-Sleep -Milliseconds $milliseconds} The next function ("choice", below) display a message and uses $Host.UK.RawUI.ReadKey to get an answer from the player. The function will keep reading characters until a valid key is pressed. A list of valid keys is passed to the function in $set: 1 function choice ($message, $set) { 2 status $message 3 do {$answer=[string]($Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")).character} 4 until ($set -match $answer) 5 return $answer 6 } So, that's the screen taken care of. The rest of the code described below plays the Blackjack game calling on the "writebuf" and "status" functions to display the results. But first, some support functions for the game, starting with the all-important shuffle: 1 # Start of Blackjack code 2 3 4 function shuffle { 5 $script:dealt = 0 # Number of cards dealt so far 6 $script:pack = 0..51 7 8 # Shuffle the pack. This is the Fisher-Yates shuffle once more! 9 $r = New-Object system.random 10 0..51| % {$j = $r.Next($_,51); $x = $pack[$_]; $pack[$_] = $pack[$j]; $pack[$j] = $x} 11 } So, "shuffle" creates a new pack/deck of cards. The script-scoped variable $script:dealt remembers how many cards have been taken from the pack while the $script:pack variable is the pack itself, represented as a 52-element array of numbers between 0 and 51. Here we see the inevitable Fisher-Yates shuffle again. I've seen lots of other very weird and wonderful shuffle techniques used throughout the scripting games, but this is really the way to randomly mix the elements of an array. See the Wikipedia article and don't use any other way OK! The shuffle function merely provides 52 numbers in a random sequence. To turn these into playing cards and to assign them to a player's hand we need some more plumbing. Function "Deal" does what is required, but before describing that we need to know something about the data structures it manipulates. First of all, the structure of a card. A card is described by a custom PowerShell object with two properties - one is the value of the card as according to the rules of the Scripting Guys' Blackjack (from 2 to 11, or 1 for Aces if required); the other is a string naming the card ("Queen of Clubs" and so on). The card is created on line 6 of function deal (below). The name, suit and value is then calculated from the card number (0..51) on lines 7-16; these lines map the 52 unique numbers from the pack to the corresponding four suits of cards we need. The $card object will end up having $card.name = "Queen of Clubs" and $card.points = 10 (for example). "Deal" now has to assign this card to a hand. For the purposes of this script a hand is another custom PowerShell object with the following properties:
As an example, here's how a hand looks at the PowerShell console: PS> $hand|ft -auto points soft cards ------ ---- ----- 13 False {Jack of Clubs, Three of Spades} PS> $hand.cards|ft -auto Name Points ---- ------ Jack of Clubs 10 Three of Spades 3 "Deal" adds the new card to the current hand (line 19, below); calculates the new points total for the hand (line 20) and checks to see if the hand now contains any soft Aces (line 21). That's the data structures dealt with (so to speak), but there's one more task for "Deal" to take care of. The addition of another card to the current hand may have caused the hand's value to exceed 21 points. If this has happened, lines 23-34 check to see if there are any soft Aces in the hand that can have their values changed from 11 points to 1 point. If there are soft Aces they're downgraded one at a time until the hand's value drops below 21. Here's the complete "Deal" function: 1 function deal ([ref] $hand) { 2 # Add a card to the specified hand. First, get the next card from the pack as a custom object 3 4 if ($script:dealt -ge 51) {Throw "Pack is empty"} 5 $c = $script:pack[$script:dealt ++ ] # Pick next card from pack (if not all dealt) 6 $card = [object]|Select Name, Points # convert card to custom object 7 $suit = ('Clubs','Diamonds','Hearts','Spades')[[math]::truncate($c / 13)] # Calculate suit 8 $value = $c % 13 + 1 9 $card.name, $card.points = $(switch ($value) { # Calculate name and points 10 1 {"Ace",11} 11 11 {"Jack",10} 12 12 {"Queen",10} 13 13 {"King",10} 14 Default {('Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten')[$value - 2],$value} 15 }) 16 $card.name=$card.name + " of $suit" 17 18 # Now add the custom card object to the hand... 19 $hand.value.cards+=$card 20 $hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum 21 $hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11}) 22 23 # If hand is now worth more than 21 points, minimize by checking for soft Aces 24 while ($hand.value.points -gt 21 -and $hand.value.soft) { 25 # Check for soft aces 26 foreach ($card in $hand.value.cards) { 27 if ($card.points -eq 11) { 28 $card.points=1 # Convert soft ace to hard 29 break; # Just change one Ace at a time 30 } 31 } 32 $hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum 33 $hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11}) # Check if any more soft Aces 34 } 35 } There's one final subtlety with the "Deal" function. We want to be able to deal cards to the player or the dealer. The obvious way to do this is to pass the player's hand or the dealer's hand to the "Deal" function and have it manipulate the appropriate set of cards. In order to do this we have to use a rather obscure (at least in PowerShell) function - passing parameters by reference. See the last paragraph of this post on the PowerShell Blog. I can't find any other references to this technique in a PowerShell article (or in Bruce's book!) - so it really is obscure! Pass by reference is indicated on the parameter declaration by using the [ref] type. This must also be specified on the line calling the function (e.g. deal ([ref] $playerhand), see code below). In addition to using the [ref] type to pass the parameter it is necessary, within the function, to refer to the contents of passed variable by using "$parametername.value". See, for example, line 19 above, where we use "$hand.value.cards+=..." rather than the more likely "$hand.cards+=...". Only one more function before we actually start to play (!), "display-hands", below, is mostly mechanical - just writing text at given parts of the screen. A couple of bits of logic suppress the initial display of the dealer's second card and prevent the dealer's score from being shown until the player has finished (or busted). 1 function display-hands { 2 $playerhand.cards|%{$y=7}{writebuf ($y++) 7 $_.Name.padright(25)} 3 $dealerhand.cards|%{$y=7}{writebuf ($y++) 36 $_.Name.padright(25)} 4 if ($playerhand.soft) {$soft=', Soft'} else {$soft=''} 5 $points="($($playerhand.points) points$soft)" 6 writebuf 4 18 $points.padright(18) 7 if (!$dealerdown -and $dealerhand.cards.count -gt 1) { 8 writebuf 8 36 ('?'.padright(25)) 9 } 10 if ($dealerdown) { 11 if ($dealerhand.soft) {$soft=', Soft'} else {$soft=''} 12 $points="($($dealerhand.points) points$soft)" 13 writebuf 4 51 $points.padright(18) 14 } 15 flush 16 } Finally, we use all these functions to play the game! Display some headers, initialise the hands, shuffle the pack and deal the first two cards each: 1 setTable 2 writebuf 1 22 ' Welcome to Blackjack! ' 'red' 'black' 3 4 writebuf 4 7 'Your Cards' 5 writebuf 5 7 '~~~~~~~~~~' 6 7 writebuf 4 36 "Dealer's Cards" 8 writebuf 5 36 '~~~~~~~~~~~~~~' 9 10 11 do { 12 # Play a new hand... 13 14 $dealerhand=,[object]|select points, soft, cards 15 $dealerhand.cards=@() 16 $playerhand=,[object]|select points, soft, cards 17 $playerhand.cards=@() 18 19 $dealerdown=$FALSE # True when dealer reveals second card in hand 20 clearTable 21 status "Shuffling Cards..."; shuffle; pause 1000 22 status "Dealing Cards..."; pause 800 23 24 # Deal first two cards 25 1..2| % { 26 deal ([ref]$playerhand); display-hands; pause 600 27 deal ([ref]$dealerhand); display-hands; pause 600 28 } 29 After all that, the code that takes care of playing the player's hand is trivial, thankfully. Every time the payer chooses "Hit" we deal a new card into the player's hand (line 7, below) and update the display: 1 $next = '' The player has finished; that's either because they stuck (on less than 21) or got 21 or more points, check these possibilities and then move on to the dealer's hand, played on lines 12-17 below: 1 # Check if result, otherwise play dealer's hand... 2 switch ($playerhand.points) { 3 21 {status '21, You Win!'; break} 4 {$_ -gt 21} {status 'Over 21. Sorry, you lose'; break} 5 default { 6 # Play dealer's hand... 7 $dealerdown=$TRUE 8 status "You stick on $($playerhand.points) points" 9 pause 1400 10 display-hands 11 pause 1200 12 while ($dealerhand.points -lt $playerhand.points) { 13 deal ([ref]$dealerhand) 14 status ("The Dealer draws the $($dealerhand.cards[-1].name)") 15 pause 2000 16 display-hands 17 } 18 status "The Dealer has $($dealerhand.points) points" 19 pause 1600 20 switch ($dealerhand.points) { 21 {$_ -gt 21} {status 'You Win!'; break} 22 {$_ -ge $playerhand.points} {status 'The Dealer Wins'} 23 } 24 } 25 } 26 27 pause 1600 28 $next = choice 'Play again (Y/N)?' 'YNQ' 29 } until ($next -ne 'y') 30 31 Clear-Host
Now, when I get bored I must convert the display functions to use MOW's fantastic cards! Here's the whole program:
1 # Blackjack, Winter Scripting Games 2008, Advanced Event 10, Chris Warwick, Get-UKPSUG 2 3 # Screen Buffer Code and auxiliary functions 4 5 Clear-Host 6 $script:pos = $Host.ui.rawui.CursorPosition 7 8 $rect = New-Object System.Management.Automation.Host.Rectangle 9 $rect.Top = $pos.y 10 $rect.Right = 70 11 $rect.Left = 0 12 $rect.Bottom = $pos.y + 25 13 $script:buf=$Host.UI.RawUI.GetBufferContents($rect) 14 15 $script:tablecolour='darkgreen' 16 $script:textcolour='black' 17 18 function setTable($colour=$script:tablecolour) { 19 $c=New-Object System.Management.Automation.Host.BufferCell 20 $c.BackgroundColor=$colour 21 $c.ForegroundColor='black' 22 $c.Character=' ' 23 24 for ($i=0; $i -le $script:buf.GetLongLength(0)-1; $i++){ 25 for ($j=0; $j -le $script:buf.GetLongLength(1)-1; $j++){ 26 $script:buf[$i,$j]=$c 27 } 28 } 29 } 30 31 function clearTable { 32 7..18|%{writebuf ($_) 7 (' ').padright(25)} 33 7..18|%{writebuf ($_) 36 (' ').padright(25)} 34 writebuf 4 18 $(' '.padright(18)) 35 writebuf 4 51 $(' '.padright(18)) 36 flush 37 } 38 39 function writebuf($x,$y,$string,$fore=$script:textcolour,$back=$script:tablecolour) { 40 $c=New-Object System.Management.Automation.Host.BufferCell 41 $c.BackgroundColor=$back 42 $c.ForegroundColor=$fore 43 $string.tochararray()|%{$c.Character=$_; $script:buf[$x,$y++]=$c} 44 } 45 46 function flush {$Host.ui.RawUI.SetBufferContents($script:pos,$script:buf)} 47 48 function status ($string) {writebuf 23 7 $string.padright(60); flush} 49 50 function pause ($milliseconds) {Start-Sleep -Milliseconds $milliseconds} 51 52 function choice ($message, $set) { 53 status $message 54 do {$answer=[string]($Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")).character} 55 until ($set -match $answer) 56 return $answer 57 } 58 59 # Start of Blackjack code 60 61 62 function shuffle { 63 $script:dealt = 0 # Number of cards dealt so far 64 $script:pack = 0..51 65 66 # Shuffle the pack. This is the Fisher-Yates shuffle once more! 67 $r = New-Object system.random 68 0..51| % {$j = $r.Next($_,51); $x = $pack[$_]; $pack[$_] = $pack[$j]; $pack[$j] = $x} 69 } 70 71 function deal ([ref] $hand) { 72 # Add a card to the specified hand. First, get the next card from the pack as a custom object 73 74 if ($script:dealt -ge 51) {Throw "Pack is empty"} 75 $c = $script:pack[$script:dealt ++ ] # Pick next card from pack (if not all dealt) 76 $card = [object]|Select Name, Points # convert card to custom object 77 $suit = ('Clubs','Diamonds','Hearts','Spades')[[math]::truncate($c / 13)] # Calculate suit 78 $value = $c % 13 + 1 79 $card.name, $card.points = $(switch ($value) { # Calculate name and points 80 1 {"Ace",11} 81 11 {"Jack",10} 82 12 {"Queen",10} 83 13 {"King",10} 84 Default {('Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten')[$value - 2],$value} 85 }) 86 $card.name=$card.name + " of $suit" 87 88 # Now add the custom card object to the hand... 89 $hand.value.cards+=$card 90 $hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum 91 $hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11}) 92 93 # If hand is now worth more than 21 points, minimize by checking for soft Aces 94 while ($hand.value.points -gt 21 -and $hand.value.soft) { 95 # Check for soft aces 96 foreach ($card in $hand.value.cards) { 97 if ($card.points -eq 11) { 98 $card.points=1 # Convert soft ace to hard 99 break; # Just change one Ace at a time 100 } 101 } 102 $hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum 103 $hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11}) # Check if any more soft Aces 104 } 105 } 106 107 function display-hands { 108 $playerhand.cards|%{$y=7}{writebuf ($y++) 7 $_.Name.padright(25)} 109 $dealerhand.cards|%{$y=7}{writebuf ($y++) 36 $_.Name.padright(25)} 110 if ($playerhand.soft) {$soft=', Soft'} else {$soft=''} 111 $points="($($playerhand.points) points$soft)" 112 writebuf 4 18 $points.padright(18) 113 if (!$dealerdown -and $dealerhand.cards.count -gt 1) { 114 writebuf 8 36 ('?'.padright(25)) 115 } 116 if ($dealerdown) { 117 if ($dealerhand.soft) {$soft=', Soft'} else {$soft=''} 118 $points="($($dealerhand.points) points$soft)" 119 writebuf 4 51 $points.padright(18) 120 } 121 flush 122 } 123 124 125 126 setTable 127 writebuf 1 22 ' Welcome to Blackjack! ' 'red' 'black' 128 129 writebuf 4 7 'Your Cards' 130 writebuf 5 7 '~~~~~~~~~~' 131 132 writebuf 4 36 "Dealer's Cards" 133 writebuf 5 36 '~~~~~~~~~~~~~~' 134 135 136 do { 137 # Play a new hand... 138 139 $dealerhand=,[object]|select points, soft, cards 140 $dealerhand.cards=@() 141 $playerhand=,[object]|select points, soft, cards 142 $playerhand.cards=@() 143 144 $dealerdown=$FALSE # True when dealer reveals second card in hand 145 clearTable 146 status "Shuffling Cards..."; shuffle; pause 1000 147 status "Dealing Cards..."; pause 800 148 149 # Deal first two cards 150 1..2| % { 151 deal ([ref]$playerhand); display-hands; pause 600 152 deal ([ref]$dealerhand); display-hands; pause 600 153 } 154 155 $next = '' 156 # Play player's hand... 157 while (($playerhand.points -lt 21) -and ($next -ne 's')) { 158 $next = Read "You have $($playerhand.points) points. Stay (S) or Hit (H)?" 'SHQ' 159 if ($next -eq 'Q') {Clear-Host; exit} 160 if ($next -eq 'H') { 161 deal ([ref]$playerhand) 162 status ("You draw the $($playerhand.cards[-1].name)") 163 pause 1000 164 display-hands 165 } 166 } 167 168 # Check if result, otherwise play dealer's hand... 169 switch ($playerhand.points) { 170 21 {status '21, You Win!'; break} 171 {$_ -gt 21} {status 'Over 21. Sorry, you lose'; break} 172 default { 173 # Play dealer's hand... 174 $dealerdown=$TRUE 175 status "You stick on $($playerhand.points) points" 176 pause 1400 177 display-hands 178 pause 1200 179 while ($dealerhand.points -lt $playerhand.points) { 180 deal ([ref]$dealerhand) 181 status ("The Dealer draws the $($dealerhand.cards[-1].name)") 182 pause 2000 183 display-hands 184 } 185 status "The Dealer has $($dealerhand.points) points" 186 pause 1600 187 switch ($dealerhand.points) { 188 {$_ -gt 21} {status 'You Win!'; break} 189 {$_ -ge $playerhand.points} {status 'The Dealer Wins'} 190 } 191 } 192 } 193 194 pause 1600 195 $next = choice 'Play again (Y/N)?' 'YNQ' 196 } until ($next -ne 'y') 197 198 Clear-Host February 29 Scripting Games, Advanced Event 8, Making MusicA 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 BallWork out a rota for games between 6 teams; randomise the list to avoid one team playing too many consecutive games. 1 $games=@() 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. February 27 Scripting Games, Advanced Event 6, Prime TimeWhen 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 PasswordCheck 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 BreakRead 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 DifferenceTake 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> February 26 Scripting Games, Sudden Death Challenge 5First, 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 February 23 Scripting Games, Sudden Death Challenge 4Very Straightforward, the key piece is the [char][int] cast. $n=Get-Content 'c:\scripts\numbers.txt' 0..($n.length/2-1)|%{$s=""}{$s+=[char][int]($n.substring($_*2,2))}{$s} Just index along the string of digits, converting pairs to the equivalent character Scripting Games, Advanced Event 4, Image is EverythingDisplay 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 ElectionAn 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. Scripting Games, Beginner Event 4, Count Me InDisplay the number of characters in the current script (irrespective of the path to the script) (get-content $MyInvocation.MyCommand.Definition|Measure-Object -Character).Characters The only trick here is knowing that $MyInvocation.MyCommand.Definition holds the path to the currently running script. Scripting Games, Beginner Event 3, Let's Get TogetherNot much to say about this one - takes the first line from all the text files in c:\scripts and concatenates them into c:\temp\newfile.txt: get-childitem 'c:\scripts\*.txt'|%{(get-content $_)[0]}>>'c:\temp\newfile.txt' The Scripting Guy's solution is better! Public folders
|
|
|||
|
|