PsProgressTimer.psm1
using namespace System.Collections.Generic; class CircularBuffer { hidden [Queue[double]]$Queue hidden [int]$Size CircularBuffer([int]$Size) { $this.Queue = [Queue[double]]::new($Size) $this.Size = $Size } [bool]IsFull() { return $this.Queue.Count -eq $this.Size } [void]Add([double]$Value) { if ($this.IsFull()) { $this.Queue.Dequeue() } $this.Queue.Enqueue($Value) } [double]Read() { return $this.Queue.Dequeue() } [double]Peek() { return $this.Queue.Peek() } [void]Resize([int]$NewSize) { if ($NewSize -lt 1) { throw [System.ArgumentException]::new("Size must be greater than 0") } if ($NewSize -ge $this.Size) { $this.Size = $NewSize return } for ($i = 0; $i++ -lt $this.Size - $NewSize; ) { $this.Queue.Dequeue() } $this.Queue.TrimExcess() } [IEnumerator[double]]GetEnumerator() { return $this.Queue.GetEnumerator() } } class ProgressTimer { hidden [CircularBuffer]$Buffer hidden [double]$BufferAverage hidden [System.Diagnostics.Stopwatch]$Stopwatch hidden [int]$TotalCount hidden [int]$UseNMostRecent hidden [double]$IntraLapTime [int]$Counter [string]$ActivityText [System.Nullable[int]]$Id [System.Nullable[int]]$ParentId [scriptblock]$Status ProgressTimer([int]$Count) { $this._init($Count, $Count) } ProgressTimer([int]$Count, [int]$UseNMostRecent) { $this._init($Count, $UseNMostRecent) } hidden [void] _init([int]$c, [int]$u) { $this.Buffer = [CircularBuffer]::new($u) $this.Stopwatch = [System.Diagnostics.Stopwatch]::new() $this.TotalCount = $c $this.Counter = 0 $this.IntraLapTime = 0 } # Starts the clock on the timer [void]Start() { $this.Stopwatch.Restart() } # Marks the iteration and uses the time taken since the last to calculate the average [int]Lap() { return $this.Lap(1) } # Marks that [n] iterations have completed and adds the average time [n] times [int]Lap([int]$Count) { if ($Count -lt 0) { throw [System.ArgumentException]::new("Value cannot be less than zero", "Count") } if ($count -eq 0) { $this.UpdateDuration() return $this.Counter } if (!$this.Stopwatch.IsRunning) { throw [System.InvalidOperationException]::new("Timer has not yet been started") } for ($i = 0; $i++ -lt $Count; ) { $this.Buffer.Add(($this.Stopwatch.Elapsed.TotalSeconds + $this.IntraLapTime) / $Count) } $this.Counter += $Count $this.BufferAverage = [Linq.Enumerable]::Average($this.Buffer.Queue) $this.IntraLapTime = 0 $this.Stopwatch.Restart() return $this.Counter } # Updates the duration of the last entry in the buffer without performing a counter increment. [void]UpdateDuration() { if (!$this.Stopwatch.IsRunning) { throw [System.InvalidOperationException]::new("Timer has not yet been started") } $this.IntraLapTime += $this.Stopwatch.Elapsed.TotalSeconds $this.Stopwatch.Restart() } # Resets the timer to initial state. Allows the object to be reused [void]Reset() { $this.Buffer.Queue.Clear() $this.Counter = 0 if ($this.Stopwatch.IsRunning) { $this.Stopwatch.Restart() } } # Estimates the seconds remaining from the average rate of the n most recent lap times [double]SecondsRemaining() { if ($this.Buffer.Queue.Count -eq 0) { return (-1) } $Remaining = $this.TotalCount - $this.Counter return ($this.BufferAverage * $Remaining) - $this.IntraLapTime } # Calculates the percent complete [double]PercentComplete() { if ($this.Buffer.Queue.Count -eq 0) { return (-1) } return [Math]::Min(100, $this.Counter / $this.TotalCount * 100) } # Gets the estimated time of completion based on the seconds remaining. [datetime]EstimatedTimeOfCompletion() { if ($this.Buffer.Queue.Count -eq 0) { return [datetime]::MaxValue } return [datetime]::Now.AddSeconds($this.SecondsRemaining()) } # Gets the estimated time of compeletion as a string [string]GetEtcString() { return $this.GetEtcString($null, "--:--:--") } # Gets the estimated time of compeletion as a string using the specified format and default string if the ETC is not defined [string]GetEtcString([string]$Format, [string]$DefaultValue) { $EndDate = $this.EstimatedTimeOfCompletion() if ($EndDate -eq [datetime]::MaxValue) { return $DefaultValue } return $EndDate.ToString($Format) } # Returns a hashtable of the current object state to be used as a splatted parameter for Write-Progress [hashtable]GetSplat() { $SplatHt = [hashtable]::new() # Default Properties $SplatHt.Add("SecondsRemaining", $this.SecondsRemaining()) $SplatHt.Add("PercentComplete", $this.PercentComplete()) $SplatHt.Add("Completed", $this.IsComplete()) # Additional properties if (![string]::IsNullOrEmpty($this.ActivityText)) { $SplatHt.Add("Activity", $this.BuildActivityText($this.ActivityText)) } if ($this.Id.HasValue) { $SplatHt.Add("Id", $this.Id.GetValueOrDefault()) } if ($this.ParentId.HasValue) { $SplatHt.Add("ParentId", $this.ParentId.GetValueOrDefault()) } if ($this.Status -ne $null) { $SplatHt.Add("Status", "($($this.Counter)/$($this.TotalCount)) " + $this.Status.GetNewClosure().InvokeReturnAsIs()) } return $SplatHt } # Writes to the Progress stream with the current object state [void]WriteProgress() { $ProgressSplat = $this.GetSplat() Write-Progress @ProgressSplat } hidden [string]BuildActivityText([string]$LeaderText) { $sb = [System.Text.StringBuilder]::new() $sb.Append($LeaderText + (" " * [int][bool]$LeaderText) ).AppendFormat("[ETC: {0}", $this.GetEtcString() ).Append("]") return $sb.ToString() } hidden [bool]IsComplete() { return $this.Counter -ge $this.TotalCount } } $ModuleDir = ([System.IO.FileInfo]$PsScriptRoot).Directory.FullName ForEach ($Path in (Get-ChildItem -Path ([Io.Path]::Combine($PsScriptRoot, "Public")))) { . $Path.FullName } ForEach ($Path in (Get-ChildItem -Path ([Io.Path]::Combine($PsScriptRoot, "Private")))) { . $Path.FullName } Export-ModuleMember -Function @("New-ProgressTimer") |