ExcelAnt.psm1
#Region '.\Private\Classes\ExcelColor.ps1' 0 function mangle_colorHexToRgb { # oh gosh. terrible hack. [OutputType('System.Drawing.Color')] param( [string]$HexStr ) if ($HexStr.Length -eq 8) { write-error '8char wip' } $alpha = 0xff $strRgb = $HexStr.Substring(0, 6) $r, $g, $b = [rgbcolor]::FromRgb( $strRgb ).ToRgb() return [System.Drawing.Color]::FromArgb( $alpha, $r, $g, $b) } class ExcelColor { # future: convert to argument transformation type [int]$Red = 0xff [int]$Green = 0xff [int]$Blue = 0xff [int]$Alpha = 0xff [System.Drawing.Color]$Color = 'white' ExcelColor ( [string]$HexStr ) { $this.Color = [ExcelColor]::FromHex( $HexStr ) } ExcelColor ( [int]$Red, [int]$Green, [int]$Blue ) { $This.Red = $Red $This.Green = $Green $This.Blue = $Blue $This.Color = [ExcelColor]::FromRGBA( $this.Red, $This.Green, $This.Blue ) } ExcelColor ( [int]$Red, [int]$Green, [int]$Blue, [int]$Alpha ) { $This.Red = $Red $This.Green = $Green $This.Blue = $Blue $This.Alpha = $Alpha $This.Color = [ExcelColor]::FromRGBA( $this.Red, $This.Green, $This.Blue, $This.Alpha ) } [string] ToString() { # outputs: [ExcelColor('#0f232490')] return [ExcelColor]::__repr__( $This ) } hidden static [string] __repr__ ( [ExcelColor]$Object ) { # outputs: [ExcelColor('#0f232490')] return '[ExcelColor(''#{0:x}{1:x}{2:x}{3:x}'')]' -f @( $Object.Red.ToString('x') $Object.Green.ToString('x') $Object.Blue.ToString('x') $Object.Alpha.ToString('x') ) } [string] ToHexString() { # outputs: #0f232490 return '#{0:x}{1:x}{2:x}{3:x}' -f @( $this.Red.ToString('x') $this.Green.ToString('x') $this.Blue.ToString('x') $this.Alpha.ToString('x') ) } # static [ExcelColor] FromHex( [string]$HexStr) { static [System.Drawing.Color] FromHex( [string]$HexStr) { [System.Drawing.Color]$res = _colorHexToRgb -HexStr $HexStr return $res } static [System.Drawing.Color] FromRGB( [int]$Red, [int]$Green, [int]$Blue ) { return [System.Drawing.Color]::FromArgb( $Red, $Green, $Blue) } static [System.Drawing.Color] FromRGBA( [int]$Red, [int]$Green, [int]$Blue, [int]$Alpha ) { return [System.Drawing.Color]::FromArgb( $Alpha, $Red, $Green, $Blue) } } # function mangle_l.NewColor { # return [ExcelColor] # } #EndRegion '.\Private\Classes\ExcelColor.ps1' 80 #Region '.\Public\0.shared.ps1' 0 $script:__xantState ??= @{} #EndRegion '.\Public\0.shared.ps1' 2 #Region '.\Public\Close-ExcelSession.ps1' 0 function Close-ExcelSession { [CmdletBinding()] param( # instead of closing, return the stored list [switch]$PassThru, # I'm not sure whether there's a reason benefit [switch]$Force ) $state = $script:__xantState.openBookList if($PassThru) { return $state } 'Books stored: {0}' -f @( $state.Count ) | Write-Information -infa 'Continue' foreach ($Book in $state) { try { Close-ExcelPackage -ExcelPackage $Book if ($Force) { $book.close() $book.dispose() } } catch { Write-Error "Failed closing`nnote: `nfuture will alias a label to a session.`ncurrently I never drop a reference explicitly`n$_" -ea 'continue' } } } #EndRegion '.\Public\Close-ExcelSession.ps1' 29 #Region '.\Public\Close-ExcelWindow.ps1' 0 function Close-ExcelWindow { <# .SYNOPSIS politely close all open excel windows. Modified files will prompt for you to save. .DESCRIPTION Polite version, does not create broken recovery files .EXAMPLE PS> Close-ExcelWindow #> [CmdletBinding()] [Alias('xL.Window.CloseAll')] $Ps = Get-Process *Excel* -ea ignore if ($PS) { $response = $ps.CloseMainWindow() } '{0} of {1} excel windows closed' -f @( ($response -eq $true).Count $ps.count ) | write-information -infa 'Continue' } #EndRegion '.\Public\Close-ExcelWindow.ps1' 22 #Region '.\Public\Get-ExcelAddress.ps1' 0 function Get-ExcelAddress { <# .NOTES future: allow autocomplete of currently open file todo: - [ ] arg completer enumerates tables, enumerates worksheets #> [Alias('xl.Addr.Lookup')] [CmdletBinding()] [OutputType('string')] param( # required param Package, worksheet, table name [Parameter(Mandatory, Position = 0)] [OfficeOpenXml.ExcelPackage]$Package, [ArgumentCompletions( 'PayloSettings', 'Changes', 'New_JCUsers', 'Previous_JCUsers', 'Metrics', 'errLog' )] [Parameter(Mandatory, Position = 1)] [string]$Worksheet, [ArgumentCompletions( 'PayloSettings', 'Changes', 'New_JCUsers', 'Previous_JCUsers', 'Metrics', 'errLog' )] [Parameter(Mandatory, Position = 2)] [string]$TableName # [switch]$TestIsValid # return bool ) 'Trying: {0}, {1}' -f @( $Worksheet, $TableName ) | Write-Verbose $sheetExists = -not $null -eq $Package.Workbook.Worksheets[$Worksheet] $tableExistsSomewhere = $TableName -in @($package.Workbook.Worksheets.Tables.Name) # $TableExistsInSameSheet = -not $null -eq $Package.Workbook.Worksheets[$Worksheet] $TableExistsInSameSheet = -not $null -eq $Package.Workbook.Worksheets[$Worksheet].Tables[$TableName] # try { # I was going to write: $Pkg.Workbook.Worksheets['Changes'].Tables['Changes'].Address.Address # write-verbose sheetExists, TableExistsSOmewhere, and TableExistsInSameSheet @{ 'SheetExists' = $sheetExists 'TableExistsSomewhere' = $tableExistsSomewhere 'TableExistsInSameSheet' = $TableExistsInSameSheet } | bdgLog -category ModuleEvent -message 'xl.Addr.Lookup' -PassThru | write-verbose return $Package.Workbook.Worksheets[ $Worksheet ].Tables[ $TableName ].Address.Address } #EndRegion '.\Public\Get-ExcelAddress.ps1' 63 #Region '.\Public\Get-ExcelPackageError.ps1' 0 # Import-Module ImportExcel @' initial 15 0.009 cd 'G:\temp\xl' 16 0.008 $whichXl = gcl |gi 19 0.031 mkdir temp_exo 20 0.009 cd .\temp_exo\ 24 0.012 $whichXl | cp -Destination '.' 25 0.006 ls 30 0.190 $src = gi JumpCloud_Changes.2023-02-24_12-22-02Z.xlsx $dest = 'dir' Expand-Archive $src -DestinationPath $dest 31 0.008 ls 33 1.207 code.cmd --new-window .\dir\ sprint - [ ] filepath from string or fileinfo - [ ] file but as an excel package - [ ] allow piping of either - [ ] maybe ValuesFromParameterType or ParameterName to auto coerce packages cleaner ? '@ | write-verbose -verbose function Get-ExcelPackageError { # filepath to an excel file # copy to g:\temp\xl\auto' # expand archive to subdir [Alias('xl.Errors.Inspect')] param( [string]$ExcelPkg # [string]$subdir, # [string]$outpath, ) throw 'NYI' '' # $coerceFilepath = Get-Item $ExcelPkg # } } # transformation type #EndRegion '.\Public\Get-ExcelPackageError.ps1' 46 #Region '.\Public\New-ExcelTypeInfo.ps1' 0 function New-ExcelTypeInfo { <# .SYNOPSIS sugar to get different excel types, exposing class and type references .examples see also: PS> find-type -FullName *excel* #> [Alias('debug.TypeInfo.ExcelColor')] [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [ValidateSet( 'ExcelColor' )][string]$TypeName ) switch($TypeName) { 'ExcelColor' { return [ExcelColor] } default { "UnhandledTypeName: $TypeName" } } return $null } #EndRegion '.\Public\New-ExcelTypeInfo.ps1' 27 #Region '.\Public\Start-ExcelSession.ps1' 0 # [Collections.Generic.List[Object]]$script:__xantState.curWorkbook ??= @{} # $script:__xantState.curWorkbook ??= @{} [Collections.Generic.List[Object]]$script:__xantState.openBookList ??= @() function Start-ExcelSession { <# .SYNOPSIS create a new excel package, either replace existing or create a new file .DESCRIPTION this is called the first time, allowing you to auto rotate the file, once per run. Not new files every export .notes ' todo - [ ] generates temp name, saves to a new sheet. - [ ] keep a reference based on a name or label which maps to the filesafetime ' # future: track names to aliases #> write-warning 'still a WIP' $nextPkg = Invoke-SafeFileTimeTemplate -infa 'Continue' $state = $script:__xantState.openBookList $state.add( $nextPkg ) 'Number of items: {0}' -f @( $state.count ) | write-verbose -Verbose foreach($book in $state) { Close-ExcelPackage -ExcelPackage $book -ea 'continue' $state.Remove( $book ) } 'hardcoded temp behavior: closing existing sessions' | write-debug # [Collections.Generic.List[Object]]$script:__xantState.openBookList.add( $nextPkg ) # throw 'StartedXlsx => "{0}"' -f @( $nextPkg.File.fullName ) | write-information -infa 'Continue' return $nextPkg } #EndRegion '.\Public\Start-ExcelSession.ps1' 41 #Region '.\Public\transformTypes\coerce.ToExcelPackage.ps1' 0 function coerce.ToExcelPackage { # <# .synopsis convert filepaths or [ExcelPackage]s, resolving to a workbook - [ ] 1 file already exists, and is fileinfo - [ ] 2 file exists, is a string path - [ ] 3 file does not exist #> # [OutputType('System.IO.FileSystemInfo')] [CMdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, position = 0)] [object]$InputObject, # # # When creating missing items, the default type is File # [Parameter()] # [ArgumentCompletions('File', 'Directory', 'SymoblicLink', 'Junction', 'HardLink')] # [string]$ItemType = 'File', # # create file if not yet existing [switch]$CreateIfMissing # [switch]$Mandatory ) begin { # $null = $InputObject } process { <# DirectoryInfo isa System.IO.FileSystemInfo FileInfo isa System.IO.FileSystemInfo #> if($InputObject -is 'OfficeOpenXml.ExcelPackage') { write-verbose 'already an ExcelPackage' return $InputObject } throw 'left off here 1] simple coerce path to package if existing create if requested commit ' if ($InputObject -is 'string') { $alreadyExists = Test-Path $InputObject '{0} is string, and exists? {1}' -f @( $InputObject | Join-String -double $alreadyExists ) | write-verbose 'AlreadyExists? {0}. CreateIfMissing? {1}' -f @( $AlreadyExists, $CreateIfMissing ) | write-verbose if ($AlreadyExists) { return $InputObject | Get-Item } 'Does not exist. CreateIfMissing? {0} using type: {1}' -f @( $CreateIfMissing $ItemType )| write-verbose if($CreateIfMissing) { 'creating: {0}' -f @( $InputObject | Join-String -double ) | write-verbose New-Item -ItemType $ItemType -path $InputObject -Force -passThru return } if(-not $AlreadyExists -and -not $CreateIfMssing){ $PSCmdlet.WriteError( [Management.Automation.ErrorRecord]::new( [ArgumentException]::new( 'File does not exist, and CreateIfMissing is not set' ), 'FileDoesNotExist', [Management.Automation.ErrorCategory]::InvalidArgument, $InputObject )) } # elkse, missing with create } 'unhandled type name: [{0}]' -f @( $InputObject.GetType().Name ) | Write-Error # return [IO.FileInfo]::new($InputObject) switch ($InputObject) { { $_ -is [System.IO.FileInfo] } { $InputObject } { $_ -is [string] } { [System.IO.FileInfo]::new($InputObject) } default { [System.IO.FileInfo]::new($InputObject) } } } end {} } #EndRegion '.\Public\transformTypes\coerce.ToExcelPackage.ps1' 118 #Region '.\Public\transformTypes\coerce.ToFileInfo.ps1' 0 function coerce.ToFileInfo { # build might work if name is coerce-ToFileSystemINfo <# .SYNOPSIS coerce strings, [FileInfo], [ExcelPackage]s to [IO.FileInfo] instances .link ExcelAnt\coerce.ToFileInfo .link ExcelAnt\coerce.ToExcelPackage .DESCRIPTION coerce/resolve data types Cases that are* supported ask for opinion on Write-Error, vs throw, vs CmdletError especialy since pipeline is involved does write-error -ea 'continue' require a return, then ? does PSCmdlet Error recordscontinue' require a return too? - [ ] 1 file already exists, and is fileinfo - [ ] 2 file exists, is a string path - [ ] 3 file does not exist #> [OutputType('System.IO.FileInfo')] [CMdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, position = 0)] [object]$InputObject, [Parameter()] [OfficeOpenXml.ExcelPackage]$ExcelPackage, # future: what is the cleaner way to pipe multiple non-objects, does 'ValueFromPipelineByPropertyName' make it worse ? # [Parameter(mandatory, ValueFromPipeline, Position=0, ValueFromPipelineByPropertyName=)] # When creating missing items, the default type is File [Parameter()] [ArgumentCompletions('File', 'Directory', 'SymoblicLink', 'Junction', 'HardLink')] [string]$ItemType = 'File', # create file if not yet existing [switch]$CreateIfMissing, [switch]$Mandatory ) begin { # $null = $InputObject } process { <# DirectoryInfo isa System.IO.FileSystemInfo FileInfo isa System.IO.FileSystemInfo #> if ($InputObject -is 'IO.FileSystemInfo') { Write-Verbose 'already a FileInfo instance' return $InputObject } if ($InputObject -is 'OfficeOpenXml.ExcelPackage') { write-verbose 'WorkBook has a FileSystemInfo' return $InputObject.File } if($ExcelPackage) { write-verbose 'WorkBook (from type param) has a FileSystemInfo' return $ExcelPackage.File } if ($InputObject -is 'string') { $alreadyExists = Test-Path $InputObject '{0} is string, and exists? {1}' -f @( $InputObject | Join-String -double $alreadyExists ) | write-verbose 'AlreadyExists? {0}. CreateIfMissing? {1}' -f @( $AlreadyExists, $CreateIfMissing ) | write-verbose if ($AlreadyExists) { return $InputObject | Get-Item } 'Does not exist. CreateIfMissing? {0} using type: {1}' -f @( $CreateIfMissing $ItemType )| write-verbose if($CreateIfMissing) { 'creating: {0}' -f @( $InputObject | Join-String -double ) | write-verbose New-Item -ItemType $ItemType -path $InputObject -Force -passThru return } if(-not $AlreadyExists -and -not $CreateIfMssing){ $PSCmdlet.WriteError( [Management.Automation.ErrorRecord]::new( [ArgumentException]::new( 'File does not exist, and CreateIfMissing is not set' ), 'FileDoesNotExist', [Management.Automation.ErrorCategory]::InvalidArgument, $InputObject )) } # elkse, missing with create } 'unhandled type name: [{0}]' -f @( $InputObject.GetType().Name ) | Write-Error # return [IO.FileInfo]::new($InputObject) switch ($InputObject) { { $_ -is [System.IO.FileInfo] } { $InputObject } { $_ -is [string] } { [System.IO.FileInfo]::new($InputObject) } default { [System.IO.FileInfo]::new($InputObject) } } } end {} } #EndRegion '.\Public\transformTypes\coerce.ToFileInfo.ps1' 141 #Region '.\Public\utils\Convert-RotateObjectProperty.ps1' 0 function Convert-RotateObjectProperty { <# .SYNOPSIS pivot into key value pairs .example Ps> b.rotateProperties $this | to-xl #> [Alias('xl.Object.RotateProperties')] param( # takes one PSObject and rotates the properties [Parameter(Mandatory, Position = 0)] [Object]$InputObject ) $InputObject.PSObject.Properties | sort Name | %{ $meta = [ordered]@{} $meta[ 'Property' ] = $_.Name $meta[ 'Value' ] = $_.Value [pscustomobject]$meta } } #EndRegion '.\Public\utils\Convert-RotateObjectProperty.ps1' 21 #Region '.\Public\utils\Enum.MembersFromTypeInfo.ps1' 0 function Enum.MembersFromTypeInfo { <# .SYNOPSIS list properties/keys from a [type] type, no objects yet. .example PS> $stuff = (gi .), (gi .\README.md) $tinfos = $stuff | % GetType | sort -Unique { $_.FullName } $tinfos | Enum.MembersFromTypeInfo | To-Xl #> [CmdletBinding()] [Alias('xl.enumerateKeysOfType')] param( [Parameter(ValueFromPipeline, Position=0, Mandatory)] [object] $InputObject ) process { if ($_ -isnot 'type') { Write-Error "Arg is not a type: $_ " } $_ | ClassExplorer\Find-Member | ForEach-Object Name | Sort-Object -Unique } } #EndRegion '.\Public\utils\Enum.MembersFromTypeInfo.ps1' 24 #Region '.\Public\utils\Filter-DropBlankProperties.ps1' 0 # function dropBlankKeys { function Filter-DropBlankProperties { <# .SYNOPSIS enumerates to mutate object, by dropping keys with blank values .DESCRIPTION #> [Alias('xl.Object.DropBlankProperties')] [CmdletBinding()] [OutputType('Hashtable')] param( [Parameter(mandatory)] [hashtable]$InputHashtable, [switch]$NoMutate ) $strUserKeyId = '[User={2} <CoId={0}, EmpId={1}>]' -f @( $finalObj.companyId $finalObj.employeeIdentifier $finalObj.userName ) if ($NoMutate) { $targetHash = [hashtable]::new( $InputHashtable ) } else { $targetHash = $InputHashtable } $msg = $targetHash.GetEnumerator() | Where-Object { [string]::IsNullOrEmpty( $_.Value ) } | ForEach-Object Name | Sort-Object -Unique | Join-String -sep ', ' -op "dropped blank fields on ${strUserKeyId}: " @{ Message = $msg } | bdgLog -Category DataIntegrity -Message $msg -PassThru | Write-Verbose $toDrop = $targetHash.GetEnumerator() | Where-Object { [string]::IsNullOrEmpty( $_.Value ) } | ForEach-Object Name foreach ($k in $toDrop) { $targetHash.Remove( $k ) } return $targetHash } # label '=== Reached final core_config.ps1 300.' $PSCommandPath # | write-warning <# AutoGen: EndOf: C:\Users\cppmo_000\SkyDrive\Documents\2022\client_BDG\self\bdg_lib\src_static\core_config.ps1 #> <# AutoGen: Begin: C:\Users\cppmo_000\SkyDrive\Documents\2022\client_BDG\self\bdg_lib\src_static\stand_alone_entry.ps1 #> #EndRegion '.\Public\utils\Filter-DropBlankProperties.ps1' 55 #Region '.\Public\utils\GetRandomColor.ps1' 0 # or import nancy? function Get-RandomColor { <# .synopsis random 24bit color .DESCRIPTION Output is raw random, no smoothing. .notes future: differentiate between Write-Color and Get-Color colors are objects to mutate write is ansi escapes as string see [pansies.RGBColor] as an idea, but, the new behavior of 7.3 gives new posibilities, see about_AnsiTerminal .EXAMPLE Pwsh> Get-RandomNancyColor Pwsh> Get-RandomNancyColor -Count 10 #> [Alias('xl.Rand.AnsiColor')] [OutputType('PoshCode.Pansies.RgbColor')] [CmdletBinding()] param( # Return more than one color [Alias('Count')][int]$TotalCount = 1 ) foreach ($i in 1..$TotalCount) { $r, $g, $b = Get-Random -Count 3 -Minimum 0 -Maximum 255 $PSStyle.Background.FromRgb($r, $g, $b) # or: [PoshCode.Pansies.RgbColor]::new($r, $g, $b) } } #EndRegion '.\Public\utils\GetRandomColor.ps1' 36 #Region '.\Public\utils\Invoke-SafeFileTimeTemplate.ps1' 0 function Invoke-SafeFileTimeTemplate { <# .SYNOPSIS timenow for safe filepaths: "2022-08-17_12-46-47Z" .notes distinct values to the level of a full second #> # [Alias('xl.New.Safetime')] [OutputType('OfficeOpenXml.ExcelPackage')] [Alias( 'xl.New.SafeTime', 'New-FileTimeTemplate' )] [CmdletBinding()] param( # [parameter(Mandatory, Position = 0)] # [string]$Label, [Parameter(Position = 0)] # [Parameter(Position = 1)] [ArgumentCompletions( 'export-{0}.xlsx', './.output/export-{0}.xlsx', '{0}.xlsx' )] [string]$NameTemplate, # Root output directory, if not template [ArgumentCompletions( 'G:\temp\xl', '([IO.Path]::GetTempPath())' )] [Parameter(Position=1)] # [Parameter(Position=2)] $RelativeTo ) $Final_NameTemplate = $NameTemplate ?? '{0}.xlsx' # $Final_RelativeTo = $RelativeTo ?? ([IO.Path]::GetTempPath()) $Final_RelativeTo = $RelativeTo ?? 'g:\temp\xl\.output' $Final_fullName = Join-Path $Final_RelativeTo $Final_NameTemplate $Render = $Safe -f @( SafeFiletimeString ) # (Get-Date).ToString('u') -replace '\s+', '_' -replace ':', '-' # always new if(test-path $render ) { 'Unexpected, filetime exists. $Render = "{0}"' -f @( $render ) | Write-Error } # Ensure full filepath exists, and delete. # Then return a new package New-Item -itemtype file -path $render -force -ea 'ignore' Remove-Item -path $render -force -ea 'ignore' $pkg = Open-ExcelPackage -Path $Render -Create -Verbose 'Created new filetime template: {0}' -f @( $Pkg.File.FullName | Join-String -double ) | Write-Information -infa 'Continue' return $Pkg } #EndRegion '.\Public\utils\Invoke-SafeFileTimeTemplate.ps1' 63 #Region '.\Public\utils\SafeFileTimeString.ps1' 0 function SafeFileTimeString { <# .SYNOPSIS timenow for safe filepaths: "2022-08-17_12-46-47Z" .notes distinct values to the level of a full second #> # [Alias('xl.New.Safetime')] [CmdletBinding()] param( # [Parameter(Mandatory, Position = 0)] ) (Get-Date).ToString('u') -replace '\s+', '_' -replace ':', '-' } #EndRegion '.\Public\utils\SafeFileTimeString.ps1' 18 |