Last active
May 1, 2024 17:05
-
-
Save jhoneill/f93ab7dbc421a55058989f76ae099b05 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SmartBuilder { # A string builder with a progress bar that automatically fluses periodically. | |
[System.Text.StringBuilder] hidden $Builder # The builder itself - its a sealed class otherwise the smartbuilder would be based on it. | |
#region supporting properties | |
[string]$RepeatingHeader = '' # If set, the repeating header is re-added as the first line after each flush | |
[scriptblock]$OnFlush = {$this.ToString()} # What to do when we flush the data (besides clearing the builder and re-adding any header) - should return a string or nothing. | |
[bool] hidden $_ShowProgress = $true # If false, don't show the progress bar | |
[string] hidden $_Activity = 'Building' # Displayed on the progress bar - accessed via $ProgressStatus | |
[string] hidden $_Status = 'Items so far' # Displayed on the progress bar with ": <ItemCount>" - accessed via $ProgressStatus | |
[bool] hidden $ProgressShown = $false # Have we shown a progress bar | |
[int]$ProgressTotalToDO = 0 # If set, the progres bar will display items process as a#54 perecentage of this number | |
[int]$ProgressEvery = 100 # How many additions between updates to the progress bar. (Frequent updates hurt performance) | |
[int]$FlushEvery = 1000 # How many additions before we flush the builder. If < 1 it will only flush manually | |
[int]$MaxSize = 1GB # Perform an additional flush if headers + added text + linebreaks becomes too great | |
[int]$AddsDone = 0 # Counter of additions (whether we add the line break after them or not) | |
#endregion | |
#region constructors - # create the builder, initialize progress, add any header - 4 permuations of capacity/header | |
SmartBuilder() {$this.init(1mb, '', $false)} | |
SmartBuilder( [int]$Capacity ) {$this.init($Capacity, '', $false)} | |
SmartBuilder( [string]$RepeatingHeader ) {$this.init(1mb, $RepeatingHeader, $false)} | |
SmartBuilder( [int]$Capacity, [string]$RepeatingHeader ) {$this.init($Capacity, $RepeatingHeader, $false)} | |
SmartBuilder( [int]$Capacity, [string]$RepeatingHeader, [bool]$HideProgress ) {$this.init($Capacity, $RepeatingHeader, $HideProgress)} | |
[void] hidden init([int]$Capacity, [string]$RepeatingHeader , [bool]$HideProgress) { | |
$this.Builder = New-Object -TypeName System.Text.StringBuilder -ArgumentList $Capacity | |
if ($RepeatingHeader) { | |
$this.RepeatingHeader = $RepeatingHeader | |
$null = $this.Builder.AppendLine($RepeatingHeader) | |
} | |
if ($HideProgress) { | |
$this._ShowProgress = $false | |
} | |
else {$this.UpdateProgress()} | |
} | |
#endregion | |
[string] ToString() { # Our ToString is the builder's .ToString() - but remove any trailing linebreaks | |
return ($this.Builder.ToString() -replace "[\r\n]+\z") | |
} | |
[string] Flush() { # Do whatever we need to with the contents of the builder, clear down and add any repeating header | |
$s = & $this.OnFlush | |
$null = $this.Builder.clear() | |
if ($this.RepeatingHeader) { | |
$null = $this.Builder.AppendLine($this.RepeatingHeader) | |
} | |
return $s | |
} | |
[string] Finish() { # Flush data if there is any, clear progress indicator | |
if ($this.ShowProgress -and $this.ProgressShown) { | |
Write-Progress -Activity $this.ProgressActivity -Completed | |
} | |
if ($this.Builder.Length -gt ( $this.RepeatingHeader.length + 2) ) { | |
return $this.flush() | |
} | |
else { return $null} | |
} | |
[void] hidden UpdateProgress() { # Update the progess bar, if there is one | |
if ($this.ShowProgress -and $this._Activity -and $this._Status) { | |
$params = @{Activity = $this.ProgressActivity | |
Status = "$($this.ProgressStatus): $($this.AddsDone)" | |
} | |
if ($this.ProgressTotalToDO -gt $this.AddsDone) { | |
$params['PercentComplete'] = 100 * $this.AddsDone / $this.ProgressTotalToDO | |
} | |
Write-Progress @params | |
$this.ProgressShown = $true | |
} | |
} | |
#region wrap the builders append/appendline we need the option to return flushed data but deal-with-blanks (the string method) OR send with no return (the two voids) | |
[string] Add( [string]$Line, [bool]$NewLine) { # Overload to append the line with/without line break; if necessary, update progress and/or flush | |
if ($NewLine) {$null = $this.Builder.AppendLine($line)} | |
else {$null = $this.Builder.Append($line) } | |
$this.AddsDone ++ | |
if ( $this.ShowProgress -and ($this.AddsDone % $this.ProgressEvery) -eq 0 ) { | |
$this.UpdateProgress() | |
} | |
if ( ($this.FlushEvery -gt 0 -and ($this.AddsDone % $this.FlushEvery) -eq 0) -or | |
($this.MaxSize -gt 0 -and ($this.maxsize -lt $this.Builder.length) ) ){ | |
return $this.Flush() } | |
else { return $null } | |
} | |
[void] Add( [string]$Line) { # Overload to append the line and leave line breaks to the caller | |
[void] $this.Add($line, $false) | |
} | |
[void] AddLine([string]$Line) { # Method to append a line adding a trailing line break | |
[void] $this.Add($line, $true) | |
} | |
#endregion | |
} | |
#region we can't have update property events in a PowerShell class but we can bolt on script properties to do things as an update | |
$extraMembers =@{ | |
ProgressStatus = @{ | |
Value = {$this._Status} | |
SecondValue = { | |
$this._Status = $Args[0] ; $this.UpdateProgress() | |
} | |
} | |
ProgressActivity = @{ | |
Value = {$this._Activity} | |
SecondValue = { | |
if (-not $this.$ProgressShown) {$this._Activity = $args[0]} | |
else { # if we have shown progress and changing the 'Activity' label, remove the old | |
Write-Progress -Activity $this._Activity -Completed | |
$this._Activity = $Args[0] ; $this.UpdateProgress() | |
} | |
} | |
} | |
ShwoProgress =@{ | |
Value = {$this._ShowProgress} | |
SecondValue = { | |
if ( $this.$ProgressShown -and -not $args[0]) { | |
Write-Progress -Activity $this._Activity -Completed | |
} | |
$this._ShowProgress = $Args[0] ; $this.UpdateProgress() | |
} | |
} | |
} | |
foreach ($member in $extraMembers.keys) { | |
$Settings = $extraMembers[$member] | |
if (-not (Get-TypeData -TypeName "SmartBuilder" ).Members.$member ) { | |
Update-TypeData -TypeName "SmartBuilder" -MemberName $member -MemberType ScriptProperty @Settings | |
} | |
} | |
#endregion | |
<# | |
.example | |
PS> $bob = New-Object smartbuilder # Builders are traditionally named "Bob" | |
PS> $bob.ProgressTotalToDO = 1500 | |
PS> $newline = $true | |
PS> 1..1500 | ForEach-Object {if ($x = $bob.Add($_,$newline)) {$x} ; sleep -Milliseconds 2 } # so we can see the progress | |
PS> $bob.Finish() | |
Create a new builder and sends 1500 items into a it. | |
For 1..999 Add($_ , $true) returns an empty, at 1000 (the default) it returns the first 1000 and clears down the builder, 1001..1500 are added returning an empty string | |
then finish() returns lines 1001..1500 | |
.example | |
PS> $bob = New-Object smartbuilder | |
PS> $bob.FlushEvery = 100 | |
PS> $bob.onFlush = {$this.toString() >> $filename} | |
PS> $bob.AddLine("Name,Length") | |
PS> dir | %{$bob.AddLine(('"{0}","{1}"' -f $_.Name,$_.length))} | |
PS> $bob.Finish() | |
PS> Write-Verbose -Verbose ("Added $($bob.AddsDone) items" ) | |
PS> notepad .\foo.txt | |
Instead of using Add($xx , $newline) from the first example, this uses Addline; AddLine($line) or Add with a single parameter | |
return nothing EVEN when a FLUSH occurs - here the flush outputs to a file so nothing is returned, | |
and the single parameter Add/Adline remove the need to handle empty return data. Now we flush every 100 items insead of the default 1000. | |
This is doing the equivalent of export-csv; there are cases where it is preferable to build a string than use | |
$hashtable = @{fieldName1=$fieldData1; $fieldName2=$fieldData2; } ; [pscustomObject]$HashTable piped to Export-csv | |
but in the example | select-object | export-CSV makes more sense ! | |
.example | |
PS> $filename = "foo.txt"; del $filename -ea 0 ; | |
PS> dir | ForEach-Object -Begin {$bob = New-Object smartbuilder -Property @{FlushEvery=100; onFlush={$this.toString() >> $filename} | |
$bob.AddLine( "Name,Length") | |
} -Process {$bob.AddLine(('"{0}","{1}"' -f $_.Name,$_.length)) | |
} -End {$bob.Finish() ; Write-Verbose ("Added $($bob.AddsDone) items" ) } | |
An alternate way of writing the previous example with the initialization and header row in the begin block of a foreach, and the finish() in the end block | |
.example | |
PS> 1..1500 | ForEach-Object -Begin { del foo.txt -ea 0 ; | |
$bob = New-Object smartbuilder -Property @{ProgressEvery=50; FlushEvery=800; | |
onFlush={Out-File -Append "foo.txt" -InputObject $this.ToString()} } ; | |
} -Process{ $Null = $bob.Add("$_ ") ; sleep -Milliseconds 5 # so we can see the progress | |
} -end { $bob.Finish() ; notepad foo.txt} | |
In this variation on the first example the builder is told to update and flush at different intevals, and | |
when it flushes the operation is to write/append its contents to foo.txt (this will add a line break after each block of 800) | |
This time the Add(xx) method is used insead of AddLine(xx) to build the items into long lines. | |
With the different intervals the first 800 are sent as one long line and the last 700 are sent by the Finish() operation | |
.example | |
# here SQL is a notional "run this on via some SQL connection" command | |
PS> $bob = New-Object smartbuilder -ArgumentList "INSERT INTO stuff (col1, col2, col3) VALUES " | |
PS> $bob.OnFlush = {SQL ($this.ToString() -replace ",\s*\z" , ";")} # run 'sql' with the insert query but replace ',' and any white space at the end with ';' | |
PS> foreach ($row in $someData) { | |
#build a line for the insert into statement ending with "," hence the replace in the script block. Send it to the builder - a batch gets sent to SQL every 1000 rows | |
#Use " where sql has ', that way we fix "'" and put O'Neill goes "O'Neill", then "O''Neill" and finally 'O''Neill', fix \ as well. | |
$ValueLine = ( '("{0}","{1}","{2}") ,' -f $row.a, $row.b, $row.c) -replace "'","''" -replace "\","\\" -replace '"',"'" | |
$null = $bob.AddLine($valueLine) | |
PS> } | |
PS> $bob.Finish() # send any left over to SQL | |
This time we use a repeating hearder to SQL INSERT queries. The smart builder is created with the "INSERT..."" as the header | |
Then each AddLine puts in a row with "('a','b','c')," | |
The flush runs 'SQL' with the output - removing the extra "," from the last line. | |
#> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment