∷ Skripte ∷ PowerShell
Die nachfolgenden Skripte sind für Schulungs- und Unterrichtszwecke gedacht.
Sie können kopiert und in der Microsoft PowerShell ISE oder sogar online ausprobiert/
① Wochentag der Geburt und Anzahl der seitdem vergangenen Tage
Die Funktion BirthCalc
im nachfolgenden Skript nimmt ein Geburtsdatum im Format JJJJ-MM-TT
entgegen und ermittelt sowohl den Wochentag der Geburt als auch die seitdem vergangenen Tage:
function BirthCalc([String]$s) { $f = "Du bist vor {0} Tagen an einem {1} geboren worden!" $w = "Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag" if ($s -match '^\d{4}(-\d\d){2}$' -and ($d = $s -as [DateTime])) { $f -f ((Get-Date).Date - $d).Days, (-split $w)[$d.DayOfWeek] } } BirthCalc 2002-02-20 # Du bist vor 6666 Tagen an einem Mittwoch geboren worden!
Um die Berechnungen für ein anderes Datum vorzunehmen, muss nur der obige Funktionsaufruf vor seiner Ausführung entsprechend angepasst werden. Eine Schnaps- oder runde Zahl hinsichtlich der Anzahl der Lebenstage kann einen guten Vorwand liefern, um einfach mal jemanden überraschend zu kontaktieren und mit Verweis auf das Ereignis zu gratulieren.
② GPX-Track verkleinern/anonymisieren
Manchmal möchte man einen Track im GPX-Format weitergeben/track.gpx
nach track-min.gpx
und entfernt dabei die Attribute ele
, time
, src
, sat
, course
, speed
, hdop
, vdop
, pdop
sowie geoidheight
.
Hinweis: Sollte die Zieldatei bereits existieren, wird sie überschrieben!
(Get-Content -Raw -LiteralPath .\track.gpx) -replace '(?s)[\t ]*<(ele|time|geoidheight|src|sat|[hvp]dop|course|speed).+?\1>\r?\n?' | Set-Content -NoNewline -LiteralPath .\track-min.gpx
Der Parameter -Raw
ist erst seit PowerShell 3 verfügbar, -NoNewline
setzt sogar mindestens Version 5 voraus; mit Einschränkungen (bzgl. der nachfolgend genannten Aspekte) ist jedoch das Skript auch ohne diese beiden Argumente lauffähig.
Die Bereinigung der GPX-Datei wird durch einen einzigen regulären Ausdruck erledigt, welcher an individuelle Bedürfnisse angepasst werden kann und auch für den Fall funktioniert, dass öffnendes und schließendes Schlüsselwort nicht in derselben Zeile stehen.
Die Art des Zeilenumbruchs (Windows, UNIX) oder der Zeichenkodierung (ASCII, UTF-8, UTF-8-BOM) bleibt unangetastet.
③ Konvertierung römischer Zahlen
Das nachfolgende Skript deklariert 3 Prozeduren für den Umgang mit römischen Zahlen:
- Die Funktion
RomanToInt
nimmt eine Zeichenkette mit denrömischen Ziffern
I
,V
,X
,L
,C
,D
,M
entgegen und konvertiert sie in eine natürliche Zahl. - Im Gegenzug wandelt die Funktion
IntToRoman
eine natürliche Zahl in eine römische um. - Das Cmdlet
ConvertTo-Roman
nimmt eine Menge von natürlichen und/oder römischen Zahlen entgegen und stellt sie tabellarisch in beiden Formaten dar, wobei implizit auch eine Normalisierung der Schreibweise vorgenommen wird (siehe nachfolgende Erläuterungen zur Variablen$r
).
Der Eingabebereich liegt dabei jeweils zwischen 1 und 3999 (bzw. I
und MMMCMXCIX
).
Für notwendige Umrechnungen greift ConvertTo-Roman
auf die Funktionen RomanToInt
und IntToRoman
zurück.
function RomanToInt([String]$s) { $n = 0 $r = '(M{1,3})?' + # Or M{1,9} for numbers up to 9999 '(?:((?:CC?|XX?|II?)[MD])|((?:DC?|C)C{0,3}))?' + '(?:(?<![XI][MD])(?:((?:XX?|II?)[CL])|((?:LX?|X)X{0,3})))?' + '(?:(?<!I[MDCL])(?:((?:II?)[XV])|((?:VI?|I)I{0,3})))?' if ($s -and $s -cmatch "^$r`$") { $h = @{ I=1; V=5; X=10; L=50; C=100; D=500; M=1000 } foreach ($k in $Matches.Keys -gt 0) { $n += $h[[String]$Matches[$k][$k % 2 - 1]] + $h[[String]$Matches[$k][-$k % 2]] * ($Matches[$k].Length - 1) * (-1, 1)[$k % 2] } } return $n } function IntToRoman([ValidateRange(0,3999)][Int]$n) { $s, $a = "", "I", "V", "X", "L", "C", "D", "M" for ($i = 0; $n; ($n = [Math]::Floor($n / 10)), ($i += 2)) { # The following line is only required for numbers > 3999 # if ($a.Count - $i -lt 3) { return $a[$i] * $n + $s } switch ($n % 10) { { 1..3 -eq $_ % 5 } { $s = $a[$i] * ($_ % 5) + $s } { 4 -le $_ } { $s = $a[$i + 1 + ($_ -eq 9)] + $s } { 4, 9 -eq $_ } { $s = $a[$i] + $s } } } return $s } function ConvertTo-Roman { param([Parameter(ValueFromPipeline=$true)]$InputObject) begin { $o = 1 | Select-Object -Property Arabic, Roman $a = { $e -as [Int] }, { RomanToInt $e } } process { foreach ($e in $InputObject) { $o.Arabic = &$a[$e -is [String]] $o.Roman = IntToRoman $o.Arabic Write-Output -InputObject $o } } } RomanToInt MXXM # 1980 IntToRoman 1980 # MCMLXXX ConvertTo-Roman MDCCCCLXXXIIII, MCMLXXXIV, $true, 1666 # Arabic Roman # ------ ----- # 1984 MCMLXXXIV # 1984 MCMLXXXIV # 1 I # 1666 MDCLXVI 1..3999 | ConvertTo-Roman | Format-Table -Autosize
In der Funktion RomanToInt
wird der Variablen $r
ein regulärer Ausdruck zugewiesen, durch den festgelegt wird, wie eine römische Zahl aussehen muss.
Dieser ist im obigen Skript derart gestaltet, dass auch einige alternative Schreibweisen wie MXXM
für 1980 (also Verdoppelung des Zeichens in subtraktiver Stellung) oder VIIII
für 9 (d. h. keine Anwendung einer Subtraktionsregel) akzeptiert werden.
Soll dies nicht erlaubt sein, so muss das Muster restriktiver formuliert werden, beispielsweise wie folgt:
$r = '(M{1,3})?' +'(?:((?:CC?|XX?|II?)[MD])|((?:DC?|C)C{0,3}))?' +'(?:(?<![XI][MD])(?:((?:XX?|II?)[CL])|((?:LX?|X)X{0,3})))?' +'(?:(?<!I[MDCL])(?:((?:II?)[XV])|((?:VI?|I)I{0,3})))?'$r = '(M{1,3})?' + '(?:(CM|CD)|(DC{0,3}|C{1,3}))?' + '(?:(XC|XL)|(LX{0,3}|X{1,3}))?' + '(?:(IX|IV)|(VI{0,3}|I{1,3}))?'
Mittels des regulären Ausdrucks wird nicht nur eine Zeichenkette aus römischen Zahlzeichen auf Gültigkeit überprüft, sondern auch in Einer-, Zehner, Hunderter- und Tausenderstellen zerlegt.
Dabei gibt es für jede Wertigkeit (bis auf die höchste) zwei Gruppierungen, wobei die jeweils erste subtraktive Schreibungen erkennt (z. B. IV
oder IIX
) und die andere alle additiven/übrigen (z. B. VI
oder IIII
, aber auch V
).
Der Schleifenkörper in der Funktion RomanToInt
lässt sich übrigens auch anders formulieren, so dass deutlicher wird, dass es sich um dieselben 3 Werte handelt, die jedoch auf unterschiedliche Art in Berechnungen einfließen – je nachdem, ob eine römische Zifferngruppe in subtraktiver oder additiver Schreibweise vorliegt:
$n += $h[[String]$Matches[$k][$k % 2 - 1]] +$h[[String]$Matches[$k][-$k % 2]] *($Matches[$k].Length - 1) * (-1, 1)[$k % 2]$a = $h[[String]$Matches[$k][-1]] $b = $Matches[$k].Length - 1 $c = $h[[String]$Matches[$k][0]] $n += &({ $a - $b * $c }, { $a * $b + $c })[$k % 2]
JavaScript-Varianten der eben beschriebenen Algorithmen sind übrigens in diesen beiden Webapps implementiert.
④ Primfaktorzerlegung
Eine sehr einfache, aber auch zeitaufwändige Methode zur Primfaktorzerlegung ist die Probedivision durch alle potentiellen Teiler größer 1. Dabei bräuchten Vielfache von bereits ausprobierten Divisoren im Weiteren eigentlich nicht mehr betrachtet werden, dies jedoch effizient zu handhaben, ist nicht unbedingt trivial. Zumindest lässt sich recht leicht eine Halbierung der Laufzeit erreichen, indem außer der 2 nur durch ungerade Zahlen dividiert wird:
function Factorize( [ValidateRange(1, [UInt64]::MaxValue / 1.01)][UInt64]$Num , [String]$Tab = "*") { [String]$s = "" while (!($Num -band 1) -and $Num -gt 2) { $Num /= 2; $s += "2$Tab" } for ([UInt64]$i = 3; $i * $i -le $Num; $i += 2) { while (!($Num % $i) -and $Num -gt $i) { $Num /= $i; $s += "$i$Tab" } } $s + $Num } Factorize 18264103043276784655 # 5*13*17*16528600039164511 Factorize 11111111111111111111 # Repunit R20 # 11*41*101*271*3541*9091*27961 Factorize 9156001667401012567 # 11*13*17*19*23*29*31*37*41*43*47*53*59 Factorize 5243639155003228607 ", " # 2243649631, 2337102497 Factorize 1111111111111111111 # Repunit prime R19 # 1111111111111111111
Im Grunde genommen durchläuft oben $i
aufsteigend die 2 sowie alle weiteren ungeraden Zahlen bis zur Quadratwurzel von $Num
, wobei jedesmal, wenn $Num
bzgl. $i
ohne Rest teilbar ist, $Num
durch $i
dividiert und $i
als Primfaktor notiert werden.
Eine JavaScript-Variante dieses Algorithmus ist übrigens in dieser Webapp implementiert.
Mit der Ziffer 5 endende Zahlen sind Vielfache von 5 und könnten somit im weiteren Verlauf – analog zu den geraden Zahlen – ebenfalls von der Probedivision ausgenommen werden.
Bei der nachfolgenden Code-Optimierung wird dies berücksichtigt, außerdem deutlich seltener die Abbruchbedingung in der for
-Schleife überprüft.
Hierdurch verkürzen sich insbesondere lange Ausführungszeiten nochmals merklich, so dass diese Implementierungsvariante nun insgesamt etwa 4-mal schneller als die ursprüngliche naive Methode ist, sämtliche Werte zwischen 2 und √$Num
als Teiler auszutesten:
while (!($Num -band 1) -and $Num -gt 2) { $Num /= 2; $s += "2$Tab" }while (!($Num % 2) -and $Num -gt 2) { $Num /= 2; $s += "2$Tab" } while (!($Num % 3) -and $Num -gt 3) { $Num /= 3; $s += "3$Tab" } while (!($Num % 5) -and $Num -gt 5) { $Num /= 5; $s += "5$Tab" }for ([UInt64]$i = 3; $i * $i -le $Num; $i += 2) {for ([UInt64]$i = 7; $i * $i -le $Num; $i += 4) { while (!($Num % $i) -and $Num -gt $i) { $Num /= $i; $s += "$i$Tab" } $i += 2 while (!($Num % $i) -and $Num -gt $i) { $Num /= $i; $s += "$i$Tab" } $i += 2 while (!($Num % $i) -and $Num -gt $i) { $Num /= $i; $s += "$i$Tab" } $i += 2 while (!($Num % $i) -and $Num -gt $i) { $Num /= $i; $s += "$i$Tab" } }
⑤ Türme von Hanoi
Die nachfolgende rekursive Funktion schlägt eine Zugfolge zur Lösung der Türme von Hanoi vor, ein Knobel- und Geduldsspiel, bei dem ein geordneter (z. B. nach oben hin immer schmaler werdender) Stapel scheibchenweise versetzt werden muss, wobei nur eine einzige weitere Hilfsposition benutzt und zu keinem Zeitpunkt an irgendeiner der 3 Stellen (Start-/Hilfs-/Zielposition) die Ordnung verletzt werden darf.
Als Parameter müssen die Anzahl der Scheiben sowie die Namen der 3 Ablegepositionen (z. B. L für Links, M für Mitte und R für Rechts) angegeben werden. Auf der ersten Ablegeposition (hier: L) ruht der Turm zu Beginn; Ziel ist es, ihn unter Einhaltung der Regeln auf die letzte Ablegeposition (hier: R) zu versetzen. Dazwischen befindet sich die temporär nutzbare Hilfsposition (hier: M). Ausgegeben wird eine Abfolge von Zügen, wobei die jeweiligen Einrückungen der Breite der gerade bewegten Scheibe entsprechen.
function Tower-of-Hanoi { param([Int]$num, [String]$src = "L", [String]$tmp = "M", [String]$dst = "R") if ($num-- -gt 0) { Tower-of-Hanoi $num $src $dst $tmp Write-Host -Object (" " * $num + $src + "->" + $dst) Tower-of-Hanoi $num $tmp $src $dst } } Tower-of-Hanoi 3 # alternativ: Tower-of-Hanoi 3 Links Mitte Rechts # L->R # L->M # R->M # L->R # M->L # M->R # L->R
Der obige Algorithmus löst die Aufgabe für n
Scheiben, indem er
n-1
Scheiben von der Start- zur Hilfsposition versetzt und dabei die Zielposition höchstens temporär nutzt,- dann die breiteste Scheibe von der Start- zur Zielposition zieht und
- abschließend die
n-1
Scheiben von der Hilfs- zur Zielposition versetzt, wobei diesmal die Startposition lediglich zwischenzeitlich genutzt wird.
Für n
≤ 2
sind die Lösungen trivial.
Durch das rekursive Vorgehen ist die Aufgabe ganz allgemein für n
Scheiben lösbar, wenn sie es zweimal für n-1
Scheiben ist.
Hieraus folgt, dass jede Scheibe doppelt so oft bewegt wird wie die nächstbreitere.
Insgesamt baut die Rekursion einen vollständigen Binärbaum der Tiefe n
an Funktionsaufrufen auf, wobei in jedem Knoten 1 Zug stattfindet, so dass sich insgesamt 2n-1
Züge und somit ein exponentieller Aufwand ergeben.
Sind die Scheiben von oben nach unten mit Farbe1
, …, Farben
eingefärbt, dann enthält in jedem Funktionsaufruf der Parameter $num
den Farbindex der zu bewegenden Scheibe.
Dies lässt sich auf andere Eigenschaften übertragen.
Um z. B. die Ausgabe an einen Stapel von (bis zu) 8 Euro-Münzen anzupassen, genügt es, im obigen Quellcode einfach nur das Cmdlet Write-Host
etwas anders zu parametrisieren:
Write-Host -Object (" " * $num + $src + "->" + $dst)Write-Host (" 1 Cent", " 2 Cent", "10 Cent", " 5 Cent", "20 Cent", " 1 Euro", "50 Cent", " 2 Euro")[$num] "von $src nach $dst"
⑥ Anonyme Funktionen und Rückruffunktionen (Callbacks)
Für die nun folgenden Betrachtungen seien zwei Funktionen namens Calculate-Sum
und Calculate-Product
angenommen, die jeweils mehrere Werte miteinander addieren bzw. multiplizieren:
function Calculate-Sum([Array]$Array) { if ($Array.Count) { $i = 0 $n = $Array[$i] while (++$i -lt $Array.Count) { $n += $Array[$i] } $n } } Calculate-Sum 3,6,1,5 # Liefert Summe von 3 + 6 + 1 + 5 => 15 function Calculate-Product([Array]$Array) { if ($Array.Count) { $i = 0 $n = $Array[$i] while (++$i -lt $Array.Count) { $n *= $Array[$i] } $n } } Calculate-Product 3,6,1,5 # Liefert Produkt von 3 * 6 * 1 * 5 => 90
Schnell fällt auf, dass die beiden obigen Implementierungen bis auf Name und Operator identisch sind, d. h. wenn es gelänge, die Operation quasi als Argument zu übergeben, ließen sich die zwei Funktionen zu einer einzigen vereinigen.
Eine Möglichkeit, dies umzusetzen, sind Rückruffunktionen, die einer Funktion höherer Ordnung (anonym) im Fluge
mitgegeben und von dieser aufgerufen werden.
Das könnte dann so aussehen:
function Calculate-Value([Array]$Array, [ScriptBlock]$Callback) { if ($Array.Count) { $i = 0 $n = $Array[$i] while (++$i -lt $Array.Count) { $n = &$Callback $n $Array[$i] } $n } } Calculate-Value 3,6,1,5 { param($x, $y) $x + $y } # Liefert Summe => 15 Calculate-Value 3,6,1,5 { param($x, $y) $x * $y } # Liefert Produkt => 90
Formal entspricht das bisherige Vorgehen den Umformungen
n += ai | ⇒ | n ← n + ai | ⇒ | n ← f + (n, ai) | mit | f + : x, y ↦ f + (x, y) := x + y | , |
n ∗= ai | ⇒ | n ← n ⋅ ai | ⇒ | n ← f∗(n, ai) | mit | f∗: x, y ↦ f∗(x, y) := x ⋅ y | . |
Auf dieser Idee basierend ist nachfolgend die Funktion Reduce-Array
ganz allgemein dazu in der Lage, ein Array auf einen einzigen Wert zu reduzieren.
Wie dies konkret geschehen soll (beispielsweise per Konkatenation, Summen-/
function Reduce-Array { param([Array]$Array = @(), [ValidateScript({ $true })][ScriptBlock]$Callback = {}, $Value) if ($null -ne $Array) { [Int]$i = $null -eq $Value if ($i -and $Array.Count) { # Wenn weder Initialwert übergeben noch Array leer, ... $Value = $Array[0] # ... dann erstes Element als Initialwert verwenden } for (; $i -lt $Array.Count; $i++) { # (Übrige) Elemente durchlaufen ... $Value = &$Callback $Value $Array[$i] # ... und jeweils Rückruffunktion anwenden } $Value } } Reduce-Array 3,6,1,5 { param($x, $y) $x + $y } # Berechnet Summe => 15 Reduce-Array 3,6,1,5 { param($x, $y) $x * $y } # Berechnet Produkt => 90 Reduce-Array 3,6,1,5 { param($x, $y) $x } # Gibt erstes Element zurück => 3 Reduce-Array 3,6,1,5 { param($x, $y) $y } # Gibt letztes Element zurück => 5 Reduce-Array 3,6,1,5 { param($x, $y) [Math]::Min($x, $y) } # Bestimmt Minimum => 1 Reduce-Array 3,6,1,5 { param($x, $y) [Math]::Max($x, $y) } # Bestimmt Maximum => 6 Reduce-Array 3,6,1,5 { param($x, $y) [String]$x + $y } # Bewirkt Konkatenation => "3615" Reduce-Array "3","6","1","5" { param($x, $y) $x + $y } # Bewirkt Konkatenation => "3615" Reduce-Array 3,6,1,5 { param($x, $y) $x + $y } "" # Bewirkt Konkatenation => "3615" Reduce-Array 3,6,1,5 { param($x) $x + 1 } 0 # Zählt Elemente analog zu (3,6,1,5).Count => 4
Bei Bedarf kann die Zeile im Schleifenkörper erweitert werden, um zusätzlich den Laufindex sowie das Array verfügbar zu machen und die Syntax derjenigen von Array.prototype.reduce() unter JavaScript anzugleichen:
$Value = &$Callback $Value $Array[$i] $i $Array # ... und jeweils Rückruffunktion anwenden
Im Wesentlichen paart der Reduce-Array
zugrundeliegende Algorithmus fortwährend Datenobjekte und lässt sie jeweils durch die Rückruffunktion fließen, und zwar nach folgendem Schema:
$Add = { param($x, $y) $x + $y } # Hinweis: Der Operator + ist überladen! &$Add (&$Add (&$Add 3 6) 1) 5 # Entspricht: Reduce-Array 3,6,1,5 $Add &$Add (&$Add (&$Add (&$Add "" 3) 6) 1) 5 # Entspricht: Reduce-Array 3,6,1,5 $Add ""
Selbstverständlich kann der Rückgabewert wieder ein Array sein:
'Original => 3,6,1,5' 3,6,1,5 | ConvertTo-Json 'Kopie => 3,6,1,5' Reduce-Array 3,6,1,5 { param($x, $y) @($x) + $y } | ConvertTo-Json 'Umkehrung => 5,1,6,3' Reduce-Array 3,6,1,5 { param($x, $y) @($y) + $x } | ConvertTo-Json 'Quadrierung => 9,36,1,25' Reduce-Array 3,6,1,5 { param($x, $y) @($x) + $y * $y } @() | ConvertTo-Json 'Verdopplung => 6,12,2,10' Reduce-Array 3,6,1,5 { param($x, $y) @($x) + $y * 2 } @() | ConvertTo-Json 'Verdopplung => "33","66","11","55"' Reduce-Array 3,6,1,5 { param($x, $y) @($x) + [String]$y * 2 } @() | ConvertTo-Json 'Verdopplung => "33","66","11","55"' Reduce-Array "3","6","1","5" { param($x, $y) @($x) + $y * 2 } @() | ConvertTo-Json 'Verdopplung => "33","66","11","55"' Reduce-Array 3,6,1,5 { param($x, $y) @($x) + [String]$y * 2 } @() | ConvertTo-Json 'Verdopplung => @(3,6),@(3,6),@(1,5),@(1,5)' Reduce-Array @(3,6),@(1,5) { param($x, $y) $x + @(,@($y) * 2) } @() | ConvertTo-Json 'Verdopplung => @(3),@(3),@(6),@(6),@(1),@(1),@(5),@(5)' Reduce-Array @(3,6,1,5) { param($x, $y) $x + @(,@($y) * 2) } @() | ConvertTo-Json 'Verdopplung => @(3,6,1,5),@(3,6,1,5)' Reduce-Array (,@(3,6,1,5)) { param($x, $y) $x + @(,@($y) * 2) } @() | ConvertTo-Json 'Verdopplung => @(3,@(@(6),1)),@(3,@(@(6),1)),@(5),@(5)' Reduce-Array @(3,@(@(6),1)),5 { param($x, $y) $x + @(,@($y) * 2) } @() | ConvertTo-Json 'Filterung (auf Elemente größer 4) => 6,5' Reduce-Array 3,6,1,5 { param($x, $y) @($x) + (@($y) -gt 4) } @() | ConvertTo-Json
Reduce-Array
lässt sich übrigens auch in mathematischen Funktionen und Reihenberechnungen einsetzen:
'Quersumme von 9876543210123456789 => 90' function Checksum([UInt64]$n) { Reduce-Array ($n -split "") { param($x, $y) [Int]$x + $y } } Checksum 9876543210123456789 'Fakultät von 17 => 355687428096000' function Factorial([Byte]$n) { Reduce-Array (1..[Math]::Max(1, $n)) { param($x, $y) $x * $y } } Factorial 17 'Eulersche Zahl (Hinterlegte Konstante vs. Taylor-Reihe im Punkt 1):' [Math]::E Reduce-Array (0..17) { param($x, $y) $x + 1 / (Factorial $y) } 0 'Kreiszahl (Hinterlegte Konstante vs. Mittelwert der Leibnitz-Reihe):' [Math]::PI (Reduce-Array (1..5E5) { param($x, $y) ($x[0] + (4, -4)[$y % 2] / ($y * 2 + 1)), $x[0] } 4 | Measure-Object -Average).Average.ToString() + " (Mittelwert)" Reduce-Array (1..5E5) { param($x, $y) ($x[0] + (4, -4)[$y -band 1] / ($y -shl 1 -bor 1)), $x[0] } 4 | % -Begin { $n = 0 } -Process { if ($n) { "$(($_ + $n) / 2) = ( $_ + $n ) / 2" } else { $n = $_ } } $Callback = { param($x, $y) $x + (4, -4)[$y -band 1] / ($y -shl 1 -bor 1) } "$((($a = Reduce-Array (1..499999) $Callback 4) + ($b = &$Callback $a 500000)) / 2) = ( $a + $b ) / 2"
Die Bit-Verschiebungen -shl
und -shr
gibt es erst seit PowerShell 3; da jedoch im Dualsystem jeder Versatz um eine Stelle nach links eine Verdopplung bewirkt, ist die Multiplikation mit 2 hierzu äquivalent.
Aus analogen Gründen ist ferner in den obigen Implementierungen $y -band 1
ergebnisgleich zu $y % 2
, außerdem entspricht die binäre ODER-Verknüpfung einer geraden Zahl mit 1 deren Inkrementierung um 1.
Einen deutlichen Geschwindigkeitsvorteil scheinen Bit-Operationen gegenüber arithmetischen Operatoren allerdings nicht zu bieten.
Nachfolgend wird die rekursive Verwendung einer Rückruffunktion demonstriert.
Ist diese anonym, kann ihr Quellcode mittels der Eigenschaft MyCommand.ScriptBlock
der automatischen Variablen $MyInvocation
ausgelesen werden.
Die Definition einer benannten Funktion ist dagegen über den Provider Function:
abrufbar.
Reduce-Array @(3,@(@(6),1)),5 { param($x, $y)return $x + 1 # Zählt Elemente ergebnisgleich zu (@(3,@(@(6),1)),5).Count => 2if ($y -is [Array]) { Reduce-Array $y $MyInvocation.MyCommand.ScriptBlock $x } else { $x + 1 } # Zählt Elemente rekursiv => 4 } 0 function Callback($x, $y) { if ($y -is [Array]) { Reduce-Array $y ${Function:Callback} $x } else { $x + 1 } # Zählt Elemente rekursiv => 4 } Reduce-Array @(3,@(@(6),1)),5 ${Function:Callback} 0
Reduce-Array
kommt mit einigen Sonderfällen zurecht:
Set-StrictMode -Version 3.0 Reduce-Array -Value 4 # Leeres Array => Gibt Startwert 4 zurück Reduce-Array @() { 4 } # Leeres Array => Gibt $null zurück Reduce-Array $null { 4 } # $null statt Array => Gibt $null zurück Reduce-Array @() -Value 4 # Leeres Array => Gibt Startwert 4 zurück Reduce-Array @($null) { 4 } # Array mit $null => Gibt Array-Element zurück Reduce-Array @($null) -Value 4 # Keine Callback-Funktion => Gibt $null zurück Reduce-Array @() $null 8 # $null statt Callback-Funktion => Bemängelt Argument Reduce-Array @(4) $null 8 # $null statt Callback-Funktion => Bemängelt Argument Reduce-Array @(4) { $_ } 8 # Undefinierte Variable $_ => Bemängelt Callback-Code Set-StrictMode -Off
Teile dieses Abschnitts einschließlich entsprechender Portierungen nach JavaScript und PHP wurden der Wikipedia als Beispiele beigetragen.
⑦ Benutzerdefinierter Alarm
Nachfolgend führt Alert-Object
in einer Endlosschleife periodisch eine Rückruffunktion mit einem Objekt als Argument aus und erzeugt einen Piepton, solange hintereinander das gleiche Ergebnis retourniert wird.
Das Wiederholungsintervall ist auf 15 Sekunden voreingestellt, lässt sich aber per Parameter nach Belieben ändern.
Durch Betätigung von Strg+C
(in der Shell) oder der Schaltfläche Vorgang beenden
(o. ä. in der IDE) kann der Lauf abgebrochen werden.
function Alert-Object { param($Object, [ValidateScript({ $true })][ScriptBlock]$Callback = {}, [Int]$Seconds = 15) for ($LastRC = $null;; Start-Sleep -Seconds $Seconds) { if (($ThisRC = &$Callback $LastRC $Object) -ne $LastRC) { $LastRC = $ThisRC } else { [Console]::Beep(500, 500) } } }
Mit einer geeigneten Rückruffunktion als Argument kann Alert-Object
zur Überwachung eines längeren Downloads (hier: Video-XXL.kGOYic_m.mp4.part
) eingesetzt werden.
Stockt dieser, dann bleibt die Dateigröße konstant und es wird der Wert retourniert, mit dem auch der Aufruf erfolgte; wird dagegen die Datei nicht gefunden, wird dasselbe (oder $null
) zurückgeliefert.
Da sich in all den Fällen das Ergebnis nicht mehr verändert, kommt es zum akustischen Signal.
$DownloadMonitor = { param($size, $path) if (!(Test-Path -PathType Leaf -LiteralPath $path)) { ($size<# = $null#>) } else { ($size = (Get-Item -LiteralPath $path).Length) } Write-Host -Object (Get-Date -Format '[yyyy-MM-dd HH:mm:ss]'), $size } Alert-Object 'C:\Downloads\Video-XXL.kGOYic_m.mp4.part' $DownloadMonitor
Alternativ lässt sich eine Weckfunktion realisieren, indem die aktuelle Zeit mit einer gewünschten (hier: 20:25
Uhr am 25
.02
.2025
– eine reine Uhrzeit würde sich auf den aktuellen Tag und ein bloßes Datum auf Mitternacht beziehen) verglichen wird.
Um ein Klingeln
zu bewirken, muss lediglich der Wert des ersten Aufrufparameters unmodifiziert zurückgegeben werden.
Alert-Object '2025-02-25 20:25' { param($ts, $alarm) Write-Host -Object (Get-Date -Format '[yyyy-MM-dd HH:mm:ss]'), $alarm if (($now = Get-Date) -lt (Get-Date -Date $alarm)) { $now } else { $ts } } 5
Jeweils entscheidend ist in beiden Anwendungsszenarien einzig die if
-Anweisung, das Cmdlet Write-Host
sorgt nur für eine regelmäßige Kontrollausgabe.
Statt oder neben der Alarmierung könnten auch andere Aktionen erfolgen, z. B. ein Herunterfahren des Rechners.
⑧ Tic-Tac-Toe / Drei gewinnt
Bei diesem klassischen, einfachen Strategiespiel für 2 Personen geht es darum, als Erster eine Horizontale, Vertikale oder Diagonale auf einem Gitter von 3×3 Feldern vollständig in den Besitz zu nehmen. Dafür besetzen zwei Spieler abwechselnd ein noch freies Feld auf dem Spielbrett. Wer anfängt, hat einen kleinen Vorteil und sollte deshalb per Los bestimmt werden.
Das nachfolgende Skript ermöglicht es, dieses Spiel gegen den Computer anzutreten.
Wegen der Interaktion mit dem Menschen kann der Quellcode leider nicht online ausgeführt werden.
Stattdessen sollte er kopiert und in der PowerShell ISE eingefügt werden, um ihn dort zu starten (jedoch gibt es alternativ an anderer Stelle auch eine im Web-Browser lauffähige JavaScript-Portierung).
Auf dem Spielbrett werden durch den Computer besetzte Felder durch das Zeichen #
markiert, und die vom Menschen belegten durch X
.
Felder, die keinem gehören und insofern noch frei sind, werden mit einer Ziffer versehen.
Wenn der Mensch am Zug ist, gibt dieser einfach eine der angezeigten Nummern zwischen 1 und 9 ein, um das entsprechende Feld zu besetzen.
$h = @{ TITLE = "TIC-TAC-TOE" # for PowerShell >=2.0, written by mawe-web.de
HOWTO = @("My symbol is '#', yours 'X';",
"enter a field number between",
"1 and 9 when it is your turn")
MYTURN = "My move".PadRight(9)
YOURTURN = "Your move"
MYWIN = "Sorry, I have won."
YOURWIN = "Congratulations, you won!"
TIE = "The game ended in a tie..."
PAD = @{ CALCULATOR = 7..9 + 4..6 + 1..3; PHONE = 1..9 }.CALCULATOR
}
# Decision making function
function f { param($p # Patterns to examine
, $m = 0 # Operating mode / expectancy value (0, 1, 2 or 3)
<# , $n = $n, $a = $a #>) # Read access variables defined outside: $n, $a
$s = "" # Initialize the list of suitable moves
foreach ($e in $p) { # Loop through the patterns
$d = 0 # Initialize the delta/score
$t = "" # Clear temporary string variable used for various purposes
foreach ($c in [String[]][Char[]]"$e") { # Loop through the positions...
if ($a[$c]) { $d += $a[$c] } # ...and calculate the pattern score...
else { $t += $c } # ...and determine blank fields
}
if ($m -eq 3 -and [Math]::Abs($d) -eq 3)
{ return -"$d".Length } # Game over! The winner is: -2=human, -1=machine
if (!$m -and $t -eq $e -or # If mode=0 and each pattern field is blank...
($m -eq 3 -and $d -eq 2) -or # ...or mode=3 and the delta is +2...
($m -and [Math]::Abs($d) -ge $m)) # ...or delta >= expectancy value...
{ $s += $t } # ...then add the blank fields to the list of useable moves
}
if ($n -lt 6 -and ($t = $s -replace "[2468]"))
{ $s = $t } # Prefer odd fields during the first moves
if ($s) { [Int][String]$s[(Get-Random -Maximum $s.Length)] } # Select a move
else { 0 } # Otherwise return 0 since there is no move to recommend
}
# Main procedure
Write-Host -Object ((,$h.TITLE + $h.HOWTO) -join "`n".PadRight(3)) # Say hello
$a = ,0 * 10 # Game board with fields numbered from 1 to 9
$p = 123, 147, 159, 258, 357, 369, 456, 789 # List of all winning patterns
$i = 10 # Index (must be 10 to skip Read-Host cmdlet during first loop)
$n = -(Get-Random -Maximum 2) # Roll the dice who starts: -1=human, 0=machine
while (($f = f $p 3) -ge 0 -and $n -le 9) { # While exit condition is not met:
while (!(1..10 -eq $i) -or $a[($i %= 10)] -or $a[$i]--)
{ $i = Read-Host -Prompt $h.YOURTURN } # (1) Get and set the human's move
if (2..10 -eq ($n += 2) -and # (2) Calculate/do the machine's move...
(($i = f $p 3) -or # ...using OR-linked ruleset
($i = f $p 2) -or
($i = f (,5)) -or
($a[5] -ge 0 -and (f (,1379) 2) -and ($i = f (,2468))) -or
($i = f (12369, 12347, 14789, 36789) 2) -or
($i = f (1..9))
) -and $i -gt 0 -and ++$a[$i]) { Write-Host -Object "$($h.MYTURN): $i" }
Write-Host -NoNewline -Object "", # (3) Display the game board
($h.PAD | ForEach-Object -Process { "${_}#X"[$a[$_]] + " `n"[!($_ % 3)] })
}
Write-Host -Object $h[("TIE", "YOURWIN", "MYWIN")[$f]] # Announce the result
Da sämtliche Texte gemeinsam in der Variablen $h
tabellarisch hinterlegt sind, ist eine sprachliche Anpassung mit keinem großen Aufwand verbunden:
$h = @{ TITLE = "TIC-TAC-TOE"HOWTO = @("My symbol is '#', yours 'X';","enter a field number between","1 and 9 when it is your turn")MYTURN = "My move".PadRight(9)YOURTURN = "Your move"MYWIN = "Sorry, I have won."YOURWIN = "Congratulations, you won!"TIE = "The game ended in a tie..."HOWTO = @() MYTURN = "Mein Zug" YOURTURN = "Dein Zug" MYWIN = "Du wurdest besiegt." YOURWIN = "Du hast gewonnen!" TIE = "Unentschieden." PAD = 7..9 + 4..6 + 1..3 }
Insofern könnte ein möglicher Spielverlauf wie folgt aussehen, so dass sich an der Ausgabe im Nachhinein recht gut nachverfolgen lässt, wie sich eine Partie entwickelt hat: (sowohl im Original als auch in Deutsch)
TIC-TAC-TOE My symbol is '#', yours 'X'; enter a field number between 1 and 9 when it is your turn My move : 5 7 8 9 4 # 6 1 2 3 Your move: 6 My move : 3 7 8 9 4 # X 1 2 # Your move: 7 My move : 2 X 8 9 4 # X 1 # # Your move: 8 My move : 1 X X 9 4 # X # # # Sorry, I have won.
TIC-TAC-TOE Mein Zug: 5 7 8 9 4 # 6 1 2 3 Dein Zug: 6 Mein Zug: 3 7 8 9 4 # X 1 2 # Dein Zug: 7 Mein Zug: 2 X 8 9 4 # X 1 # # Dein Zug: 8 Mein Zug: 1 X X 9 4 # X # # # Du wurdest besiegt.
Der Funktion f()
im obigen Quellcode werden Muster von Feldern in Form von Ziffernfolgen sowie ein Erwartungswert als Parameter übergeben, ferner greift sie lesend auf die extern deklarierten Variablen $n
und $a
zu [Zeilen 13-15].
Sie berechnet für jedes Muster eine Punktzahl, die der Differenz zwischen den eigenen und generischen Steinen auf seinen Feldern entspricht, und notiert sich außerdem die Nummern der noch unbesetzten Felder [Zeilen 20-23], z. B.:
Gehören im Muster 123 die Felder 2 sowie 3 ein und derselben Partei und ist das Feld 1 leer, dann führt dies zur Punktzahl 2 und Notierung der Ziffer 1.
Wenn die Differenz/Punktzahl mindestens dem Erwartungswert entspricht, werden die Nummern der unbesetzten Felder der Ergebnismenge hinzugefügt; für den Erwartungswert 0 wird dies jedoch nur dann gemacht, wenn sämtliche Felder des Musters unbesetzt sind [Zeilen 26-29].
Aus der Ergebnismenge wird eine Nummer zufällig ausgewählt und als Zugvorschlag von der Funktion zurückgegeben, oder 0, wenn die Ergebnismenge leer ist [Zeilen 33-34].
Für den Erwartungswert 3 findet die Funktion Muster, die vollständig von einer der beiden Spielparteien besetzt sind, und gibt in diesem Fall -1 oder -2 zurück, je nachdem, ob das gefundene Muster im Besitz des Computers oder des menschlichen Gegners ist [Zeilen 24-25].
Ansonsten findet die Funktion für den Erwartungswert 3 zudem alle Muster mit der Punktzahl 2, die der Computer mit nur einem Zug auf das noch freie Feld vervollständigen kann; ggf. gibt sie die Nummer eines der Freifelder als Zugempfehlung zurück [Zeile 27].
Solange die Partie noch jung ist und bislang weniger als 5 Züge erfolgt sind, wird der Zugvorschlag – wenn möglich – nur aus den ungeraden Nummern der Ergebnismenge ausgewählt [Zeilen 31-32].
In der Hauptprozedur des Skripts wird die Funktion f()
wiederholt mit verschiedenen Mustern und Erwartungswerten aufgerufen, und zwar solange, bis ein Zugvorschlag zurückgegeben wird und getätigt werden kann [Zeilen 45-52]; dabei wird von der Booleschen Kurzschlussauswertung umfangreich Gebrauch gemacht.
Ferner werden in der Hauptschleife der Zug des menschlichen Gegners (als Ziffer zwischen 1 und 9) erfragt, geprüft und gesetzt [Zeilen 43-44] sowie die aktuelle Spielsituation visualisiert, wobei vom Computer besetzte Felder mit einer Raute (#
) markiert werden und alle gegnerischen mit einem Kreuz (X
) [Zeilen 53-54].
Die Partie wird beendet, wenn eine Horizontale, Diagonale oder Vertikale durch eine der Parteien alleinig und vollständig besetzt ist oder es kein unbesetztes Feld mehr gibt; wer anfängt, bestimmt der Zufall [Zeilen 41-42].
Der im Skript implementierte Algorithmus kämpft nicht ganz so verbissen um einen Sieg wie die von Newell und Simon verwendete Strategie, sollte aber zumindest ein Unentschieden erzielen.
Und er kommt mit äußerst wenigen Code-Zeilen aus: Gerade mal 2 ineinander verschachtelte Schleifen [Zeilen 16-30], 4 If
-Bedingungen [Zeilen 21-22, 26-29, 31-32 und 33-34] und 6 Zugregeln [Zeilen 46-51] bilden den Kern der Entscheidungsfindung; das meiste andere ist eher der Ablaufsteuerung oder Benutzerschnittstelle zuzurechnen.
Durch Auskommentierung der Anweisungen in den Zeilen 47, 48, 49, 50 und/oder 31-32 kann der Computer in seiner Spielweise geschwächt und – evtl. mit ein bisschen Glück – geschlagen werden.
Siehe auch:
darebalogun/tic-tac-toe (in C++)
WesleyyC/Tic-Tac-Toe (in Python)
Creating Tic-tac-toe in Swift: Artificial intelligence
⑨ Und zum Schluss ist alles im Fluss …
… aber in welchem? – Obwohl der nachfolgende Zeichensalat
so aussieht, als sei das Haustier kurz mal eben über die Tastatur geflitzt, schafft es PowerShell, ihn zu einem durchaus lesbaren Ergebnis auszuwerten.
Wer erfahren im Umgang mit der Skriptsprache ist und Spaß am Rätseln hat, kommt vielleicht auch ohne eine explizite Ausführung darauf, welche Ausgabe dieser Einzeiler bewirkt:
,[Char](6%-4*''''[-1]-1)+("$("p","s")"[@(1/.5)+-!!8..!0]|%{"$_".Trim()*2})-Join[Char](1*3*5*7)#~9)
Tipp: Forme den obigen Ausdruck um und vereinfache/
Alle Angaben sind ohne jegliche Gewähr. Der Autor übernimmt keinerlei Verantwortung oder Haftung für Fehler oder Ungenauigkeiten, die in diesem Dokument auftreten können. Anmerkungen, Ergänzungen und Verbesserungsvorschläge zu diesem Artikel werden gerne entgegengenommen.