From da7a70752c1d31b0230623fa57f5a24c29d11cb4 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Thu, 26 Apr 2018 16:19:59 +0100 Subject: [PATCH 01/17] Added Compare worksheet Added Compate-worksheet function (in its own PS1) Updated ColorCompletion.ps1 to hold argument completers for set row, and set column, and removed duplicates from formatting.ps1 Fixed case of Path param in Open-ExcelPackage Added comments about date format (m/d/yy is trapped and translated to local date) --- ColorCompletion.ps1 | 17 +++-- Export-Excel.ps1 | 2 +- ImportExcel.psm1 | 4 +- Open-ExcelPackage.ps1 | 2 +- Set-Column.ps1 | 2 +- Set-Row.ps1 | 2 +- compare-WorkSheet.ps1 | 155 ++++++++++++++++++++++++++++++++++++++++++ formatting.ps1 | 10 +-- 8 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 compare-WorkSheet.ps1 diff --git a/ColorCompletion.ps1 b/ColorCompletion.ps1 index 3c24572..17f80aa 100644 --- a/ColorCompletion.ps1 +++ b/ColorCompletion.ps1 @@ -1,4 +1,4 @@ -Function ColorCompletion { +Function ColorCompletion { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) [System.Drawing.KnownColor].GetFields() | Where-Object {$_.IsStatic -and $_.name -like "$wordToComplete*" } | Sort-Object name | ForEach-Object {New-CompletionResult $_.name $_.name @@ -7,10 +7,19 @@ Function ColorCompletion { if (Get-Command -Name register-argumentCompleter -ErrorAction SilentlyContinue) { Register-ArgumentCompleter -CommandName Export-Excel -ParameterName TitleBackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName ForeGroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName DataBarColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Format -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName ForeGroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Format -ParameterName FontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Column -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Column -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Column -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Row -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Row -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Row -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion } \ No newline at end of file diff --git a/Export-Excel.ps1 b/Export-Excel.ps1 index 5748505..1f6e2f3 100644 --- a/Export-Excel.ps1 +++ b/Export-Excel.ps1 @@ -444,7 +444,7 @@ {$_ -is [DateTime]} { #region Save a date with an international valid format $TargetCell.Value = $_ - $TargetCell.Style.Numberformat.Format = 'm/d/yy h:mm' + $TargetCell.Style.Numberformat.Format = 'm/d/yy h:mm' # This is not a custom format, but a preset recognized as date and localized. Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$_' as date" break #endregion diff --git a/ImportExcel.psm1 b/ImportExcel.psm1 index d88e8ae..8b2b355 100644 --- a/ImportExcel.psm1 +++ b/ImportExcel.psm1 @@ -3,6 +3,8 @@ . $PSScriptRoot\AddConditionalFormatting.ps1 . $PSScriptRoot\Charting.ps1 . $PSScriptRoot\ColorCompletion.ps1 +. $PSScriptRoot\ConvertExcelToImageFile.ps1 +. $PSScriptRoot\Compare-WorkSheet.ps1 . $PSScriptRoot\ConvertFromExcelData.ps1 . $PSScriptRoot\ConvertFromExcelToSQLInsert.ps1 . $PSScriptRoot\ConvertToExcelXlsx.ps1 @@ -31,7 +33,7 @@ . $PSScriptRoot\SetFormat.ps1 . $PSScriptRoot\TrackingUtils.ps1 . $PSScriptRoot\Update-FirstObjectProperties.ps1 -. $PSScriptRoot\ConvertExcelToImageFile.ps1 + New-Alias -Name Use-ExcelData -Value "ConvertFrom-ExcelData" -Force diff --git a/Open-ExcelPackage.ps1 b/Open-ExcelPackage.ps1 index ccb4b3a..6789dfd 100644 --- a/Open-ExcelPackage.ps1 +++ b/Open-ExcelPackage.ps1 @@ -11,7 +11,7 @@ This will open the file at $xlPath, select sheet1 apply formatting to two blocks of the sheet and close the package #> [OutputType([OfficeOpenXml.ExcelPackage])] - Param ([Parameter(Mandatory=$true)]$path, + Param ([Parameter(Mandatory=$true)]$Path, [switch]$KillExcel) if($KillExcel) { diff --git a/Set-Column.ps1 b/Set-Column.ps1 index 32da5bb..5246a6a 100644 --- a/Set-Column.ps1 +++ b/Set-Column.ps1 @@ -108,7 +108,7 @@ else { $cellData = $Value} if ($cellData -match "^=") { $Worksheet.Cells[$Row, $Column].Formula = $cellData } else { $Worksheet.Cells[$Row, $Column].Value = $cellData } - if ($cellData -is [datetime]) { $Worksheet.Cells[$Row, $Column].Style.Numberformat.Format = 'm/d/yy h:mm' } + if ($cellData -is [datetime]) { $Worksheet.Cells[$Row, $Column].Style.Numberformat.Format = 'm/d/yy h:mm' } # This is not a custom format, but a preset recognized as date and localized. }} #region Apply formatting if ($Underline) { diff --git a/Set-Row.ps1 b/Set-Row.ps1 index e66850e..9250c1a 100644 --- a/Set-Row.ps1 +++ b/Set-Row.ps1 @@ -112,7 +112,7 @@ else{$cellData = $Value} if ($cellData -match "^=") { $Worksheet.Cells[$Row, $column].Formula = $cellData } else { $Worksheet.Cells[$Row, $Column].Value = $cellData } - if ($cellData -is [datetime]) { $Worksheet.Cells[$Row, $Column].Style.Numberformat.Format = 'm/d/yy h:mm' } + if ($cellData -is [datetime]) { $Worksheet.Cells[$Row, $Column].Style.Numberformat.Format = 'm/d/yy h:mm' } # This is not a custom format, but a preset recognized as date and localized. }} #region Apply formatting if ($Underline) { diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 new file mode 100644 index 0000000..b05d3e1 --- /dev/null +++ b/compare-WorkSheet.ps1 @@ -0,0 +1,155 @@ +Function Compare-Worksheet { +<# + .Synopsis + Compares two worksheets with the same name in different files. + .Description + This command takes two file names, a worksheet name and a name for a key column. + It reads the worksheet from each file and decides the column names. + It builds as hashtable of the key column values and the rows they appear in + It then uses PowerShell's compare object command to compare the sheets (explicity checking all column names which have not been excluded) + For the difference rows it adds the row number for the key of that row - we have to add the key after doing the comparison, + otherwise rows will be considered as different simply because they have different row numbers + We also add the name of the file in which the difference occurs. + If -BackgroundColor is specified the difference rows will be changed to that background. + .Example + compare-WorkSheet -Referencefile 'Server1.xlsx' -Differencefile 'Server2.xlsx' -WorkSheetName Products -key IdentifyingNumber -ExcludeProperty Install* | format-table + The two workbooks in this example contain the result of redirecting a subset of properties from Get-WmiObject -Class win32_product to Export-Excel + The command compares the "products" pages in the two workbooks, but we don't want a match if the software was installed on a + different date or from a different place, so Excluding Install* removes InstallDate and InstallSource. The results will be presented as a table. + .Example + compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName Services -key Name -BackgroundColor lightGreen + This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel + This command compares the "services" pages and highlights the rows in the spreadsheet files. + Here the -Differencefile and -Referencefile parameter switches are assumed + .Example + compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName Services -BackgroundColor lightGreen -fontColor Red -Show + This builds on the previous example: this time Where two rows in the services have the same name, this will also highlight the changed cells in red. + This example will open the Excel files and omits the -key parameter because "Name" will be assumed to the label for the key column + .Example + compare-WorkSheet 'Pester-tests.xlsx' 'Pester-tests.xlsx' -WorkSheetName 'Server1','Server2' -Property "full Description","Executed","Result" -Key "full Description" -FontColor Red -TabColor Yellow -Show + This time the reference file and the difference file are the same file and two different sheets are used. Because the tests include the + machine name and time the test was run only a limited set of columns. +#> +[cmdletbinding()] + Param( + #First file to compare + [parameter(Mandatory=$true)] + $Referencefile , + #Second file to compare + [parameter(Mandatory=$true)] + $Differencefile , + #Name(s) of worksheets to compare. + $WorkSheetName = "Sheet1", + #Name of a column which is unique and will be used to add a row to the DIFF object, default is "Name" + $Key = "Name" , + #Properties to include in the DIFF - supports wildcards, default is "*" + $Property = "*" , + #Properties to exclude from the the search - supports wildcards + $ExcludeProperty , + #If specified, highlights the DIFF rows + [System.Drawing.Color]$BackgroundColor, + #If specified identifies the tabs which contain DIFF rows (ignored if -backgroundColor is omitted) + [System.Drawing.Color]$TabColor, + #If specified, highlights the DIFF columns in rows which have the same key. + [System.Drawing.Color]$FontColor, + #If specified opens the Excel workbooks instead of outputting the diff to the console + [Switch]$Show + ) + + $oneFile = ((Resolve-Path -Path $Referencefile).path -eq (Resolve-Path -Path $Differencefile).path) + + #If we have one file , we mush have two different worksheet names. If we have two files we can a single string or two strings. + if ($onefile -and ( ($WorkSheetName.count -ne 2) -or $WorkSheetName[0] -eq $WorkSheetName[1] ) ) { + Write-Warning -Message "If both the Reference and difference file are the same then worksheet name must provide 2 different names" + return + } + if ($WorkSheetName.count -eq 2) {$worksheet1 = $WorkSheetName[0] ; $WorkSheet2 = $WorkSheetName[1]} + elseif ($WorkSheetName -is [string]) {$worksheet1 = $WorkSheet2 = $WorkSheetName} + else {Write-Warning -Message "You must provide either a single worksheet name or two names." ; return } + + #If the paths are wrong, files are locked or the worksheet names are wrong we won't be able to continue + try { + $Sheet1 = Import-Excel -Path $Referencefile -WorksheetName $WorkSheet1 -ErrorAction stop + $Sheet2 = Import-Excel -Path $Differencefile -WorksheetName $WorkSheet2 -ErrorAction stop + } + Catch {Write-Warning -Message "Could not read the worksheet from $Referencefile and/or $Differencefile." ; return } + + #Get Column headings and create a hash table of Name to column letter. + $headings = $Sheet1[-1].psobject.Properties.name # This preserves the sequence - using get-member would sort them alphabetically! + $Columns = @{} + $i = 65 ; foreach ($h in $headings) {$Columns[$h] = [char]($i ++) } + + #Make a list of properties headings using the Property (default "*") and ExcludeProperty parameters + $PropList = @() + foreach ($p in $Property) {$PropList += ($headings.where({$_ -like $p}) )} + foreach ($p in $ExcludeProperty) {$PropList = $PropList.where({$_ -notlike $p}) } + $PropList = $PropList | Select-Object -Unique + if (($headings -contains $key) -and ($PropList -notcontains $Key)) {$PropList += $Key} + if ($PropList.Count -eq 0) {Write-Warning -Message "No Columns are selected with -Property = '$Property' and -excludeProperty = '$ExcludeProperty'." ; return} + + #If we add the row numbes to data and include them in the diff, inserting a row will mean all subsequent rows are different so instead ... + #... build hash tables with the "key" column as the key and the row in the spreadsheet where it appears as the value. Row 1 is headers so the first data row is 2 + $rows1 = @{} ; + $rows2 = @{} ; + if ($PropList -contains $Key) { + $i = 2 ; foreach ($row in $Sheet1) {$rows1[$row.$key] = ($i ++) } + $i = 2 ; foreach ($row in $Sheet2) {$rows2[$row.$key] = ($i ++) } + } + else {Write-Warning -Message "Could not find a column '$key' to use as a key - DIFF rows will not have numbers."} + + #Do the comparison and add file,sheet and row to the result - these are prefixed with "_" to show they are added but the addition still might fail so make sure we have some DIFF + $diff = Compare-Object $Sheet1 $Sheet2 -Property $PropList + $diff = $diff | Select-Object -Property (@( + @{n="_Side"; e={$_.SideIndicator }} + @{n="_File"; e={if ($_.SideIndicator -eq '=>') {$Differencefile} else {$Referencefile } }} , + @{n="_Sheet"; e={if ($_.SideIndicator -eq '=>') {$worksheet2 } else {$worksheet1 } }} , + @{n='_Row'; e={if ($_.$key -and $_.SideIndicator -eq '=>') {$rows2[$_.$key]} elseif ($_.$key) {$rows1[$_.$key]} else { "" } }} + ) + $PropList) #| Sort-Object -Property row,file + + #if BackgroundColor was specified, set it on extra or extra or changed rows - but remember we we only have row numbers if we have a key + if (($PropList -contains $Key) -and $BackgroundColor) { + #Differences may only exist in one file. So gather the changes for each file; open the file, update each impacted row, save the file + $updates = $diff | Group-object -Property "_File" + foreach ($file in $updates) { + try {$xl = Open-ExcelPackage -Path $file.name } + catch {Write-warning -Message "Can't open $($file.Name) for writing." ; return} + foreach ($row in $file.group) { + $ws = $xl.Workbook.Worksheets[$row._Sheet] + $range = $ws.Dimension -replace "\d+",$row._row + Set-Format -WorkSheet $ws -Range $range -BackgroundColor $BackgroundColor + } + if ($TabColor) { + foreach ($tab in ($file.group._sheet | Select-Object -Unique)) { + $xl.Workbook.Worksheets[$tab].TabColor = $TabColor + } + } + $xl.save() ; $xl.Stream.Close() ; $xl.Dispose() + } + } + + #if font colour was specified, set it on changed properties where the same key appears in both sheets. + if (($PropList -contains $Key) -and $FontColor) { + $updates = $diff | Group-object -Property $Key | where {$_.count -eq 2} + if ($updates) { + $XL1 = Open-ExcelPackage -path $Referencefile + if ($oneFile ) {$xl2 = $xl1} + else {$xl2 = Open-ExcelPackage -path $Differencefile } + foreach ($u in $updates) { + foreach ($p in $proplist) { + if($u.Group[0].$p -ne $u.Group[1].$p ) { + Set-Format -WorkSheet $xl1.Workbook.Worksheets[$u.Group[0]._sheet] -Range ($Columns[$p] + $u.Group[0]._Row) -FontColor $FontColor + Set-Format -WorkSheet $xl2.Workbook.Worksheets[$u.Group[1]._sheet] -Range ($Columns[$p] + $u.Group[1]._Row) -FontColor $FontColor + } + } + } + $xl1.Save() ; $xl1.Stream.Close() ; $xl1.Dispose() + if (-not $oneFile) {$xl2.Save() ; $xl2.Stream.Close() ; $xl2.Dispose()} + } + } + + if ($show) { + Start-Process -FilePath $Referencefile + if (-not $oneFile) { Start-Process -FilePath $Differencefile } + } + else {return $diff} +} \ No newline at end of file diff --git a/formatting.ps1 b/formatting.ps1 index efc703e..5d6d26a 100644 --- a/formatting.ps1 +++ b/formatting.ps1 @@ -224,12 +224,4 @@ Function ColorCompletion{ } } -if (Get-Command -Name register-argumentCompleter -ErrorAction SilentlyContinue) { - Register-ArgumentCompleter -CommandName Export-Excel -ParameterName TitleBackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName ForeGroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName DataBarColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Format -ParameterName FontColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Format -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Format -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion -} \ No newline at end of file + \ No newline at end of file From 9632664c2ce1587aefd4ff2a41ddbb4c89e30266 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 27 Apr 2018 10:33:48 +0100 Subject: [PATCH 02/17] Added paramters to Compare worksheet Now has -Gridview, and supports startrow, headernames and NoHeader (as per import Excel) and ensures the headers don't clash. --- compare-WorkSheet.ps1 | 49 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index b05d3e1..bc138fc 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -12,23 +12,28 @@ We also add the name of the file in which the difference occurs. If -BackgroundColor is specified the difference rows will be changed to that background. .Example - compare-WorkSheet -Referencefile 'Server1.xlsx' -Differencefile 'Server2.xlsx' -WorkSheetName Products -key IdentifyingNumber -ExcludeProperty Install* | format-table + Compare-WorkSheet -Referencefile 'Server1.xlsx' -Differencefile 'Server2.xlsx' -WorkSheetName Products -key IdentifyingNumber -ExcludeProperty Install* | format-table The two workbooks in this example contain the result of redirecting a subset of properties from Get-WmiObject -Class win32_product to Export-Excel The command compares the "products" pages in the two workbooks, but we don't want a match if the software was installed on a different date or from a different place, so Excluding Install* removes InstallDate and InstallSource. The results will be presented as a table. .Example - compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName Services -key Name -BackgroundColor lightGreen + Compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName Services -key Name -BackgroundColor lightGreen This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel This command compares the "services" pages and highlights the rows in the spreadsheet files. Here the -Differencefile and -Referencefile parameter switches are assumed .Example - compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName Services -BackgroundColor lightGreen -fontColor Red -Show + Compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName Services -BackgroundColor lightGreen -fontColor Red -Show This builds on the previous example: this time Where two rows in the services have the same name, this will also highlight the changed cells in red. This example will open the Excel files and omits the -key parameter because "Name" will be assumed to the label for the key column .Example - compare-WorkSheet 'Pester-tests.xlsx' 'Pester-tests.xlsx' -WorkSheetName 'Server1','Server2' -Property "full Description","Executed","Result" -Key "full Description" -FontColor Red -TabColor Yellow -Show + Compare-WorkSheet 'Pester-tests.xlsx' 'Pester-tests.xlsx' -WorkSheetName 'Server1','Server2' -Property "full Description","Executed","Result" -Key "full Description" -FontColor Red -TabColor Yellow -Show This time the reference file and the difference file are the same file and two different sheets are used. Because the tests include the machine name and time the test was run only a limited set of columns. + .Example + Compare-WorkSheet - 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName general -Startrow 2 -Headername Label,value -Key Label -GridView + The "General" page has a title and two unlabelled columns with the CPU, Memory, Domain, Disk and so on + So this version starts at row 2 to skip the tiltle and labels the first column "label" and the Second "Value"; the label acts as the key + and the result is display on using grid view. Note that grid view works best when the number of columns is small. #> [cmdletbinding()] Param( @@ -46,6 +51,14 @@ $Property = "*" , #Properties to exclude from the the search - supports wildcards $ExcludeProperty , + #Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. + [Parameter(ParameterSetName='B', Mandatory)] + [String[]]$Headername, + #Automatically generate property names (P1, P2, P3, ..) instead of the using the values the top row of the sheet + [Parameter(ParameterSetName='C', Mandatory)] + [switch]$NoHeader, + #The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. + [int]$Startrow, #If specified, highlights the DIFF rows [System.Drawing.Color]$BackgroundColor, #If specified identifies the tabs which contain DIFF rows (ignored if -backgroundColor is omitted) @@ -53,11 +66,15 @@ #If specified, highlights the DIFF columns in rows which have the same key. [System.Drawing.Color]$FontColor, #If specified opens the Excel workbooks instead of outputting the diff to the console - [Switch]$Show + [Switch]$Show, + #If specified, tries to the show the DIFF in a gridview. (Works best with few columns) + [switch]$GridView ) $oneFile = ((Resolve-Path -Path $Referencefile).path -eq (Resolve-Path -Path $Differencefile).path) + if ($Key -eq "Name" -and $NoHeader) {$key = "p1"} + #If we have one file , we mush have two different worksheet names. If we have two files we can a single string or two strings. if ($onefile -and ( ($WorkSheetName.count -ne 2) -or $WorkSheetName[0] -eq $WorkSheetName[1] ) ) { Write-Warning -Message "If both the Reference and difference file are the same then worksheet name must provide 2 different names" @@ -68,9 +85,11 @@ else {Write-Warning -Message "You must provide either a single worksheet name or two names." ; return } #If the paths are wrong, files are locked or the worksheet names are wrong we won't be able to continue + $params= @{ ErrorAction = [System.Management.Automation.ActionPreference]::Stop } + foreach ($p in @("HeaderName","NoHeader","StartRow")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} try { - $Sheet1 = Import-Excel -Path $Referencefile -WorksheetName $WorkSheet1 -ErrorAction stop - $Sheet2 = Import-Excel -Path $Differencefile -WorksheetName $WorkSheet2 -ErrorAction stop + $Sheet1 = Import-Excel -Path $Referencefile -WorksheetName $WorkSheet1 @params + $Sheet2 = Import-Excel -Path $Differencefile -WorksheetName $WorkSheet2 @Params } Catch {Write-Warning -Message "Could not read the worksheet from $Referencefile and/or $Differencefile." ; return } @@ -104,7 +123,7 @@ @{n="_File"; e={if ($_.SideIndicator -eq '=>') {$Differencefile} else {$Referencefile } }} , @{n="_Sheet"; e={if ($_.SideIndicator -eq '=>') {$worksheet2 } else {$worksheet1 } }} , @{n='_Row'; e={if ($_.$key -and $_.SideIndicator -eq '=>') {$rows2[$_.$key]} elseif ($_.$key) {$rows1[$_.$key]} else { "" } }} - ) + $PropList) #| Sort-Object -Property row,file + ) + $PropList) | Sort-Object -Property row,file #if BackgroundColor was specified, set it on extra or extra or changed rows - but remember we we only have row numbers if we have a key if (($PropList -contains $Key) -and $BackgroundColor) { @@ -147,9 +166,19 @@ } } - if ($show) { + if ($show) { Start-Process -FilePath $Referencefile if (-not $oneFile) { Start-Process -FilePath $Differencefile } } - else {return $diff} + elseif ($GridView) { + if ($StartRow) {$lastrow = $StartRow} else {$lastRow = 1} + $diff | Group-Object -Property $key | foreach { + $hash = [ordered]@{row = $lastRow; $key = $_.Name; } ; + foreach ($row IN $_.Group) { + if ($row._Side -eq "=>") {$lastRow = $hash.row = $row._Row } + foreach ($p in $proplist.Where({$_ -ne $key})) {$hash[($row._Side+$P)] =$row.$P} + } + [Pscustomobject]$hash } | Sort-Object -Property row| Update-FirstObjectProperties | Out-GridView -Title "Comparing $Referencefile::$worksheet1 (=>) with $Differencefile::$WorkSheet2 (<=)" + } + else {return $diff} } \ No newline at end of file From 31573ee803c7a069adf6e2166980cabc6a66b57d Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 27 Apr 2018 15:57:12 +0100 Subject: [PATCH 03/17] Default parameter set was missing --- compare-WorkSheet.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index bc138fc..89886c9 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -35,7 +35,7 @@ So this version starts at row 2 to skip the tiltle and labels the first column "label" and the Second "Value"; the label acts as the key and the result is display on using grid view. Note that grid view works best when the number of columns is small. #> -[cmdletbinding()] +[cmdletbinding(DefaultParameterSetName)] Param( #First file to compare [parameter(Mandatory=$true)] From 1e115d5ede651f08a5eea42866d7982d44066e5e Mon Sep 17 00:00:00 2001 From: jhoneill Date: Wed, 2 May 2018 12:38:13 +0100 Subject: [PATCH 04/17] Lots of changes to the compare-worksheet module --- ColorCompletion.ps1 | 33 ++++---- compare-WorkSheet.ps1 | 181 ++++++++++++++++++++++++------------------ 2 files changed, 120 insertions(+), 94 deletions(-) diff --git a/ColorCompletion.ps1 b/ColorCompletion.ps1 index 17f80aa..b771a4d 100644 --- a/ColorCompletion.ps1 +++ b/ColorCompletion.ps1 @@ -6,20 +6,21 @@ } if (Get-Command -Name register-argumentCompleter -ErrorAction SilentlyContinue) { - Register-ArgumentCompleter -CommandName Export-Excel -ParameterName TitleBackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName DataBarColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName ForeGroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Format -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Format -ParameterName FontColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Format -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Column -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Column -ParameterName FontColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Column -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Row -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Row -ParameterName FontColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Set-Row -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Export-Excel -ParameterName TitleBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName AllDataBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName DataBarColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName ForeGroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Format -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Format -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Format -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Column -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Column -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Column -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Row -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Row -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Set-Row -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion } \ No newline at end of file diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index 89886c9..c938f6f 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -1,4 +1,4 @@ -Function Compare-Worksheet { +Function Compare-WorkSheet { <# .Synopsis Compares two worksheets with the same name in different files. @@ -26,27 +26,30 @@ This builds on the previous example: this time Where two rows in the services have the same name, this will also highlight the changed cells in red. This example will open the Excel files and omits the -key parameter because "Name" will be assumed to the label for the key column .Example - Compare-WorkSheet 'Pester-tests.xlsx' 'Pester-tests.xlsx' -WorkSheetName 'Server1','Server2' -Property "full Description","Executed","Result" -Key "full Description" -FontColor Red -TabColor Yellow -Show + Compare-WorkSheet 'Pester-tests.xlsx' 'Pester-tests.xlsx' -WorkSheetName 'Server1','Server2' -Property "full Description","Executed","Result" -Key "full Description" This time the reference file and the difference file are the same file and two different sheets are used. Because the tests include the machine name and time the test was run only a limited set of columns. .Example - Compare-WorkSheet - 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName general -Startrow 2 -Headername Label,value -Key Label -GridView - The "General" page has a title and two unlabelled columns with the CPU, Memory, Domain, Disk and so on - So this version starts at row 2 to skip the tiltle and labels the first column "label" and the Second "Value"; the label acts as the key - and the result is display on using grid view. Note that grid view works best when the number of columns is small. + Compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName general -Startrow 2 -Headername Label,value -Key Label -GridView -ExcludeDifferent + The "General" page has a title and two unlabelled columns with a row forCPU, Memory, Domain, Disk and so on + So the command is instructed to starts at row 2 to skip the title and to name the columns: the first is "label" and the Second "Value"; + the label acts as the key. This time we interested the rows which are the same in both sheets, + and the result is displayed using grid view. Note that grid view works best when the number of columns is small. + .Example + Compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName general -Startrow 2 -Headername Label,value -Key Label -BackgroundColor White -Show -AllDataBackgroundColor LightGray + This version of the previous command lightlights all the cells in lightgray and then sets the changed rows back to white; only + the unchanged rows are highlighted #> [cmdletbinding(DefaultParameterSetName)] Param( #First file to compare - [parameter(Mandatory=$true)] + [parameter(Mandatory=$true,Position=0)] $Referencefile , #Second file to compare - [parameter(Mandatory=$true)] + [parameter(Mandatory=$true,Position=1)] $Differencefile , #Name(s) of worksheets to compare. $WorkSheetName = "Sheet1", - #Name of a column which is unique and will be used to add a row to the DIFF object, default is "Name" - $Key = "Name" , #Properties to include in the DIFF - supports wildcards, default is "*" $Property = "*" , #Properties to exclude from the the search - supports wildcards @@ -58,82 +61,93 @@ [Parameter(ParameterSetName='C', Mandatory)] [switch]$NoHeader, #The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. - [int]$Startrow, + [int]$Startrow = 1, + #If specified, highlights all the cells - so you can make Equal cells one colour, and Diff cells another. + [System.Drawing.Color]$AllDataBackgroundColor, #If specified, highlights the DIFF rows [System.Drawing.Color]$BackgroundColor, #If specified identifies the tabs which contain DIFF rows (ignored if -backgroundColor is omitted) [System.Drawing.Color]$TabColor, + #Name of a column which is unique and will be used to add a row to the DIFF object, default is "Name" + $Key = "Name" , #If specified, highlights the DIFF columns in rows which have the same key. [System.Drawing.Color]$FontColor, - #If specified opens the Excel workbooks instead of outputting the diff to the console - [Switch]$Show, - #If specified, tries to the show the DIFF in a gridview. (Works best with few columns) - [switch]$GridView + #If specified opens the Excel workbooks instead of outputting the diff to the console (unless -passthru is also specified) + [Switch]$Show, + #If specified, the command tries to the show the DIFF in a Gridview and not on the console. (unless-Passthru is also specified). This Works best with few columns selected, and requires a key + [switch]$GridView, + #If specified -Passthrough full set of diff data is returned without filtering to the specified properties + [Switch]$PassThru, + #If specified the result will include equal rows as well. By default only different rows are returned + [Switch]$IncludeEqual, + #If Specified the result includes only the rows where both are equal + [Switch]$ExcludeDifferent ) - - $oneFile = ((Resolve-Path -Path $Referencefile).path -eq (Resolve-Path -Path $Differencefile).path) - - if ($Key -eq "Name" -and $NoHeader) {$key = "p1"} - + + #if the filenames don't resolve, give up now. + try { $oneFile = ((Resolve-Path -Path $Referencefile -ErrorAction Stop).path -eq (Resolve-Path -Path $Differencefile -ErrorAction Stop).path)} + Catch { Write-Warning -Message "Could not Resolve the filenames." ; return } + #If we have one file , we mush have two different worksheet names. If we have two files we can a single string or two strings. if ($onefile -and ( ($WorkSheetName.count -ne 2) -or $WorkSheetName[0] -eq $WorkSheetName[1] ) ) { Write-Warning -Message "If both the Reference and difference file are the same then worksheet name must provide 2 different names" return } - if ($WorkSheetName.count -eq 2) {$worksheet1 = $WorkSheetName[0] ; $WorkSheet2 = $WorkSheetName[1]} - elseif ($WorkSheetName -is [string]) {$worksheet1 = $WorkSheet2 = $WorkSheetName} + if ($WorkSheetName.count -eq 2) {$worksheet1 = $WorkSheetName[0] ; $WorkSheet2 = $WorkSheetName[1]} + elseif ($WorkSheetName -is [string]) {$worksheet1 = $WorkSheet2 = $WorkSheetName} else {Write-Warning -Message "You must provide either a single worksheet name or two names." ; return } - #If the paths are wrong, files are locked or the worksheet names are wrong we won't be able to continue $params= @{ ErrorAction = [System.Management.Automation.ActionPreference]::Stop } foreach ($p in @("HeaderName","NoHeader","StartRow")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} - try { + try { $Sheet1 = Import-Excel -Path $Referencefile -WorksheetName $WorkSheet1 @params $Sheet2 = Import-Excel -Path $Differencefile -WorksheetName $WorkSheet2 @Params } - Catch {Write-Warning -Message "Could not read the worksheet from $Referencefile and/or $Differencefile." ; return } - + Catch {Write-Warning -Message "Could not read the worksheet from $Referencefile and/or $Differencefile." ; return } + #Get Column headings and create a hash table of Name to column letter. $headings = $Sheet1[-1].psobject.Properties.name # This preserves the sequence - using get-member would sort them alphabetically! - $Columns = @{} - $i = 65 ; foreach ($h in $headings) {$Columns[$h] = [char]($i ++) } + $headings | ForEach-Object -Begin {$columns = @{} ; } -Process {$Columns[$_] = [char]($i ++) } + + #Make a list of property headings using the Property (default "*") and ExcludeProperty parameters + if ($Key -eq "Name" -and $NoHeader) {$key = "p1"} + $propList = @() + foreach ($p in $Property) {$propList += ($headings.where({$_ -like $p}) )} + foreach ($p in $ExcludeProperty) {$propList = $propList.where({$_ -notlike $p}) } + if (($headings -contains $key) -and ($propList -notcontains $Key)) {$propList += $Key} + $propList = $propList | Select-Object -Unique + if ($propList.Count -eq 0) {Write-Warning -Message "No Columns are selected with -Property = '$Property' and -excludeProperty = '$ExcludeProperty'." ; return} - #Make a list of properties headings using the Property (default "*") and ExcludeProperty parameters - $PropList = @() - foreach ($p in $Property) {$PropList += ($headings.where({$_ -like $p}) )} - foreach ($p in $ExcludeProperty) {$PropList = $PropList.where({$_ -notlike $p}) } - $PropList = $PropList | Select-Object -Unique - if (($headings -contains $key) -and ($PropList -notcontains $Key)) {$PropList += $Key} - if ($PropList.Count -eq 0) {Write-Warning -Message "No Columns are selected with -Property = '$Property' and -excludeProperty = '$ExcludeProperty'." ; return} - - #If we add the row numbes to data and include them in the diff, inserting a row will mean all subsequent rows are different so instead ... - #... build hash tables with the "key" column as the key and the row in the spreadsheet where it appears as the value. Row 1 is headers so the first data row is 2 - $rows1 = @{} ; - $rows2 = @{} ; - if ($PropList -contains $Key) { - $i = 2 ; foreach ($row in $Sheet1) {$rows1[$row.$key] = ($i ++) } - $i = 2 ; foreach ($row in $Sheet2) {$rows2[$row.$key] = ($i ++) } - } - else {Write-Warning -Message "Could not find a column '$key' to use as a key - DIFF rows will not have numbers."} - - #Do the comparison and add file,sheet and row to the result - these are prefixed with "_" to show they are added but the addition still might fail so make sure we have some DIFF - $diff = Compare-Object $Sheet1 $Sheet2 -Property $PropList - $diff = $diff | Select-Object -Property (@( - @{n="_Side"; e={$_.SideIndicator }} - @{n="_File"; e={if ($_.SideIndicator -eq '=>') {$Differencefile} else {$Referencefile } }} , - @{n="_Sheet"; e={if ($_.SideIndicator -eq '=>') {$worksheet2 } else {$worksheet1 } }} , - @{n='_Row'; e={if ($_.$key -and $_.SideIndicator -eq '=>') {$rows2[$_.$key]} elseif ($_.$key) {$rows1[$_.$key]} else { "" } }} - ) + $PropList) | Sort-Object -Property row,file - - #if BackgroundColor was specified, set it on extra or extra or changed rows - but remember we we only have row numbers if we have a key - if (($PropList -contains $Key) -and $BackgroundColor) { - #Differences may only exist in one file. So gather the changes for each file; open the file, update each impacted row, save the file - $updates = $diff | Group-object -Property "_File" + #Add RowNumber, Sheetname and file name to every row + $i = $startRow + 1 ; foreach ($row in $Sheet1) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++) + Add-Member -InputObject $row -MemberType NoteProperty -Name "_Sheet" -Value $worksheet1 + Add-Member -InputObject $row -MemberType NoteProperty -Name "_File" -Value $Referencefile} + $i = $startRow + 1 ; foreach ($row in $Sheet2) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++) + Add-Member -InputObject $row -MemberType NoteProperty -Name "_Sheet" -Value $worksheet2 + Add-Member -InputObject $row -MemberType NoteProperty -Name "_File" -Value $Differencefile} + + if ($ExcludeDifferent -and -not $IncludeEqual) {$IncludeEqual = $true} + #Do the comparison and add file,sheet and row to the result - these are prefixed with "_" to show they are added the addition will fail if the sheet has these properties so split the operations + $diff = Compare-Object -ReferenceObject $Sheet1 -DifferenceObject $Sheet2 -Property $propList -PassThru -IncludeEqual:$IncludeEqual -ExcludeDifferent:$ExcludeDifferent | + Sort-Object -Property "_Row","File" + + #if BackgroundColor was specified, set it on extra or extra or changed rows + if ($BackgroundColor ) { + #Differences may only exist in one file. So gather the changes for each file; open the file, update each impacted row in the shee, save the file + $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property "_File" foreach ($file in $updates) { try {$xl = Open-ExcelPackage -Path $file.name } catch {Write-warning -Message "Can't open $($file.Name) for writing." ; return} + if ($AllDataBackgroundColor) { + $file.Group._sheet | Sort-Object -Unique | ForEach-Object { + $ws = $xl.Workbook.Worksheets[$_] + if ($headerName) {$range = "A" + $startrow + ":" + $ws.dimension.end.address} + else {$range = "A" + ($startrow + 1) + ":" + $ws.dimension.end.address} + Set-Format -WorkSheet $ws -BackgroundColor $AllDataBackgroundColor -Range $Range + } + } foreach ($row in $file.group) { - $ws = $xl.Workbook.Worksheets[$row._Sheet] + $ws = $xl.Workbook.Worksheets[$row._Sheet] $range = $ws.Dimension -replace "\d+",$row._row Set-Format -WorkSheet $ws -Range $range -BackgroundColor $BackgroundColor } @@ -145,16 +159,15 @@ $xl.save() ; $xl.Stream.Close() ; $xl.Dispose() } } - #if font colour was specified, set it on changed properties where the same key appears in both sheets. - if (($PropList -contains $Key) -and $FontColor) { - $updates = $diff | Group-object -Property $Key | where {$_.count -eq 2} + if ($FontColor -and ($propList -contains $Key) ) { + $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property $Key | where {$_.count -eq 2} if ($updates) { $XL1 = Open-ExcelPackage -path $Referencefile if ($oneFile ) {$xl2 = $xl1} else {$xl2 = Open-ExcelPackage -path $Differencefile } foreach ($u in $updates) { - foreach ($p in $proplist) { + foreach ($p in $propList) { if($u.Group[0].$p -ne $u.Group[1].$p ) { Set-Format -WorkSheet $xl1.Workbook.Worksheets[$u.Group[0]._sheet] -Range ($Columns[$p] + $u.Group[0]._Row) -FontColor $FontColor Set-Format -WorkSheet $xl2.Workbook.Worksheets[$u.Group[1]._sheet] -Range ($Columns[$p] + $u.Group[1]._Row) -FontColor $FontColor @@ -165,20 +178,32 @@ if (-not $oneFile) {$xl2.Save() ; $xl2.Stream.Close() ; $xl2.Dispose()} } } + elseif ($FontColor) {Write-Warning -Message "To match rows to set changed cells, you must specify -Key and it must match one of the included properties" } - if ($show) { - Start-Process -FilePath $Referencefile - if (-not $oneFile) { Start-Process -FilePath $Differencefile } + if ($show) { + Start-Process -FilePath $Referencefile + if (-not $oneFile) { Start-Process -FilePath $Differencefile } + } + elseif ($GridView) { + $Sheet2 | ForEach-Object -Begin {$Rowhash = @{} } -Process {$Rowhash[$_.$key] = $_._row } + if ($StartRow) { $rowCount1 = $StartRow} else { $rowCount1 = 1} + $rowCount2 = $null + $diff | Group-Object -Property $key | Sort-Object -Property @{e={($_.group | Measure-Object -Property _row -Maximum).maximum} } | ForEach-Object { + $hash = [ordered]@{"") {$rowCount1 = $hash["Row"] = $rowCount2 + $Hash[$key] = $keyVal + foreach ($p in $propList.Where({$_ -ne $key})) { + if ($row.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$row.$P} + else {$hash[($row.SideIndicator+$P)] =$row.$P} + } + } + [Pscustomobject]$hash } | Sort-Object -Property ")" } - elseif ($GridView) { - if ($StartRow) {$lastrow = $StartRow} else {$lastRow = 1} - $diff | Group-Object -Property $key | foreach { - $hash = [ordered]@{row = $lastRow; $key = $_.Name; } ; - foreach ($row IN $_.Group) { - if ($row._Side -eq "=>") {$lastRow = $hash.row = $row._Row } - foreach ($p in $proplist.Where({$_ -ne $key})) {$hash[($row._Side+$P)] =$row.$P} - } - [Pscustomobject]$hash } | Sort-Object -Property row| Update-FirstObjectProperties | Out-GridView -Title "Comparing $Referencefile::$worksheet1 (=>) with $Differencefile::$WorkSheet2 (<=)" - } - else {return $diff} + elseif (-not $PassThru) {return ($diff | Select-Object -Property (@(@{n="_Side";e={$_.SideIndicator}},"_File" ,"_Sheet","_Row") + $propList))} + if ( $PassThru) {return $diff } } \ No newline at end of file From a50363e55f7e742e248a3360b5e09c8e403dfb01 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Wed, 2 May 2018 17:03:13 +0100 Subject: [PATCH 05/17] One last bug in compare, and fixed bug #310 in Set-Format --- SetFormat.ps1 | 4 ++-- compare-WorkSheet.ps1 | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SetFormat.ps1 b/SetFormat.ps1 index 80d043b..3c24aa4 100644 --- a/SetFormat.ps1 +++ b/SetFormat.ps1 @@ -83,7 +83,7 @@ begin { #Allow Set-Format to take Worksheet and range parameters (like Add Contitional formatting) - convert them to an address if ($WorkSheet -and $Range) {$Address = $WorkSheet.Cells[$Range] } - } + } process { if ($Address -is [Array]) { @@ -150,7 +150,7 @@ if ($Address -is [OfficeOpenXml.ExcelRow] ) {$Address.Height = $Height } elseif ($Address -is [OfficeOpenXml.ExcelRange] ) { ($Address.Start.Row)..($Address.Start.Row + $Address.Rows) | - ForEach-Object {$ws.Row($_).Height = $Height } + ForEach-Object {$Address.WorkSheet.Row($_).Height = $Height } } else {Write-Warning -Message ("Can set the height of a row or a range but not a {0} object" -f ($Address.GetType().name)) } } diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index c938f6f..13e741d 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -132,7 +132,7 @@ Sort-Object -Property "_Row","File" #if BackgroundColor was specified, set it on extra or extra or changed rows - if ($BackgroundColor ) { + if ($diff -and $BackgroundColor) { #Differences may only exist in one file. So gather the changes for each file; open the file, update each impacted row in the shee, save the file $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property "_File" foreach ($file in $updates) { @@ -160,7 +160,7 @@ } } #if font colour was specified, set it on changed properties where the same key appears in both sheets. - if ($FontColor -and ($propList -contains $Key) ) { + if ($diff -and $FontColor -and ($propList -contains $Key) ) { $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property $Key | where {$_.count -eq 2} if ($updates) { $XL1 = Open-ExcelPackage -path $Referencefile @@ -178,7 +178,7 @@ if (-not $oneFile) {$xl2.Save() ; $xl2.Stream.Close() ; $xl2.Dispose()} } } - elseif ($FontColor) {Write-Warning -Message "To match rows to set changed cells, you must specify -Key and it must match one of the included properties" } + elseif ($diff -and $FontColor) {Write-Warning -Message "To match rows to set changed cells, you must specify -Key and it must match one of the included properties" } if ($show) { Start-Process -FilePath $Referencefile From a022f0ae1bd5847b980958962d4ba1f63144a1fa Mon Sep 17 00:00:00 2001 From: jhoneill Date: Thu, 3 May 2018 15:46:30 +0100 Subject: [PATCH 06/17] Color completion wasn't working and reworked gridview for compare --- ColorCompletion.ps1 | 4 +- compare-WorkSheet.ps1 | 115 +++++++++++++++++++++++++++++------------- 2 files changed, 81 insertions(+), 38 deletions(-) diff --git a/ColorCompletion.ps1 b/ColorCompletion.ps1 index b771a4d..12b6f81 100644 --- a/ColorCompletion.ps1 +++ b/ColorCompletion.ps1 @@ -7,13 +7,13 @@ if (Get-Command -Name register-argumentCompleter -ErrorAction SilentlyContinue) { Register-ArgumentCompleter -CommandName Export-Excel -ParameterName TitleBackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName AllDataBackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName DataBarColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Add-ConditionalFormatting -ParameterName ForeGroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName AllDataBackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName TabColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName FontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index 13e741d..92a19c8 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -12,25 +12,30 @@ We also add the name of the file in which the difference occurs. If -BackgroundColor is specified the difference rows will be changed to that background. .Example - Compare-WorkSheet -Referencefile 'Server1.xlsx' -Differencefile 'Server2.xlsx' -WorkSheetName Products -key IdentifyingNumber -ExcludeProperty Install* | format-table + Compare-WorkSheet -Referencefile 'Server56.xlsx' -Differencefile 'Server57.xlsx' -WorkSheetName Products -key IdentifyingNumber -ExcludeProperty Install* | format-table The two workbooks in this example contain the result of redirecting a subset of properties from Get-WmiObject -Class win32_product to Export-Excel - The command compares the "products" pages in the two workbooks, but we don't want a match if the software was installed on a - different date or from a different place, so Excluding Install* removes InstallDate and InstallSource. The results will be presented as a table. + The command compares the "products" pages in the two workbooks, but we don't want to register a differnce if if the software was installed on a + different date or from a different place, so Excluding Install* removes InstallDate and InstallSource. + This data doesn't have a "name" column" so we specify the "IdentifyingNumber" column as the key. + The results will be presented as a table. .Example - Compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName Services -key Name -BackgroundColor lightGreen + compare-WorkSheet "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -GridView This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel - This command compares the "services" pages and highlights the rows in the spreadsheet files. - Here the -Differencefile and -Referencefile parameter switches are assumed + Here the -Differencefile and -Referencefile parameter switches are assumed , and the default setting for -key ("Name") works for services + This will display the differences between the "services" sheets using a grid view .Example - Compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName Services -BackgroundColor lightGreen -fontColor Red -Show - This builds on the previous example: this time Where two rows in the services have the same name, this will also highlight the changed cells in red. - This example will open the Excel files and omits the -key parameter because "Name" will be assumed to the label for the key column + Compare-WorkSheet 'Server54.xlsx' 'Server55.xlsx' -WorkSheetName Services -BackgroundColor lightGreen + This version of the command outputs the differences between the "services" pages and also highlights any different rows in the spreadsheet files. + .Example + Compare-WorkSheet 'Server54.xlsx' 'Server55.xlsx' -WorkSheetName Services -BackgroundColor lightGreen -FontColor Red -Show + This builds on the previous example: this time Where two changed rows have the value in the "name" column (the default value for -key), + this version adds highlighting of the changed cells in red; and then opens the Excel file. .Example Compare-WorkSheet 'Pester-tests.xlsx' 'Pester-tests.xlsx' -WorkSheetName 'Server1','Server2' -Property "full Description","Executed","Result" -Key "full Description" This time the reference file and the difference file are the same file and two different sheets are used. Because the tests include the - machine name and time the test was run only a limited set of columns. + machine name and time the test was run the command specifies a limited set of columns should be used. .Example - Compare-WorkSheet 'Server1.xlsx' 'Server2.xlsx' -WorkSheetName general -Startrow 2 -Headername Label,value -Key Label -GridView -ExcludeDifferent + Compare-WorkSheet 'Server54.xlsx' 'Server55.xlsx' -WorkSheetName general -Startrow 2 -Headername Label,value -Key Label -GridView -ExcludeDifferent The "General" page has a title and two unlabelled columns with a row forCPU, Memory, Domain, Disk and so on So the command is instructed to starts at row 2 to skip the title and to name the columns: the first is "label" and the Second "Value"; the label acts as the key. This time we interested the rows which are the same in both sheets, @@ -119,10 +124,12 @@ if ($propList.Count -eq 0) {Write-Warning -Message "No Columns are selected with -Property = '$Property' and -excludeProperty = '$ExcludeProperty'." ; return} #Add RowNumber, Sheetname and file name to every row - $i = $startRow + 1 ; foreach ($row in $Sheet1) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++) + $FirstDataRow = $startRow + 1 + if ($Headername -or $NoHeader) {$FirstDataRow -- } + $i = $FirstDataRow ; foreach ($row in $Sheet1) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++) Add-Member -InputObject $row -MemberType NoteProperty -Name "_Sheet" -Value $worksheet1 Add-Member -InputObject $row -MemberType NoteProperty -Name "_File" -Value $Referencefile} - $i = $startRow + 1 ; foreach ($row in $Sheet2) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++) + $i = $FirstDataRow ; foreach ($row in $Sheet2) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++) Add-Member -InputObject $row -MemberType NoteProperty -Name "_Sheet" -Value $worksheet2 Add-Member -InputObject $row -MemberType NoteProperty -Name "_File" -Value $Differencefile} @@ -132,7 +139,7 @@ Sort-Object -Property "_Row","File" #if BackgroundColor was specified, set it on extra or extra or changed rows - if ($diff -and $BackgroundColor) { + if ($diff -and $BackgroundColor) { #Differences may only exist in one file. So gather the changes for each file; open the file, update each impacted row in the shee, save the file $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property "_File" foreach ($file in $updates) { @@ -160,7 +167,7 @@ } } #if font colour was specified, set it on changed properties where the same key appears in both sheets. - if ($diff -and $FontColor -and ($propList -contains $Key) ) { + if ($diff -and $FontColor -and ($propList -contains $Key) ) { $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property $Key | where {$_.count -eq 2} if ($updates) { $XL1 = Open-ExcelPackage -path $Referencefile @@ -178,32 +185,68 @@ if (-not $oneFile) {$xl2.Save() ; $xl2.Stream.Close() ; $xl2.Dispose()} } } - elseif ($diff -and $FontColor) {Write-Warning -Message "To match rows to set changed cells, you must specify -Key and it must match one of the included properties" } + elseif ($diff -and $FontColor) {Write-Warning -Message "To match rows to set changed cells, you must specify -Key and it must match one of the included properties." } - if ($show) { + #if nothing was found write a message which wont be redirected + if (-not $diff) {Write-Host "Comparison of $Referencefile::$worksheet1 and $Differencefile::$WorkSheet2 returned no results." } + + if ($show) { Start-Process -FilePath $Referencefile - if (-not $oneFile) { Start-Process -FilePath $Differencefile } + if (-not $oneFile) { Start-Process -FilePath $Differencefile } + if ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" } } - elseif ($GridView) { - $Sheet2 | ForEach-Object -Begin {$Rowhash = @{} } -Process {$Rowhash[$_.$key] = $_._row } - if ($StartRow) { $rowCount1 = $StartRow} else { $rowCount1 = 1} - $rowCount2 = $null - $diff | Group-Object -Property $key | Sort-Object -Property @{e={($_.group | Measure-Object -Property _row -Maximum).maximum} } | ForEach-Object { - $hash = [ordered]@{"") {$rowCount1 = $hash["Row"] = $rowCount2 - $Hash[$key] = $keyVal + elseif ($GridView -and $propList -contains $key) { + + + if ($IncludeEqual -and -not $ExcludeDifferent) { + $GroupedRows = $diff | Group-Object -Property $key + } + else { #to get the right now numbers on the grid we need to have all the rows. + $GroupedRows = Compare-Object -ReferenceObject $Sheet1 -DifferenceObject $Sheet2 -Property $propList -PassThru -IncludeEqual | + Group-Object -Property $key + } + #Additions, deletions and unchanged rows will give a group of 1; changes will give a group of 2 . + + #If one sheet has extra rows we can get a single "==" result from compare, but with the row from the reference sheet + #but the row in the other sheet might so we will look up the row number from the key field build a hash table for that + $Sheet2 | ForEach-Object -Begin {$Rowhash = @{} } -Process {$Rowhash[$_.$key] = $_._row } + + $ExpandedDiff = ForEach ($g in $GroupedRows) { + #we're going to create a custom object from a hash table. We want the fields to be ordered + $hash = [ordered]@{} + foreach ($result IN $g.Group) { + # if result indicates equal or "in Reference" set the reference side row. If we did that on a previous result keep it. Otherwise set to "blank" + if ($result.sideindicator -ne "=>") {$hash["Row"] = $Rowhash[$g.Name] + #position the key as the next field (only appears once) + $Hash[$key] = $g.Name + #For all the other fields we care about create <=FieldName and/or =>FieldName foreach ($p in $propList.Where({$_ -ne $key})) { - if ($row.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$row.$P} - else {$hash[($row.SideIndicator+$P)] =$row.$P} + if ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} + else {$hash[($result.SideIndicator+$P)] =$result.$P} } } - [Pscustomobject]$hash } | Sort-Object -Property ")" + [Pscustomobject]$hash + } + + #Sort by reference row number, and fill in any blanks in the difference-row column + $ExpandedDiff = $ExpandedDiff | Sort-Object -Property "row") {$ExpandedDiff[$i].">row" = $ExpandedDiff[$i-1].">row" } } + #Sort by difference row number, and fill in any blanks in the reference-row column + $ExpandedDiff = $ExpandedDiff | Sort-Object -Property ">row" + for ($i = 1; $i -lt $ExpandedDiff.Count; $i++) {if (-not $ExpandedDiff[$i]."row" } + elseif ( $IncludeEqual) {$ExpandedDiff = $ExpandedDiff | Sort-Object -Property "row" } + else {$ExpandedDiff = $ExpandedDiff.where({$_.side -ne "=="}) | Sort-Object -Property "row" } + $ExpandedDiff | Update-FirstObjectProperties | Out-GridView -Title "Comparing $Referencefile::$worksheet1 (<=) with $Differencefile::$WorkSheet2 (=>)" } - elseif (-not $PassThru) {return ($diff | Select-Object -Property (@(@{n="_Side";e={$_.SideIndicator}},"_File" ,"_Sheet","_Row") + $propList))} - if ( $PassThru) {return $diff } + elseif ($GridView ) {Write-Warning -Message "To use -GridView you must specify -Key and it must match one of the included properties." } + elseif (-not $PassThru) {return ($diff | Select-Object -Property (@(@{n="_Side";e={$_.SideIndicator}},"_File" ,"_Sheet","_Row") + $propList))} + if ( $PassThru) {return $diff } } \ No newline at end of file From 21108f51369be9ab1308eecdae09d126ba681d22 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Thu, 3 May 2018 17:12:20 +0100 Subject: [PATCH 07/17] fat fingers ! --- compare-WorkSheet.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index 92a19c8..8ee571e 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -112,7 +112,7 @@ #Get Column headings and create a hash table of Name to column letter. $headings = $Sheet1[-1].psobject.Properties.name # This preserves the sequence - using get-member would sort them alphabetically! - $headings | ForEach-Object -Begin {$columns = @{} ; } -Process {$Columns[$_] = [char]($i ++) } + $headings | ForEach-Object -Begin {$columns = @{} ; $i=65 } -Process {$Columns[$_] = [char]($i ++) } #Make a list of property headings using the Property (default "*") and ExcludeProperty parameters if ($Key -eq "Name" -and $NoHeader) {$key = "p1"} From 6a53d3ddc90e530840f58453897fa0f137036b80 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Sun, 13 May 2018 17:35:31 +0100 Subject: [PATCH 08/17] Minor tidying. Making case consistent, and various things analyzer friendly; added Timeout to Send-SQL..., --- Examples/SQL+FillColumns+Pivot/Example.ps1 | 2 +- Export-ExcelSheet.ps1 | 2 +- Export-charts.ps1 | 16 ++++++------ Send-SqlDataToExcel.ps1 | 30 +++++++++++++--------- compare-WorkSheet.ps1 | 2 +- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Examples/SQL+FillColumns+Pivot/Example.ps1 b/Examples/SQL+FillColumns+Pivot/Example.ps1 index 79b8e8b..0c03515 100644 --- a/Examples/SQL+FillColumns+Pivot/Example.ps1 +++ b/Examples/SQL+FillColumns+Pivot/Example.ps1 @@ -1,4 +1,4 @@ - ipmo C:\Users\mcp\Documents\GitHub\ImportExcel\ImportExcel.psd1 -Force -Verbose + Import-Module -name ImportExcel -Force -Verbose $sql = @" SELECT rootfile.baseName , rootfile.extension , Image.fileWidth AS width , image.fileHeight AS height , diff --git a/Export-ExcelSheet.ps1 b/Export-ExcelSheet.ps1 index fc095e7..b703d0f 100644 --- a/Export-ExcelSheet.ps1 +++ b/Export-ExcelSheet.ps1 @@ -19,7 +19,7 @@ function Export-ExcelSheet { $xl = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Path $workbook = $xl.Workbook - $targetSheets = $workbook.Worksheets | Where {$_.Name -Match $SheetName} + $targetSheets = $workbook.Worksheets | Where-Object {$_.Name -Match $SheetName} $params = @{} + $PSBoundParameters $params.Remove("OutputPath") diff --git a/Export-charts.ps1 b/Export-charts.ps1 index 196d8e5..27ec7ab 100644 --- a/Export-charts.ps1 +++ b/Export-charts.ps1 @@ -14,7 +14,7 @@ Param ( #Path to the Excel file whose chars we will export. $Path = "C:\Users\public\Documents\stats.xlsx", #If specified, output file objects representing the image files. - [switch]$passthru, + [switch]$Passthru, #Format to write - JPG by default [ValidateSet("JPG","PNG","GIF")] $OutputType = "JPG", @@ -23,15 +23,14 @@ Param ( ) #if no output folder was specified, set destination to the folder where the Excel file came from -if (-not $Destination) {$Destination = Split-Path -Path $path -Parent } +if (-not $Destination) {$Destination = Split-Path -Path $Path -Parent } #Call up Excel and tell it to open the file. try { $excelApp = New-Object -ComObject "Excel.Application" } catch { Write-Warning "Could not start Excel application - which usually means it is not installed." ; return } -try { $excelWorkBook = $excelApp.Workbooks.Open($path) } -catch { Write-Warning "Could not start Excel application - which usually means it is not installed." ; return } - +try { $excelWorkBook = $excelApp.Workbooks.Open($Path) } +catch { Write-Warning -Message "Could not Open $Path." ; return } #For each worksheet, for each chart, jump to the chart, create a filename of "WorksheetName_ChartTitle.jpg", and export the file. foreach ($excelWorkSheet in $excelWorkBook.Worksheets) { @@ -41,11 +40,12 @@ foreach ($excelWorkSheet in $excelWorkBook.Worksheets) { $excelApp.Goto($excelchart.TopLeftCell,$true) $imagePath = Join-Path -Path $Destination -ChildPath ($excelWorkSheet.Name + "_" + ($excelchart.Chart.ChartTitle.Text -split "\s\d\d:\d\d,")[0] + ".$OutputType") if ( $excelchart.Chart.Export($imagePath, $OutputType, $false) ) { # Export returs true/false for success/failure - if ($passThru) {Get-Item -Path $imagePath } # when succesful return a file object (-passthru) or print a verbose message, write warning for any failures + if ($Passthru) {Get-Item -Path $imagePath } # when succesful return a file object (-Passthru) or print a verbose message, write warning for any failures else {Write-Verbose -Message "Exported $imagePath"} } else {Write-Warning -Message "Failure exporting $imagePath" } } } - -$excelApp.Quit() \ No newline at end of file +$excelApp.DisplayAlerts = $false +$excelWorkBook.Close($false,$null,$null) +$excelApp.Quit() diff --git a/Send-SqlDataToExcel.ps1 b/Send-SqlDataToExcel.ps1 index 15a1b1c..96d0db7 100644 --- a/Send-SqlDataToExcel.ps1 +++ b/Send-SqlDataToExcel.ps1 @@ -1,5 +1,7 @@ Function Send-SQLDataToExcel { -<# + [CmdLetBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] + <# .Synopsis Runs a SQL query and inserts the results into an ExcelSheet, more efficiently than sending it via Export-Excel .Description @@ -23,7 +25,7 @@ -#> + #> param ( #Database connection string; either DSN=ODBC_Data_Source_Name, a full odbc or SQL Connection string, or the name of a SQL server [Parameter(ParameterSetName="SQLConnection", Mandatory=$true)] @@ -41,6 +43,9 @@ #The SQL query to run [Parameter(Mandatory=$true)] [string]$SQL, + #Override the default query time of 30 seconds. + [int]$QueryTimeout, + #File name for the Excel File $Path, [String]$WorkSheetname = 'Sheet1', [Switch]$KillExcel, @@ -92,9 +97,9 @@ #We were either given a session object or a connection string (with, optionally a MSSQLServer parameter) # If we got -MSSQLServer, create a SQL connection, if we didn't but we got -Connection create an ODBC connection if ($MsSQLserver) { - if ($connection -notmatch "=") {$Connection = "server=$Connection;trusted_connection=true;timeout=60"} + if ($Connection -notmatch "=") {$Connection = "server=$Connection;trusted_connection=true;timeout=60"} $Session = New-Object -TypeName System.Data.SqlClient.SqlConnection -ArgumentList $Connection - if ($Session.State -ne 'Open') {$session.Open()} + if ($Session.State -ne 'Open') {$Session.Open()} if ($DataBase) {$Session.ChangeDatabase($DataBase) } } elseif ($Connection) { @@ -102,30 +107,31 @@ } #A session was either passed in or just created. If it's a SQL one make a SQL DataAdapter, otherwise make an ODBC one - if ($Session.gettype().name -match "SqlConnection") { + if ($Session.GetType().name -match "SqlConnection") { $dataAdapter = New-Object -TypeName System.Data.SqlClient.SqlDataAdapter -ArgumentList ( - New-Object -TypeName System.Data.SqlClient.SqlCommand -ArgumentList $sql, $Session) + New-Object -TypeName System.Data.SqlClient.SqlCommand -ArgumentList $SQL, $Session) } else { $dataAdapter = New-Object -TypeName System.Data.Odbc.OdbcDataAdapter -ArgumentList ( - New-Object -TypeName System.Data.Odbc.OdbcCommand -ArgumentList $sql, $Session ) + New-Object -TypeName System.Data.Odbc.OdbcCommand -ArgumentList $SQL, $Session ) } - + if ($QueryTimeout) {$dataAdapter.SelectCommand.CommandTimeout = $ServerTimeout} + #Both adapter types output the same kind of table, create one and fill it from the adapter $dataTable = New-Object -TypeName System.Data.DataTable $rowCount = $dataAdapter.fill($dataTable) - Write-Verbose "Query returned $rowcount row(s)" + Write-Verbose -Message "Query returned $rowCount row(s)" #ExportExcel user a -NoHeader parameter so that's what we use here, but needs to be the other way around. - $PrintHeaders = -not $NoHeader + $printHeaders = -not $NoHeader if ($Title) {$r = $StartRow +1 } else {$r = $StartRow} #Get our Excel sheet and fill it with the data $excelPackage = Export-Excel -Path $Path -WorkSheetname $WorkSheetname -PassThru - $excelPackage.Workbook.Worksheets[$WorkSheetname].Cells[$r,$StartColumn].LoadFromDataTable($dataTable, $PrintHeaders ) | Out-Null + $excelPackage.Workbook.Worksheets[$WorkSheetname].Cells[$r,$StartColumn].LoadFromDataTable($dataTable, $printHeaders ) | Out-Null #Call export-excel with any parameters which don't relate to the SQL query - "Connection", "Database" , "Session", "MsSQLserver", "Destination" , "sql" ,"Path" | foreach-object {$null = $PSBoundParameters.Remove($_) } + "Connection", "Database" , "Session", "MsSQLserver", "Destination" , "SQL" ,"Path" | ForEach-Object {$null = $PSBoundParameters.Remove($_) } Export-Excel -ExcelPackage $excelPackage @PSBoundParameters #If we were not passed a session close the session we created. diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index 8ee571e..85d91d8 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -168,7 +168,7 @@ } #if font colour was specified, set it on changed properties where the same key appears in both sheets. if ($diff -and $FontColor -and ($propList -contains $Key) ) { - $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property $Key | where {$_.count -eq 2} + $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property $Key | Where-Object {$_.count -eq 2} if ($updates) { $XL1 = Open-ExcelPackage -path $Referencefile if ($oneFile ) {$xl2 = $xl1} From bf8e8ed6bf25c0fe964ebe49703d7f853aaf161c Mon Sep 17 00:00:00 2001 From: jhoneill Date: Mon, 14 May 2018 11:38:23 +0100 Subject: [PATCH 09/17] Added Merge-Workshet. Made perf improvements to Import-Excel --- ColorCompletion.ps1 | 4 + ImportExcel.psm1 | 555 +++++++++++++++++++++----------------------- merge-worksheet.ps1 | 213 +++++++++++++++++ 3 files changed, 478 insertions(+), 294 deletions(-) create mode 100644 merge-worksheet.ps1 diff --git a/ColorCompletion.ps1 b/ColorCompletion.ps1 index 12b6f81..bfd811a 100644 --- a/ColorCompletion.ps1 +++ b/ColorCompletion.ps1 @@ -14,6 +14,10 @@ if (Get-Command -Name register-argumentCompleter -ErrorAction SilentlyContinue) Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName TabColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-Worksheet -ParameterName AddBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-Worksheet -ParameterName ChangeBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-Worksheet ` -ParameterName DeleteBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-Worksheet -ParameterName KeyFontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName FontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion diff --git a/ImportExcel.psm1 b/ImportExcel.psm1 index 8b2b355..e80ef95 100644 --- a/ImportExcel.psm1 +++ b/ImportExcel.psm1 @@ -1,241 +1,243 @@ -Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" - -. $PSScriptRoot\AddConditionalFormatting.ps1 -. $PSScriptRoot\Charting.ps1 -. $PSScriptRoot\ColorCompletion.ps1 -. $PSScriptRoot\ConvertExcelToImageFile.ps1 -. $PSScriptRoot\Compare-WorkSheet.ps1 -. $PSScriptRoot\ConvertFromExcelData.ps1 -. $PSScriptRoot\ConvertFromExcelToSQLInsert.ps1 -. $PSScriptRoot\ConvertToExcelXlsx.ps1 -. $PSScriptRoot\Copy-ExcelWorkSheet.ps1 -. $PSScriptRoot\Export-Excel.ps1 -. $PSScriptRoot\Export-ExcelSheet.ps1 -. $PSScriptRoot\Get-ExcelColumnName.ps1 -. $PSScriptRoot\Get-ExcelSheetInfo.ps1 -. $PSScriptRoot\Get-ExcelWorkbookInfo.ps1 -. $PSScriptRoot\Get-HtmlTable.ps1 -. $PSScriptRoot\Get-Range.ps1 -. $PSScriptRoot\Get-XYRange.ps1 -. $PSScriptRoot\Import-Html.ps1 -. $PSScriptRoot\InferData.ps1 -. $PSScriptRoot\Invoke-Sum.ps1 -. $PSScriptRoot\New-ConditionalFormattingIconSet.ps1 -. $PSScriptRoot\New-ConditionalText.ps1 -. $PSScriptRoot\New-ExcelChart.ps1 -. $PSScriptRoot\New-PSItem.ps1 -. $PSScriptRoot\Open-ExcelPackage.ps1 -. $PSScriptRoot\Pivot.ps1 -. $PSScriptRoot\Send-SQLDataToExcel.ps1 -. $PSScriptRoot\Set-CellStyle.ps1 -. $PSScriptRoot\Set-Column.ps1 -. $PSScriptRoot\Set-Row.ps1 -. $PSScriptRoot\SetFormat.ps1 -. $PSScriptRoot\TrackingUtils.ps1 -. $PSScriptRoot\Update-FirstObjectProperties.ps1 +#region import everything we need + Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" + . $PSScriptRoot\AddConditionalFormatting.ps1 + . $PSScriptRoot\Charting.ps1 + . $PSScriptRoot\ColorCompletion.ps1 + . $PSScriptRoot\ConvertExcelToImageFile.ps1 + . $PSScriptRoot\Compare-WorkSheet.ps1 + . $PSScriptRoot\ConvertFromExcelData.ps1 + . $PSScriptRoot\ConvertFromExcelToSQLInsert.ps1 + . $PSScriptRoot\ConvertToExcelXlsx.ps1 + . $PSScriptRoot\Copy-ExcelWorkSheet.ps1 + . $PSScriptRoot\Export-Excel.ps1 + . $PSScriptRoot\Export-ExcelSheet.ps1 + . $PSScriptRoot\Get-ExcelColumnName.ps1 + . $PSScriptRoot\Get-ExcelSheetInfo.ps1 + . $PSScriptRoot\Get-ExcelWorkbookInfo.ps1 + . $PSScriptRoot\Get-HtmlTable.ps1 + . $PSScriptRoot\Get-Range.ps1 + . $PSScriptRoot\Get-XYRange.ps1 + . $PSScriptRoot\Import-Html.ps1 + . $PSScriptRoot\InferData.ps1 + . $PSScriptRoot\Invoke-Sum.ps1 + . $PSScriptRoot\Merge-Worksheet.ps1 + . $PSScriptRoot\New-ConditionalFormattingIconSet.ps1 + . $PSScriptRoot\New-ConditionalText.ps1 + . $PSScriptRoot\New-ExcelChart.ps1 + . $PSScriptRoot\New-PSItem.ps1 + . $PSScriptRoot\Open-ExcelPackage.ps1 + . $PSScriptRoot\Pivot.ps1 + . $PSScriptRoot\Send-SQLDataToExcel.ps1 + . $PSScriptRoot\Set-CellStyle.ps1 + . $PSScriptRoot\Set-Column.ps1 + . $PSScriptRoot\Set-Row.ps1 + . $PSScriptRoot\SetFormat.ps1 + . $PSScriptRoot\TrackingUtils.ps1 + . $PSScriptRoot\Update-FirstObjectProperties.ps1 -New-Alias -Name Use-ExcelData -Value "ConvertFrom-ExcelData" -Force + New-Alias -Name Use-ExcelData -Value "ConvertFrom-ExcelData" -Force -if ($PSVersionTable.PSVersion.Major -ge 5) { - . $PSScriptRoot\Plot.ps1 + if ($PSVersionTable.PSVersion.Major -ge 5) { + . $PSScriptRoot\Plot.ps1 - Function New-Plot { - Param() + Function New-Plot { + Param() + + [PSPlot]::new() + } - [PSPlot]::new() } - -} -else { - Write-Warning 'PowerShell 5 is required for plot.ps1' - Write-Warning 'PowerShell Excel is ready, except for that functionality' -} - + else { + Write-Warning 'PowerShell 5 is required for plot.ps1' + Write-Warning 'PowerShell Excel is ready, except for that functionality' + } +#endregion Function Import-Excel { - <# - .SYNOPSIS - Create custom objects from the rows in an Excel worksheet. + <# + .SYNOPSIS + Create custom objects from the rows in an Excel worksheet. - .DESCRIPTION - The Import-Excel cmdlet creates custom objects from the rows in an Excel worksheet. Each row represents one object. All of this is possible without installing Microsoft Excel and by using the .NET library ‘EPPLus.dll’. + .DESCRIPTION + The Import-Excel cmdlet creates custom objects from the rows in an Excel worksheet. Each row represents one object. All of this is possible without installing Microsoft Excel and by using the .NET library ‘EPPLus.dll’. - By default, the property names of the objects are retrieved from the column headers. Because an object cannot have a blanc property name, only columns with column headers will be imported. + By default, the property names of the objects are retrieved from the column headers. Because an object cannot have a blanc property name, only columns with column headers will be imported. - If the default behavior is not desired and you want to import the complete worksheet ‘as is’, the parameter ‘-NoHeader’ can be used. In case you want to provide your own property names, you can use the parameter ‘-HeaderName’. + If the default behavior is not desired and you want to import the complete worksheet ‘as is’, the parameter ‘-NoHeader’ can be used. In case you want to provide your own property names, you can use the parameter ‘-HeaderName’. - .PARAMETER Path - Specifies the path to the Excel file. + .PARAMETER Path + Specifies the path to the Excel file. - .PARAMETER WorksheetName - Specifies the name of the worksheet in the Excel workbook to import. By default, if no name is provided, the first worksheet will be imported. + .PARAMETER WorksheetName + Specifies the name of the worksheet in the Excel workbook to import. By default, if no name is provided, the first worksheet will be imported. - .PARAMETER DataOnly - Import only rows and columns that contain data, empty rows and empty columns are not imported. + .PARAMETER DataOnly + Import only rows and columns that contain data, empty rows and empty columns are not imported. - .PARAMETER HeaderName - Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. + .PARAMETER HeaderName + Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. - In case you provide less header names than there is data in the worksheet, then only the data with a corresponding header name will be imported and the data without header name will be disregarded. + In case you provide less header names than there is data in the worksheet, then only the data with a corresponding header name will be imported and the data without header name will be disregarded. - In case you provide more header names than there is data in the worksheet, then all data will be imported and all objects will have all the property names you defined in the header names. As such, the last properties will be blanc as there is no data for them. + In case you provide more header names than there is data in the worksheet, then all data will be imported and all objects will have all the property names you defined in the header names. As such, the last properties will be blanc as there is no data for them. - .PARAMETER NoHeader - Automatically generate property names (P1, P2, P3, ..) instead of the ones defined in the column headers of the TopRow. + .PARAMETER NoHeader + Automatically generate property names (P1, P2, P3, ..) instead of the ones defined in the column headers of the TopRow. - This switch is best used when you want to import the complete worksheet ‘as is’ and are not concerned with the property names. + This switch is best used when you want to import the complete worksheet ‘as is’ and are not concerned with the property names. - .PARAMETER StartRow - The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. + .PARAMETER StartRow + The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. - When the parameters ‘-NoHeader’ and ‘-HeaderName’ are not provided, this row will contain the column headers that will be used as property names. When one of both parameters are provided, the property names are automatically created and this row will be treated as a regular row containing data. + When the parameters ‘-NoHeader’ and ‘-HeaderName’ are not provided, this row will contain the column headers that will be used as property names. When one of both parameters are provided, the property names are automatically created and this row will be treated as a regular row containing data. - .PARAMETER Password - Accepts a string that will be used to open a password protected Excel file. + .PARAMETER Password + Accepts a string that will be used to open a password protected Excel file. + + .EXAMPLE + Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the column names defined in the first row. In case a column doesn’t have a column header (usually in row 1 when ‘-StartRow’ is not used), then the unnamed columns will be skipped and the data in those columns will not be imported. + + ---------------------------------------------- + | File: Movies.xlsx - Sheet: Actors | + ---------------------------------------------- + | A B C | + |1 First Name Address | + |2 Chuck Norris California | + |3 Jean-Claude Vandamme Brussels | + ---------------------------------------------- + + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors + + First Name: Chuck + Address : California + + First Name: Jean-Claude + Address : Brussels + + Notice that column 'B' is not imported because there's no value in cell 'B1' that can be used as property name for the objects. + + .EXAMPLE + Import the complete Excel worksheet ‘as is’ by using the ‘-NoHeader’ switch. One object is created for each row. The property names of the objects will be automatically generated (P1, P2, P3, ..). + + ---------------------------------------------- + | File: Movies.xlsx - Sheet: Actors | + ---------------------------------------------- + | A B C | + |1 First Name Address | + |2 Chuck Norris California | + |3 Jean-Claude Vandamme Brussels | + ---------------------------------------------- + + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -NoHeader + + P1: First Name + P2: + P3: Address + + P1: Chuck + P2: Norris + P3: California + + P1: Jean-Claude + P2: Vandamme + P3: Brussels + + Notice that the column header (row 1) is imported as an object too. .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the column names defined in the first row. In case a column doesn’t have a column header (usually in row 1 when ‘-StartRow’ is not used), then the unnamed columns will be skipped and the data in those columns will not be imported. + Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the names defined in the parameter ‘-HeaderName’. The properties are named starting from the most left column (A) to the right. In case no value is present in one of the columns, that property will have an empty value. - ---------------------------------------------- - | File: Movies.xlsx - Sheet: Actors | - ---------------------------------------------- - | A B C | - |1 First Name Address | - |2 Chuck Norris California | - |3 Jean-Claude Vandamme Brussels | - ---------------------------------------------- + ---------------------------------------------------------- + | File: Movies.xlsx - Sheet: Movies | + ---------------------------------------------------------- + | A B C D | + |1 The Bodyguard 1992 9 | + |2 The Matrix 1999 8 | + |3 | + |4 Skyfall 2012 9 | + ---------------------------------------------------------- - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies -HeaderName 'Movie name', 'Year', 'Rating', 'Genre' - First Name: Chuck - Address : California + Movie name: The Bodyguard + Year : 1992 + Rating : 9 + Genre : - First Name: Jean-Claude - Address : Brussels + Movie name: The Matrix + Year : 1999 + Rating : 8 + Genre : - Notice that column 'B' is not imported because there's no value in cell 'B1' that can be used as property name for the objects. + Movie name: + Year : + Rating : + Genre : + + Movie name: Skyfall + Year : 2012 + Rating : 9 + Genre : + + Notice that empty rows are imported and that data for the property 'Genre' is not present in the worksheet. As such, the 'Genre' property will be blanc for all objects. .EXAMPLE - Import the complete Excel worksheet ‘as is’ by using the ‘-NoHeader’ switch. One object is created for each row. The property names of the objects will be automatically generated (P1, P2, P3, ..). + Import data from an Excel worksheet. One object is created for each row. The property names of the objects are automatically generated by using the switch ‘-NoHeader’ (P1, P@, P#, ..). The switch ‘-DataOnly’ will speed up the import because empty rows and empty columns are not imported. - ---------------------------------------------- - | File: Movies.xlsx - Sheet: Actors | - ---------------------------------------------- - | A B C | - |1 First Name Address | - |2 Chuck Norris California | - |3 Jean-Claude Vandamme Brussels | - ---------------------------------------------- + ---------------------------------------------------------- + | File: Movies.xlsx - Sheet: Movies | + ---------------------------------------------------------- + | A B C D | + |1 The Bodyguard 1992 9 | + |2 The Matrix 1999 8 | + |3 | + |4 Skyfall 2012 9 | + ---------------------------------------------------------- - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -NoHeader + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies –NoHeader -DataOnly - P1: First Name - P2: - P3: Address + P1: The Bodyguard + P2: 1992 + P3: 9 - P1: Chuck - P2: Norris - P3: California + P1: The Matrix + P2: 1999 + P3: 8 - P1: Jean-Claude - P2: Vandamme - P3: Brussels + P1: Skyfall + P2: 2012 + P3: 9 - Notice that the column header (row 1) is imported as an object too. + Notice that empty rows and empty columns are not imported. - .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the names defined in the parameter ‘-HeaderName’. The properties are named starting from the most left column (A) to the right. In case no value is present in one of the columns, that property will have an empty value. +.EXAMPLE + Import data from an Excel worksheet. One object is created for each row. The property names are provided with the ‘-HeaderName’ parameter. The import will start from row 2 and empty columns and rows are not imported. - ---------------------------------------------------------- - | File: Movies.xlsx - Sheet: Movies | - ---------------------------------------------------------- - | A B C D | - |1 The Bodyguard 1992 9 | - |2 The Matrix 1999 8 | - |3 | - |4 Skyfall 2012 9 | - ---------------------------------------------------------- + ---------------------------------------------------------- + | File: Movies.xlsx - Sheet: Actors | + ---------------------------------------------------------- + | A B C D | + |1 Chuck Norris California | + |2 | + |3 Jean-Claude Vandamme Brussels | + ---------------------------------------------------------- - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies -HeaderName 'Movie name', 'Year', 'Rating', 'Genre' + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -DataOnly -HeaderName 'FirstName', 'SecondName', 'City' –StartRow 2 - Movie name: The Bodyguard - Year : 1992 - Rating : 9 - Genre : + FirstName : Jean-Claude + SecondName: Vandamme + City : Brussels - Movie name: The Matrix - Year : 1999 - Rating : 8 - Genre : + Notice that only 1 object is imported with only 3 properties. Column B and row 2 are empty and have been disregarded by using the switch '-DataOnly'. The property names have been named with the values provided with the parameter '-HeaderName'. Row number 1 with ‘Chuck Norris’ has not been imported, because we started the import from row 2 with the parameter ‘-StartRow 2’. - Movie name: - Year : - Rating : - Genre : + .LINK + https://github.com/dfinke/ImportExcel - Movie name: Skyfall - Year : 2012 - Rating : 9 - Genre : - - Notice that empty rows are imported and that data for the property 'Genre' is not present in the worksheet. As such, the 'Genre' property will be blanc for all objects. - - .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names of the objects are automatically generated by using the switch ‘-NoHeader’ (P1, P@, P#, ..). The switch ‘-DataOnly’ will speed up the import because empty rows and empty columns are not imported. - - ---------------------------------------------------------- - | File: Movies.xlsx - Sheet: Movies | - ---------------------------------------------------------- - | A B C D | - |1 The Bodyguard 1992 9 | - |2 The Matrix 1999 8 | - |3 | - |4 Skyfall 2012 9 | - ---------------------------------------------------------- - - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies –NoHeader -DataOnly - - P1: The Bodyguard - P2: 1992 - P3: 9 - - P1: The Matrix - P2: 1999 - P3: 8 - - P1: Skyfall - P2: 2012 - P3: 9 - - Notice that empty rows and empty columns are not imported. - - .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names are provided with the ‘-HeaderName’ parameter. The import will start from row 2 and empty columns and rows are not imported. - - ---------------------------------------------------------- - | File: Movies.xlsx - Sheet: Actors | - ---------------------------------------------------------- - | A B C D | - |1 Chuck Norris California | - |2 | - |3 Jean-Claude Vandamme Brussels | - ---------------------------------------------------------- - - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -DataOnly -HeaderName 'FirstName', 'SecondName', 'City' –StartRow 2 - - FirstName : Jean-Claude - SecondName: Vandamme - City : Brussels - - Notice that only 1 object is imported with only 3 properties. Column B and row 2 are empty and have been disregarded by using the switch '-DataOnly'. The property names have been named with the values provided with the parameter '-HeaderName'. Row number 1 with ‘Chuck Norris’ has not been imported, because we started the import from row 2 with the parameter ‘-StartRow 2’. - - .LINK - https://github.com/dfinke/ImportExcel - - .NOTES - #> + .NOTES + #> [CmdLetBinding(DefaultParameterSetName)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] Param ( [Alias('FullName')] [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline, Position=0, Mandatory)] @@ -251,34 +253,13 @@ Function Import-Excel { [Switch]$NoHeader, [Alias('HeaderRow','TopRow')] [ValidateRange(1, 9999)] - [Int]$StartRow, + [Int]$StartRow = 1, [Switch]$DataOnly, [ValidateNotNullOrEmpty()] [String]$Password ) - Begin { - Function Add-Property { - <# - .SYNOPSIS - Add the property name and value to the hashtable that will create a new object for each row. - #> - - Param ( - [Parameter(Mandatory)] - [String]$Name, - $Value - ) - - Try { - $NewRow.$Name = $Value - Write-Verbose "Import cell '$($Worksheet.Cells[$R, $P.Column].Address)' with property name '$Name' and value '$Value'" - } - Catch { - throw "Failed adding the property name '$Name' with value '$Value': $_" - } - } - + $sw = [System.Diagnostics.Stopwatch]::StartNew() Function Get-PropertyNames { <# .SYNOPSIS @@ -313,7 +294,7 @@ Function Import-Excel { } foreach ($C in $Columns) { - $Worksheet.Cells[$StartRow,$C] | where {$_.Value} | Select-Object @{N='Column'; E={$C}}, Value + $Worksheet.Cells[$StartRow,$C] | Where-Object {$_.Value} | Select-Object @{N='Column'; E={$C}}, Value } } } @@ -328,24 +309,24 @@ Function Import-Excel { #region Open file $Path = (Resolve-Path $Path).ProviderPath Write-Verbose "Import Excel workbook '$Path' with worksheet '$Worksheetname'" - + $Stream = New-Object -TypeName System.IO.FileStream -ArgumentList $Path, 'Open', 'Read', 'ReadWrite' - + if ($Password) { $Excel = New-Object -TypeName OfficeOpenXml.ExcelPackage - - Try { - $Excel.Load($Stream,$Password) - } - Catch { - throw "Password '$Password' is not correct." - } + + Try { + $Excel.Load($Stream,$Password) + } + Catch { + throw "Password '$Password' is not correct." + } } else { $Excel = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Stream } #endregion - + #region Select worksheet if ($WorksheetName) { if (-not ($Worksheet = $Excel.Workbook.Worksheets[$WorkSheetName])) { @@ -356,74 +337,59 @@ Function Import-Excel { $Worksheet = $Excel.Workbook.Worksheets | Select-Object -First 1 } #endregion - - #region Set the top row - if (((-not ($NoHeader -or $HeaderName)) -and ($StartRow -eq 0))) { - $StartRow = 1 - } - #endregion - - if (-not ($AllCells = $Worksheet.Cells | where {($_.Start.Row -ge $StartRow)})) { - Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' is empty after StartRow '$StartRow'" + Write-Debug $sw.Elapsed.TotalMilliseconds + #region Get rows and columns + #If we are doing dataonly it is quicker to work out which rows to ignore before processing the cells. + if ($DataOnly) { + #If we are using headers startrow will be the headerrow so examine data from startRow + 1, + if ($NoHeader) {$range = "A" + ($StartRow ) + ":" + $Worksheet.Dimension.End.Address } + else {$range = "A" + ($StartRow + 1 ) + ":" + $Worksheet.Dimension.End.Address } + #We're going to look at every cell and build 2 hash tables holding rows & columns which contain data. + #Want to Avoid 'select unique' operations & large Sorts, becuse time time taken increases with square + #of number of items (PS uses heapsort at large size). Instead keep a list of what we have seen, + #using Hash tables: "we've seen it" is all we need, no need to worry about "seen it before" / "Seen it many times" + $colHash = @{} + $rowHash = @{} + foreach ($cell in $Worksheet.Cells[$range]) { + if ($cell.Value -ne $null) {$colHash[$cell.Start.Column]=1; $rowHash[$cell.Start.row]=1 } + } + $rows = ($StartRow..($Worksheet.Dimension.End.Row)).Where({$rowHash[$_]}) + $columns = (1..($Worksheet.Dimension.End.Column) ).Where({$colHash[$_]}) } else { - #region Get rows and columns - if ($DataOnly) { - $CellsWithValues = $AllCells | where {$_.Value} - - $Columns = $CellsWithValues.Start.Column | Sort-Object -Unique - $Rows = $CellsWithValues.Start.Row | Sort-Object -Unique - } - else { - $LastColumn = $AllCells.Start.Column | Sort-Object -Unique | Select-Object -Last 1 - $Columns = 1..$LastColumn - - $LastRow = $AllCells.Start.Row | Sort-Object -Unique | Select-Object -Last 1 - $Rows = $StartRow..$LastRow | where {($_ -ge $StartRow) -and ($_ -gt 0)} - } - #endregion - - #region Create property names - if ((-not $Columns) -or (-not ($PropertyNames = Get-PropertyNames -Columns $Columns -StartRow $StartRow))) { - throw "No column headers found on top row '$StartRow'. If column headers in the worksheet are not a requirement then please use the '-NoHeader' or '-HeaderName' parameter." - } - - if ($Duplicates = $PropertyNames | Group-Object Value | where Count -GE 2) { - throw "Duplicate column headers found on row '$StartRow' in columns '$($Duplicates.Group.Column)'. Column headers must be unique, if this is not a requirement please use the '-NoHeader' or '-HeaderName' parameter." - } - #endregion - - #region Filter out rows with data in columns that don't have a column header - if ($DataOnly -and (-not $NoHeader)) { - $Rows = $CellsWithValues.Start | where {$PropertyNames.Column -contains $_.Column} | - Sort-Object Row -Unique | Select-Object -ExpandProperty Row - } - #endregion - - #region Filter out the top row when it contains column headers - if (-not ($NoHeader -or $HeaderName)) { - $Rows = $Rows | where {$_ -gt $StartRow} - } - #endregion - - if (-not $Rows) { - Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' contains no data in the rows after top row '$StartRow'" - } - else { - #region Create one object per row - foreach ($R in $Rows) { - Write-Verbose "Import row '$R'" - $NewRow = [Ordered]@{} - - foreach ($P in $PropertyNames) { - Add-Property -Name $P.Value -Value $Worksheet.Cells[$R, $P.Column].Value - } - - [PSCustomObject]$NewRow - } - #endregion - } + $Columns = ($Worksheet.Dimension.Start.Column)..($Worksheet.Dimension.End.Column) + if ($NoHeader) {$Rows = ( $StartRow)..($Worksheet.Dimension.End.Row) } + else {$Rows = (1 + $StartRow)..($Worksheet.Dimension.End.Row) } } + #endregion + #region Create property names + if ((-not $Columns) -or (-not ($PropertyNames = Get-PropertyNames -Columns $Columns -StartRow $StartRow))) { + throw "No column headers found on top row '$StartRow'. If column headers in the worksheet are not a requirement then please use the '-NoHeader' or '-HeaderName' parameter." + } + if ($Duplicates = $PropertyNames | Group-Object Value | Where-Object Count -GE 2) { + throw "Duplicate column headers found on row '$StartRow' in columns '$($Duplicates.Group.Column)'. Column headers must be unique, if this is not a requirement please use the '-NoHeader' or '-HeaderName' parameter." + } + #endregion + Write-Debug $sw.Elapsed.TotalMilliseconds + if (-not $Rows) { + Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' contains no data in the rows after top row '$StartRow'" + } + else { + #region Create one object per row + foreach ($R in $Rows) { + Write-Verbose "Import row '$R'" + $NewRow = [Ordered]@{} + + foreach ($P in $PropertyNames) { + $NewRow[$P.Value] = $Worksheet.Cells[$R, $P.Column].Value + Write-Verbose "Import cell '$($Worksheet.Cells[$R, $P.Column].Address)' with property name '$($p.Value)' and value '$($Worksheet.Cells[$R, $P.Column].Value)'." + } + + [PSCustomObject]$NewRow + } + #endregion + } + Write-Debug $sw.Elapsed.TotalMilliseconds } Catch { throw "Failed importing the Excel workbook '$Path' with worksheet '$Worksheetname': $_" @@ -499,7 +465,7 @@ function ConvertFrom-ExcelSheet { $xl = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $stream $workbook = $xl.Workbook - $targetSheets = $workbook.Worksheets | Where {$_.Name -like $SheetName} + $targetSheets = $workbook.Worksheets | Where-Object {$_.Name -like $SheetName} $params = @{} + $PSBoundParameters $params.Remove("OutputPath") @@ -518,10 +484,11 @@ function ConvertFrom-ExcelSheet { $stream.Close() $stream.Dispose() - $xl.Dispose() + $xl.Dispose() } function Export-MultipleExcelSheets { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] param( [Parameter(Mandatory=$true)] $Path, diff --git a/merge-worksheet.ps1 b/merge-worksheet.ps1 new file mode 100644 index 0000000..631a4fc --- /dev/null +++ b/merge-worksheet.ps1 @@ -0,0 +1,213 @@ +Function Merge-Worksheet { + <# + .Synopsis + Merges two worksheets (or other objects) into a single worksheet with differences marked up. + .Description + The Compare-Worksheet command takes two worksheets and marks differences in the source document, and optionally outputs a grid showing the changes. + By contrast the Merge-Worksheet command takes the worksheets and combines them into a single sheet showing the old and new data side by side . + Although it is designed to work with Excel data it can work with arrays of any kind of object; so it can be a merge *of* worksheets, or a merge *to* worksheet. + .Example + merge-worksheet "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -OutputFile Services.xlsx -OutputSheetName 54-55 -show + The workbooks contain audit information for two servers, one page contains a list of services. This command creates a worksheet named 54-55 + in a workbook named services and shows all the services and their differences, and opens it in Excel + .Example + merge-worksheet "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -OutputFile Services.xlsx -OutputSheetName 54-55 -HideEqual -AddBackgroundColor LightBlue -show + This modifies the previous command to hide the equal rows in the output sheet and changes the color used to mark rows "Added" to the second file. + .Example + merge-worksheet -OutputFile .\j1.xlsx -OutputSheetName test11 -ReferenceObject (dir .\ImportExcel\4.0.7) -DifferenceObject (dir '\ImportExcel\4.0.8') -Property Length -Show + This version compares two directories, and marks what has changed. + Because no "Key" property is given, "Name" is assumed to be the key and the only other property examined is length. + Files which are added or deleted or have changedd size will be highlighed in the output sheet. Changes to dates or other attributes will be ignored + .Example + merge-worksheet -Outf .\dummy.xlsx -RefO (dir .\ImportExcel\4.0.7) -DiffO (dir .\ImportExcel\4.0.8') -Pr Length -WhatIf -Passthru | Out-GridView + This time no file is written because -WhatIf is specified, and -Passthru causes the results to go Out-Gridview. This version uses aliases to shorten the parameters, + (OutputFileName can be "outFile" and the sheet "OutSheet" : DifferenceObject & RefeenceObject can be DiffObject & RefObject) + #> + [cmdletbinding(SupportsShouldProcess=$true)] + Param( + #First Excel file to compare. You can compare two Excel files or two other objects but not one of each. + [parameter(ParameterSetName='A',Mandatory=$true,Position=0)] + [parameter(ParameterSetName='B',Mandatory=$true,Position=0)] + [parameter(ParameterSetName='C',Mandatory=$true,Position=0)] + $Referencefile , + + #Second Excel file to compare. + [parameter(ParameterSetName='A',Mandatory=$true,Position=1)] + [parameter(ParameterSetName='B',Mandatory=$true,Position=1)] + [parameter(ParameterSetName='C',Mandatory=$true,Position=1)] + $Differencefile , + + #Name(s) of worksheets to compare, + [parameter(ParameterSetName='A',Position=2)] + [parameter(ParameterSetName='B',Position=2)] + [parameter(ParameterSetName='C',Position=2)] + $WorkSheetName = "Sheet1", + + #The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. + [parameter(ParameterSetName='A')] + [parameter(ParameterSetName='B')] + [parameter(ParameterSetName='C')] + [int]$Startrow = 1, + + #Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. + [Parameter(ParameterSetName='B',Mandatory=$true)] + [String[]]$Headername, + + #Automatically generate property names (P1, P2, P3, ..) instead of the using the values the top row of the sheet. + [Parameter(ParameterSetName='C',Mandatory=$true)] + [switch]$NoHeader, + + [parameter(ParameterSetName='D',Mandatory=$true)] + [Alias('RefObject')] + $ReferenceObject , + [parameter(ParameterSetName='D',Mandatory=$true)] + [Alias('DiffObject')] + $DifferenceObject , + + #File to hold merged data. + [parameter(Mandatory=$true,Position=3)] + [Alias('OutFile')] + $OutputFile , + #Name of worksheet to output - if none specified will use the reference worksheet name. + [Alias('OutSheet')] + $OutputSheetName = "Sheet1", + #Properties to include in the DIFF - supports wildcards, default is "*". + $Property = "*" , + #Properties to exclude from the the search - supports wildcards. + $ExcludeProperty , + #Name of a column which is unique used to pair up rows from the refence and difference side, default is "Name". + $Key = "Name" , + #Sets the font color for the "key" field; this means you can filter by color to get only changed rows. + [System.Drawing.Color]$KeyFontColor = "DarkRed", + #Sets the background color for changed rows. + [System.Drawing.Color]$ChangeBackgroundColor = "Orange", + #Sets the background color for rows in the reference but deleted from the difference sheet. + [System.Drawing.Color]$DeleteBackgroundColor = "LightPink", + #Sets the background color for rows not in the reference but added to the difference sheet. + [System.Drawing.Color]$AddBackgroundColor = "PaleGreen", + #if Specified hides the rows in the spreadsheet that are equal and only shows changes, added or deleted rows. + [switch]$HideEqual , + #If specified outputs the data to the pipeline (you can add -whatif so it the command only outputs to the command) + [switch]$Passthru , + #If specified, opens the output workbook. + [Switch]$Show + ) + +#region Read Excel data + if ($Referencefile -and $Differencefile) { + #if the filenames don't resolve, give up now. + try { $oneFile = ((Resolve-Path -Path $Referencefile -ErrorAction Stop).path -eq (Resolve-Path -Path $Differencefile -ErrorAction Stop).path)} + Catch { Write-Warning -Message "Could not Resolve the filenames." ; return } + + #If we have one file , we must have two different worksheet names. If we have two files worksheet can be a single string or two strings. + if ($onefile -and ( ($WorkSheetName.count -ne 2) -or $WorkSheetName[0] -eq $WorkSheetName[1] ) ) { + Write-Warning -Message "If both the Reference and difference file are the same then worksheet name must provide 2 different names" + return + } + if ($WorkSheetName.count -eq 2) {$worksheet1 = $WorkSheetName[0] ; $WorkSheet2 = $WorkSheetName[1]} + elseif ($WorkSheetName -is [string]) {$worksheet1 = $WorkSheet2 = $WorkSheetName} + else {Write-Warning -Message "You must provide either a single worksheet name or two names." ; return } + + $params= @{ ErrorAction = [System.Management.Automation.ActionPreference]::Stop } + foreach ($p in @("HeaderName","NoHeader","StartRow")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} + try { + $ReferenceObject = Import-Excel -Path $Referencefile -WorksheetName $WorkSheet1 @params + $DifferenceObject = Import-Excel -Path $Differencefile -WorksheetName $WorkSheet2 @Params + } + Catch {Write-Warning -Message "Could not read the worksheet from $Referencefile and/or $Differencefile." ; return } + $firstDataRow = + 1 ; + if ($NoHeader) {$firstDataRow = $Startrow } else {$firstDataRow = $Startrow + 1} + } + else {$firstDataRow = 1 } +#endregion + +#region Set lists of properties and row numbers + #Make a list of properties/headings using the Property (default "*") and ExcludeProperty parameters + $propList = @() + $headings = $ReferenceObject[-1].psobject.Properties.Name # This preserves the sequence - using get-member would sort them alphabetically! + if ($NoHeader -and "Name" -eq $Key) {$Key = "p1"} + if ($headings -notcontains $Key) {Write-Warning -Message "You need to specify one of the headings in the sheet '$worksheet1' as a key." ; return } + foreach ($p in $Property) { $propList += ($headings.where({$_ -like $p}) )} + foreach ($p in $ExcludeProperty) { $propList = $propList.where({$_ -notlike $p}) } + if ($propList -notcontains $Key) { $propList += $Key} #If $key isn't one of the headings we will have bailed by now + $propList = $propList | Select-Object -Unique #so, prolist must contain at least $key if nothing else + #Build the list of the properties to output, in order. + $diffpart = @() + $refpart = @() + foreach ($p in $proplist.Where({$key -ne $_}) ) {$refPart += "<=$p" ; $diffPart += "=>$p" } + $outputProps = @($key) + $refpart + $diffpart + #Key will go in column A, last reference column will be A if there is one property, B if there are two, C if theere are 3 etc + $lastRefCol = [char](64 + $propList.count) + #First difference column will be the next one (we'll trap the case of only having the key later) + $FirstDiffCol = [char](65 + $propList.count) + #Last difference column will be A if there is one property, C if there are two, E if there are 3 + $lastDiffCol = [char](64 + 2 * $propList.count) + + #Add RowNumber to every row + #If one sheet has extra rows we can get a single "==" result from compare, but with the row from the reference sheet + #but the row in the other sheet might so we will look up the row number from the key field build a hash table for that + + $Rowhash = @{} + $i = $firstDataRow ; foreach ($row in $ReferenceObject) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++) -Force} + $i = $firstDataRow ; foreach ($row in $DifferenceObject) {Add-Member -InputObject $row -MemberType NoteProperty -Name "_Row" -Value ($i ++) -Force + $Rowhash[$row.$key] = $row._row + } +#endregion + + $expandedDiff = Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject -Property $propList -PassThru -IncludeEqual | + Group-Object -Property $key | ForEach-Object { + #The value of the key column is the name of the group. + $keyval = $_.name + #we're going to create a custom object from a hash table. We want the fields to be ordered + $hash = [ordered]@{} + foreach ($result in $_.Group) { + if ($result.SideIndicator -ne "=>") {$hash["Row"] = $Rowhash[$keyval] + $hash[$key] = $keyval + #For all the other fields we care about, create <=FieldName and/or =>FieldName + foreach ($p in $propList.Where({$_ -ne $key})) { + if ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} + else {$hash[($result.SideIndicator+$P)] =$result.$P} + } + } + [Pscustomobject]$hash + } | Sort-Object -Property "row") {$expandedDiff[$i].">row" = $expandedDiff[$i-1].">row" } } + #Sort by difference row number, and fill in any blanks in the reference-row column + $expandedDiff = $expandedDiff | Sort-Object -Property ">row" + for ($i = 1; $i -lt $expandedDiff.Count; $i++) {if (-not $expandedDiff[$i]."" ) { + $range = $ws.Dimension -replace "\d+", ($i + 2 ) + Set-Format -WorkSheet $ws -Range $range -BackgroundColor $ChangeBackgroundColor + } + elseif ( $expandedDiff[$i].side -eq "<=" ) { + $range = "A" + ($i + 2 ) + ":" + $lastRefCol + ($i + 2 ) + Set-Format -WorkSheet $ws -Range $range -BackgroundColor $DeleteBackgroundColor + } + elseif ( $expandedDiff[$i].side -eq "=>" ) { + if ($propList.count -gt 1) { + $range = $FirstDiffCol + ($i + 2 ) + ":" + $lastDiffCol + ($i + 2 ) + Set-Format -WorkSheet $ws -Range $range -BackgroundColor $AddBackgroundColor + } + Set-Format -WorkSheet $ws -Range ("A" + ($i + 2 )) -BackgroundColor $AddBackgroundColor + } + } + Close-ExcelPackage -ExcelPackage $xl -Show:$Show + } + if ($PassThru) {$expandedDiff} +} \ No newline at end of file From 807990c4baffa817236d6f74f85f4dfee639cc8b Mon Sep 17 00:00:00 2001 From: jhoneill Date: Mon, 14 May 2018 14:21:49 +0100 Subject: [PATCH 10/17] Fixed an error in Compare-Worksheet when only 1 row is different --- compare-WorkSheet.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index 85d91d8..e39a898 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -45,7 +45,7 @@ This version of the previous command lightlights all the cells in lightgray and then sets the changed rows back to white; only the unchanged rows are highlighted #> -[cmdletbinding(DefaultParameterSetName)] + [cmdletbinding(DefaultParameterSetName)] Param( #First file to compare [parameter(Mandatory=$true,Position=0)] @@ -135,7 +135,7 @@ if ($ExcludeDifferent -and -not $IncludeEqual) {$IncludeEqual = $true} #Do the comparison and add file,sheet and row to the result - these are prefixed with "_" to show they are added the addition will fail if the sheet has these properties so split the operations - $diff = Compare-Object -ReferenceObject $Sheet1 -DifferenceObject $Sheet2 -Property $propList -PassThru -IncludeEqual:$IncludeEqual -ExcludeDifferent:$ExcludeDifferent | + [PSCustomObject[]]$diff = Compare-Object -ReferenceObject $Sheet1 -DifferenceObject $Sheet2 -Property $propList -PassThru -IncludeEqual:$IncludeEqual -ExcludeDifferent:$ExcludeDifferent | Sort-Object -Property "_Row","File" #if BackgroundColor was specified, set it on extra or extra or changed rows From 39a68e71c49a55217ee1e066c722e933cf67a30b Mon Sep 17 00:00:00 2001 From: jhoneill Date: Sat, 26 May 2018 22:24:36 +0100 Subject: [PATCH 11/17] Tidying of case, parameter clarity, removal of aliasess. Added timeout to send-SqlDataToExcel Added Merge WorkSheet Fixed bugs in Compare-Worksheet --- AddConditionalFormatting.ps1 | 7 +- ColorCompletion.ps1 | 4 + Examples/SQL+FillColumns+Pivot/Example.ps1 | 2 +- Export-Excel.ps1 | 12 +- Export-ExcelSheet.ps1 | 2 +- Export-charts.ps1 | 16 +- ImportExcel.psd1 | 2 +- ImportExcel.psm1 | 555 ++++++++++----------- InstallModule.ps1 | 1 + Merge-worksheet.ps1 | 275 ++++++++++ README.md | 3 + Send-SqlDataToExcel.ps1 | 30 +- compare-WorkSheet.ps1 | 6 +- 13 files changed, 586 insertions(+), 329 deletions(-) create mode 100644 Merge-worksheet.ps1 diff --git a/AddConditionalFormatting.ps1 b/AddConditionalFormatting.ps1 index fc89b8f..00b4c6a 100644 --- a/AddConditionalFormatting.ps1 +++ b/AddConditionalFormatting.ps1 @@ -85,11 +85,12 @@ [switch]$StrikeThru ) #Allow add conditional formatting to work like Set-Format (with single ADDRESS parameter) split it to get worksheet and Range of cells. - If ($Address -and -not $WorkSheet -and -not $Range) { + if ($Address -and -not $WorkSheet -and -not $Range) { $WorkSheet = $Address.Worksheet[0] $Range = $Address.Address - } - If ($ThreeIconsSet) {$rule = $WorkSheet.ConditionalFormatting.AddThreeIconSet($Range , $ThreeIconsSet)} + } + if ($rule -eq "Databar" -and -not $databarColor) {Write-Warning -Message "-DatabarColor must be specified for the Databar rule type" } + if ( $ThreeIconsSet) {$rule = $WorkSheet.ConditionalFormatting.AddThreeIconSet($Range , $ThreeIconsSet)} elseif ($FourIconsSet) {$rule = $WorkSheet.ConditionalFormatting.AddFourIconSet( $Range , $FourIconsSet) } elseif ($FiveIconsSet) {$rule = $WorkSheet.ConditionalFormatting.AddFiveIconSet( $Range , $IconType) } elseif ($DataBarColor) {$rule = $WorkSheet.ConditionalFormatting.AddDatabar( $Range , $DataBarColor) } diff --git a/ColorCompletion.ps1 b/ColorCompletion.ps1 index 12b6f81..bfd811a 100644 --- a/ColorCompletion.ps1 +++ b/ColorCompletion.ps1 @@ -14,6 +14,10 @@ if (Get-Command -Name register-argumentCompleter -ErrorAction SilentlyContinue) Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName FontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Compare-Worksheet -ParameterName TabColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-Worksheet -ParameterName AddBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-Worksheet -ParameterName ChangeBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-Worksheet ` -ParameterName DeleteBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-Worksheet -ParameterName KeyFontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName FontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion diff --git a/Examples/SQL+FillColumns+Pivot/Example.ps1 b/Examples/SQL+FillColumns+Pivot/Example.ps1 index 79b8e8b..0c03515 100644 --- a/Examples/SQL+FillColumns+Pivot/Example.ps1 +++ b/Examples/SQL+FillColumns+Pivot/Example.ps1 @@ -1,4 +1,4 @@ - ipmo C:\Users\mcp\Documents\GitHub\ImportExcel\ImportExcel.psd1 -Force -Verbose + Import-Module -name ImportExcel -Force -Verbose $sql = @" SELECT rootfile.baseName , rootfile.extension , Image.fileWidth AS width , image.fileHeight AS height , diff --git a/Export-Excel.ps1 b/Export-Excel.ps1 index 1f6e2f3..bbf3c3a 100644 --- a/Export-Excel.ps1 +++ b/Export-Excel.ps1 @@ -18,7 +18,7 @@ .PARAMETER TargetData Data to insert onto the worksheet - this is often provided from the pipeline. .PARAMETER ExcludeProperty - Speficies properties which may exist in the target data but should not be placed on the worksheet + Specifies properties which may exist in the target data but should not be placed on the worksheet .PARAMETER Title Text of a title to be placed in Cell A1 .PARAMETER TitleBold @@ -32,9 +32,9 @@ .PARAMETER IncludePivotTable Adds a Pivot table using the data in the worksheet .PARAMETER PivotRows - Name(s) columns from the spreadhseet which will prvoide the row name(s) in the pivot table + Name(s) columns from the spreadhseet which will provide the row name(s) in the pivot table .PARAMETER PivotColumns - Name(s) columns from the spreadhseet which will prvoide the Column name(s) in the pivot table + Name(s) columns from the spreadhseet which will provide the Column name(s) in the pivot table .PARAMETER PivotData Hash table in the form ColumnName = Average|Count|CountNums|Max|Min|Product|None|StdDev|StdDevP|Sum|Var|VarP to provide the data in the Pivot table .PARAMETER PivotTableDefinition, @@ -62,13 +62,13 @@ .PARAMETER TableStyle Selects the style for the named table - defaults to 'Medium6' .PARAMETER ExcelChartDefinition - A hash table containing ChartType, Title, NoLegend, ShowCategory, ShowPecent, Yrange, Xrange and SeriesHeader for one or more [non-pivot] charts + A hash table containing ChartType, Title, NoLegend, ShowCategory, ShowPercent, Yrange, Xrange and SeriesHeader for one or more [non-pivot] charts .PARAMETER HideSheet Name(s) of Sheet(s) to hide in the workbook .PARAMETER KillExcel Closes Excel - prevents errors writing to the file because Excel has it open .PARAMETER AutoNameRange - Makes each column a named range + Makes each column a named range .PARAMETER StartRow Row to start adding data. 1 by default. Row 1 will contain the title if any. Then headers will appear (Unless -No header is specified) then the data appears .PARAMETER StartColumn @@ -1056,4 +1056,4 @@ function New-PivotTableDefinition { $parameters.Remove('PivotTableName') @{$PivotTableName = $parameters} -} \ No newline at end of file +} diff --git a/Export-ExcelSheet.ps1 b/Export-ExcelSheet.ps1 index fc095e7..b703d0f 100644 --- a/Export-ExcelSheet.ps1 +++ b/Export-ExcelSheet.ps1 @@ -19,7 +19,7 @@ function Export-ExcelSheet { $xl = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Path $workbook = $xl.Workbook - $targetSheets = $workbook.Worksheets | Where {$_.Name -Match $SheetName} + $targetSheets = $workbook.Worksheets | Where-Object {$_.Name -Match $SheetName} $params = @{} + $PSBoundParameters $params.Remove("OutputPath") diff --git a/Export-charts.ps1 b/Export-charts.ps1 index 196d8e5..27ec7ab 100644 --- a/Export-charts.ps1 +++ b/Export-charts.ps1 @@ -14,7 +14,7 @@ Param ( #Path to the Excel file whose chars we will export. $Path = "C:\Users\public\Documents\stats.xlsx", #If specified, output file objects representing the image files. - [switch]$passthru, + [switch]$Passthru, #Format to write - JPG by default [ValidateSet("JPG","PNG","GIF")] $OutputType = "JPG", @@ -23,15 +23,14 @@ Param ( ) #if no output folder was specified, set destination to the folder where the Excel file came from -if (-not $Destination) {$Destination = Split-Path -Path $path -Parent } +if (-not $Destination) {$Destination = Split-Path -Path $Path -Parent } #Call up Excel and tell it to open the file. try { $excelApp = New-Object -ComObject "Excel.Application" } catch { Write-Warning "Could not start Excel application - which usually means it is not installed." ; return } -try { $excelWorkBook = $excelApp.Workbooks.Open($path) } -catch { Write-Warning "Could not start Excel application - which usually means it is not installed." ; return } - +try { $excelWorkBook = $excelApp.Workbooks.Open($Path) } +catch { Write-Warning -Message "Could not Open $Path." ; return } #For each worksheet, for each chart, jump to the chart, create a filename of "WorksheetName_ChartTitle.jpg", and export the file. foreach ($excelWorkSheet in $excelWorkBook.Worksheets) { @@ -41,11 +40,12 @@ foreach ($excelWorkSheet in $excelWorkBook.Worksheets) { $excelApp.Goto($excelchart.TopLeftCell,$true) $imagePath = Join-Path -Path $Destination -ChildPath ($excelWorkSheet.Name + "_" + ($excelchart.Chart.ChartTitle.Text -split "\s\d\d:\d\d,")[0] + ".$OutputType") if ( $excelchart.Chart.Export($imagePath, $OutputType, $false) ) { # Export returs true/false for success/failure - if ($passThru) {Get-Item -Path $imagePath } # when succesful return a file object (-passthru) or print a verbose message, write warning for any failures + if ($Passthru) {Get-Item -Path $imagePath } # when succesful return a file object (-Passthru) or print a verbose message, write warning for any failures else {Write-Verbose -Message "Exported $imagePath"} } else {Write-Warning -Message "Failure exporting $imagePath" } } } - -$excelApp.Quit() \ No newline at end of file +$excelApp.DisplayAlerts = $false +$excelWorkBook.Close($false,$null,$null) +$excelApp.Quit() diff --git a/ImportExcel.psd1 b/ImportExcel.psd1 index 807d835..169f557 100644 --- a/ImportExcel.psd1 +++ b/ImportExcel.psd1 @@ -4,7 +4,7 @@ RootModule = 'ImportExcel.psm1' # Version number of this module. -ModuleVersion = '4.0.13' +ModuleVersion = '4.0.14' # ID used to uniquely identify this module GUID = '60dd4136-feff-401a-ba27-a84458c57ede' diff --git a/ImportExcel.psm1 b/ImportExcel.psm1 index 8b2b355..e80ef95 100644 --- a/ImportExcel.psm1 +++ b/ImportExcel.psm1 @@ -1,241 +1,243 @@ -Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" - -. $PSScriptRoot\AddConditionalFormatting.ps1 -. $PSScriptRoot\Charting.ps1 -. $PSScriptRoot\ColorCompletion.ps1 -. $PSScriptRoot\ConvertExcelToImageFile.ps1 -. $PSScriptRoot\Compare-WorkSheet.ps1 -. $PSScriptRoot\ConvertFromExcelData.ps1 -. $PSScriptRoot\ConvertFromExcelToSQLInsert.ps1 -. $PSScriptRoot\ConvertToExcelXlsx.ps1 -. $PSScriptRoot\Copy-ExcelWorkSheet.ps1 -. $PSScriptRoot\Export-Excel.ps1 -. $PSScriptRoot\Export-ExcelSheet.ps1 -. $PSScriptRoot\Get-ExcelColumnName.ps1 -. $PSScriptRoot\Get-ExcelSheetInfo.ps1 -. $PSScriptRoot\Get-ExcelWorkbookInfo.ps1 -. $PSScriptRoot\Get-HtmlTable.ps1 -. $PSScriptRoot\Get-Range.ps1 -. $PSScriptRoot\Get-XYRange.ps1 -. $PSScriptRoot\Import-Html.ps1 -. $PSScriptRoot\InferData.ps1 -. $PSScriptRoot\Invoke-Sum.ps1 -. $PSScriptRoot\New-ConditionalFormattingIconSet.ps1 -. $PSScriptRoot\New-ConditionalText.ps1 -. $PSScriptRoot\New-ExcelChart.ps1 -. $PSScriptRoot\New-PSItem.ps1 -. $PSScriptRoot\Open-ExcelPackage.ps1 -. $PSScriptRoot\Pivot.ps1 -. $PSScriptRoot\Send-SQLDataToExcel.ps1 -. $PSScriptRoot\Set-CellStyle.ps1 -. $PSScriptRoot\Set-Column.ps1 -. $PSScriptRoot\Set-Row.ps1 -. $PSScriptRoot\SetFormat.ps1 -. $PSScriptRoot\TrackingUtils.ps1 -. $PSScriptRoot\Update-FirstObjectProperties.ps1 +#region import everything we need + Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" + . $PSScriptRoot\AddConditionalFormatting.ps1 + . $PSScriptRoot\Charting.ps1 + . $PSScriptRoot\ColorCompletion.ps1 + . $PSScriptRoot\ConvertExcelToImageFile.ps1 + . $PSScriptRoot\Compare-WorkSheet.ps1 + . $PSScriptRoot\ConvertFromExcelData.ps1 + . $PSScriptRoot\ConvertFromExcelToSQLInsert.ps1 + . $PSScriptRoot\ConvertToExcelXlsx.ps1 + . $PSScriptRoot\Copy-ExcelWorkSheet.ps1 + . $PSScriptRoot\Export-Excel.ps1 + . $PSScriptRoot\Export-ExcelSheet.ps1 + . $PSScriptRoot\Get-ExcelColumnName.ps1 + . $PSScriptRoot\Get-ExcelSheetInfo.ps1 + . $PSScriptRoot\Get-ExcelWorkbookInfo.ps1 + . $PSScriptRoot\Get-HtmlTable.ps1 + . $PSScriptRoot\Get-Range.ps1 + . $PSScriptRoot\Get-XYRange.ps1 + . $PSScriptRoot\Import-Html.ps1 + . $PSScriptRoot\InferData.ps1 + . $PSScriptRoot\Invoke-Sum.ps1 + . $PSScriptRoot\Merge-Worksheet.ps1 + . $PSScriptRoot\New-ConditionalFormattingIconSet.ps1 + . $PSScriptRoot\New-ConditionalText.ps1 + . $PSScriptRoot\New-ExcelChart.ps1 + . $PSScriptRoot\New-PSItem.ps1 + . $PSScriptRoot\Open-ExcelPackage.ps1 + . $PSScriptRoot\Pivot.ps1 + . $PSScriptRoot\Send-SQLDataToExcel.ps1 + . $PSScriptRoot\Set-CellStyle.ps1 + . $PSScriptRoot\Set-Column.ps1 + . $PSScriptRoot\Set-Row.ps1 + . $PSScriptRoot\SetFormat.ps1 + . $PSScriptRoot\TrackingUtils.ps1 + . $PSScriptRoot\Update-FirstObjectProperties.ps1 -New-Alias -Name Use-ExcelData -Value "ConvertFrom-ExcelData" -Force + New-Alias -Name Use-ExcelData -Value "ConvertFrom-ExcelData" -Force -if ($PSVersionTable.PSVersion.Major -ge 5) { - . $PSScriptRoot\Plot.ps1 + if ($PSVersionTable.PSVersion.Major -ge 5) { + . $PSScriptRoot\Plot.ps1 - Function New-Plot { - Param() + Function New-Plot { + Param() + + [PSPlot]::new() + } - [PSPlot]::new() } - -} -else { - Write-Warning 'PowerShell 5 is required for plot.ps1' - Write-Warning 'PowerShell Excel is ready, except for that functionality' -} - + else { + Write-Warning 'PowerShell 5 is required for plot.ps1' + Write-Warning 'PowerShell Excel is ready, except for that functionality' + } +#endregion Function Import-Excel { - <# - .SYNOPSIS - Create custom objects from the rows in an Excel worksheet. + <# + .SYNOPSIS + Create custom objects from the rows in an Excel worksheet. - .DESCRIPTION - The Import-Excel cmdlet creates custom objects from the rows in an Excel worksheet. Each row represents one object. All of this is possible without installing Microsoft Excel and by using the .NET library ‘EPPLus.dll’. + .DESCRIPTION + The Import-Excel cmdlet creates custom objects from the rows in an Excel worksheet. Each row represents one object. All of this is possible without installing Microsoft Excel and by using the .NET library ‘EPPLus.dll’. - By default, the property names of the objects are retrieved from the column headers. Because an object cannot have a blanc property name, only columns with column headers will be imported. + By default, the property names of the objects are retrieved from the column headers. Because an object cannot have a blanc property name, only columns with column headers will be imported. - If the default behavior is not desired and you want to import the complete worksheet ‘as is’, the parameter ‘-NoHeader’ can be used. In case you want to provide your own property names, you can use the parameter ‘-HeaderName’. + If the default behavior is not desired and you want to import the complete worksheet ‘as is’, the parameter ‘-NoHeader’ can be used. In case you want to provide your own property names, you can use the parameter ‘-HeaderName’. - .PARAMETER Path - Specifies the path to the Excel file. + .PARAMETER Path + Specifies the path to the Excel file. - .PARAMETER WorksheetName - Specifies the name of the worksheet in the Excel workbook to import. By default, if no name is provided, the first worksheet will be imported. + .PARAMETER WorksheetName + Specifies the name of the worksheet in the Excel workbook to import. By default, if no name is provided, the first worksheet will be imported. - .PARAMETER DataOnly - Import only rows and columns that contain data, empty rows and empty columns are not imported. + .PARAMETER DataOnly + Import only rows and columns that contain data, empty rows and empty columns are not imported. - .PARAMETER HeaderName - Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. + .PARAMETER HeaderName + Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. - In case you provide less header names than there is data in the worksheet, then only the data with a corresponding header name will be imported and the data without header name will be disregarded. + In case you provide less header names than there is data in the worksheet, then only the data with a corresponding header name will be imported and the data without header name will be disregarded. - In case you provide more header names than there is data in the worksheet, then all data will be imported and all objects will have all the property names you defined in the header names. As such, the last properties will be blanc as there is no data for them. + In case you provide more header names than there is data in the worksheet, then all data will be imported and all objects will have all the property names you defined in the header names. As such, the last properties will be blanc as there is no data for them. - .PARAMETER NoHeader - Automatically generate property names (P1, P2, P3, ..) instead of the ones defined in the column headers of the TopRow. + .PARAMETER NoHeader + Automatically generate property names (P1, P2, P3, ..) instead of the ones defined in the column headers of the TopRow. - This switch is best used when you want to import the complete worksheet ‘as is’ and are not concerned with the property names. + This switch is best used when you want to import the complete worksheet ‘as is’ and are not concerned with the property names. - .PARAMETER StartRow - The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. + .PARAMETER StartRow + The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. - When the parameters ‘-NoHeader’ and ‘-HeaderName’ are not provided, this row will contain the column headers that will be used as property names. When one of both parameters are provided, the property names are automatically created and this row will be treated as a regular row containing data. + When the parameters ‘-NoHeader’ and ‘-HeaderName’ are not provided, this row will contain the column headers that will be used as property names. When one of both parameters are provided, the property names are automatically created and this row will be treated as a regular row containing data. - .PARAMETER Password - Accepts a string that will be used to open a password protected Excel file. + .PARAMETER Password + Accepts a string that will be used to open a password protected Excel file. + + .EXAMPLE + Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the column names defined in the first row. In case a column doesn’t have a column header (usually in row 1 when ‘-StartRow’ is not used), then the unnamed columns will be skipped and the data in those columns will not be imported. + + ---------------------------------------------- + | File: Movies.xlsx - Sheet: Actors | + ---------------------------------------------- + | A B C | + |1 First Name Address | + |2 Chuck Norris California | + |3 Jean-Claude Vandamme Brussels | + ---------------------------------------------- + + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors + + First Name: Chuck + Address : California + + First Name: Jean-Claude + Address : Brussels + + Notice that column 'B' is not imported because there's no value in cell 'B1' that can be used as property name for the objects. + + .EXAMPLE + Import the complete Excel worksheet ‘as is’ by using the ‘-NoHeader’ switch. One object is created for each row. The property names of the objects will be automatically generated (P1, P2, P3, ..). + + ---------------------------------------------- + | File: Movies.xlsx - Sheet: Actors | + ---------------------------------------------- + | A B C | + |1 First Name Address | + |2 Chuck Norris California | + |3 Jean-Claude Vandamme Brussels | + ---------------------------------------------- + + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -NoHeader + + P1: First Name + P2: + P3: Address + + P1: Chuck + P2: Norris + P3: California + + P1: Jean-Claude + P2: Vandamme + P3: Brussels + + Notice that the column header (row 1) is imported as an object too. .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the column names defined in the first row. In case a column doesn’t have a column header (usually in row 1 when ‘-StartRow’ is not used), then the unnamed columns will be skipped and the data in those columns will not be imported. + Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the names defined in the parameter ‘-HeaderName’. The properties are named starting from the most left column (A) to the right. In case no value is present in one of the columns, that property will have an empty value. - ---------------------------------------------- - | File: Movies.xlsx - Sheet: Actors | - ---------------------------------------------- - | A B C | - |1 First Name Address | - |2 Chuck Norris California | - |3 Jean-Claude Vandamme Brussels | - ---------------------------------------------- + ---------------------------------------------------------- + | File: Movies.xlsx - Sheet: Movies | + ---------------------------------------------------------- + | A B C D | + |1 The Bodyguard 1992 9 | + |2 The Matrix 1999 8 | + |3 | + |4 Skyfall 2012 9 | + ---------------------------------------------------------- - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies -HeaderName 'Movie name', 'Year', 'Rating', 'Genre' - First Name: Chuck - Address : California + Movie name: The Bodyguard + Year : 1992 + Rating : 9 + Genre : - First Name: Jean-Claude - Address : Brussels + Movie name: The Matrix + Year : 1999 + Rating : 8 + Genre : - Notice that column 'B' is not imported because there's no value in cell 'B1' that can be used as property name for the objects. + Movie name: + Year : + Rating : + Genre : + + Movie name: Skyfall + Year : 2012 + Rating : 9 + Genre : + + Notice that empty rows are imported and that data for the property 'Genre' is not present in the worksheet. As such, the 'Genre' property will be blanc for all objects. .EXAMPLE - Import the complete Excel worksheet ‘as is’ by using the ‘-NoHeader’ switch. One object is created for each row. The property names of the objects will be automatically generated (P1, P2, P3, ..). + Import data from an Excel worksheet. One object is created for each row. The property names of the objects are automatically generated by using the switch ‘-NoHeader’ (P1, P@, P#, ..). The switch ‘-DataOnly’ will speed up the import because empty rows and empty columns are not imported. - ---------------------------------------------- - | File: Movies.xlsx - Sheet: Actors | - ---------------------------------------------- - | A B C | - |1 First Name Address | - |2 Chuck Norris California | - |3 Jean-Claude Vandamme Brussels | - ---------------------------------------------- + ---------------------------------------------------------- + | File: Movies.xlsx - Sheet: Movies | + ---------------------------------------------------------- + | A B C D | + |1 The Bodyguard 1992 9 | + |2 The Matrix 1999 8 | + |3 | + |4 Skyfall 2012 9 | + ---------------------------------------------------------- - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -NoHeader + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies –NoHeader -DataOnly - P1: First Name - P2: - P3: Address + P1: The Bodyguard + P2: 1992 + P3: 9 - P1: Chuck - P2: Norris - P3: California + P1: The Matrix + P2: 1999 + P3: 8 - P1: Jean-Claude - P2: Vandamme - P3: Brussels + P1: Skyfall + P2: 2012 + P3: 9 - Notice that the column header (row 1) is imported as an object too. + Notice that empty rows and empty columns are not imported. - .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the names defined in the parameter ‘-HeaderName’. The properties are named starting from the most left column (A) to the right. In case no value is present in one of the columns, that property will have an empty value. +.EXAMPLE + Import data from an Excel worksheet. One object is created for each row. The property names are provided with the ‘-HeaderName’ parameter. The import will start from row 2 and empty columns and rows are not imported. - ---------------------------------------------------------- - | File: Movies.xlsx - Sheet: Movies | - ---------------------------------------------------------- - | A B C D | - |1 The Bodyguard 1992 9 | - |2 The Matrix 1999 8 | - |3 | - |4 Skyfall 2012 9 | - ---------------------------------------------------------- + ---------------------------------------------------------- + | File: Movies.xlsx - Sheet: Actors | + ---------------------------------------------------------- + | A B C D | + |1 Chuck Norris California | + |2 | + |3 Jean-Claude Vandamme Brussels | + ---------------------------------------------------------- - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies -HeaderName 'Movie name', 'Year', 'Rating', 'Genre' + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -DataOnly -HeaderName 'FirstName', 'SecondName', 'City' –StartRow 2 - Movie name: The Bodyguard - Year : 1992 - Rating : 9 - Genre : + FirstName : Jean-Claude + SecondName: Vandamme + City : Brussels - Movie name: The Matrix - Year : 1999 - Rating : 8 - Genre : + Notice that only 1 object is imported with only 3 properties. Column B and row 2 are empty and have been disregarded by using the switch '-DataOnly'. The property names have been named with the values provided with the parameter '-HeaderName'. Row number 1 with ‘Chuck Norris’ has not been imported, because we started the import from row 2 with the parameter ‘-StartRow 2’. - Movie name: - Year : - Rating : - Genre : + .LINK + https://github.com/dfinke/ImportExcel - Movie name: Skyfall - Year : 2012 - Rating : 9 - Genre : - - Notice that empty rows are imported and that data for the property 'Genre' is not present in the worksheet. As such, the 'Genre' property will be blanc for all objects. - - .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names of the objects are automatically generated by using the switch ‘-NoHeader’ (P1, P@, P#, ..). The switch ‘-DataOnly’ will speed up the import because empty rows and empty columns are not imported. - - ---------------------------------------------------------- - | File: Movies.xlsx - Sheet: Movies | - ---------------------------------------------------------- - | A B C D | - |1 The Bodyguard 1992 9 | - |2 The Matrix 1999 8 | - |3 | - |4 Skyfall 2012 9 | - ---------------------------------------------------------- - - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies –NoHeader -DataOnly - - P1: The Bodyguard - P2: 1992 - P3: 9 - - P1: The Matrix - P2: 1999 - P3: 8 - - P1: Skyfall - P2: 2012 - P3: 9 - - Notice that empty rows and empty columns are not imported. - - .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names are provided with the ‘-HeaderName’ parameter. The import will start from row 2 and empty columns and rows are not imported. - - ---------------------------------------------------------- - | File: Movies.xlsx - Sheet: Actors | - ---------------------------------------------------------- - | A B C D | - |1 Chuck Norris California | - |2 | - |3 Jean-Claude Vandamme Brussels | - ---------------------------------------------------------- - - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -DataOnly -HeaderName 'FirstName', 'SecondName', 'City' –StartRow 2 - - FirstName : Jean-Claude - SecondName: Vandamme - City : Brussels - - Notice that only 1 object is imported with only 3 properties. Column B and row 2 are empty and have been disregarded by using the switch '-DataOnly'. The property names have been named with the values provided with the parameter '-HeaderName'. Row number 1 with ‘Chuck Norris’ has not been imported, because we started the import from row 2 with the parameter ‘-StartRow 2’. - - .LINK - https://github.com/dfinke/ImportExcel - - .NOTES - #> + .NOTES + #> [CmdLetBinding(DefaultParameterSetName)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] Param ( [Alias('FullName')] [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline, Position=0, Mandatory)] @@ -251,34 +253,13 @@ Function Import-Excel { [Switch]$NoHeader, [Alias('HeaderRow','TopRow')] [ValidateRange(1, 9999)] - [Int]$StartRow, + [Int]$StartRow = 1, [Switch]$DataOnly, [ValidateNotNullOrEmpty()] [String]$Password ) - Begin { - Function Add-Property { - <# - .SYNOPSIS - Add the property name and value to the hashtable that will create a new object for each row. - #> - - Param ( - [Parameter(Mandatory)] - [String]$Name, - $Value - ) - - Try { - $NewRow.$Name = $Value - Write-Verbose "Import cell '$($Worksheet.Cells[$R, $P.Column].Address)' with property name '$Name' and value '$Value'" - } - Catch { - throw "Failed adding the property name '$Name' with value '$Value': $_" - } - } - + $sw = [System.Diagnostics.Stopwatch]::StartNew() Function Get-PropertyNames { <# .SYNOPSIS @@ -313,7 +294,7 @@ Function Import-Excel { } foreach ($C in $Columns) { - $Worksheet.Cells[$StartRow,$C] | where {$_.Value} | Select-Object @{N='Column'; E={$C}}, Value + $Worksheet.Cells[$StartRow,$C] | Where-Object {$_.Value} | Select-Object @{N='Column'; E={$C}}, Value } } } @@ -328,24 +309,24 @@ Function Import-Excel { #region Open file $Path = (Resolve-Path $Path).ProviderPath Write-Verbose "Import Excel workbook '$Path' with worksheet '$Worksheetname'" - + $Stream = New-Object -TypeName System.IO.FileStream -ArgumentList $Path, 'Open', 'Read', 'ReadWrite' - + if ($Password) { $Excel = New-Object -TypeName OfficeOpenXml.ExcelPackage - - Try { - $Excel.Load($Stream,$Password) - } - Catch { - throw "Password '$Password' is not correct." - } + + Try { + $Excel.Load($Stream,$Password) + } + Catch { + throw "Password '$Password' is not correct." + } } else { $Excel = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Stream } #endregion - + #region Select worksheet if ($WorksheetName) { if (-not ($Worksheet = $Excel.Workbook.Worksheets[$WorkSheetName])) { @@ -356,74 +337,59 @@ Function Import-Excel { $Worksheet = $Excel.Workbook.Worksheets | Select-Object -First 1 } #endregion - - #region Set the top row - if (((-not ($NoHeader -or $HeaderName)) -and ($StartRow -eq 0))) { - $StartRow = 1 - } - #endregion - - if (-not ($AllCells = $Worksheet.Cells | where {($_.Start.Row -ge $StartRow)})) { - Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' is empty after StartRow '$StartRow'" + Write-Debug $sw.Elapsed.TotalMilliseconds + #region Get rows and columns + #If we are doing dataonly it is quicker to work out which rows to ignore before processing the cells. + if ($DataOnly) { + #If we are using headers startrow will be the headerrow so examine data from startRow + 1, + if ($NoHeader) {$range = "A" + ($StartRow ) + ":" + $Worksheet.Dimension.End.Address } + else {$range = "A" + ($StartRow + 1 ) + ":" + $Worksheet.Dimension.End.Address } + #We're going to look at every cell and build 2 hash tables holding rows & columns which contain data. + #Want to Avoid 'select unique' operations & large Sorts, becuse time time taken increases with square + #of number of items (PS uses heapsort at large size). Instead keep a list of what we have seen, + #using Hash tables: "we've seen it" is all we need, no need to worry about "seen it before" / "Seen it many times" + $colHash = @{} + $rowHash = @{} + foreach ($cell in $Worksheet.Cells[$range]) { + if ($cell.Value -ne $null) {$colHash[$cell.Start.Column]=1; $rowHash[$cell.Start.row]=1 } + } + $rows = ($StartRow..($Worksheet.Dimension.End.Row)).Where({$rowHash[$_]}) + $columns = (1..($Worksheet.Dimension.End.Column) ).Where({$colHash[$_]}) } else { - #region Get rows and columns - if ($DataOnly) { - $CellsWithValues = $AllCells | where {$_.Value} - - $Columns = $CellsWithValues.Start.Column | Sort-Object -Unique - $Rows = $CellsWithValues.Start.Row | Sort-Object -Unique - } - else { - $LastColumn = $AllCells.Start.Column | Sort-Object -Unique | Select-Object -Last 1 - $Columns = 1..$LastColumn - - $LastRow = $AllCells.Start.Row | Sort-Object -Unique | Select-Object -Last 1 - $Rows = $StartRow..$LastRow | where {($_ -ge $StartRow) -and ($_ -gt 0)} - } - #endregion - - #region Create property names - if ((-not $Columns) -or (-not ($PropertyNames = Get-PropertyNames -Columns $Columns -StartRow $StartRow))) { - throw "No column headers found on top row '$StartRow'. If column headers in the worksheet are not a requirement then please use the '-NoHeader' or '-HeaderName' parameter." - } - - if ($Duplicates = $PropertyNames | Group-Object Value | where Count -GE 2) { - throw "Duplicate column headers found on row '$StartRow' in columns '$($Duplicates.Group.Column)'. Column headers must be unique, if this is not a requirement please use the '-NoHeader' or '-HeaderName' parameter." - } - #endregion - - #region Filter out rows with data in columns that don't have a column header - if ($DataOnly -and (-not $NoHeader)) { - $Rows = $CellsWithValues.Start | where {$PropertyNames.Column -contains $_.Column} | - Sort-Object Row -Unique | Select-Object -ExpandProperty Row - } - #endregion - - #region Filter out the top row when it contains column headers - if (-not ($NoHeader -or $HeaderName)) { - $Rows = $Rows | where {$_ -gt $StartRow} - } - #endregion - - if (-not $Rows) { - Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' contains no data in the rows after top row '$StartRow'" - } - else { - #region Create one object per row - foreach ($R in $Rows) { - Write-Verbose "Import row '$R'" - $NewRow = [Ordered]@{} - - foreach ($P in $PropertyNames) { - Add-Property -Name $P.Value -Value $Worksheet.Cells[$R, $P.Column].Value - } - - [PSCustomObject]$NewRow - } - #endregion - } + $Columns = ($Worksheet.Dimension.Start.Column)..($Worksheet.Dimension.End.Column) + if ($NoHeader) {$Rows = ( $StartRow)..($Worksheet.Dimension.End.Row) } + else {$Rows = (1 + $StartRow)..($Worksheet.Dimension.End.Row) } } + #endregion + #region Create property names + if ((-not $Columns) -or (-not ($PropertyNames = Get-PropertyNames -Columns $Columns -StartRow $StartRow))) { + throw "No column headers found on top row '$StartRow'. If column headers in the worksheet are not a requirement then please use the '-NoHeader' or '-HeaderName' parameter." + } + if ($Duplicates = $PropertyNames | Group-Object Value | Where-Object Count -GE 2) { + throw "Duplicate column headers found on row '$StartRow' in columns '$($Duplicates.Group.Column)'. Column headers must be unique, if this is not a requirement please use the '-NoHeader' or '-HeaderName' parameter." + } + #endregion + Write-Debug $sw.Elapsed.TotalMilliseconds + if (-not $Rows) { + Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' contains no data in the rows after top row '$StartRow'" + } + else { + #region Create one object per row + foreach ($R in $Rows) { + Write-Verbose "Import row '$R'" + $NewRow = [Ordered]@{} + + foreach ($P in $PropertyNames) { + $NewRow[$P.Value] = $Worksheet.Cells[$R, $P.Column].Value + Write-Verbose "Import cell '$($Worksheet.Cells[$R, $P.Column].Address)' with property name '$($p.Value)' and value '$($Worksheet.Cells[$R, $P.Column].Value)'." + } + + [PSCustomObject]$NewRow + } + #endregion + } + Write-Debug $sw.Elapsed.TotalMilliseconds } Catch { throw "Failed importing the Excel workbook '$Path' with worksheet '$Worksheetname': $_" @@ -499,7 +465,7 @@ function ConvertFrom-ExcelSheet { $xl = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $stream $workbook = $xl.Workbook - $targetSheets = $workbook.Worksheets | Where {$_.Name -like $SheetName} + $targetSheets = $workbook.Worksheets | Where-Object {$_.Name -like $SheetName} $params = @{} + $PSBoundParameters $params.Remove("OutputPath") @@ -518,10 +484,11 @@ function ConvertFrom-ExcelSheet { $stream.Close() $stream.Dispose() - $xl.Dispose() + $xl.Dispose() } function Export-MultipleExcelSheets { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] param( [Parameter(Mandatory=$true)] $Path, diff --git a/InstallModule.ps1 b/InstallModule.ps1 index 6877963..87fd591 100644 --- a/InstallModule.ps1 +++ b/InstallModule.ps1 @@ -25,6 +25,7 @@ Begin { 'AddConditionalFormatting.ps1', 'Charting.ps1', 'ColorCompletion.ps1', + 'Compare-Worksheet.ps1', 'ConvertFromExcelData.ps1', 'ConvertFromExcelToSQLInsert.ps1', 'ConvertExcelToImageFile.ps1', diff --git a/Merge-worksheet.ps1 b/Merge-worksheet.ps1 new file mode 100644 index 0000000..a7d2c17 --- /dev/null +++ b/Merge-worksheet.ps1 @@ -0,0 +1,275 @@ +Function Merge-Worksheet { + <# + .Synopsis + Merges two worksheets (or other objects) into a single worksheet with differences marked up. + .Description + The Compare-Worksheet command takes two worksheets and marks differences in the source document, and optionally outputs a grid showing the changes. + By contrast the Merge-Worksheet command takes the worksheets and combines them into a single sheet showing the old and new data side by side . + Although it is designed to work with Excel data it can work with arrays of any kind of object; so it can be a merge *of* worksheets, or a merge *to* worksheet. + .Example + merge-worksheet "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -OutputFile Services.xlsx -OutputSheetName 54-55 -show + The workbooks contain audit information for two servers, one page contains a list of services. This command creates a worksheet named 54-55 + in a workbook named services and shows all the services and their differences, and opens it in Excel + .Example + merge-worksheet "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -OutputFile Services.xlsx -OutputSheetName 54-55 -HideEqual -AddBackgroundColor LightBlue -show + This modifies the previous command to hide the equal rows in the output sheet and changes the color used to mark rows "Added" to the second file. + .Example + merge-worksheet -OutputFile .\j1.xlsx -OutputSheetName test11 -ReferenceObject (dir .\ImportExcel\4.0.7) -DifferenceObject (dir '\ImportExcel\4.0.8') -Property Length -Show + This version compares two directories, and marks what has changed. + Because no "Key" property is given, "Name" is assumed to be the key and the only other property examined is length. + Files which are added or deleted or have changedd size will be highlighed in the output sheet. Changes to dates or other attributes will be ignored + .Example + merge-worksheet -Outf .\dummy.xlsx -RefO (dir .\ImportExcel\4.0.7) -DiffO (dir .\ImportExcel\4.0.8') -Pr Length -WhatIf -Passthru | Out-GridView + This time no file is written because -WhatIf is specified, and -Passthru causes the results to go Out-Gridview. This version uses aliases to shorten the parameters, + (OutputFileName can be "outFile" and the sheet "OutSheet" : DifferenceObject & RefeenceObject can be DiffObject & RefObject) + #> + [cmdletbinding(SupportsShouldProcess=$true)] + Param( + #First Excel file to compare. You can compare two Excel files or two other objects but not one of each. + [parameter(ParameterSetName='A',Mandatory=$true,Position=0)] + [parameter(ParameterSetName='B',Mandatory=$true,Position=0)] + [parameter(ParameterSetName='C',Mandatory=$true,Position=0)] + $Referencefile , + + #Second Excel file to compare. + [parameter(ParameterSetName='A',Mandatory=$true,Position=1)] + [parameter(ParameterSetName='B',Mandatory=$true,Position=1)] + [parameter(ParameterSetName='C',Mandatory=$true,Position=1)] + [parameter(ParameterSetName='E',Mandatory=$true,Position=1)] + $Differencefile , + + #Name(s) of worksheets to compare, + [parameter(ParameterSetName='A',Position=2)] + [parameter(ParameterSetName='B',Position=2)] + [parameter(ParameterSetName='C',Position=2)] + [parameter(ParameterSetName='E',Position=2)] + $WorkSheetName = "Sheet1", + + #The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. + [parameter(ParameterSetName='A')] + [parameter(ParameterSetName='B')] + [parameter(ParameterSetName='C')] + [parameter(ParameterSetName='E')] + [int]$Startrow = 1, + + #Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. + [Parameter(ParameterSetName='B',Mandatory=$true)] + [String[]]$Headername, + + #Automatically generate property names (P1, P2, P3, ..) instead of the using the values the top row of the sheet. + [Parameter(ParameterSetName='C',Mandatory=$true)] + [switch]$NoHeader, + + [parameter(ParameterSetName='D',Mandatory=$true)] + [parameter(ParameterSetName='E',Mandatory=$true)] + [Alias('RefObject')] + $ReferenceObject , + [parameter(ParameterSetName='D',Mandatory=$true,Position=1)] + [Alias('DiffObject')] + $DifferenceObject , + [parameter(ParameterSetName='D',Position=2)] + $DiffPrefix = "=>" , + #File to hold merged data. + [parameter(Position=3)] + [Alias('OutFile')] + $OutputFile , + #Name of worksheet to output - if none specified will use the reference worksheet name. + [parameter(Position=4)] + [Alias('OutSheet')] + $OutputSheetName = "Sheet1", + #Properties to include in the DIFF - supports wildcards, default is "*". + $Property = "*" , + #Properties to exclude from the the search - supports wildcards. + $ExcludeProperty , + #Name of a column which is unique used to pair up rows from the refence and difference side, default is "Name". + $Key = "Name" , + #Sets the font color for the "key" field; this means you can filter by color to get only changed rows. + [System.Drawing.Color]$KeyFontColor = "DarkRed", + #Sets the background color for changed rows. + [System.Drawing.Color]$ChangeBackgroundColor = "Orange", + #Sets the background color for rows in the reference but deleted from the difference sheet. + [System.Drawing.Color]$DeleteBackgroundColor = "LightPink", + #Sets the background color for rows not in the reference but added to the difference sheet. + [System.Drawing.Color]$AddBackgroundColor = "PaleGreen", + #if Specified hides the rows in the spreadsheet that are equal and only shows changes, added or deleted rows. + [switch]$HideEqual , + #If specified outputs the data to the pipeline (you can add -whatif so it the command only outputs to the command) + [switch]$Passthru , + #If specified, opens the output workbook. + [Switch]$Show + ) + + #region Read Excel data + if ($Referencefile -and $Differencefile) { + #if the filenames don't resolve, give up now. + try { $oneFile = ((Resolve-Path -Path $Referencefile -ErrorAction Stop).path -eq (Resolve-Path -Path $Differencefile -ErrorAction Stop).path)} + Catch { Write-Warning -Message "Could not Resolve the filenames." ; return } + + #If we have one file , we must have two different worksheet names. If we have two files $worksheetName can be a single string or two strings. + if ($onefile -and ( ($WorkSheetName.count -ne 2) -or $WorkSheetName[0] -eq $WorkSheetName[1] ) ) { + Write-Warning -Message "If both the Reference and difference file are the same then worksheet name must provide 2 different names" + return + } + if ($WorkSheetName.count -eq 2) {$workSheet2 = $DiffPrefix = $WorkSheetName[1] ; $worksheet1 = $WorkSheetName[0] ; } + elseif ($WorkSheetName -is [string]) {$worksheet2 = $workSheet1 = $WorkSheetName ; + $DiffPrefix = (Split-Path -Path $Differencefile -Leaf) -replace "\.xlsx$","" } + else {Write-Warning -Message "You must provide either a single worksheet name or two names." ; return } + + $params= @{ ErrorAction = [System.Management.Automation.ActionPreference]::Stop } + foreach ($p in @("HeaderName","NoHeader","StartRow")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} + try { + $ReferenceObject = Import-Excel -Path $Referencefile -WorksheetName $WorkSheet1 @params + $DifferenceObject = Import-Excel -Path $Differencefile -WorksheetName $WorkSheet2 @Params + } + Catch {Write-Warning -Message "Could not read the worksheet from $Referencefile::$worksheet1 and/or $Differencefile::$worksheet2." ; return } + if ($NoHeader) {$firstDataRow = $Startrow } else {$firstDataRow = $Startrow + 1} + } + elseif ( $Differencefile) { + if ($WorkSheetName -isnot [string]) {Write-Warning -Message "You must provide a single worksheet name." ; return } + $params = @{WorkSheetName=$WorkSheetName; Path=$Differencefile; ErrorAction = [System.Management.Automation.ActionPreference]::Stop ;} + try {$DifferenceObject = Import-Excel @Params } + Catch {Write-Warning -Message "Could not read the worksheet '$WorkSheetName' from $Differencefile::$WorkSheetName." ; return } + $DiffPrefix = (Split-Path -Path $Differencefile -Leaf) -replace "\.xlsx$","" + if ($NoHeader) {$firstDataRow = $Startrow } else {$firstDataRow = $Startrow + 1} + } + else { $firstDataRow = 1 } + #endregion + + #region Set lists of properties and row numbers + #Make a list of properties/headings using the Property (default "*") and ExcludeProperty parameters + $propList = @() + $DifferenceObject = $DifferenceObject | Update-FirstObjectProperties + $headings = $DifferenceObject[0].psobject.Properties.Name # This preserves the sequence - using get-member would sort them alphabetically! There may be extra properties in + if ($NoHeader -and "Name" -eq $Key) {$Key = "p1"} + if ($headings -notcontains $Key -and + ('*' -ne $Key)) {Write-Warning -Message "You need to specify one of the headings in the sheet '$worksheet1' as a key." ; return } + foreach ($p in $Property) { $propList += ($headings.where({$_ -like $p}) )} + foreach ($p in $ExcludeProperty) { $propList = $propList.where({$_ -notlike $p}) } + if (($propList -notcontains $Key) -and + ('*' -ne $Key)) { $propList += $Key} #If $key isn't one of the headings we will have bailed by now + $propList = $propList | Select-Object -Unique #so, prolist must contain at least $key if nothing else + + #If key is "*" we treat it differently , and we will create a script property which concatenates all the Properties in $Proplist + $ConCatblock = [scriptblock]::Create( ($proplist | ForEach-Object {'$this."' + $_ + '"'}) -join " + ") + + #Build the list of the properties to output, in order. + $diffpart = @() + $refpart = @() + foreach ($p in $proplist.Where({$key -ne $_}) ) {$refPart += $p ; $diffPart += "$DiffPrefix $p" } + #Last reference column will be A if there the only one property (which might be the key), B if there are two properties, C if there are 3 etc + $lastRefCol = [char](64 + $propList.count) + #First difference column will be the next one (we'll trap the case of only having the key later) + $FirstDiffCol = [char](65 + $propList.count) + + if ($key -ne '*') { + $outputProps = @($key) + $refpart + $diffpart + #If we are using a single column as the key, don't duplicate it, so the last difference column will be A if there is one property, C if there are two, E if there are 3 + $lastDiffCol = [char](63 + 2 * $propList.count) + } + else { + $outputProps = @( ) + $refpart + $diffpart + #If we not using a single column as a key all columns are duplicated so, the Last difference column will be B if there is one property, D if there are two, F if there are 3 + $lastDiffCol = [char](64 + 2 * $propList.count) + } + + #Add RowNumber to every row + #If one sheet has extra rows we can get a single "==" result from compare, with the row from the reference sheet, but + #the row in the other sheet might be different so we will look up the row number from the key field - build a hash table for that here + #If we have "*" as the key ad the script property to concatenate the [selected] properties. + + $Rowhash = @{} + $rowNo = $firstDataRow + foreach ($row in $ReferenceObject) { + if ($row._row -eq $null) {Add-Member -InputObject $row -MemberType NoteProperty -Value ($rowNo ++) -Name "_Row" } + else {$rowNo++ } + if ($Key -eq '*' ) {Add-Member -InputObject $row -MemberType ScriptProperty -Value $ConCatblock -Name "_All" } + } + $rowNo = $firstDataRow + foreach ($row in $DifferenceObject) { + Add-Member -InputObject $row -MemberType NoteProperty -Value $rowNo -Name "$DiffPrefix Row" -Force + if ($Key -eq '*' ) { + Add-Member -InputObject $row -MemberType ScriptProperty -Value $ConCatblock -Name "_All" + $Rowhash[$row._All] = $rowNo + } + else {$Rowhash[$row.$key] = $rowNo } + $rowNo ++ + } + if ($Key -eq '*') {$key = "_ALL"} + #endregion + $expandedDiff = Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject -Property $propList -PassThru -IncludeEqual | + Group-Object -Property $key | ForEach-Object { + #The value of the key column is the name of the group. + $keyval = $_.name + #we're going to create a custom object from a hash table. ??Might no longer need to preserve the field order + $hash = [ordered]@{} + foreach ($result in $_.Group) { + if ($result.SideIndicator -ne "=>") {$hash["_Row"] = $result._Row } + elseif (-not $hash["$DiffPrefix Row"]) {$hash["_Row"] = "" } + #if we have already set the side, be this must the second record, so set side to indicate "changed" + if ($hash.Side) {$hash.Side = "<>"} else {$hash["Side"] = $result.SideIndicator} + switch ($hash.side) { + '==' { $hash["$DiffPrefix is"] = 'Same' } + '=>' { $hash["$DiffPrefix is"] = 'Added' } + '<>' { if (-not $hash["_Row"]) { + $hash["$DiffPrefix is"] = 'Added' + } + else { + $hash["$DiffPrefix is"] = 'Changed' + } + } + '<=' { $hash["$DiffPrefix is"] = 'Removed'} + } + #find the number of the row in the the "difference" object which has this key. If it is the object is only the reference this will be blank. + $hash["$DiffPrefix Row"] = $Rowhash[$keyval] + $hash[$key] = $keyval + #Create FieldName and/or =>FieldName columns + foreach ($p in $result.psobject.Properties.name.where({$_ -ne $key -and $_ -ne "SideIndicator" -and $_ -ne "$DiffPrefix Row" })) { + if ($result.SideIndicator -eq "==" -and $p -in $propList) + {$hash[("$p")] = $hash[("$DiffPrefix $p")] = $result.$P} + elseif ($result.SideIndicator -eq "==" -or $result.SideIndicator -eq "<=") + {$hash[("$p")] = $result.$P} + elseif ($result.SideIndicator -eq "=>") { $hash[("$DiffPrefix $p")] = $result.$P} + } + } + [Pscustomobject]$hash + } | Sort-Object -Property "_row" + + #Already sorted by reference row number, fill in any blanks in the difference-row column + for ($i = 1; $i -lt $expandedDiff.Count; $i++) {if (-not $expandedDiff[$i]."$DiffPrefix Row") {$expandedDiff[$i]."$DiffPrefix Row" = $expandedDiff[$i-1]."$DiffPrefix Row" } } + + #Now re-Sort by difference row number, and fill in any blanks in the reference-row column + $expandedDiff = $expandedDiff | Sort-Object -Property "$DiffPrefix Row" + for ($i = 1; $i -lt $expandedDiff.Count; $i++) {if (-not $expandedDiff[$i]."_Row") {$expandedDiff[$i]."_Row" = $expandedDiff[$i-1]."_Row" } } + + $AllProps = @("_Row") + $OutputProps + $expandedDiff[0].psobject.properties.name.where({$_ -notin ($outputProps + @("_row","side","SideIndicator","_ALL" ))}) + + if ($PassThru -or -not $OutputFile) {return ($expandedDiff | Select-Object -Property $allprops | Sort-Object -Property "_row", "$DiffPrefix Row" | Update-FirstObjectProperties ) } + elseif ($PSCmdlet.ShouldProcess($OutputFile,"Write Output to Excel file")) { + $expandedDiff = $expandedDiff | Sort-Object -Property "_row", "$DiffPrefix Row" + $xl = $expandedDiff | Select-Object -Property $OutputProps | Update-FirstObjectProperties | + Export-Excel -Path $OutputFile -WorkSheetname $OutputSheetName -FreezeTopRow -BoldTopRow -AutoSize -AutoFilter -PassThru + $ws = $xl.Workbook.Worksheets[$OutputSheetName] + for ($i = 0; $i -lt $expandedDiff.Count; $i++ ) { + if ( $expandedDiff[$i].side -ne "==" ) { + Set-Format -WorkSheet $ws -Range ("A" + ($i + 2 )) -FontColor $KeyFontColor + } + elseif ( $HideEqual ) {$ws.row($i+2).hidden = $true } + if ( $expandedDiff[$i].side -eq "<>" ) { + $range = $ws.Dimension -replace "\d+", ($i + 2 ) + Set-Format -WorkSheet $ws -Range $range -BackgroundColor $ChangeBackgroundColor + } + elseif ( $expandedDiff[$i].side -eq "<=" ) { + $range = "A" + ($i + 2 ) + ":" + $lastRefCol + ($i + 2 ) + Set-Format -WorkSheet $ws -Range $range -BackgroundColor $DeleteBackgroundColor + } + elseif ( $expandedDiff[$i].side -eq "=>" ) { + if ($propList.count -gt 1) { + $range = $FirstDiffCol + ($i + 2 ) + ":" + $lastDiffCol + ($i + 2 ) + Set-Format -WorkSheet $ws -Range $range -BackgroundColor $AddBackgroundColor + } + Set-Format -WorkSheet $ws -Range ("A" + ($i + 2 )) -BackgroundColor $AddBackgroundColor + } + } + Close-ExcelPackage -ExcelPackage $xl -Show:$Show + } + } \ No newline at end of file diff --git a/README.md b/README.md index ea39b56..aefb734 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/dfi # What's new +- [James O'Neill](https://twitter.com/jamesoneill) added `Compare-Worksheet` + - Compares two worksheets with the same name in different files. + #### 4/22/2018 Thanks to the community yet again - [ili101](https://github.com/ili101) for fixes and features diff --git a/Send-SqlDataToExcel.ps1 b/Send-SqlDataToExcel.ps1 index 15a1b1c..96d0db7 100644 --- a/Send-SqlDataToExcel.ps1 +++ b/Send-SqlDataToExcel.ps1 @@ -1,5 +1,7 @@ Function Send-SQLDataToExcel { -<# + [CmdLetBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] + <# .Synopsis Runs a SQL query and inserts the results into an ExcelSheet, more efficiently than sending it via Export-Excel .Description @@ -23,7 +25,7 @@ -#> + #> param ( #Database connection string; either DSN=ODBC_Data_Source_Name, a full odbc or SQL Connection string, or the name of a SQL server [Parameter(ParameterSetName="SQLConnection", Mandatory=$true)] @@ -41,6 +43,9 @@ #The SQL query to run [Parameter(Mandatory=$true)] [string]$SQL, + #Override the default query time of 30 seconds. + [int]$QueryTimeout, + #File name for the Excel File $Path, [String]$WorkSheetname = 'Sheet1', [Switch]$KillExcel, @@ -92,9 +97,9 @@ #We were either given a session object or a connection string (with, optionally a MSSQLServer parameter) # If we got -MSSQLServer, create a SQL connection, if we didn't but we got -Connection create an ODBC connection if ($MsSQLserver) { - if ($connection -notmatch "=") {$Connection = "server=$Connection;trusted_connection=true;timeout=60"} + if ($Connection -notmatch "=") {$Connection = "server=$Connection;trusted_connection=true;timeout=60"} $Session = New-Object -TypeName System.Data.SqlClient.SqlConnection -ArgumentList $Connection - if ($Session.State -ne 'Open') {$session.Open()} + if ($Session.State -ne 'Open') {$Session.Open()} if ($DataBase) {$Session.ChangeDatabase($DataBase) } } elseif ($Connection) { @@ -102,30 +107,31 @@ } #A session was either passed in or just created. If it's a SQL one make a SQL DataAdapter, otherwise make an ODBC one - if ($Session.gettype().name -match "SqlConnection") { + if ($Session.GetType().name -match "SqlConnection") { $dataAdapter = New-Object -TypeName System.Data.SqlClient.SqlDataAdapter -ArgumentList ( - New-Object -TypeName System.Data.SqlClient.SqlCommand -ArgumentList $sql, $Session) + New-Object -TypeName System.Data.SqlClient.SqlCommand -ArgumentList $SQL, $Session) } else { $dataAdapter = New-Object -TypeName System.Data.Odbc.OdbcDataAdapter -ArgumentList ( - New-Object -TypeName System.Data.Odbc.OdbcCommand -ArgumentList $sql, $Session ) + New-Object -TypeName System.Data.Odbc.OdbcCommand -ArgumentList $SQL, $Session ) } - + if ($QueryTimeout) {$dataAdapter.SelectCommand.CommandTimeout = $ServerTimeout} + #Both adapter types output the same kind of table, create one and fill it from the adapter $dataTable = New-Object -TypeName System.Data.DataTable $rowCount = $dataAdapter.fill($dataTable) - Write-Verbose "Query returned $rowcount row(s)" + Write-Verbose -Message "Query returned $rowCount row(s)" #ExportExcel user a -NoHeader parameter so that's what we use here, but needs to be the other way around. - $PrintHeaders = -not $NoHeader + $printHeaders = -not $NoHeader if ($Title) {$r = $StartRow +1 } else {$r = $StartRow} #Get our Excel sheet and fill it with the data $excelPackage = Export-Excel -Path $Path -WorkSheetname $WorkSheetname -PassThru - $excelPackage.Workbook.Worksheets[$WorkSheetname].Cells[$r,$StartColumn].LoadFromDataTable($dataTable, $PrintHeaders ) | Out-Null + $excelPackage.Workbook.Worksheets[$WorkSheetname].Cells[$r,$StartColumn].LoadFromDataTable($dataTable, $printHeaders ) | Out-Null #Call export-excel with any parameters which don't relate to the SQL query - "Connection", "Database" , "Session", "MsSQLserver", "Destination" , "sql" ,"Path" | foreach-object {$null = $PSBoundParameters.Remove($_) } + "Connection", "Database" , "Session", "MsSQLserver", "Destination" , "SQL" ,"Path" | ForEach-Object {$null = $PSBoundParameters.Remove($_) } Export-Excel -ExcelPackage $excelPackage @PSBoundParameters #If we were not passed a session close the session we created. diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index 8ee571e..e39a898 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -45,7 +45,7 @@ This version of the previous command lightlights all the cells in lightgray and then sets the changed rows back to white; only the unchanged rows are highlighted #> -[cmdletbinding(DefaultParameterSetName)] + [cmdletbinding(DefaultParameterSetName)] Param( #First file to compare [parameter(Mandatory=$true,Position=0)] @@ -135,7 +135,7 @@ if ($ExcludeDifferent -and -not $IncludeEqual) {$IncludeEqual = $true} #Do the comparison and add file,sheet and row to the result - these are prefixed with "_" to show they are added the addition will fail if the sheet has these properties so split the operations - $diff = Compare-Object -ReferenceObject $Sheet1 -DifferenceObject $Sheet2 -Property $propList -PassThru -IncludeEqual:$IncludeEqual -ExcludeDifferent:$ExcludeDifferent | + [PSCustomObject[]]$diff = Compare-Object -ReferenceObject $Sheet1 -DifferenceObject $Sheet2 -Property $propList -PassThru -IncludeEqual:$IncludeEqual -ExcludeDifferent:$ExcludeDifferent | Sort-Object -Property "_Row","File" #if BackgroundColor was specified, set it on extra or extra or changed rows @@ -168,7 +168,7 @@ } #if font colour was specified, set it on changed properties where the same key appears in both sheets. if ($diff -and $FontColor -and ($propList -contains $Key) ) { - $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property $Key | where {$_.count -eq 2} + $updates = $diff.where({$_.SideIndicator -ne "=="}) | Group-object -Property $Key | Where-Object {$_.count -eq 2} if ($updates) { $XL1 = Open-ExcelPackage -path $Referencefile if ($oneFile ) {$xl2 = $xl1} From cd52f3c70485a90f2d5ef182ebc23e6c706caf6a Mon Sep 17 00:00:00 2001 From: jhoneill Date: Sun, 27 May 2018 19:43:41 +0100 Subject: [PATCH 12/17] Added Merge-worksheet --- Merge-worksheet.ps1 | 125 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/Merge-worksheet.ps1 b/Merge-worksheet.ps1 index a7d2c17..b9e832a 100644 --- a/Merge-worksheet.ps1 +++ b/Merge-worksheet.ps1 @@ -272,4 +272,129 @@ } Close-ExcelPackage -ExcelPackage $xl -Show:$Show } + } + + Function Merge-MulipleSheets { + Param ( + [Parameter(Mandatory=$true,ValueFromPipeline=$true)] + [string[]]$Path , + #The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. + [int]$Startrow = 1, + + #Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. + [String[]]$Headername, + + #Automatically generate property names (P1, P2, P3, ..) instead of the using the values the top row of the sheet. + [switch]$NoHeader, + + #Name(s) of worksheets to compare, + $WorkSheetName = "Sheet1", + #File to write output to + [Alias('OutFile')] + $OutputFile = ".\temp.xlsx", + #Name of worksheet to output - if none specified will use the reference worksheet name. + [Alias('OutSheet')] + $OutputSheetName = "Sheet1", + #Properties to include in the DIFF - supports wildcards, default is "*". + $Property = "*" , + #Properties to exclude from the the search - supports wildcards. + $ExcludeProperty , + #Name of a column which is unique used to pair up rows from the refence and difference side, default is "Name". + $Key = "Name" , + #Sets the font color for the "key" field; this means you can filter by color to get only changed rows. + [System.Drawing.Color]$KeyFontColor = "Red", + #Sets the background color for changed rows. + [System.Drawing.Color]$ChangeBackgroundColor = "Orange", + #Sets the background color for rows in the reference but deleted from the difference sheet. + [System.Drawing.Color]$DeleteBackgroundColor = "LightPink", + #Sets the background color for rows not in the reference but added to the difference sheet. + [System.Drawing.Color]$AddBackgroundColor = "Orange", + #if Specified hides the columns in the spreadsheet that contain the row numbers + [switch]$HideRowNumbers , + #If specified outputs the data to the pipeline (you can add -whatif so it the command only outputs to the command) + [switch]$Passthru , + #If specified, opens the output workbook. + [Switch]$Show + ) + begin { $filestoProcess = @() } + process { $filestoProcess += $Path} + end { + if ( $filestoProcess.count -lt 2) {Write-Warning -Message "Need at least two files to process"; return} + + #Set up the parameters we will pass to merge worksheet + Get-Variable -Name 'HeaderName','NoHeader','StartRow','Key','Property','ExcludeProperty','WorkSheetName' -ErrorAction SilentlyContinue | + Where-Object {$_.Value} | ForEach-Object -Begin {$params= @{} } -Process {$params[$_.Name] = $_.Value} + + Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($filestoProcess[-1]) against $($filestoProcess[0]). " + $merged = Merge-Worksheet @params -Referencefile $filestoProcess[0] -Differencefile $filestoProcess[-1] + $nextFileNo = 2 + while ($nextFileNo -lt $filestoProcess.count -and $merged) { + Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($filestoProcess[-$nextFileNo]) against $($filestoProcess[0]). " + $merged = Merge-Worksheet @params -ReferenceObject $merged -Differencefile $filestoProcess[-$nextFileNo] + $nextFileNo ++ + } + if (-not $merged) {Write-Warning -Message "The merge operation did not return any data."; return } + + Write-Progress -Activity "Merging sheets" -CurrentOperation "Creating output sheet '$OutputSheetName' in $OutputFile" + $excel = $merged | Sort-Object "_row" | Update-FirstObjectProperties | Export-Excel -Path $OutputFile -WorkSheetname $OutputSheetName -ClearSheet -FreezeTopRow -BoldTopRow -AutoFilter -PassThru + $sheet = $excel.Workbook.Worksheets[$OutputSheetName] + + #We will put in a conditional format for "if all the others are not flagged as 'same'" to mark rows where something is added, removed or changed + $sameChecks = @() + + #All the 'difference' columns in the sheet are labeled with the file they came from, 'reference' columns need their + #headers prefixed with the ref file name, $colnames is the basis of a regular expression to identify what should have $refPrefix appended + $colNames = @("_Row","^$Key`$") + $refPrefix = (Split-Path -Path $filestoProcess[0] -Leaf) -replace "\.xlsx$"," " + + Write-Progress -Activity "Merging sheets" -CurrentOperation "Applying formatting to sheet '$OutputSheetName' in $OutputFile" + #Find the column headings which are in the form "diffFile is"; which will hold 'Same', 'Added' or 'Changed' + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { + #Work leftwards across the headings applying conditional formatting which says + # 'Format this cell if the "IS" column has a value of ...' until you find a heading which doesn't have the prefix. + $prefix = $cell.value -replace "\sIS$","" + $columnNo = $cell.start.Column -1 + $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) + while ($sheet.cells[$cellAddr].value -match $prefix) { + $condFormattingParams = @{RuleType='Expression'; BackgroundPattern='None'; WorkSheet=$sheet; Range=$([OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R[1]C[$columnNo]:R[1048576]C[$columnNo]",0,0)) } + Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Added"' ) -BackgroundColor $AddBackgroundColor + Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Changed"') -BackgroundColor $ChangeBackgroundColor + Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Removed"') -BackgroundColor $DeleteBackgroundColor + $columnNo -- + $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) + } + #build up a list of prefixes in $colnames - we'll use that to set headers on rows from the reference file; and build up the "if the 'is' cell isn't same" list + $colNames += $prefix + $sameChecks += (($cell.Address -replace "1","2") +'<>"Same"') + } + + #For all the columns which don't match one of the Diff-file prefixes or "_Row" or the 'Key' columnn name; add the reference file prefix to their header. + $nameRegex = $colNames -Join "|" + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -Notmatch $nameRegex}) ) { + $cell.Value = $refPrefix + $cell.Value + } + #We've made a bunch of things wider so now is the time to autofit columns. Any hiding has to come AFTER this, because it unhides things + $sheet.Cells.AutoFitColumns() + + #if we have a key field (we didn't concatenate all fields) use what we built up in $sameChecks to apply conditional formatting to it (Row no will be in column A, Key in Column B) + if ($Key -ne '*') { + Add-ConditionalFormatting -WorkSheet $sheet -Range "B2:B1048576" -ForeGroundColor $KeyFontColor -BackgroundPattern 'None' -RuleType Expression -ConditionValue ("OR(" +($sameChecks -join ",") +")" ) + } + #Go back over the headings to find and hide the "is" columns; + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { + $sheet.Column($cell.start.Column).HIDDEN = $true + } + + #If specified, look over the headings for "row" and hide the columns which say "this was in row such-and-such" + if ($HideRowNumbers) { + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "Row$"}) ) { + $sheet.Column($cell.start.Column).HIDDEN = $true + } + } + + Close-ExcelPackage -ExcelPackage $excel -Show:$Show + Write-Progress -Activity "Merging sheets" -Completed + } + + } \ No newline at end of file From dc9bff8240df3e2a25463cb2c5b22519bd6ea376 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Sun, 27 May 2018 20:30:07 +0100 Subject: [PATCH 13/17] Added Multiple Merge to Merge-Worksheet.ps1 --- multi-merge.ps1 | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 multi-merge.ps1 diff --git a/multi-merge.ps1 b/multi-merge.ps1 new file mode 100644 index 0000000..31a778b --- /dev/null +++ b/multi-merge.ps1 @@ -0,0 +1,120 @@ + Param ( + [Parameter(Mandatory=$true,ValueFromPipeline=$true)] + [string[]]$Path , + #The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. + [int]$Startrow = 1, + + #Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. + [String[]]$Headername, + + #Automatically generate property names (P1, P2, P3, ..) instead of the using the values the top row of the sheet. + [switch]$NoHeader, + + #Name(s) of worksheets to compare, + $WorkSheetName = "Sheet1", + #File to write output to + [Alias('OutFile')] + $OutputFile = ".\temp.xlsx", + #Name of worksheet to output - if none specified will use the reference worksheet name. + [Alias('OutSheet')] + $OutputSheetName = "Sheet1", + #Properties to include in the DIFF - supports wildcards, default is "*". + $Property = "*" , + #Properties to exclude from the the search - supports wildcards. + $ExcludeProperty , + #Name of a column which is unique used to pair up rows from the refence and difference side, default is "Name". + $Key = "Name" , + #Sets the font color for the "key" field; this means you can filter by color to get only changed rows. + [System.Drawing.Color]$KeyFontColor = "Red", + #Sets the background color for changed rows. + [System.Drawing.Color]$ChangeBackgroundColor = "Orange", + #Sets the background color for rows in the reference but deleted from the difference sheet. + [System.Drawing.Color]$DeleteBackgroundColor = "LightPink", + #Sets the background color for rows not in the reference but added to the difference sheet. + [System.Drawing.Color]$AddBackgroundColor = "Orange", + #if Specified hides the columns in the spreadsheet that contain the row numbers + [switch]$HideRowNumbers , + #If specified outputs the data to the pipeline (you can add -whatif so it the command only outputs to the command) + [switch]$Passthru , + #If specified, opens the output workbook. + [Switch]$Show + ) + begin { $filestoProcess = @() } + process { $filestoProcess += $Path} + end { + if ( $filestoProcess.count -lt 2) {Write-Warning -Message "Need at least two files to process"; return} + + #Set up the parameters we will pass to merge worksheet + Get-Variable -Name 'HeaderName','NoHeader','StartRow','Key','Property','ExcludeProperty','WorkSheetName' -ErrorAction SilentlyContinue | + Where-Object {$_.Value} | ForEach-Object -Begin {$params= @{} } -Process {$params[$_.Name] = $_.Value} + + Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($filestoProcess[-1]) against $($filestoProcess[0]). " + $merged = Merge-Worksheet @params -Referencefile $filestoProcess[0] -Differencefile $filestoProcess[-1] + $nextFileNo = 2 + while ($nextFileNo -lt $filestoProcess.count -and $merged) { + Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($filestoProcess[-$nextFileNo]) against $($filestoProcess[0]). " + $merged = Merge-Worksheet @params -ReferenceObject $merged -Differencefile $filestoProcess[-$nextFileNo] + $nextFileNo ++ + } + if (-not $merged) {Write-Warning -Message "The merge operation did not return any data."; return } + + Write-Progress -Activity "Merging sheets" -CurrentOperation "Creating output sheet '$OutputSheetName' in $OutputFile" + $excel = $merged | Sort-Object "_row" | Update-FirstObjectProperties | Export-Excel -Path $OutputFile -WorkSheetname $OutputSheetName -ClearSheet -FreezeTopRow -BoldTopRow -AutoFilter -PassThru + $sheet = $excel.Workbook.Worksheets[$OutputSheetName] + + #We will put in a conditional format for "if all the others are not flagged as 'same'" to mark rows where something is added, removed or changed + $sameChecks = @() + + #All the 'difference' columns in the sheet are labeled with the file they came from, 'reference' columns need their + #headers prefixed with the ref file name, $colnames is the basis of a regular expression to identify what should have $refPrefix appended + $colNames = @("_Row","^$Key`$") + $refPrefix = (Split-Path -Path $filestoProcess[0] -Leaf) -replace "\.xlsx$"," " + + Write-Progress -Activity "Merging sheets" -CurrentOperation "Applying formatting to sheet '$OutputSheetName' in $OutputFile" + #Find the column headings which are in the form "diffFile is"; which will hold 'Same', 'Added' or 'Changed' + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { + #Work leftwards across the headings applying conditional formatting which says + # 'Format this cell if the "IS" column has a value of ...' until you find a heading which doesn't have the prefix. + $prefix = $cell.value -replace "\sIS$","" + $columnNo = $cell.start.Column -1 + $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) + while ($sheet.cells[$cellAddr].value -match $prefix) { + $condFormattingParams = @{RuleType='Expression'; BackgroundPattern='None'; WorkSheet=$sheet; Range=$([OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R[1]C[$columnNo]:R[1048576]C[$columnNo]",0,0)) } + Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Added"' ) -BackgroundColor $AddBackgroundColor + Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Changed"') -BackgroundColor $ChangeBackgroundColor + Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Removed"') -BackgroundColor $DeleteBackgroundColor + $columnNo -- + $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) + } + #build up a list of prefixes in $colnames - we'll use that to set headers on rows from the reference file; and build up the "if the 'is' cell isn't same" list + $colNames += $prefix + $sameChecks += (($cell.Address -replace "1","2") +'<>"Same"') + } + + #For all the columns which don't match one of the Diff-file prefixes or "_Row" or the 'Key' columnn name; add the reference file prefix to their header. + $nameRegex = $colNames -Join "|" + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -Notmatch $nameRegex}) ) { + $cell.Value = $refPrefix + $cell.Value + } + #We've made a bunch of things wider so now is the time to autofit columns. Any hiding has to come AFTER this, because it unhides things + $sheet.Cells.AutoFitColumns() + + #if we have a key field (we didn't concatenate all fields) use what we built up in $sameChecks to apply conditional formatting to it (Row no will be in column A, Key in Column B) + if ($Key -ne '*') { + Add-ConditionalFormatting -WorkSheet $sheet -Range "B2:B1048576" -ForeGroundColor $KeyFontColor -BackgroundPattern 'None' -RuleType Expression -ConditionValue ("OR(" +($sameChecks -join ",") +")" ) + } + #Go back over the headings to find and hide the "is" columns; + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { + $sheet.Column($cell.start.Column).HIDDEN = $true + } + + #If specified, look over the headings for "row" and hide the columns which say "this was in row such-and-such" + if ($HideRowNumbers) { + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "Row$"}) ) { + $sheet.Column($cell.start.Column).HIDDEN = $true + } + } + + Close-ExcelPackage -ExcelPackage $excel -Show:$Show + Write-Progress -Activity "Merging sheets" -Completed + } From 15f1839d29b9b1930c83a97db00dd7fa8d7ff2de Mon Sep 17 00:00:00 2001 From: jhoneill Date: Sun, 27 May 2018 20:44:02 +0100 Subject: [PATCH 14/17] Added Merge Multiple worksheet --- Merge-worksheet.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Merge-worksheet.ps1 b/Merge-worksheet.ps1 index b9e832a..520a573 100644 --- a/Merge-worksheet.ps1 +++ b/Merge-worksheet.ps1 @@ -395,6 +395,6 @@ Close-ExcelPackage -ExcelPackage $excel -Show:$Show Write-Progress -Activity "Merging sheets" -Completed } + } - - } \ No newline at end of file + \ No newline at end of file From 2a62dc9b457b4b54a647d1dd49dc84d00026abac Mon Sep 17 00:00:00 2001 From: jhoneill Date: Tue, 29 May 2018 17:36:15 +0100 Subject: [PATCH 15/17] Making Merge-Worksheet, and Merge-MultipleWorksheet ready to release --- Merge-worksheet.ps1 | 215 +++++++++++++++++++++++++++++--------------- 1 file changed, 143 insertions(+), 72 deletions(-) diff --git a/Merge-worksheet.ps1 b/Merge-worksheet.ps1 index 520a573..5f1da13 100644 --- a/Merge-worksheet.ps1 +++ b/Merge-worksheet.ps1 @@ -9,19 +9,20 @@ .Example merge-worksheet "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -OutputFile Services.xlsx -OutputSheetName 54-55 -show The workbooks contain audit information for two servers, one page contains a list of services. This command creates a worksheet named 54-55 - in a workbook named services and shows all the services and their differences, and opens it in Excel + in a workbook named services which shows all the services and their differences, and opens it in Excel .Example merge-worksheet "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -OutputFile Services.xlsx -OutputSheetName 54-55 -HideEqual -AddBackgroundColor LightBlue -show - This modifies the previous command to hide the equal rows in the output sheet and changes the color used to mark rows "Added" to the second file. + This modifies the previous command to hide the equal rows in the output sheet and changes the color used to mark rows added to the second file. .Example - merge-worksheet -OutputFile .\j1.xlsx -OutputSheetName test11 -ReferenceObject (dir .\ImportExcel\4.0.7) -DifferenceObject (dir '\ImportExcel\4.0.8') -Property Length -Show + merge-worksheet -OutputFile .\j1.xlsx -OutputSheetName test11 -ReferenceObject (dir .\ImportExcel\4.0.7) -DifferenceObject (dir .\ImportExcel\4.0.8) -Property Length -Show This version compares two directories, and marks what has changed. Because no "Key" property is given, "Name" is assumed to be the key and the only other property examined is length. - Files which are added or deleted or have changedd size will be highlighed in the output sheet. Changes to dates or other attributes will be ignored + Files which are added or deleted or have changed size will be highlighed in the output sheet. Changes to dates or other attributes will be ignored .Example - merge-worksheet -Outf .\dummy.xlsx -RefO (dir .\ImportExcel\4.0.7) -DiffO (dir .\ImportExcel\4.0.8') -Pr Length -WhatIf -Passthru | Out-GridView - This time no file is written because -WhatIf is specified, and -Passthru causes the results to go Out-Gridview. This version uses aliases to shorten the parameters, - (OutputFileName can be "outFile" and the sheet "OutSheet" : DifferenceObject & RefeenceObject can be DiffObject & RefObject) + merge-worksheet -RefO (dir .\ImportExcel\4.0.7) -DiffO (dir .\ImportExcel\4.0.8) -Pr Length | Out-GridView + This time no file is written and the results -which include all properties, not just length, are output and sent to Out-Gridview. + This version uses aliases to shorten the parameters, + (OutputFileName can be "outFile" and the sheet "OutSheet" : DifferenceObject & ReferenceObject can be DiffObject & RefObject) #> [cmdletbinding(SupportsShouldProcess=$true)] Param( @@ -68,6 +69,7 @@ [Alias('DiffObject')] $DifferenceObject , [parameter(ParameterSetName='D',Position=2)] + [parameter(ParameterSetName='E',Position=3)] $DiffPrefix = "=>" , #File to hold merged data. [parameter(Position=3)] @@ -129,7 +131,9 @@ $params = @{WorkSheetName=$WorkSheetName; Path=$Differencefile; ErrorAction = [System.Management.Automation.ActionPreference]::Stop ;} try {$DifferenceObject = Import-Excel @Params } Catch {Write-Warning -Message "Could not read the worksheet '$WorkSheetName' from $Differencefile::$WorkSheetName." ; return } - $DiffPrefix = (Split-Path -Path $Differencefile -Leaf) -replace "\.xlsx$","" + if ($DiffPrefix -eq "=>" ) { + $DiffPrefix = (Split-Path -Path $Differencefile -Leaf) -replace "\.xlsx$","" + } if ($NoHeader) {$firstDataRow = $Startrow } else {$firstDataRow = $Startrow + 1} } else { $firstDataRow = 1 } @@ -272,10 +276,46 @@ } Close-ExcelPackage -ExcelPackage $xl -Show:$Show } - } +} - Function Merge-MulipleSheets { - Param ( +Function Merge-MulipleSheets { +<# + .Synopsis + Merges worksheets into a single worksheet with differences marked up. + .Description + The Merge worksheet command combines 2 sheets. Merge-MultipleSheets is designed to merge more than 2. + So if asked to merge sheets A,B,C which contain Services, with a Name, Displayname and Start mode, where "name" is treated as the key + it calls Merge-Worksheet to merge Name, Displayname and Start mode,from sheets A and C the result has column headings + -Row, Name, DisplayName, Startmode, C-DisplayName, C-StartMode C-Is, C-Row + Then it calls merge-worsheet with this result and sheet B, comparing 'Name', 'Displayname' and 'Start mode' columns on each side and outputting + _Row, Name, DisplayName, Startmode, B-DisplayName, B-StartMode B-Is, B-Row, C-DisplayName, C-StartMode C-Is, C-Row + Any columns in the "reference" side which are not used in the comparison are appended on the right, which is we compare the sheets in reverse order + The "Is" column holds "Same", "Added", "Removed" or "Changed" and is used for conditional formatting in the output sheet (this is hidden by default), + and when the data is written to Excel the "reference" columns "DisplayName" and "Start" are renamed "A-DisplayName" and "A-Start" + Conditional formatting is also applied to the "key" column (name in this case) so the view can be filtered to rows with changes by filtering this column on color. + + Note: the processing order can affect what is seen as a change. For example if there is an extra item in sheet B in the example above, + Sheet C will be processed and that row and nothing will be seen to be missing. When sheet B is processed it is marked as an addition, and the conditional formatting marks + the entries from sheet A to show that a values were added in at least one sheet. + However of Sheet B is the reference sheet, A and C will be seen to have an item removed; and if B is processed before C, the extra item is known when C is processed and + so C is considered to be missing that item. + .Example + dir Server*.xlsx | Merge-MulipleSheets -WorkSheetName Services -OutputFile Test2.xlsx -OutputSheetName Services -Show + We are auditing servers and each one has a workbook in the current directory which contains a "Services" worksheet (the result of + Get-WmiObject -Class win32_service | Select-Object -Property Name, Displayname, Startmode + No key is specified so the key is assumed to be the "Name" column. The files are merged and the result is opened on completion. + .Example + dir Serv*.xlsx | Merge-MulipleSheets -WorkSheetName Software -Key "*" -ExcludeProperty Install* -OutputFile Test2.xlsx -OutputSheetName Software -Show + The server audit files in the previous example also have "Software" worksheet, but no single field on that sheet works as a key. + Specifying "*" for the key produces a compound key using all non-excluded fields (and the installation date and file location are excluded). + .Example + Merge-MulipleSheets -Path hotfixes.xlsx -WorkSheetName Serv* -Key hotfixid -OutputFile test2.xlsx -OutputSheetName hotfixes -HideRowNumbers -Show + This time all the servers have written their hofix information to their own worksheets in a shared Excel workbook named "Hotfixes" + (the information was obtained by running Get-Hotfix | Sort-Object -Property description,hotfixid | Select-Object -Property Description,HotfixID) + This ignores any sheets which are not named "Serv*", and uses the HotfixID as the key ; in this version the row numbers are hidden. +#> + + param ( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] [string[]]$Path , #The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. @@ -316,85 +356,116 @@ #If specified, opens the output workbook. [Switch]$Show ) - begin { $filestoProcess = @() } - process { $filestoProcess += $Path} + begin { $filestoProcess = @() } + process { $filestoProcess += $Path} end { - if ( $filestoProcess.count -lt 2) {Write-Warning -Message "Need at least two files to process"; return} - - #Set up the parameters we will pass to merge worksheet - Get-Variable -Name 'HeaderName','NoHeader','StartRow','Key','Property','ExcludeProperty','WorkSheetName' -ErrorAction SilentlyContinue | - Where-Object {$_.Value} | ForEach-Object -Begin {$params= @{} } -Process {$params[$_.Name] = $_.Value} - - Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($filestoProcess[-1]) against $($filestoProcess[0]). " - $merged = Merge-Worksheet @params -Referencefile $filestoProcess[0] -Differencefile $filestoProcess[-1] - $nextFileNo = 2 - while ($nextFileNo -lt $filestoProcess.count -and $merged) { - Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($filestoProcess[-$nextFileNo]) against $($filestoProcess[0]). " - $merged = Merge-Worksheet @params -ReferenceObject $merged -Differencefile $filestoProcess[-$nextFileNo] - $nextFileNo ++ - } - if (-not $merged) {Write-Warning -Message "The merge operation did not return any data."; return } + if ($filestoProcess.Count -eq 1 -and $WorkSheetName -match '\*') { + Write-Progress -Activity "Merging sheets" -CurrentOperation "Expanding * to names of sheets in $($filestoProcess[0]). " + $excel = Open-ExcelPackage -Path $filestoProcess + $WorksheetName = $excel.Workbook.Worksheets.Name.where({$_ -like $WorkSheetName}) + Close-ExcelPackage -NoSave -ExcelPackage $excel + } - Write-Progress -Activity "Merging sheets" -CurrentOperation "Creating output sheet '$OutputSheetName' in $OutputFile" - $excel = $merged | Sort-Object "_row" | Update-FirstObjectProperties | Export-Excel -Path $OutputFile -WorkSheetname $OutputSheetName -ClearSheet -FreezeTopRow -BoldTopRow -AutoFilter -PassThru - $sheet = $excel.Workbook.Worksheets[$OutputSheetName] + #Merge indentically named sheets in different work books; + if ($filestoProcess.Count -ge 2 -and $WorkSheetName -is "string" ) { + Get-Variable -Name 'HeaderName','NoHeader','StartRow','Key','Property','ExcludeProperty','WorkSheetName' -ErrorAction SilentlyContinue | + Where-Object {$_.Value} | ForEach-Object -Begin {$params= @{} } -Process {$params[$_.Name] = $_.Value} + + Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($filestoProcess[-1]) against $($filestoProcess[0]). " + $merged = Merge-Worksheet @params -Referencefile $filestoProcess[0] -Differencefile $filestoProcess[-1] + $nextFileNo = 2 + while ($nextFileNo -lt $filestoProcess.count -and $merged) { + Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($filestoProcess[-$nextFileNo]) against $($filestoProcess[0]). " + $merged = Merge-Worksheet @params -ReferenceObject $merged -Differencefile $filestoProcess[-$nextFileNo] + $nextFileNo ++ + } + } + #Merge different sheets from one workbook + elseif ($filestoProcess.Count -eq 1 -and $WorkSheetName.Count -ge 2 ) { + Get-Variable -Name 'HeaderName','NoHeader','StartRow','Key','Property','ExcludeProperty' -ErrorAction SilentlyContinue | + Where-Object {$_.Value} | ForEach-Object -Begin {$params= @{} } -Process {$params[$_.Name] = $_.Value} + + Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($WorkSheetName[-1]) against $($WorkSheetName[0]). " + $merged = Merge-Worksheet @params -Referencefile $filestoProcess[0] -Differencefile $filestoProcess[0] -WorkSheetName $WorkSheetName[0,-1] + $nextSheetNo = 2 + while ($nextSheetNo -lt $WorkSheetName.count -and $merged) { + Write-Progress -Activity "Merging sheets" -CurrentOperation "Comparing $($WorkSheetName[-$nextSheetNo]) against $($WorkSheetName[0]). " + $merged = Merge-Worksheet @params -ReferenceObject $merged -Differencefile $filestoProcess[0] -WorkSheetName $WorkSheetName[-$nextSheetNo] -DiffPrefix $WorkSheetName[-$nextSheetNo] + $nextSheetNo ++ + } + } + #We either need one worksheet name and many files or one file and many sheets. + else { Write-Warning -Message "Need at least two files to process" ; return } + #if the process didn't return data then abandon now. + if (-not $merged) {Write-Warning -Message "The merge operation did not return any data."; return } + + Write-Progress -Activity "Merging sheets" -CurrentOperation "Creating output sheet '$OutputSheetName' in $OutputFile" + $excel = $merged | Sort-Object "_row" | Update-FirstObjectProperties | + Export-Excel -Path $OutputFile -WorkSheetname $OutputSheetName -ClearSheet -BoldTopRow -AutoFilter -PassThru + $sheet = $excel.Workbook.Worksheets[$OutputSheetName] - #We will put in a conditional format for "if all the others are not flagged as 'same'" to mark rows where something is added, removed or changed - $sameChecks = @() - - #All the 'difference' columns in the sheet are labeled with the file they came from, 'reference' columns need their - #headers prefixed with the ref file name, $colnames is the basis of a regular expression to identify what should have $refPrefix appended - $colNames = @("_Row","^$Key`$") - $refPrefix = (Split-Path -Path $filestoProcess[0] -Leaf) -replace "\.xlsx$"," " - - Write-Progress -Activity "Merging sheets" -CurrentOperation "Applying formatting to sheet '$OutputSheetName' in $OutputFile" - #Find the column headings which are in the form "diffFile is"; which will hold 'Same', 'Added' or 'Changed' - foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { + #We will put in a conditional format for "if all the others are not flagged as 'same'" to mark rows where something is added, removed or changed + $sameChecks = @() + + #All the 'difference' columns in the sheet are labeled with the file they came from, 'reference' columns need their + #headers prefixed with the ref file name, $colnames is the basis of a regular expression to identify what should have $refPrefix appended + $colNames = @("_Row") + if ($key -ne "*") + {$colnames += $Key} + if ($filesToProcess.Count -ge 2) { + $refPrefix = (Split-Path -Path $filestoProcess[0] -Leaf) -replace "\.xlsx$"," " + } + else {$refPrefix = $WorkSheetName[0] } + Write-Progress -Activity "Merging sheets" -CurrentOperation "Applying formatting to sheet '$OutputSheetName' in $OutputFile" + #Find the column headings which are in the form "diffFile is"; which will hold 'Same', 'Added' or 'Changed' + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { #Work leftwards across the headings applying conditional formatting which says # 'Format this cell if the "IS" column has a value of ...' until you find a heading which doesn't have the prefix. - $prefix = $cell.value -replace "\sIS$","" - $columnNo = $cell.start.Column -1 - $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) + $prefix = $cell.value -replace "\sIS$","" + $columnNo = $cell.start.Column -1 + $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) while ($sheet.cells[$cellAddr].value -match $prefix) { $condFormattingParams = @{RuleType='Expression'; BackgroundPattern='None'; WorkSheet=$sheet; Range=$([OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R[1]C[$columnNo]:R[1048576]C[$columnNo]",0,0)) } Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Added"' ) -BackgroundColor $AddBackgroundColor Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Changed"') -BackgroundColor $ChangeBackgroundColor Add-ConditionalFormatting @condFormattingParams -ConditionValue ($cell.Address + '="Removed"') -BackgroundColor $DeleteBackgroundColor $columnNo -- - $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) + $cellAddr = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R1C$columnNo",1,$columnNo) } #build up a list of prefixes in $colnames - we'll use that to set headers on rows from the reference file; and build up the "if the 'is' cell isn't same" list - $colNames += $prefix - $sameChecks += (($cell.Address -replace "1","2") +'<>"Same"') - } + $colNames += $prefix + $sameChecks += (($cell.Address -replace "1","2") +'<>"Same"') + } - #For all the columns which don't match one of the Diff-file prefixes or "_Row" or the 'Key' columnn name; add the reference file prefix to their header. - $nameRegex = $colNames -Join "|" - foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -Notmatch $nameRegex}) ) { - $cell.Value = $refPrefix + $cell.Value - } - #We've made a bunch of things wider so now is the time to autofit columns. Any hiding has to come AFTER this, because it unhides things - $sheet.Cells.AutoFitColumns() - - #if we have a key field (we didn't concatenate all fields) use what we built up in $sameChecks to apply conditional formatting to it (Row no will be in column A, Key in Column B) - if ($Key -ne '*') { - Add-ConditionalFormatting -WorkSheet $sheet -Range "B2:B1048576" -ForeGroundColor $KeyFontColor -BackgroundPattern 'None' -RuleType Expression -ConditionValue ("OR(" +($sameChecks -join ",") +")" ) - } - #Go back over the headings to find and hide the "is" columns; - foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { + #For all the columns which don't match one of the Diff-file prefixes or "_Row" or the 'Key' columnn name; add the reference file prefix to their header. + $nameRegex = $colNames -Join "|" + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -Notmatch $nameRegex}) ) { + $cell.Value = $refPrefix + $cell.Value + $condFormattingParams = @{RuleType='Expression'; BackgroundPattern='None'; WorkSheet=$sheet; Range=[OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R[2]C[$($cell.start.column)]:R[1048576]C[$($cell.start.column)]",0,0)} + Add-ConditionalFormatting @condFormattingParams -ConditionValue ("OR(" +(($sameChecks -join ",") -replace '<>"Same"','="Added"') +")" ) -BackgroundColor $DeleteBackgroundColor + } + #We've made a bunch of things wider so now is the time to autofit columns. Any hiding has to come AFTER this, because it unhides things + $sheet.Cells.AutoFitColumns() + + #if we have a key field (we didn't concatenate all fields) use what we built up in $sameChecks to apply conditional formatting to it (Row no will be in column A, Key in Column B) + if ($Key -ne '*') { + Add-ConditionalFormatting -WorkSheet $sheet -Range "B2:B1048576" -ForeGroundColor $KeyFontColor -BackgroundPattern 'None' -RuleType Expression -ConditionValue ("OR(" +($sameChecks -join ",") +")" ) + $sheet.view.FreezePanes(2, 3) + } + else {$sheet.view.FreezePanes(2, 2) } + #Go back over the headings to find and hide the "is" columns; + foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "\sIS$"}) ) { $sheet.Column($cell.start.Column).HIDDEN = $true - } + } - #If specified, look over the headings for "row" and hide the columns which say "this was in row such-and-such" - if ($HideRowNumbers) { + #If specified, look over the headings for "row" and hide the columns which say "this was in row such-and-such" + if ($HideRowNumbers) { foreach ($cell in $sheet.Cells[($sheet.Dimension.Address -replace "\d+$","1")].Where({$_.value -match "Row$"}) ) { $sheet.Column($cell.start.Column).HIDDEN = $true } - } + } - Close-ExcelPackage -ExcelPackage $excel -Show:$Show - Write-Progress -Activity "Merging sheets" -Completed + Close-ExcelPackage -ExcelPackage $excel -Show:$Show + Write-Progress -Activity "Merging sheets" -Completed } - } - - \ No newline at end of file +} From f2be21f955fbea050d6364bed6e33e8a4a35b764 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Wed, 30 May 2018 00:35:26 +0100 Subject: [PATCH 16/17] Added Merge-MultipleSheets to argument completers --- ColorCompletion.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ColorCompletion.ps1 b/ColorCompletion.ps1 index bfd811a..b5d307a 100644 --- a/ColorCompletion.ps1 +++ b/ColorCompletion.ps1 @@ -17,7 +17,11 @@ if (Get-Command -Name register-argumentCompleter -ErrorAction SilentlyContinue) Register-ArgumentCompleter -CommandName Merge-Worksheet -ParameterName AddBackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Merge-Worksheet -ParameterName ChangeBackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Merge-Worksheet ` -ParameterName DeleteBackgroundColor -ScriptBlock $Function:ColorCompletion - Register-ArgumentCompleter -CommandName Merge-Worksheet -ParameterName KeyFontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-MulipleSheets -ParameterName KeyFontColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-MulipleSheets -ParameterName AddBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-MulipleSheets -ParameterName ChangeBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-MulipleSheets ` -ParameterName DeleteBackgroundColor -ScriptBlock $Function:ColorCompletion + Register-ArgumentCompleter -CommandName Merge-MulipleSheets -ParameterName KeyFontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName BackgroundColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName FontColor -ScriptBlock $Function:ColorCompletion Register-ArgumentCompleter -CommandName Set-Format -ParameterName PatternColor -ScriptBlock $Function:ColorCompletion From e47e1d99c1fc5dde987c118575fb8c5288787ced Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 8 Jun 2018 10:43:57 +0100 Subject: [PATCH 17/17] Added EndRow, StartColumn, EndColumn to Import-Excel Start row already existed. Aliases allow you to specify -no header -top 10 -bottom 20 -left 6 -right 8 Start row can be below end right row, and start (left) column can be to the right of the end column - this allows read in reverse order, but does generate a warning. --- ImportExcel.psm1 | 1044 +++++++++++++++++++++++----------------------- 1 file changed, 528 insertions(+), 516 deletions(-) diff --git a/ImportExcel.psm1 b/ImportExcel.psm1 index e80ef95..552c964 100644 --- a/ImportExcel.psm1 +++ b/ImportExcel.psm1 @@ -1,516 +1,528 @@ -#region import everything we need - Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" - . $PSScriptRoot\AddConditionalFormatting.ps1 - . $PSScriptRoot\Charting.ps1 - . $PSScriptRoot\ColorCompletion.ps1 - . $PSScriptRoot\ConvertExcelToImageFile.ps1 - . $PSScriptRoot\Compare-WorkSheet.ps1 - . $PSScriptRoot\ConvertFromExcelData.ps1 - . $PSScriptRoot\ConvertFromExcelToSQLInsert.ps1 - . $PSScriptRoot\ConvertToExcelXlsx.ps1 - . $PSScriptRoot\Copy-ExcelWorkSheet.ps1 - . $PSScriptRoot\Export-Excel.ps1 - . $PSScriptRoot\Export-ExcelSheet.ps1 - . $PSScriptRoot\Get-ExcelColumnName.ps1 - . $PSScriptRoot\Get-ExcelSheetInfo.ps1 - . $PSScriptRoot\Get-ExcelWorkbookInfo.ps1 - . $PSScriptRoot\Get-HtmlTable.ps1 - . $PSScriptRoot\Get-Range.ps1 - . $PSScriptRoot\Get-XYRange.ps1 - . $PSScriptRoot\Import-Html.ps1 - . $PSScriptRoot\InferData.ps1 - . $PSScriptRoot\Invoke-Sum.ps1 - . $PSScriptRoot\Merge-Worksheet.ps1 - . $PSScriptRoot\New-ConditionalFormattingIconSet.ps1 - . $PSScriptRoot\New-ConditionalText.ps1 - . $PSScriptRoot\New-ExcelChart.ps1 - . $PSScriptRoot\New-PSItem.ps1 - . $PSScriptRoot\Open-ExcelPackage.ps1 - . $PSScriptRoot\Pivot.ps1 - . $PSScriptRoot\Send-SQLDataToExcel.ps1 - . $PSScriptRoot\Set-CellStyle.ps1 - . $PSScriptRoot\Set-Column.ps1 - . $PSScriptRoot\Set-Row.ps1 - . $PSScriptRoot\SetFormat.ps1 - . $PSScriptRoot\TrackingUtils.ps1 - . $PSScriptRoot\Update-FirstObjectProperties.ps1 - - - New-Alias -Name Use-ExcelData -Value "ConvertFrom-ExcelData" -Force - - if ($PSVersionTable.PSVersion.Major -ge 5) { - . $PSScriptRoot\Plot.ps1 - - Function New-Plot { - Param() - - [PSPlot]::new() - } - - } - else { - Write-Warning 'PowerShell 5 is required for plot.ps1' - Write-Warning 'PowerShell Excel is ready, except for that functionality' - } -#endregion -Function Import-Excel { - <# - .SYNOPSIS - Create custom objects from the rows in an Excel worksheet. - - .DESCRIPTION - The Import-Excel cmdlet creates custom objects from the rows in an Excel worksheet. Each row represents one object. All of this is possible without installing Microsoft Excel and by using the .NET library ‘EPPLus.dll’. - - By default, the property names of the objects are retrieved from the column headers. Because an object cannot have a blanc property name, only columns with column headers will be imported. - - If the default behavior is not desired and you want to import the complete worksheet ‘as is’, the parameter ‘-NoHeader’ can be used. In case you want to provide your own property names, you can use the parameter ‘-HeaderName’. - - .PARAMETER Path - Specifies the path to the Excel file. - - .PARAMETER WorksheetName - Specifies the name of the worksheet in the Excel workbook to import. By default, if no name is provided, the first worksheet will be imported. - - .PARAMETER DataOnly - Import only rows and columns that contain data, empty rows and empty columns are not imported. - - .PARAMETER HeaderName - Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. - - In case you provide less header names than there is data in the worksheet, then only the data with a corresponding header name will be imported and the data without header name will be disregarded. - - In case you provide more header names than there is data in the worksheet, then all data will be imported and all objects will have all the property names you defined in the header names. As such, the last properties will be blanc as there is no data for them. - - .PARAMETER NoHeader - Automatically generate property names (P1, P2, P3, ..) instead of the ones defined in the column headers of the TopRow. - - This switch is best used when you want to import the complete worksheet ‘as is’ and are not concerned with the property names. - - .PARAMETER StartRow - The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. - - When the parameters ‘-NoHeader’ and ‘-HeaderName’ are not provided, this row will contain the column headers that will be used as property names. When one of both parameters are provided, the property names are automatically created and this row will be treated as a regular row containing data. - - .PARAMETER Password - Accepts a string that will be used to open a password protected Excel file. - - .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the column names defined in the first row. In case a column doesn’t have a column header (usually in row 1 when ‘-StartRow’ is not used), then the unnamed columns will be skipped and the data in those columns will not be imported. - - ---------------------------------------------- - | File: Movies.xlsx - Sheet: Actors | - ---------------------------------------------- - | A B C | - |1 First Name Address | - |2 Chuck Norris California | - |3 Jean-Claude Vandamme Brussels | - ---------------------------------------------- - - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors - - First Name: Chuck - Address : California - - First Name: Jean-Claude - Address : Brussels - - Notice that column 'B' is not imported because there's no value in cell 'B1' that can be used as property name for the objects. - - .EXAMPLE - Import the complete Excel worksheet ‘as is’ by using the ‘-NoHeader’ switch. One object is created for each row. The property names of the objects will be automatically generated (P1, P2, P3, ..). - - ---------------------------------------------- - | File: Movies.xlsx - Sheet: Actors | - ---------------------------------------------- - | A B C | - |1 First Name Address | - |2 Chuck Norris California | - |3 Jean-Claude Vandamme Brussels | - ---------------------------------------------- - - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -NoHeader - - P1: First Name - P2: - P3: Address - - P1: Chuck - P2: Norris - P3: California - - P1: Jean-Claude - P2: Vandamme - P3: Brussels - - Notice that the column header (row 1) is imported as an object too. - - .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the names defined in the parameter ‘-HeaderName’. The properties are named starting from the most left column (A) to the right. In case no value is present in one of the columns, that property will have an empty value. - - ---------------------------------------------------------- - | File: Movies.xlsx - Sheet: Movies | - ---------------------------------------------------------- - | A B C D | - |1 The Bodyguard 1992 9 | - |2 The Matrix 1999 8 | - |3 | - |4 Skyfall 2012 9 | - ---------------------------------------------------------- - - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies -HeaderName 'Movie name', 'Year', 'Rating', 'Genre' - - Movie name: The Bodyguard - Year : 1992 - Rating : 9 - Genre : - - Movie name: The Matrix - Year : 1999 - Rating : 8 - Genre : - - Movie name: - Year : - Rating : - Genre : - - Movie name: Skyfall - Year : 2012 - Rating : 9 - Genre : - - Notice that empty rows are imported and that data for the property 'Genre' is not present in the worksheet. As such, the 'Genre' property will be blanc for all objects. - - .EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names of the objects are automatically generated by using the switch ‘-NoHeader’ (P1, P@, P#, ..). The switch ‘-DataOnly’ will speed up the import because empty rows and empty columns are not imported. - - ---------------------------------------------------------- - | File: Movies.xlsx - Sheet: Movies | - ---------------------------------------------------------- - | A B C D | - |1 The Bodyguard 1992 9 | - |2 The Matrix 1999 8 | - |3 | - |4 Skyfall 2012 9 | - ---------------------------------------------------------- - - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies –NoHeader -DataOnly - - P1: The Bodyguard - P2: 1992 - P3: 9 - - P1: The Matrix - P2: 1999 - P3: 8 - - P1: Skyfall - P2: 2012 - P3: 9 - - Notice that empty rows and empty columns are not imported. - -.EXAMPLE - Import data from an Excel worksheet. One object is created for each row. The property names are provided with the ‘-HeaderName’ parameter. The import will start from row 2 and empty columns and rows are not imported. - - ---------------------------------------------------------- - | File: Movies.xlsx - Sheet: Actors | - ---------------------------------------------------------- - | A B C D | - |1 Chuck Norris California | - |2 | - |3 Jean-Claude Vandamme Brussels | - ---------------------------------------------------------- - - PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -DataOnly -HeaderName 'FirstName', 'SecondName', 'City' –StartRow 2 - - FirstName : Jean-Claude - SecondName: Vandamme - City : Brussels - - Notice that only 1 object is imported with only 3 properties. Column B and row 2 are empty and have been disregarded by using the switch '-DataOnly'. The property names have been named with the values provided with the parameter '-HeaderName'. Row number 1 with ‘Chuck Norris’ has not been imported, because we started the import from row 2 with the parameter ‘-StartRow 2’. - - .LINK - https://github.com/dfinke/ImportExcel - - .NOTES - #> - - [CmdLetBinding(DefaultParameterSetName)] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] - Param ( - [Alias('FullName')] - [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline, Position=0, Mandatory)] - [ValidateScript( {(Test-Path -Path $_ -PathType Leaf) -and ($_ -match '.xls$|.xlsx$|.xlsm$')})] - [String]$Path, - [Alias('Sheet')] - [Parameter(Position=1)] - [ValidateNotNullOrEmpty()] - [String]$WorksheetName, - [Parameter(ParameterSetName='B', Mandatory)] - [String[]]$HeaderName, - [Parameter(ParameterSetName='C', Mandatory)] - [Switch]$NoHeader, - [Alias('HeaderRow','TopRow')] - [ValidateRange(1, 9999)] - [Int]$StartRow = 1, - [Switch]$DataOnly, - [ValidateNotNullOrEmpty()] - [String]$Password - ) - Begin { - $sw = [System.Diagnostics.Stopwatch]::StartNew() - Function Get-PropertyNames { - <# - .SYNOPSIS - Create objects containing the column number and the column name for each of the different header types. - #> - - Param ( - [Parameter(Mandatory)] - [Int[]]$Columns, - [Parameter(Mandatory)] - [Int]$StartRow - ) - - Try { - if ($NoHeader) { - $i = 0 - foreach ($C in $Columns) { - $i++ - $C | Select-Object @{N='Column'; E={$_}}, @{N='Value'; E={'P' + $i}} - } - } - elseif ($HeaderName) { - $i = 0 - foreach ($H in $HeaderName) { - $H | Select-Object @{N='Column'; E={$Columns[$i]}}, @{N='Value'; E={$H}} - $i++ - } - } - else { - if ($StartRow -eq 0) { - throw 'The top row can never be equal to 0 when we need to retrieve headers from the worksheet.' - } - - foreach ($C in $Columns) { - $Worksheet.Cells[$StartRow,$C] | Where-Object {$_.Value} | Select-Object @{N='Column'; E={$C}}, Value - } - } - } - Catch { - throw "Failed creating property names: $_" - } - } - } - - Process { - Try { - #region Open file - $Path = (Resolve-Path $Path).ProviderPath - Write-Verbose "Import Excel workbook '$Path' with worksheet '$Worksheetname'" - - $Stream = New-Object -TypeName System.IO.FileStream -ArgumentList $Path, 'Open', 'Read', 'ReadWrite' - - if ($Password) { - $Excel = New-Object -TypeName OfficeOpenXml.ExcelPackage - - Try { - $Excel.Load($Stream,$Password) - } - Catch { - throw "Password '$Password' is not correct." - } - } - else { - $Excel = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Stream - } - #endregion - - #region Select worksheet - if ($WorksheetName) { - if (-not ($Worksheet = $Excel.Workbook.Worksheets[$WorkSheetName])) { - throw "Worksheet '$WorksheetName' not found, the workbook only contains the worksheets '$($Excel.Workbook.Worksheets)'. If you only wish to select the first worksheet, please remove the '-WorksheetName' parameter." - } - } - else { - $Worksheet = $Excel.Workbook.Worksheets | Select-Object -First 1 - } - #endregion - Write-Debug $sw.Elapsed.TotalMilliseconds - #region Get rows and columns - #If we are doing dataonly it is quicker to work out which rows to ignore before processing the cells. - if ($DataOnly) { - #If we are using headers startrow will be the headerrow so examine data from startRow + 1, - if ($NoHeader) {$range = "A" + ($StartRow ) + ":" + $Worksheet.Dimension.End.Address } - else {$range = "A" + ($StartRow + 1 ) + ":" + $Worksheet.Dimension.End.Address } - #We're going to look at every cell and build 2 hash tables holding rows & columns which contain data. - #Want to Avoid 'select unique' operations & large Sorts, becuse time time taken increases with square - #of number of items (PS uses heapsort at large size). Instead keep a list of what we have seen, - #using Hash tables: "we've seen it" is all we need, no need to worry about "seen it before" / "Seen it many times" - $colHash = @{} - $rowHash = @{} - foreach ($cell in $Worksheet.Cells[$range]) { - if ($cell.Value -ne $null) {$colHash[$cell.Start.Column]=1; $rowHash[$cell.Start.row]=1 } - } - $rows = ($StartRow..($Worksheet.Dimension.End.Row)).Where({$rowHash[$_]}) - $columns = (1..($Worksheet.Dimension.End.Column) ).Where({$colHash[$_]}) - } - else { - $Columns = ($Worksheet.Dimension.Start.Column)..($Worksheet.Dimension.End.Column) - if ($NoHeader) {$Rows = ( $StartRow)..($Worksheet.Dimension.End.Row) } - else {$Rows = (1 + $StartRow)..($Worksheet.Dimension.End.Row) } - } - #endregion - #region Create property names - if ((-not $Columns) -or (-not ($PropertyNames = Get-PropertyNames -Columns $Columns -StartRow $StartRow))) { - throw "No column headers found on top row '$StartRow'. If column headers in the worksheet are not a requirement then please use the '-NoHeader' or '-HeaderName' parameter." - } - if ($Duplicates = $PropertyNames | Group-Object Value | Where-Object Count -GE 2) { - throw "Duplicate column headers found on row '$StartRow' in columns '$($Duplicates.Group.Column)'. Column headers must be unique, if this is not a requirement please use the '-NoHeader' or '-HeaderName' parameter." - } - #endregion - Write-Debug $sw.Elapsed.TotalMilliseconds - if (-not $Rows) { - Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' contains no data in the rows after top row '$StartRow'" - } - else { - #region Create one object per row - foreach ($R in $Rows) { - Write-Verbose "Import row '$R'" - $NewRow = [Ordered]@{} - - foreach ($P in $PropertyNames) { - $NewRow[$P.Value] = $Worksheet.Cells[$R, $P.Column].Value - Write-Verbose "Import cell '$($Worksheet.Cells[$R, $P.Column].Address)' with property name '$($p.Value)' and value '$($Worksheet.Cells[$R, $P.Column].Value)'." - } - - [PSCustomObject]$NewRow - } - #endregion - } - Write-Debug $sw.Elapsed.TotalMilliseconds - } - Catch { - throw "Failed importing the Excel workbook '$Path' with worksheet '$Worksheetname': $_" - } - Finally { - $Stream.Close() - $Stream.Dispose() - $Excel.Dispose() - $Excel = $null - } - } -} - -function Add-WorkSheet { - param( - #TODO Use parametersets to allow a workbook to be passed instead of a package - [Parameter(Mandatory=$true, ValueFromPipeline=$true)] - [OfficeOpenXml.ExcelPackage] $ExcelPackage, - [Parameter(Mandatory=$true)] - [string] $WorkSheetname, - [switch] $ClearSheet, - [Switch] $NoClobber - ) - - $ws = $ExcelPackage.Workbook.Worksheets[$WorkSheetname] - if($ClearSheet -and $ws) {$ExcelPackage.Workbook.Worksheets.Delete($WorkSheetname) ; $ws = $null } - if(!$ws) { - Write-Verbose "Add worksheet '$WorkSheetname'" - $ws=$ExcelPackage.Workbook.Worksheets.Add($WorkSheetname) - } - - return $ws -} - -function ConvertFrom-ExcelSheet { - <# - .Synopsis - Reads an Excel file an converts the data to a delimited text file - - .Example - ConvertFrom-ExcelSheet .\TestSheets.xlsx .\data - Reads each sheet in TestSheets.xlsx and outputs it to the data directory as the sheet name with the extension .txt - - .Example - ConvertFrom-ExcelSheet .\TestSheets.xlsx .\data sheet?0 - Reads and outputs sheets like Sheet10 and Sheet20 form TestSheets.xlsx and outputs it to the data directory as the sheet name with the extension .txt - #> - - [CmdletBinding()] - param - ( - [Alias("FullName")] - [Parameter(Mandatory = $true)] - [String] - $Path, - [String] - $OutputPath = '.\', - [String] - $SheetName="*", - [ValidateSet('ASCII', 'BigEndianUniCode','Default','OEM','UniCode','UTF32','UTF7','UTF8')] - [string] - $Encoding = 'UTF8', - [ValidateSet('.txt', '.log','.csv')] - [string] - $Extension = '.csv', - [ValidateSet(';', ',')] - [string] - $Delimiter = ';' - ) - - $Path = (Resolve-Path $Path).Path - $stream = New-Object -TypeName System.IO.FileStream -ArgumentList $Path,"Open","Read","ReadWrite" - $xl = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $stream - $workbook = $xl.Workbook - - $targetSheets = $workbook.Worksheets | Where-Object {$_.Name -like $SheetName} - - $params = @{} + $PSBoundParameters - $params.Remove("OutputPath") - $params.Remove("SheetName") - $params.Remove('Extension') - $params.NoTypeInformation = $true - - Foreach ($sheet in $targetSheets) - { - Write-Verbose "Exporting sheet: $($sheet.Name)" - - $params.Path = "$OutputPath\$($Sheet.Name)$Extension" - - Import-Excel $Path -Sheet $($sheet.Name) | Export-Csv @params - } - - $stream.Close() - $stream.Dispose() - $xl.Dispose() -} - -function Export-MultipleExcelSheets { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] - param( - [Parameter(Mandatory=$true)] - $Path, - [Parameter(Mandatory=$true)] - [hashtable]$InfoMap, - [string]$Password, - [Switch]$Show, - [Switch]$AutoSize - ) - - $parameters = @{}+$PSBoundParameters - $parameters.Remove("InfoMap") - $parameters.Remove("Show") - - $parameters.Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) - - foreach ($entry in $InfoMap.GetEnumerator()) { - Write-Progress -Activity "Exporting" -Status "$($entry.Key)" - $parameters.WorkSheetname=$entry.Key - - & $entry.Value | Export-Excel @parameters - } - - if($Show) {Invoke-Item $Path} -} +#region import everything we need + Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" + . $PSScriptRoot\AddConditionalFormatting.ps1 + . $PSScriptRoot\Charting.ps1 + . $PSScriptRoot\ColorCompletion.ps1 + . $PSScriptRoot\ConvertExcelToImageFile.ps1 + . $PSScriptRoot\Compare-WorkSheet.ps1 + . $PSScriptRoot\ConvertFromExcelData.ps1 + . $PSScriptRoot\ConvertFromExcelToSQLInsert.ps1 + . $PSScriptRoot\ConvertToExcelXlsx.ps1 + . $PSScriptRoot\Copy-ExcelWorkSheet.ps1 + . $PSScriptRoot\Export-Excel.ps1 + . $PSScriptRoot\Export-ExcelSheet.ps1 + . $PSScriptRoot\Get-ExcelColumnName.ps1 + . $PSScriptRoot\Get-ExcelSheetInfo.ps1 + . $PSScriptRoot\Get-ExcelWorkbookInfo.ps1 + . $PSScriptRoot\Get-HtmlTable.ps1 + . $PSScriptRoot\Get-Range.ps1 + . $PSScriptRoot\Get-XYRange.ps1 + . $PSScriptRoot\Import-Html.ps1 + . $PSScriptRoot\InferData.ps1 + . $PSScriptRoot\Invoke-Sum.ps1 + . $PSScriptRoot\Merge-Worksheet.ps1 + . $PSScriptRoot\New-ConditionalFormattingIconSet.ps1 + . $PSScriptRoot\New-ConditionalText.ps1 + . $PSScriptRoot\New-ExcelChart.ps1 + . $PSScriptRoot\New-PSItem.ps1 + . $PSScriptRoot\Open-ExcelPackage.ps1 + . $PSScriptRoot\Pivot.ps1 + . $PSScriptRoot\Send-SQLDataToExcel.ps1 + . $PSScriptRoot\Set-CellStyle.ps1 + . $PSScriptRoot\Set-Column.ps1 + . $PSScriptRoot\Set-Row.ps1 + . $PSScriptRoot\SetFormat.ps1 + . $PSScriptRoot\TrackingUtils.ps1 + . $PSScriptRoot\Update-FirstObjectProperties.ps1 + + + New-Alias -Name Use-ExcelData -Value "ConvertFrom-ExcelData" -Force + + if ($PSVersionTable.PSVersion.Major -ge 5) { + . $PSScriptRoot\Plot.ps1 + + Function New-Plot { + Param() + + [PSPlot]::new() + } + + } + else { + Write-Warning 'PowerShell 5 is required for plot.ps1' + Write-Warning 'PowerShell Excel is ready, except for that functionality' + } +#endregion +Function Import-Excel { + <# + .SYNOPSIS + Create custom objects from the rows in an Excel worksheet. + + .DESCRIPTION + The Import-Excel cmdlet creates custom objects from the rows in an Excel worksheet. Each row represents one object. All of this is possible without installing Microsoft Excel and by using the .NET library ‘EPPLus.dll’. + + By default, the property names of the objects are retrieved from the column headers. Because an object cannot have a blanc property name, only columns with column headers will be imported. + + If the default behavior is not desired and you want to import the complete worksheet ‘as is’, the parameter ‘-NoHeader’ can be used. In case you want to provide your own property names, you can use the parameter ‘-HeaderName’. + + .PARAMETER Path + Specifies the path to the Excel file. + + .PARAMETER WorksheetName + Specifies the name of the worksheet in the Excel workbook to import. By default, if no name is provided, the first worksheet will be imported. + + .PARAMETER DataOnly + Import only rows and columns that contain data, empty rows and empty columns are not imported. + + .PARAMETER HeaderName + Specifies custom property names to use, instead of the values defined in the column headers of the TopRow. + + In case you provide less header names than there is data in the worksheet, then only the data with a corresponding header name will be imported and the data without header name will be disregarded. + + In case you provide more header names than there is data in the worksheet, then all data will be imported and all objects will have all the property names you defined in the header names. As such, the last properties will be blanc as there is no data for them. + + .PARAMETER NoHeader + Automatically generate property names (P1, P2, P3, ..) instead of the ones defined in the column headers of the TopRow. + + This switch is best used when you want to import the complete worksheet ‘as is’ and are not concerned with the property names. + + .PARAMETER StartRow + The row from where we start to import data, all rows above the StartRow are disregarded. By default this is the first row. + + When the parameters ‘-NoHeader’ and ‘-HeaderName’ are not provided, this row will contain the column headers that will be used as property names. When one of both parameters are provided, the property names are automatically created and this row will be treated as a regular row containing data. + + .PARAMETER EndRow + By default all rows up to the last cell in the sheet will be imported. If specified, import stops at this row. + + .PARAMETER Password + Accepts a string that will be used to open a password protected Excel file. + + .EXAMPLE + Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the column names defined in the first row. In case a column doesn’t have a column header (usually in row 1 when ‘-StartRow’ is not used), then the unnamed columns will be skipped and the data in those columns will not be imported. + + ---------------------------------------------- + | File: Movies.xlsx - Sheet: Actors | + ---------------------------------------------- + | A B C | + |1 First Name Address | + |2 Chuck Norris California | + |3 Jean-Claude Vandamme Brussels | + ---------------------------------------------- + + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors + + First Name: Chuck + Address : California + + First Name: Jean-Claude + Address : Brussels + + Notice that column 'B' is not imported because there's no value in cell 'B1' that can be used as property name for the objects. + + .EXAMPLE + Import the complete Excel worksheet ‘as is’ by using the ‘-NoHeader’ switch. One object is created for each row. The property names of the objects will be automatically generated (P1, P2, P3, ..). + + ---------------------------------------------- + | File: Movies.xlsx - Sheet: Actors | + ---------------------------------------------- + | A B C | + |1 First Name Address | + |2 Chuck Norris California | + |3 Jean-Claude Vandamme Brussels | + ---------------------------------------------- + + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -NoHeader + + P1: First Name + P2: + P3: Address + + P1: Chuck + P2: Norris + P3: California + + P1: Jean-Claude + P2: Vandamme + P3: Brussels + + Notice that the column header (row 1) is imported as an object too. + + .EXAMPLE + Import data from an Excel worksheet. One object is created for each row. The property names of the objects consist of the names defined in the parameter ‘-HeaderName’. The properties are named starting from the most left column (A) to the right. In case no value is present in one of the columns, that property will have an empty value. + + ---------------------------------------------------------- + | File: Movies.xlsx - Sheet: Movies | + ---------------------------------------------------------- + | A B C D | + |1 The Bodyguard 1992 9 | + |2 The Matrix 1999 8 | + |3 | + |4 Skyfall 2012 9 | + ---------------------------------------------------------- + + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies -HeaderName 'Movie name', 'Year', 'Rating', 'Genre' + + Movie name: The Bodyguard + Year : 1992 + Rating : 9 + Genre : + + Movie name: The Matrix + Year : 1999 + Rating : 8 + Genre : + + Movie name: + Year : + Rating : + Genre : + + Movie name: Skyfall + Year : 2012 + Rating : 9 + Genre : + + Notice that empty rows are imported and that data for the property 'Genre' is not present in the worksheet. As such, the 'Genre' property will be blanc for all objects. + + .EXAMPLE + Import data from an Excel worksheet. One object is created for each row. The property names of the objects are automatically generated by using the switch ‘-NoHeader’ (P1, P@, P#, ..). The switch ‘-DataOnly’ will speed up the import because empty rows and empty columns are not imported. + + ---------------------------------------------------------- + | File: Movies.xlsx - Sheet: Movies | + ---------------------------------------------------------- + | A B C D | + |1 The Bodyguard 1992 9 | + |2 The Matrix 1999 8 | + |3 | + |4 Skyfall 2012 9 | + ---------------------------------------------------------- + + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Movies –NoHeader -DataOnly + + P1: The Bodyguard + P2: 1992 + P3: 9 + + P1: The Matrix + P2: 1999 + P3: 8 + + P1: Skyfall + P2: 2012 + P3: 9 + + Notice that empty rows and empty columns are not imported. + +.EXAMPLE + Import data from an Excel worksheet. One object is created for each row. The property names are provided with the ‘-HeaderName’ parameter. The import will start from row 2 and empty columns and rows are not imported. + + ---------------------------------------------------------- + | File: Movies.xlsx - Sheet: Actors | + ---------------------------------------------------------- + | A B C D | + |1 Chuck Norris California | + |2 | + |3 Jean-Claude Vandamme Brussels | + ---------------------------------------------------------- + + PS C:\> Import-Excel -Path 'C:\Movies.xlsx' -WorkSheetname Actors -DataOnly -HeaderName 'FirstName', 'SecondName', 'City' –StartRow 2 + + FirstName : Jean-Claude + SecondName: Vandamme + City : Brussels + + Notice that only 1 object is imported with only 3 properties. Column B and row 2 are empty and have been disregarded by using the switch '-DataOnly'. The property names have been named with the values provided with the parameter '-HeaderName'. Row number 1 with ‘Chuck Norris’ has not been imported, because we started the import from row 2 with the parameter ‘-StartRow 2’. + + .LINK + https://github.com/dfinke/ImportExcel + + .NOTES + #> + + [CmdLetBinding(DefaultParameterSetName)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] + Param ( + [Alias('FullName')] + [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline, Position=0, Mandatory)] + [ValidateScript( {(Test-Path -Path $_ -PathType Leaf) -and ($_ -match '.xls$|.xlsx$|.xlsm$')})] + [String]$Path, + [Alias('Sheet')] + [Parameter(Position=1)] + [ValidateNotNullOrEmpty()] + [String]$WorksheetName, + [Parameter(ParameterSetName='B', Mandatory)] + [String[]]$HeaderName , + [Parameter(ParameterSetName='C', Mandatory)] + [Switch]$NoHeader , + [Alias('HeaderRow','TopRow')] + [ValidateRange(1, 9999)] + [Int]$StartRow = 1, + [Alias('StopRow','BottomRow')] + [Int]$EndRow , + [Alias('LeftColumn')] + [Int]$StartColumn = 1, + [Alias('RightColumn')] + [Int]$EndColumn , + [Switch]$DataOnly, + [ValidateNotNullOrEmpty()] + [String]$Password + ) + Begin { + $sw = [System.Diagnostics.Stopwatch]::StartNew() + Function Get-PropertyNames { + <# + .SYNOPSIS + Create objects containing the column number and the column name for each of the different header types. + #> + + Param ( + [Parameter(Mandatory)] + [Int[]]$Columns, + [Parameter(Mandatory)] + [Int]$StartRow + ) + + Try { + if ($NoHeader) { + $i = 0 + foreach ($C in $Columns) { + $i++ + $C | Select-Object @{N='Column'; E={$_}}, @{N='Value'; E={'P' + $i}} + } + } + elseif ($HeaderName) { + $i = 0 + foreach ($H in $HeaderName) { + $H | Select-Object @{N='Column'; E={$Columns[$i]}}, @{N='Value'; E={$H}} + $i++ + } + } + else { + if ($StartRow -eq 0) { + throw 'The top row can never be equal to 0 when we need to retrieve headers from the worksheet.' + } + + foreach ($C in $Columns) { + $Worksheet.Cells[$StartRow,$C] | Where-Object {$_.Value} | Select-Object @{N='Column'; E={$C}}, Value + } + } + } + Catch { + throw "Failed creating property names: $_" + } + } + } + + Process { + Try { + #region Open file + $Path = (Resolve-Path $Path).ProviderPath + Write-Verbose "Import Excel workbook '$Path' with worksheet '$Worksheetname'" + + $Stream = New-Object -TypeName System.IO.FileStream -ArgumentList $Path, 'Open', 'Read', 'ReadWrite' + + if ($Password) { + $Excel = New-Object -TypeName OfficeOpenXml.ExcelPackage + + Try { + $Excel.Load($Stream,$Password) + } + Catch { + throw "Password '$Password' is not correct." + } + } + else { + $Excel = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Stream + } + #endregion + + #region Select worksheet + if ($WorksheetName) { + if (-not ($Worksheet = $Excel.Workbook.Worksheets[$WorkSheetName])) { + throw "Worksheet '$WorksheetName' not found, the workbook only contains the worksheets '$($Excel.Workbook.Worksheets)'. If you only wish to select the first worksheet, please remove the '-WorksheetName' parameter." + } + } + else { + $Worksheet = $Excel.Workbook.Worksheets | Select-Object -First 1 + } + #endregion + Write-Debug $sw.Elapsed.TotalMilliseconds + #region Get rows and columns + #If we are doing dataonly it is quicker to work out which rows to ignore before processing the cells. + if (-not $EndRow ) {$EndRow = $Worksheet.Dimension.End.Row } + if (-not $EndColumn) {$EndColumn = $Worksheet.Dimension.End.Column } + $endAddress = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R[$EndRow]C[$EndColumn]",0,0) + if ($DataOnly) { + #If we are using headers startrow will be the headerrow so examine data from startRow + 1, + if ($NoHeader) {$range = "A" + ($StartRow ) + ":" + $endAddress } + else {$range = "A" + ($StartRow + 1 ) + ":" + $endAddress } + #We're going to look at every cell and build 2 hash tables holding rows & columns which contain data. + #Want to Avoid 'select unique' operations & large Sorts, becuse time time taken increases with square + #of number of items (PS uses heapsort at large size). Instead keep a list of what we have seen, + #using Hash tables: "we've seen it" is all we need, no need to worry about "seen it before" / "Seen it many times" + $colHash = @{} + $rowHash = @{} + foreach ($cell in $Worksheet.Cells[$range]) { + if ($cell.Value -ne $null) {$colHash[$cell.Start.Column]=1; $rowHash[$cell.Start.row]=1 } + } + $rows = ( $StartRow..$EndRow ).Where({$rowHash[$_]}) + $columns = ($StartColumn..$EndColumn).Where({$colHash[$_]}) + } + else { + $Columns = $StartColumn..$EndColumn ; if ($StartColumn -gt $EndColumn) {Write-Warning -Message "Selecting columns $StartColumn to $EndColumn might give odd results."} + if ($NoHeader) {$Rows = ( $StartRow)..$EndRow ; if ($StartRow -gt $EndRow) {Write-Warning -Message "Selecting rows $StartRow to $EndRow might give odd results."} } + else {$Rows = (1 + $StartRow)..$EndRow ; if ($StartRow -ge $EndRow) {Write-Warning -Message "Selecting $StartRow as the header with data in $(1+$StartRow) to $EndRow might give odd results."}} + } + #endregion + #region Create property names + if ((-not $Columns) -or (-not ($PropertyNames = Get-PropertyNames -Columns $Columns -StartRow $StartRow))) { + throw "No column headers found on top row '$StartRow'. If column headers in the worksheet are not a requirement then please use the '-NoHeader' or '-HeaderName' parameter." + } + if ($Duplicates = $PropertyNames | Group-Object Value | Where-Object Count -GE 2) { + throw "Duplicate column headers found on row '$StartRow' in columns '$($Duplicates.Group.Column)'. Column headers must be unique, if this is not a requirement please use the '-NoHeader' or '-HeaderName' parameter." + } + #endregion + Write-Debug $sw.Elapsed.TotalMilliseconds + if (-not $Rows) { + Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' contains no data in the rows after top row '$StartRow'" + } + else { + #region Create one object per row + foreach ($R in $Rows) { + Write-Verbose "Import row '$R'" + $NewRow = [Ordered]@{} + + foreach ($P in $PropertyNames) { + $NewRow[$P.Value] = $Worksheet.Cells[$R, $P.Column].Value + Write-Verbose "Import cell '$($Worksheet.Cells[$R, $P.Column].Address)' with property name '$($p.Value)' and value '$($Worksheet.Cells[$R, $P.Column].Value)'." + } + + [PSCustomObject]$NewRow + } + #endregion + } + Write-Debug $sw.Elapsed.TotalMilliseconds + } + Catch { + throw "Failed importing the Excel workbook '$Path' with worksheet '$Worksheetname': $_" + } + Finally { + $Stream.Close() + $Stream.Dispose() + $Excel.Dispose() + $Excel = $null + } + } +} + +function Add-WorkSheet { + param( + #TODO Use parametersets to allow a workbook to be passed instead of a package + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [OfficeOpenXml.ExcelPackage] $ExcelPackage, + [Parameter(Mandatory=$true)] + [string] $WorkSheetname, + [switch] $ClearSheet, + [Switch] $NoClobber + ) + + $ws = $ExcelPackage.Workbook.Worksheets[$WorkSheetname] + if($ClearSheet -and $ws) {$ExcelPackage.Workbook.Worksheets.Delete($WorkSheetname) ; $ws = $null } + if(!$ws) { + Write-Verbose "Add worksheet '$WorkSheetname'" + $ws=$ExcelPackage.Workbook.Worksheets.Add($WorkSheetname) + } + + return $ws +} + +function ConvertFrom-ExcelSheet { + <# + .Synopsis + Reads an Excel file an converts the data to a delimited text file + + .Example + ConvertFrom-ExcelSheet .\TestSheets.xlsx .\data + Reads each sheet in TestSheets.xlsx and outputs it to the data directory as the sheet name with the extension .txt + + .Example + ConvertFrom-ExcelSheet .\TestSheets.xlsx .\data sheet?0 + Reads and outputs sheets like Sheet10 and Sheet20 form TestSheets.xlsx and outputs it to the data directory as the sheet name with the extension .txt + #> + + [CmdletBinding()] + param + ( + [Alias("FullName")] + [Parameter(Mandatory = $true)] + [String] + $Path, + [String] + $OutputPath = '.\', + [String] + $SheetName="*", + [ValidateSet('ASCII', 'BigEndianUniCode','Default','OEM','UniCode','UTF32','UTF7','UTF8')] + [string] + $Encoding = 'UTF8', + [ValidateSet('.txt', '.log','.csv')] + [string] + $Extension = '.csv', + [ValidateSet(';', ',')] + [string] + $Delimiter = ';' + ) + + $Path = (Resolve-Path $Path).Path + $stream = New-Object -TypeName System.IO.FileStream -ArgumentList $Path,"Open","Read","ReadWrite" + $xl = New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $stream + $workbook = $xl.Workbook + + $targetSheets = $workbook.Worksheets | Where-Object {$_.Name -like $SheetName} + + $params = @{} + $PSBoundParameters + $params.Remove("OutputPath") + $params.Remove("SheetName") + $params.Remove('Extension') + $params.NoTypeInformation = $true + + Foreach ($sheet in $targetSheets) + { + Write-Verbose "Exporting sheet: $($sheet.Name)" + + $params.Path = "$OutputPath\$($Sheet.Name)$Extension" + + Import-Excel $Path -Sheet $($sheet.Name) | Export-Csv @params + } + + $stream.Close() + $stream.Dispose() + $xl.Dispose() +} + +function Export-MultipleExcelSheets { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")] + param( + [Parameter(Mandatory=$true)] + $Path, + [Parameter(Mandatory=$true)] + [hashtable]$InfoMap, + [string]$Password, + [Switch]$Show, + [Switch]$AutoSize + ) + + $parameters = @{}+$PSBoundParameters + $parameters.Remove("InfoMap") + $parameters.Remove("Show") + + $parameters.Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + + foreach ($entry in $InfoMap.GetEnumerator()) { + Write-Progress -Activity "Exporting" -Status "$($entry.Key)" + $parameters.WorkSheetname=$entry.Key + + & $entry.Value | Export-Excel @parameters + } + + if($Show) {Invoke-Item $Path} +}