From 520bb079e5d863931d650e369e7faa9ab7d973d1 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 15 Jun 2018 10:23:06 +0100 Subject: [PATCH 01/11] Changes to Export-Excel & New-PivotTableDefinition Export-Excel.ps1 * Added MoveToStart, MoveBefore and MoveAfter Parameters * Added support for URI item properties. If a property is of type URI it is created as a hyperlink * Fixed issue where AutoNamedRange, NamedRange, and TableName do not work when appending to a sheet which already contains the range(s) / table * Fixed issues when pivottables / charts already exist and an export tries to create them again. * Allowed PivotFilter and PivotDataToColumn, ChartHeight/width ChartRow/Column, ChartRow/ColumnPixelOffsets, to be passed in -PivotTableDefinition and specified when running New-PivotDefinition. * Bad column-names specified for Pivots now generate warnings instead of throwing. * Removed the need to specify a fill type when specifying a title background color * "flattened out" small "called-once" function , add-title, convert-toNumber and Stop-ExcelProcess. --- Export-Excel.ps1 | 2149 +++++++++++++++++++++++----------------------- 1 file changed, 1090 insertions(+), 1059 deletions(-) diff --git a/Export-Excel.ps1 b/Export-Excel.ps1 index bbf3c3a..329174d 100644 --- a/Export-Excel.ps1 +++ b/Export-Excel.ps1 @@ -1,1059 +1,1090 @@ -Function Export-Excel { - <# - .SYNOPSIS - Export data to an Excel worksheet. - - .DESCRIPTION - Export data to an Excel file and where possible try to convert numbers so Excel recognizes them as numbers instead of text. After all. Excel is a spreadsheet program used for number manipulation and calculations. In case the number conversion is not desired, use the parameter '-NoNumberConversion *'. - .PARAMETER Path - Path to a new or existing .XLSX file - .PARAMETER ExcelPackage - An object representing an Excel Package - usually this is returned by specifying -Passthru allowing multiple commands to work on the same Workbook without saving and reloading each time. - .PARAMETER WorkSheetName - The name of a sheet within the workbook - "Sheet1" by default - .PARAMETER ClearSheet - If specified Export-Excel will remove any existing worksheet with the selected name. The Default behaviour is to overwrite cells in this sheet as needed (but leaving non-overwritten ones in place) - .PARAMETER Append - If specified data will be added to the end of an existing sheet, using the same column headings. - .PARAMETER TargetData - Data to insert onto the worksheet - this is often provided from the pipeline. - .PARAMETER ExcludeProperty - 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 - Sets the title in boldface type - .PARAMETER TitleSize - Sets the point size for the title - .PARAMETER TitleBackgroundColor - Sets the cell background to solid and the chose colour for the title cell - .PARAMETER Password - Sets password protection on the workbook - .PARAMETER IncludePivotTable - Adds a Pivot table using the data in the worksheet - .PARAMETER PivotRows - 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 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, - HashTable(s) with Sheet PivotTows, PivotColumns, PivotData, IncludePivotChart and ChartType values to make it easier to specify a definition or multiple Pivots. - .PARAMETER IncludePivotChart, - Include a chart with the Pivot table - implies Include Pivot Table. - .PARAMETER NoLegend - Exclude the legend from the pivot chart - .PARAMETER ShowCategory - Add category labels to the pivot chart - .PARAMETER ShowPercent - Add Percentage labels to the pivot chart - .PARAMETER ConditionalText - Applies a 'Conditional formatting rule' in Excel on all the cells. When specific conditions are met a rule is triggered. - .PARAMETER NoNumberConversion - By default we convert all values to numbers if possible, but this isn't always desirable. NoNumberConversion allows you to add exceptions for the conversion. Wildcards (like '*') are allowed. - .PARAMETER BoldTopRow - Makes the top Row boldface. - .PARAMETER NoHeader - Does not put field names at the top of columns - .PARAMETER RangeName - Makes the data in the worksheet a named range - .PARAMETER TableName - Makes the data in the worksheet a table with a name applies a style to it. Name must not contain spaces - .PARAMETER TableStyle - Selects the style for the named table - defaults to 'Medium6' - .PARAMETER ExcelChartDefinition - 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 - .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 - Column to start adding data - 1 by default - - .PARAMETER FreezeTopRow - Freezes headers etc. in the top row - .PARAMETER FreezeFirstColumn - Freezes titles etc. in the left column - .PARAMETER FreezeTopRowFirstColumn - Freezes top row and left column (equivalent to Freeze pane 2,2 ) - .PARAMETER FreezePane - Freezes panes at specified coordinates (in the form RowNumber , ColumnNumber) - .PARAMETER AutoFilter - Enables the 'Filter' in Excel on the complete header row. So users can easily sort, filter and/or search the data in the select column from within Excel. - - .PARAMETER AutoSize - Sizes the width of the Excel column to the maximum width needed to display all the containing data in that cell. - - .PARAMETER Now - The 'Now' switch is a shortcut that creates automatically a temporary file, enables 'AutoSize', 'AutoFiler' and 'Show', and opens the file immediately. - - .PARAMETER NumberFormat - Formats all values that can be converted to a number to the format specified. - - Examples: - # integer (not really needed unless you need to round numbers, Excel with use default cell properties) - '0' - - # integer without displaying the number 0 in the cell - '#' - - # number with 1 decimal place - '0.0' - - # number with 2 decimal places - '0.00' - - # number with 2 decimal places and thousand separator - '#,##0.00' - - # number with 2 decimal places and thousand separator and money symbol - '€#,##0.00' - - # percentage (1 = 100%, 0.01 = 1%) - '0%' - - # Blue color for positive numbers and a red color for negative numbers. All numbers will proceed a dollar sign '$'. - '[Blue]$#,##0.00;[Red]-$#,##0.00' - - .PARAMETER Show - Opens the Excel file immediately after creation. Convenient for viewing the results instantly without having to search for the file first. - .PARAMETER PassThru - If specified, Export-Excel returns an object representing the Excel package without saving the package first. To save it you need to call the save or Saveas method or send it back to Export-Excel - - .EXAMPLE - Get-Process | Export-Excel .\Test.xlsx -show - Export all the processes to the Excel file 'Test.xlsx' and open the file immediately. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - Write-Output -1 668 34 777 860 -0.5 119 -0.1 234 788 | - Export-Excel @ExcelParams -NumberFormat '[Blue]$#,##0.00;[Red]-$#,##0.00' - - Exports all data to the Excel file 'Excel.xslx' and colors the negative values in 'Red' and the positive values in 'Blue'. It will also add a dollar sign '$' in front of the rounded numbers to two decimal characters behind the comma. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - [PSCustOmobject][Ordered]@{ - Date = Get-Date - Formula1 = '=SUM(F2:G2)' - String1 = 'My String' - String2 = 'a' - IPAddress = '10.10.25.5' - Number1 = '07670' - Number2 = '0,26' - Number3 = '1.555,83' - Number4 = '1.2' - Number5 = '-31' - PhoneNr1 = '+32 44' - PhoneNr2 = '+32 4 4444 444' - PhoneNr3 = '+3244444444' - } | Export-Excel @ExcelParams -NoNumberConversion IPAddress, Number1 - - Exports all data to the Excel file 'Excel.xslx' and tries to convert all values to numbers where possible except for 'IPAddress' and 'Number1'. These are stored in the sheet 'as is', without being converted to a number. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - [PSCustOmobject][Ordered]@{ - Date = Get-Date - Formula1 = '=SUM(F2:G2)' - String1 = 'My String' - String2 = 'a' - IPAddress = '10.10.25.5' - Number1 = '07670' - Number2 = '0,26' - Number3 = '1.555,83' - Number4 = '1.2' - Number5 = '-31' - PhoneNr1 = '+32 44' - PhoneNr2 = '+32 4 4444 444' - PhoneNr3 = '+3244444444' - } | Export-Excel @ExcelParams -NoNumberConversion * - - Exports all data to the Excel file 'Excel.xslx' as is, no number conversion will take place. This means that Excel will show the exact same data that you handed over to the 'Export-Excel' function. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - Write-Output 489 668 299 777 860 151 119 497 234 788 | - Export-Excel @ExcelParams -ConditionalText $( - New-ConditionalText -ConditionalType GreaterThan 525 -ConditionalTextColor DarkRed -BackgroundColor LightPink - ) - - Exports data that will have a 'Conditional formatting rule' in Excel on these cells that will show the background fill color in 'LightPink' and the text color in 'DarkRed' when the value is greater then '525'. In case this condition is not met the color will be the default, black text on a white background. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - Get-Service | Select Name, Status, DisplayName, ServiceName | - Export-Excel @ExcelParams -ConditionalText $( - New-ConditionalText Stop DarkRed LightPink - New-ConditionalText Running Blue Cyan - ) - - Export all services to an Excel sheet where all cells have a 'Conditional formatting rule' in Excel that will show the background fill color in 'LightPink' and the text color in 'DarkRed' when the value contains the word 'Stop'. If the value contains the word 'Running' it will have a background fill color in 'Cyan' and a text color 'Blue'. In case none of these conditions are met the color will be the default, black text on a white background. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - - $Array = @() - - $Obj1 = [PSCustomObject]@{ - Member1 = 'First' - Member2 = 'Second' - } - - $Obj2 = [PSCustomObject]@{ - Member1 = 'First' - Member2 = 'Second' - Member3 = 'Third' - } - - $Obj3 = [PSCustomObject]@{ - Member1 = 'First' - Member2 = 'Second' - Member3 = 'Third' - Member4 = 'Fourth' - } - - $Array = $Obj1, $Obj2, $Obj3 - $Array | Out-GridView -Title 'Not showing Member3 and Member4' - $Array | Update-FirstObjectProperties | Export-Excel @ExcelParams -WorkSheetname Numbers - - Updates the first object of the array by adding property 'Member3' and 'Member4'. Afterwards. all objects are exported to an Excel file and all column headers are visible. - - .EXAMPLE - Get-Process | Export-Excel .\test.xlsx -WorkSheetname Processes -IncludePivotTable -Show -PivotRows Company -PivotData PM - - .EXAMPLE - Get-Process | Export-Excel .\test.xlsx -WorkSheetname Processes -ChartType PieExploded3D -IncludePivotChart -IncludePivotTable -Show -PivotRows Company -PivotData PM - - .EXAMPLE - Get-Service | Export-Excel 'c:\temp\test.xlsx' -Show -IncludePivotTable -PivotRows status -PivotData @{status='count'} - - .EXAMPLE - $pt = [ordered]@{} - $pt.pt1=@{ SourceWorkSheet = 'Sheet1'; - PivotRows = 'Status' - PivotData = @{'Status'='count'} - IncludePivotChart = $true - ChartType = 'BarClustered3D' - } - $pt.pt2=@{ SourceWorkSheet = 'Sheet2'; - PivotRows = 'Company' - PivotData = @{'Company'='count'} - IncludePivotChart = $true - ChartType = 'PieExploded3D' - } - Remove-Item -Path .\test.xlsx - Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -AutoSize - Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' - Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show - - This example defines two pivot tables. Then it puts Service data on Sheet1 with one call to Export-Excel and Process Data on sheet2 with a second call to Export-Excel - The thrid and final call adds the two pivot tables and opens the spreadsheet in Excel - - - .EXAMPLE - Remove-Item -Path .\test.xlsx - $excel = Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -PassThru - $excel.Workbook.Worksheets["Sheet1"].Row(1).style.font.bold = $true - $excel.Workbook.Worksheets["Sheet1"].Column(3 ).width = 29 - $excel.Workbook.Worksheets["Sheet1"].Column(3 ).Style.wraptext = $true - $excel.Save() - $excel.Dispose() - Start-Process .\test.xlsx - - This example uses -passthrough - put service information into sheet1 of the work book and saves the excelPackageObject in $Excel - It then uses the package object to apply formatting, and then saves the workbook and disposes of the object before loading the document in Excel. - - .EXAMPLE - $excel = Get-Process | Select-Object -Property Name,Company,Handles,CPU,PM,NPM,WS | Export-Excel -Path .\test.xlsx -ClearSheet -WorkSheetname "Processes" -PassThru - $sheet = $excel.Workbook.Worksheets["Processes"] - $sheet.Column(1) | Set-Format -Bold -AutoFit - $sheet.Column(2) | Set-Format -Width 29 -WrapText - $sheet.Column(3) | Set-Format -HorizontalAlignment Right -NFormat "#,###" - Set-Format -Address $sheet.Cells["E1:H1048576"] -HorizontalAlignment Right -NFormat "#,###" - Set-Format -Address $sheet.Column(4) -HorizontalAlignment Right -NFormat "#,##0.0" -Bold - Set-Format -Address $sheet.Row(1) -Bold -HorizontalAlignment Center - Add-ConditionalFormatting -WorkSheet $sheet -Range "D2:D1048576" -DataBarColor Red - Add-ConditionalFormatting -WorkSheet $sheet -Range "G2:G1048576" -RuleType GreaterThan -ConditionValue "104857600" -ForeGroundColor Red - foreach ($c in 5..9) {Set-Format $sheet.Column($c) -AutoFit } - Export-Excel -ExcelPackage $excel -WorkSheetname "Processes" -IncludePivotChart -ChartType ColumnClustered -NoLegend -PivotRows company -PivotData @{'Name'='Count'} -Show - - This a more sophisticated version of the previous example showing different ways of using Set-Format, and also adding conditional formatting. - In the final command a Pivot chart is added and the workbook is opened in Excel. - - .LINK - https://github.com/dfinke/ImportExcel - #> - - [CmdletBinding(DefaultParameterSetName = 'Default')] - Param( - [Parameter(ParameterSetName = "Default", Position = 0)] - [Parameter(ParameterSetName = "Table" , Position = 0)] - [String]$Path, - [Parameter(Mandatory = $true, ParameterSetName = "PackageDefault")] - [Parameter(Mandatory = $true, ParameterSetName = "PackageTable")] - [OfficeOpenXml.ExcelPackage]$ExcelPackage, - [Parameter(ValueFromPipeline = $true)] - $TargetData, - [String]$Password, - [String]$WorkSheetname = 'Sheet1', - [switch]$ClearSheet, - [switch]$Append, - [String]$Title, - [OfficeOpenXml.Style.ExcelFillStyle]$TitleFillPattern = 'None', - [Switch]$TitleBold, - [Int]$TitleSize = 22, - [System.Drawing.Color]$TitleBackgroundColor, - [Switch]$IncludePivotTable, - [String[]]$PivotRows, - [String[]]$PivotColumns, - $PivotData, - [String[]]$PivotFilter, - [Switch]$PivotDataToColumn, - [Hashtable]$PivotTableDefinition, - [Switch]$IncludePivotChart, - [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', - [Switch]$NoLegend, - [Switch]$ShowCategory, - [Switch]$ShowPercent, - [Switch]$AutoSize, - [Switch]$Show, - [Switch]$NoClobber, - [Switch]$FreezeTopRow, - [Switch]$FreezeFirstColumn, - [Switch]$FreezeTopRowFirstColumn, - [Int[]]$FreezePane, - [Parameter(ParameterSetName = 'Default')] - [Parameter(ParameterSetName = 'PackageDefault')] - [Switch]$AutoFilter, - [Switch]$BoldTopRow, - [Switch]$NoHeader, - [String]$RangeName, - [ValidateScript( { - if ($_.Contains(' ')) { - throw 'Tablename has spaces.' - } - elseif (-not $_) { - throw 'Tablename is null or empty.' - } - elseif ($_[0] -notmatch '[a-z]') { - throw 'Tablename starts with an invalid character.' - } - else { - $true - } - })] - [Parameter(ParameterSetName = 'Table' , Mandatory = $true)] - [Parameter(ParameterSetName = 'PackageTable' , Mandatory = $true)] - [String]$TableName, - [Parameter(ParameterSetName = 'Table')] - [Parameter(ParameterSetName = 'PackageTable')] - [OfficeOpenXml.Table.TableStyles]$TableStyle = 'Medium6', - [Object[]]$ExcelChartDefinition, - [String[]]$HideSheet, - [Switch]$KillExcel, - [Switch]$AutoNameRange, - [Int]$StartRow = 1, - [Int]$StartColumn = 1, - [Switch]$PassThru, - [String]$Numberformat = 'General', - [string[]]$ExcludeProperty, - [String[]]$NoNumberConversion, - [Object[]]$ConditionalFormat, - [Object[]]$ConditionalText, - [ScriptBlock]$CellStyleSB, - [Parameter(ParameterSetName = 'Now')] - # [Parameter(ParameterSetName = 'TableNow')] - [Switch]$Now, - [Switch]$ReturnRange, - [Switch]$NoTotalsInPivot, - [Switch]$ReZip - ) - - Begin { - function Find-WorkSheet { - param ( - $WorkSheetName - ) - - $pkg.Workbook.Worksheets | Where-Object {$_.name -match $WorkSheetName} - } - - Function Add-CellValue { - <# - .SYNOPSIS - Save a value in an Excel cell. - - .DESCRIPTION - DateTime objects are always converted to a DateTime format in Excel. And formulas are always - saved as formulas. - - Numerical values will be converted to numbers as defined in the regional settings of the local - system. In case the parameter 'NoNumberConversion' is used, we don't convert to number and leave - the value 'as is'. In case of conversion failure, we also leave the value 'as is'. - #> - - Param ( - [Object]$TargetCell, - [Object]$CellValue - ) - - Switch ($CellValue) { - {($_ -is [String]) -and ($_.StartsWith('='))} { - #region Save an Excel formula - $TargetCell.Formula = $_ - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$_' as formula" - break - #endregion - } - - {$_ -is [DateTime]} { - #region Save a date with an international valid format - $TargetCell.Value = $_ - $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 - } - - {(($NoNumberConversion) -and ($NoNumberConversion -contains $Name)) -or - ($NoNumberConversion -eq '*')} { - #regioon Save a value without converting to number - $TargetCell.Value = $_ - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' unconverted" - break - #endregion - } - - Default { - #region Save a value as a number if possible - if (($Number = ConvertTo-Number $_) -ne $null) { - $TargetCell.Value = $Number - $targetCell.Style.Numberformat.Format = $Numberformat - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as number converted from '$_' with format '$Numberformat'" - } - else { - $TargetCell.Value = $_ - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as string" - } - break - #endregion - } - } - } - - Function Add-Title { - <# - .SYNOPSIS - Add a title row to the Excel worksheet. - #> - - $ws.Cells[$Row, $StartColumn].Value = $Title - $ws.Cells[$Row, $StartColumn].Style.Font.Size = $TitleSize - - if ($TitleBold) { - #set title to Bold if -TitleBold was specified. - #Otherwise the default will be unbolded. - $ws.Cells[$Row, $StartColumn].Style.Font.Bold = $True - } - $ws.Cells[$Row, $StartColumn].Style.Fill.PatternType = $TitleFillPattern - - #can only set TitleBackgroundColor if TitleFillPattern is something other than None - if ($TitleBackgroundColor -AND ($TitleFillPattern -ne 'None')) { - $ws.Cells[$Row, $StartColumn].Style.Fill.BackgroundColor.SetColor($TitleBackgroundColor) - } - elseif ($TitleBackgroundColor) { - Write-Warning "Title Background Color ignored. You must set the TitleFillPattern parameter to a value other than 'None'. Try 'Solid'." - } - } - - Function ConvertTo-Number { - <# - .SYNOPSIS - Convert a value to a number - #> - - Param ( - [String]$Value - ) - - $R = $null - - if ([Double]::TryParse([String]$Value, [System.Globalization.NumberStyles]::Any, - [System.Globalization.NumberFormatInfo]::CurrentInfo, [Ref]$R)) { - $R - } - } - - Function Stop-ExcelProcess { - <# - .SYNOPSIS - Stop the Excel process when it's running. - #> - - Get-Process excel -ErrorAction Ignore | Stop-Process - while (Get-Process excel -ErrorAction Ignore) {} - } - Try { - $script:Header = $null - if ($append -and $clearSheet) {throw "You can't use -Append AND -ClearSheet."} - if ($KillExcel) { - Stop-ExcelProcess - } - - if ($PSBoundParameters.Keys.Count -eq 0 -Or $Now) { - $Path = [System.IO.Path]::GetTempFileName() -replace '\.tmp', '.xlsx' - $Show = $true - $AutoSize = $true - if (!$TableName) { - $AutoFilter = $true - } - } - - if ($ExcelPackage) { - $pkg = $ExcelPackage - $Path = $pkg.File - } - Else { - $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) - - $targetPath = Split-Path $Path - if (!(Test-Path $targetPath)) { - Write-Debug "Base path $($targetPath) does not exist, creating" - $null = mkdir $targetPath -ErrorAction Ignore - } - elseif (Test-Path $Path) { - Write-Debug "Path '$Path' already exists" - } - - $pkg = New-Object OfficeOpenXml.ExcelPackage $Path - } - - [OfficeOpenXml.ExcelWorksheet]$ws = $pkg | Add-WorkSheet -WorkSheetname $WorkSheetname -NoClobber:$NoClobber -ClearSheet:$ClearSheet #Add worksheet doesn't take any action for -noClobber - foreach ($format in $ConditionalFormat ) { - $target = "Add$($format.Formatter)" - $rule = ($ws.ConditionalFormatting).PSObject.Methods[$target].Invoke($format.Range, $format.IconType) - $rule.Reverse = $format.Reverse - } - - if ($append) { - $headerRange = $ws.Dimension.Address -replace "\d+$", "1" - #if there is a title or anything else above the header row, specifying StartRow will skip it. - if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} - #$script:Header = $ws.Cells[$headerrange].Value - #using a slightly odd syntax otherwise header ends up as a 2D array - $ws.Cells[$headerRange].Value | foreach -Begin {$Script:header = @()} -Process {$Script:header += $_ } - $row = $ws.Dimension.Rows - Write-Debug -Message ("Appending: headers are " + ($script:Header -join ", ") + "Start row $row") - } - elseif ($Title) { - #Can only add a title if not appending - $Row = $StartRow - Add-Title - $Row ++ ; $startRow ++ - } - else { - $Row = $StartRow - - } - $ColumnIndex = $StartColumn - $firstTimeThru = $true - $isDataTypeValueType = $false - $pattern = 'string|bool|byte|char|decimal|double|float|int|long|sbyte|short|uint|ulong|ushort' - } - Catch { - if ($AlreadyExists) { - #Is this set anywhere ? - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': The worksheet '$WorkSheetname' already exists." - } - else { - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" - } - } - } - - Process { - if ($TargetData) { - Try { - if ($firstTimeThru) { - $firstTimeThru = $false - $isDataTypeValueType = $TargetData.GetType().name -match $pattern - Write-Debug "DataTypeName is '$($TargetData.GetType().name)' isDataTypeValueType '$isDataTypeValueType'" - } - - if ($isDataTypeValueType) { - $ColumnIndex = $StartColumn - - Add-CellValue -TargetCell $ws.Cells[$Row, $ColumnIndex] -CellValue $TargetData - - $ColumnIndex += 1 - $Row += 1 - } - else { - #region Add headers - if (-not $script:Header) { - $ColumnIndex = $StartColumn - $script:Header = $TargetData.PSObject.Properties.Name | Where-Object {$_ -notin $ExcludeProperty} - - if ($NoHeader) { - # Don't push the headers to the spread sheet - $Row -= 1 - } - else { - foreach ($Name in $script:Header) { - $ws.Cells[$Row, $ColumnIndex].Value = $Name - Write-Verbose "Cell '$Row`:$ColumnIndex' add header '$Name'" - $ColumnIndex += 1 - } - } - } - #endregion - - $Row += 1 - $ColumnIndex = $StartColumn - - foreach ($Name in $script:Header) { - #region Add non header values - Add-CellValue -TargetCell $ws.Cells[$Row, $ColumnIndex] -CellValue $TargetData.$Name - - $ColumnIndex += 1 - #endregion - } - } - } - Catch { - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" - } - } - } - - End { - Try { - if ($AutoNameRange) { - if (-not $script:header) { - $headerRange = $ws.Dimension.Address -replace "\d+$", "1" - #if there is a title or anything else above the header row, specifying StartRow will skip it. - if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} - #using a slightly odd syntax otherwise header ends up as a 2D array - $ws.Cells[$headerRange].Value | foreach -Begin {$Script:header = @()} -Process {$Script:header += $_ } - } - $totalRows = $ws.Dimension.End.Row - $totalColumns = $ws.Dimension.Columns - foreach ($c in 0..($totalColumns - 1)) { - $targetRangeName = "$($script:Header[$c])" - $targetColumn = $c + $StartColumn - $theCell = $ws.Cells[($startrow + 1), $targetColumn, $totalRows , $targetColumn ] - $ws.Names.Add($targetRangeName, $theCell) | Out-Null - - if ([OfficeOpenXml.FormulaParsing.ExcelUtilities.ExcelAddressUtil]::IsValidAddress($targetRangeName)) { - Write-Warning "AutoNameRange: Property name '$targetRangeName' is also a valid Excel address and may cause issues. Consider renaming the property name." - } - } - } - - if ($Title) { - $startAddress = $ws.Dimension.Start.address -replace "$($ws.Dimension.Start.row)`$", "$($ws.Dimension.Start.row + 1)" - } - else { - $startAddress = $ws.Dimension.Start.Address - } - - $dataRange = "{0}:{1}" -f $startAddress, $ws.Dimension.End.Address - - Write-Debug "Data Range '$dataRange'" - - if (-not [String]::IsNullOrEmpty($RangeName)) { - $ws.Names.Add($RangeName, $ws.Cells[$dataRange]) | Out-Null - } - - if (-not [String]::IsNullOrEmpty($TableName)) { - $csr = $StartRow - - $csc = $StartColumn - $cer = $ws.Dimension.End.Row - $cec = $ws.Dimension.End.Column # was $script:Header.Count - - $targetRange = $ws.Cells[$csr, $csc, $cer, $cec] - #if we're appending data the table may already exist: but excel doesn't like the result if I put - # if ($ws.Tables[$TableName]) {$ws.Tables.Delete($TableName) } - $tbl = $ws.Tables.Add($targetRange, $TableName) - $tbl.TableStyle = $TableStyle - } - - $PivotTableStartCell = "A1" - if($PivotFilter) {$PivotTableStartCell = "A3"} - - if ($PivotTableDefinition) { - foreach ($item in $PivotTableDefinition.GetEnumerator()) { - $targetName = $item.Key - $pivotTableName = $targetName #+ 'PivotTable' - #Make sure the Pivot table sheet doesn't already exist - try { $pkg.Workbook.Worksheets.Delete( $pivotTableName) } catch {} - $wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber - $pivotTableDataName = $targetName + 'PivotTableData' - - if (!$item.Value.SourceWorkSheet) { - $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) - } - else { - $workSheet = Find-WorkSheet $item.Value.SourceWorkSheet - - if ($workSheet) { - $targetStartAddress = $workSheet.Dimension.Start.Address - $targetDataRange = "{0}:{1}" -f $targetStartAddress, $workSheet.Dimension.End.Address - - $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $workSheet.Cells[$targetDataRange], $pivotTableDataName) - } - } - - switch ($item.Value.Keys) { - "PivotRows" { - foreach ($Row in $item.Value.PivotRows) { - $null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) - } - } - - "PivotColumns" { - foreach ($Column in $item.Value.PivotColumns) { - $null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column]) - } - } - - "PivotData" { - $pivotData = $item.Value.PivotData - if ($PivotData -is [HashTable] -or $PivotData -is [System.Collections.Specialized.OrderedDictionary]) { - $PivotData.Keys | ForEach-Object { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) - $df.Function = $PivotData.$_ - } - } - else { - foreach ($Item in $PivotData) { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$Item]) - $df.Function = 'Count' - } - } - - if ($PivotDataToColumn) { - $pivotTable.DataOnRows = $false - } - } - - "IncludePivotChart" { - $ChartType = "Pie" - if ($item.Value.ChartType) { - $ChartType = $item.Value.ChartType - } - - $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) - $chart.SetPosition(0, 0, 4, 0) #Changed position to top row, next to a chart which doesn't pivot on columns - $chart.SetSize(600, 400) - if ($chart.DataLabel) { - $chart.DataLabel.ShowCategory = [boolean]$item.value.ShowCategory - $chart.DataLabel.ShowPercent = [boolean]$item.value.ShowPercent - } - if ([boolean]$item.value.NoLegend) {$chart.Legend.Remove()} - if ($item.value.ChartTitle) {$chart.Title.Text = $item.value.chartTitle} - } - } - - if($item.Value.NoTotalsInPivot) { - $pivotTable.RowGrandTotals = $false - } - } - } - - if ($IncludePivotTable -or $IncludePivotChart) { - #changed so -includePivotChart Implies -includePivotTable. - $pivotTableName = $WorkSheetname + 'PivotTable' - #Make sure the Pivot table sheet doesn't already exist - try { $pkg.Workbook.Worksheets.Delete( $pivotTableName) } catch {} - $wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber - - $wsPivot.View.TabSelected = $true - - $pivotTableDataName = $WorkSheetname + 'PivotTableData' - - $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) - - if ($PivotRows) { - foreach ($Row in $PivotRows) { - $null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) - } - } - - if ($PivotColumns) { - foreach ($Column in $PivotColumns) { - $null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column]) - } - } - - if ($PivotData) { - if ($PivotData -is [HashTable] -or $PivotData -is [System.Collections.Specialized.OrderedDictionary]) { - $PivotData.Keys | ForEach-Object { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) - $df.Function = $PivotData.$_ - } - } - else { - foreach ($Item in $PivotData) { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$Item]) - $df.Function = 'Count' - } - } - if ($PivotDataToColumn) { - $pivotTable.DataOnRows = $false - } - } - - if($NoTotalsInPivot) { - $pivotTable.RowGrandTotals = $false - } - - if ($IncludePivotChart) { - $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) - if ($chart.DataLabel) { - $chart.DataLabel.ShowCategory = $ShowCategory - $chart.DataLabel.ShowPercent = $ShowPercent - } - $chart.SetPosition(0, 26, 2, 26) # if Pivot table is rows+data only it will be 2 columns wide if has pivot columns we don't know how wide it will be - if ($NoLegend) { - $chart.Legend.Remove() - } - - } - } - - if($pivotTable -and $PivotFilter) { - - foreach($pFilter in $PivotFilter) { - $null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter]) - } - } - - - if ($Password) { - $ws.Protection.SetPassword($Password) - } - - if ($AutoFilter) { - $ws.Cells[$dataRange].AutoFilter = $true - } - - if ($FreezeTopRow) { - $ws.View.FreezePanes(2, 1) - } - - if ($FreezeTopRowFirstColumn) { - $ws.View.FreezePanes(2, 2) - } - - if ($FreezeFirstColumn) { - $ws.View.FreezePanes(1, 2) - } - - if ($FreezePane) { - $freezeRow, $freezeColumn = $FreezePane - if (-not $freezeColumn -or $freezeColumn -eq 0) { - $freezeColumn = 1 - } - - if ($freezeRow -gt 1) { - $ws.View.FreezePanes($freezeRow, $freezeColumn) - } - } - if ($BoldTopRow) { - if ($Title) { - $range = $ws.Dimension.Address -replace '\d+', '2' - } - else { - $range = $ws.Dimension.Address -replace '\d+', '1' - } - - $ws.Cells[$range].Style.Font.Bold = $true - } - if ($AutoSize) { - $ws.Cells.AutoFitColumns() - } - - foreach ($Sheet in $HideSheet) { - $pkg.Workbook.WorkSheets[$Sheet].Hidden = 'Hidden' - } - - $chartCount = 0 - foreach ($chartDef in $ExcelChartDefinition) { - $ChartName = 'Chart' + (Split-Path -Leaf ([System.IO.path]::GetTempFileName())) -replace 'tmp|\.', '' - $chart = $ws.Drawings.AddChart($ChartName, $chartDef.ChartType) - $chart.Title.Text = $chartDef.Title - - if ($chartDef.NoLegend) { - $chart.Legend.Remove() - } - - if ($chart.Datalabel -ne $null) { - $chart.Datalabel.ShowCategory = $chartDef.ShowCategory - $chart.Datalabel.ShowPercent = $chartDef.ShowPercent - } - - $chart.SetPosition($chartDef.Row, $chartDef.RowOffsetPixels, $chartDef.Column, $chartDef.ColumnOffsetPixels) - $chart.SetSize($chartDef.Width, $chartDef.Height) - - $chartDefCount = @($chartDef.YRange).Count - if ($chartDefCount -eq 1) { - $Series = $chart.Series.Add($chartDef.YRange, $chartDef.XRange) - - $SeriesHeader = $chartDef.SeriesHeader - if (-not $SeriesHeader) { - $SeriesHeader = 'Series 1' - } - - $Series.Header = $SeriesHeader - } - else { - for ($idx = 0; $idx -lt $chartDefCount; $idx += 1) { - $Series = $chart.Series.Add($chartDef.YRange[$idx], $chartDef.XRange) - - if ($chartDef.SeriesHeader.Count -gt 0) { - $SeriesHeader = $chartDef.SeriesHeader[$idx] - } - - if (-not $SeriesHeader) { - $SeriesHeader = "Series $($idx)" - } - - $Series.Header = $SeriesHeader - $SeriesHeader = $null - } - } - } - - if ($ConditionalText) { - foreach ($targetConditionalText in $ConditionalText) { - $target = "Add$($targetConditionalText.ConditionalType)" - - $Range = $targetConditionalText.Range - if (-not $Range) { - $Range = $ws.Dimension.Address - } - - $rule = ($ws.Cells[$Range].ConditionalFormatting).PSObject.Methods[$target].Invoke() - - if ($targetConditionalText.Text) { - if ($targetConditionalText.ConditionalType -match 'equal|notequal|lessthan|lessthanorequal|greaterthan|greaterthanorequal') { - $rule.Formula = $targetConditionalText.Text - } - else { - $rule.Text = $targetConditionalText.Text - } - } - - $rule.Style.Font.Color.Color = $targetConditionalText.ConditionalTextColor - $rule.Style.Fill.PatternType = $targetConditionalText.PatternType - $rule.Style.Fill.BackgroundColor.Color = $targetConditionalText.BackgroundColor - } - } - - if ($CellStyleSB) { - $TotalRows = $ws.Dimension.Rows - $LastColumn = (Get-ExcelColumnName $ws.Dimension.Columns).ColumnName - & $CellStyleSB $ws $TotalRows $LastColumn - } - - if ($PassThru) { - $pkg - } - else { - if ($ReturnRange) { - $ws.Dimension.Address - } - - - - $pkg.Save() - - if ($ReZip) { - write-verbose "Re-Zipping $($pkg.file) using .NET ZIP library" - $zipAssembly = "System.IO.Compression.Filesystem" - try { - Add-Type -assembly $zipAssembly -ErrorAction stop - } catch { - write-error "The -ReZip parameter requires .NET Framework 4.5 or later to be installed. Recommend to install Powershell v4+" - continue - } - - $TempZipPath = Join-Path -path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName()) - [io.compression.zipfile]::ExtractToDirectory($pkg.File,$TempZipPath) | Out-Null - Remove-Item $pkg.File -Force - [io.compression.zipfile]::CreateFromDirectory($TempZipPath,$pkg.File) | Out-Null - } - - $pkg.Dispose() - - if ($Show) { - Invoke-Item $Path - } - } - } - Catch { - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" - } - } -} - -function New-PivotTableDefinition { - param( - [Parameter(Mandatory)] - [Alias("PivtoTableName")]#Previous typo - use alias to avoid breaking scripts - $PivotTableName, - $SourceWorkSheet, - $PivotRows, - [hashtable]$PivotData, - $PivotColumns, - [Switch]$IncludePivotChart, - [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', - [Switch]$NoLegend, - [Switch]$ShowCategory, - [Switch]$ShowPercent, - [String]$ChartTitle, - [Switch]$NoTotalsInPivot - ) - - $parameters = @{} + $PSBoundParameters - $parameters.Remove('PivotTableName') - - @{$PivotTableName = $parameters} -} +function Export-Excel { + <# + .SYNOPSIS + Export data to an Excel worksheet. + + .DESCRIPTION + Export data to an Excel file and where possible try to convert numbers so Excel recognizes them as numbers instead of text. After all. Excel is a spreadsheet program used for number manipulation and calculations. In case the number conversion is not desired, use the parameter '-NoNumberConversion *'. + .PARAMETER Path + Path to a new or existing .XLSX file + .PARAMETER ExcelPackage + An object representing an Excel Package - usually this is returned by specifying -Passthru allowing multiple commands to work on the same Workbook without saving and reloading each time. + .PARAMETER WorkSheetName + The name of a sheet within the workbook - "Sheet1" by default + .PARAMETER ClearSheet + If specified Export-Excel will remove any existing worksheet with the selected name. The Default behaviour is to overwrite cells in this sheet as needed (but leaving non-overwritten ones in place) + .PARAMETER Append + If specified data will be added to the end of an existing sheet, using the same column headings. + .PARAMETER TargetData + Data to insert onto the worksheet - this is often provided from the pipeline. + .PARAMETER ExcludeProperty + 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 + Sets the title in boldface type + .PARAMETER TitleSize + Sets the point size for the title + .PARAMETER TitleBackgroundColor + Sets the cell background color for the title cell + .PARAMETER TitleFillPattern + Sets the fill pattern for the title cell + .PARAMETER Password + Sets password protection on the workbook + .PARAMETER IncludePivotTable + Adds a Pivot table using the data in the worksheet + .PARAMETER PivotRows + 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 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, + HashTable(s) with Sheet PivotTows, PivotColumns, PivotData, IncludePivotChart and ChartType values to make it easier to specify a definition or multiple Pivots. + .PARAMETER IncludePivotChart, + Include a chart with the Pivot table - implies Include Pivot Table. + .PARAMETER NoLegend + Exclude the legend from the pivot chart + .PARAMETER ShowCategory + Add category labels to the pivot chart + .PARAMETER ShowPercent + Add Percentage labels to the pivot chart + .PARAMETER ConditionalText + Applies a 'Conditional formatting rule' in Excel on all the cells. When specific conditions are met a rule is triggered. + .PARAMETER NoNumberConversion + By default we convert all values to numbers if possible, but this isn't always desirable. NoNumberConversion allows you to add exceptions for the conversion. Wildcards (like '*') are allowed. + .PARAMETER BoldTopRow + Makes the top Row boldface + .PARAMETER NoHeader + Does not put field names at the top of columns + .PARAMETER RangeName + Makes the data in the worksheet a named range + .PARAMETER TableName + Makes the data in the worksheet a table with a name applies a style to it. Name must not contain spaces + .PARAMETER TableStyle + Selects the style for the named table - defaults to 'Medium6' + .PARAMETER ExcelChartDefinition + 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 MoveToStart + If specified, the worksheet will be moved to the start of the workbook. + MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified + .PARAMETER MoveToEnd + If specified, the worksheet will be moved to the end of the workbook. + (This is the default position for newly created sheets, but this can be used to move existing sheets) + .PARAMETER MoveBefore + If specified, the worksheet will be moved before the nominated one (which can be a postion starting from 1, or a name). + MoveBefore takes precedence over MoveAfter if both are specified + .PARAMETER MoveAfter + If specified, the worksheet will be moved after the nominated one (which can be a postion starting from 1, or a name or *). + If * is used, the worksheet names will be examined starting with the first one sheet placed after the last sheet which comes before it alphabetically. + .PARAMETER KillExcel + Closes Excel - prevents errors writing to the file because Excel has it open + .PARAMETER AutoNameRange + 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 + Column to start adding data - 1 by default + .PARAMETER FreezeTopRow + Freezes headers etc. in the top row + .PARAMETER FreezeFirstColumn + Freezes titles etc. in the left column + .PARAMETER FreezeTopRowFirstColumn + Freezes top row and left column (equivalent to Freeze pane 2,2 ) + .PARAMETER FreezePane + Freezes panes at specified coordinates (in the form RowNumber , ColumnNumber) + .PARAMETER AutoFilter + Enables the 'Filter' in Excel on the complete header row. So users can easily sort, filter and/or search the data in the select column from within Excel. + .PARAMETER AutoSize + Sizes the width of the Excel column to the maximum width needed to display all the containing data in that cell. + .PARAMETER Now + The 'Now' switch is a shortcut that creates automatically a temporary file, enables 'AutoSize', 'AutoFiler' and 'Show', and opens the file immediately. + .PARAMETER NumberFormat + Formats all values that can be converted to a number to the format specified. + + Examples: + # integer (not really needed unless you need to round numbers, Excel with use default cell properties) + '0' + + # integer without displaying the number 0 in the cell + '#' + + # number with 1 decimal place + '0.0' + + # number with 2 decimal places + '0.00' + + # number with 2 decimal places and thousand separator + '#,##0.00' + + # number with 2 decimal places and thousand separator and money symbol + '€#,##0.00' + + # percentage (1 = 100%, 0.01 = 1%) + '0%' + + # Blue color for positive numbers and a red color for negative numbers. All numbers will proceed a dollar sign '$'. + '[Blue]$#,##0.00;[Red]-$#,##0.00' + + .PARAMETER Show + Opens the Excel file immediately after creation. Convenient for viewing the results instantly without having to search for the file first. + .PARAMETER PassThru + If specified, Export-Excel returns an object representing the Excel package without saving the package first. To save it you need to call the save or Saveas method or send it back to Export-Excel + + .EXAMPLE + Get-Process | Export-Excel .\Test.xlsx -show + Export all the processes to the Excel file 'Test.xlsx' and open the file immediately. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + Write-Output -1 668 34 777 860 -0.5 119 -0.1 234 788 | + Export-Excel @ExcelParams -NumberFormat '[Blue]$#,##0.00;[Red]-$#,##0.00' + + Exports all data to the Excel file 'Excel.xslx' and colors the negative values in 'Red' and the positive values in 'Blue'. It will also add a dollar sign '$' in front of the rounded numbers to two decimal characters behind the comma. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + [PSCustOmobject][Ordered]@{ + Date = Get-Date + Formula1 = '=SUM(F2:G2)' + String1 = 'My String' + String2 = 'a' + IPAddress = '10.10.25.5' + Number1 = '07670' + Number2 = '0,26' + Number3 = '1.555,83' + Number4 = '1.2' + Number5 = '-31' + PhoneNr1 = '+32 44' + PhoneNr2 = '+32 4 4444 444' + PhoneNr3 = '+3244444444' + } | Export-Excel @ExcelParams -NoNumberConversion IPAddress, Number1 + + Exports all data to the Excel file 'Excel.xslx' and tries to convert all values to numbers where possible except for 'IPAddress' and 'Number1'. These are stored in the sheet 'as is', without being converted to a number. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + [PSCustOmobject][Ordered]@{ + Date = Get-Date + Formula1 = '=SUM(F2:G2)' + String1 = 'My String' + String2 = 'a' + IPAddress = '10.10.25.5' + Number1 = '07670' + Number2 = '0,26' + Number3 = '1.555,83' + Number4 = '1.2' + Number5 = '-31' + PhoneNr1 = '+32 44' + PhoneNr2 = '+32 4 4444 444' + PhoneNr3 = '+3244444444' + } | Export-Excel @ExcelParams -NoNumberConversion * + + Exports all data to the Excel file 'Excel.xslx' as is, no number conversion will take place. This means that Excel will show the exact same data that you handed over to the 'Export-Excel' function. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + Write-Output 489 668 299 777 860 151 119 497 234 788 | + Export-Excel @ExcelParams -ConditionalText $( + New-ConditionalText -ConditionalType GreaterThan 525 -ConditionalTextColor DarkRed -BackgroundColor LightPink + ) + + Exports data that will have a 'Conditional formatting rule' in Excel on these cells that will show the background fill color in 'LightPink' and the text color in 'DarkRed' when the value is greater then '525'. In case this condition is not met the color will be the default, black text on a white background. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + Get-Service | Select Name, Status, DisplayName, ServiceName | + Export-Excel @ExcelParams -ConditionalText $( + New-ConditionalText Stop DarkRed LightPink + New-ConditionalText Running Blue Cyan + ) + + Export all services to an Excel sheet where all cells have a 'Conditional formatting rule' in Excel that will show the background fill color in 'LightPink' and the text color in 'DarkRed' when the value contains the word 'Stop'. If the value contains the word 'Running' it will have a background fill color in 'Cyan' and a text color 'Blue'. In case none of these conditions are met the color will be the default, black text on a white background. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + + $Array = @() + + $Obj1 = [PSCustomObject]@{ + Member1 = 'First' + Member2 = 'Second' + } + + $Obj2 = [PSCustomObject]@{ + Member1 = 'First' + Member2 = 'Second' + Member3 = 'Third' + } + + $Obj3 = [PSCustomObject]@{ + Member1 = 'First' + Member2 = 'Second' + Member3 = 'Third' + Member4 = 'Fourth' + } + + $Array = $Obj1, $Obj2, $Obj3 + $Array | Out-GridView -Title 'Not showing Member3 and Member4' + $Array | Update-FirstObjectProperties | Export-Excel @ExcelParams -WorkSheetname Numbers + + Updates the first object of the array by adding property 'Member3' and 'Member4'. Afterwards. all objects are exported to an Excel file and all column headers are visible. + + .EXAMPLE + Get-Process | Export-Excel .\test.xlsx -WorkSheetname Processes -IncludePivotTable -Show -PivotRows Company -PivotData PM + + .EXAMPLE + Get-Process | Export-Excel .\test.xlsx -WorkSheetname Processes -ChartType PieExploded3D -IncludePivotChart -IncludePivotTable -Show -PivotRows Company -PivotData PM + + .EXAMPLE + Get-Service | Export-Excel 'c:\temp\test.xlsx' -Show -IncludePivotTable -PivotRows status -PivotData @{status='count'} + + .EXAMPLE + $pt = [ordered]@{} + $pt.pt1=@{ SourceWorkSheet = 'Sheet1'; + PivotRows = 'Status' + PivotData = @{'Status'='count'} + IncludePivotChart = $true + ChartType = 'BarClustered3D' + } + $pt.pt2=@{ SourceWorkSheet = 'Sheet2'; + PivotRows = 'Company' + PivotData = @{'Company'='count'} + IncludePivotChart = $true + ChartType = 'PieExploded3D' + } + Remove-Item -Path .\test.xlsx + Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -AutoSize + Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' + Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show + + This example defines two pivot tables. Then it puts Service data on Sheet1 with one call to Export-Excel and Process Data on sheet2 with a second call to Export-Excel + The thrid and final call adds the two pivot tables and opens the spreadsheet in Excel + + + .EXAMPLE + Remove-Item -Path .\test.xlsx + $excel = Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -PassThru + $excel.Workbook.Worksheets["Sheet1"].Row(1).style.font.bold = $true + $excel.Workbook.Worksheets["Sheet1"].Column(3 ).width = 29 + $excel.Workbook.Worksheets["Sheet1"].Column(3 ).Style.wraptext = $true + $excel.Save() + $excel.Dispose() + Start-Process .\test.xlsx + + This example uses -passthrough - put service information into sheet1 of the work book and saves the excelPackageObject in $Excel + It then uses the package object to apply formatting, and then saves the workbook and disposes of the object before loading the document in Excel. + + .EXAMPLE + $excel = Get-Process | Select-Object -Property Name,Company,Handles,CPU,PM,NPM,WS | Export-Excel -Path .\test.xlsx -ClearSheet -WorkSheetname "Processes" -PassThru + $sheet = $excel.Workbook.Worksheets["Processes"] + $sheet.Column(1) | Set-Format -Bold -AutoFit + $sheet.Column(2) | Set-Format -Width 29 -WrapText + $sheet.Column(3) | Set-Format -HorizontalAlignment Right -NFormat "#,###" + Set-Format -Address $sheet.Cells["E1:H1048576"] -HorizontalAlignment Right -NFormat "#,###" + Set-Format -Address $sheet.Column(4) -HorizontalAlignment Right -NFormat "#,##0.0" -Bold + Set-Format -Address $sheet.Row(1) -Bold -HorizontalAlignment Center + Add-ConditionalFormatting -WorkSheet $sheet -Range "D2:D1048576" -DataBarColor Red + Add-ConditionalFormatting -WorkSheet $sheet -Range "G2:G1048576" -RuleType GreaterThan -ConditionValue "104857600" -ForeGroundColor Red + foreach ($c in 5..9) {Set-Format $sheet.Column($c) -AutoFit } + Export-Excel -ExcelPackage $excel -WorkSheetname "Processes" -IncludePivotChart -ChartType ColumnClustered -NoLegend -PivotRows company -PivotData @{'Name'='Count'} -Show + + This a more sophisticated version of the previous example showing different ways of using Set-Format, and also adding conditional formatting. + In the final command a Pivot chart is added and the workbook is opened in Excel. + + .LINK + https://github.com/dfinke/ImportExcel + #> + + [CmdletBinding(DefaultParameterSetName = 'Default')] + Param( + [Parameter(ParameterSetName = "Default", Position = 0)] + [Parameter(ParameterSetName = "Table" , Position = 0)] + [String]$Path, + [Parameter(Mandatory = $true, ParameterSetName = "PackageDefault")] + [Parameter(Mandatory = $true, ParameterSetName = "PackageTable")] + [OfficeOpenXml.ExcelPackage]$ExcelPackage, + [Parameter(ValueFromPipeline = $true)] + $TargetData, + [Switch]$Show, + [String]$WorkSheetname = 'Sheet1', + [String]$Password, + [switch]$ClearSheet, + [switch]$Append, + [String]$Title, + [OfficeOpenXml.Style.ExcelFillStyle]$TitleFillPattern = 'None', + [Switch]$TitleBold, + [Int]$TitleSize = 22, + [System.Drawing.Color]$TitleBackgroundColor, + [Switch]$IncludePivotTable, + [String[]]$PivotRows, + [String[]]$PivotColumns, + $PivotData, + [String[]]$PivotFilter, + [Switch]$PivotDataToColumn, + [Hashtable]$PivotTableDefinition, + [Switch]$IncludePivotChart, + [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', + [Switch]$NoLegend, + [Switch]$ShowCategory, + [Switch]$ShowPercent, + [Switch]$AutoSize, + [Switch]$NoClobber, + [Switch]$FreezeTopRow, + [Switch]$FreezeFirstColumn, + [Switch]$FreezeTopRowFirstColumn, + [Int[]]$FreezePane, + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'PackageDefault')] + [Switch]$AutoFilter, + [Switch]$BoldTopRow, + [Switch]$NoHeader, + [String]$RangeName, + [ValidateScript( { + if ($_.Contains(' ')) { + throw 'Tablename has spaces.' + } + elseif (-not $_) { + throw 'Tablename is null or empty.' + } + elseif ($_[0] -notmatch '[a-z]') { + throw 'Tablename starts with an invalid character.' + } + else { + $true + } + })] + [Parameter(ParameterSetName = 'Table' , Mandatory = $true)] + [Parameter(ParameterSetName = 'PackageTable' , Mandatory = $true)] + [String]$TableName, + [Parameter(ParameterSetName = 'Table')] + [Parameter(ParameterSetName = 'PackageTable')] + [OfficeOpenXml.Table.TableStyles]$TableStyle = 'Medium6', + [Object[]]$ExcelChartDefinition, + [String[]]$HideSheet, + [Switch]$MoveToStart, + [Switch]$MoveToEnd, + $MoveBefore , + $MoveAfter , + [Switch]$KillExcel, + [Switch]$AutoNameRange, + [Int]$StartRow = 1, + [Int]$StartColumn = 1, + [Switch]$PassThru, + [String]$Numberformat = 'General', + [string[]]$ExcludeProperty, + [String[]]$NoNumberConversion, + [Object[]]$ConditionalFormat, + [Object[]]$ConditionalText, + [ScriptBlock]$CellStyleSB, + [Parameter(ParameterSetName = 'Now')] + # [Parameter(ParameterSetName = 'TableNow')] + [Switch]$Now, + [Switch]$ReturnRange, + [Switch]$NoTotalsInPivot, + [Switch]$ReZip + ) + + Begin { + function Add-CellValue { + <# + .SYNOPSIS + Save a value in an Excel cell. + + .DESCRIPTION + DateTime objects are always converted to a DateTime format in Excel. And formulas are always + saved as formulas. + + Numerical values will be converted to numbers as defined in the regional settings of the local + system. In case the parameter 'NoNumberConversion' is used, we don't convert to number and leave + the value 'as is'. In case of conversion failure, we also leave the value 'as is'. + #> + + Param ( + [Object]$TargetCell, + [Object]$CellValue + ) + + Switch ($CellValue) { + {($_ -is [String]) -and ($_.StartsWith('='))} { + #region Save an Excel formula + $TargetCell.Formula = $_ + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$_' as formula" + break + #endregion + } + { $_ -is [URI] } { + #region Save a hyperlink + $TargetCell.Value = $_.AbsoluteUri + $TargetCell.HyperLink = $_ + $TargetCell.Style.Font.Color.SetColor([System.Drawing.Color]::Blue) + $TargetCell.Style.Font.UnderLine = $true + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($_.AbsoluteUri)' as Hyperlink" + break + #endregion + } + { $_ -is [DateTime]} { + #region Save a date with an international valid format + $TargetCell.Value = $_ + $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 + } + + {(($NoNumberConversion) -and ($NoNumberConversion -contains $Name)) -or + ($NoNumberConversion -eq '*')} { + #region Save a value without converting to number + $TargetCell.Value = $_ + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' unconverted" + break + #endregion + } + + Default { + #region Save a value as a number if possible + $number = $null + if ([Double]::TryParse([String]$_, [System.Globalization.NumberStyles]::Any, + [System.Globalization.NumberFormatInfo]::CurrentInfo, [Ref]$number)) { + $TargetCell.Value = $number + $targetCell.Style.Numberformat.Format = $Numberformat + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as number converted from '$_' with format '$Numberformat'" + } + else { + $TargetCell.Value = $_ + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as string" + } + break + #endregion + } + } + } + + Try { + $script:Header = $null + if ($append -and $clearSheet) {throw "You can't use -Append AND -ClearSheet."} + if ($KillExcel) { + Get-Process excel -ErrorAction Ignore | Stop-Process + while (Get-Process excel -ErrorAction Ignore) {} + } + + if ($PSBoundParameters.Keys.Count -eq 0 -Or $Now) { + $Path = [System.IO.Path]::GetTempFileName() -replace '\.tmp', '.xlsx' + $Show = $true + $AutoSize = $true + if (!$TableName) { + $AutoFilter = $true + } + } + + if ($ExcelPackage) { + $pkg = $ExcelPackage + $Path = $pkg.File + } + Else { + $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + + $targetPath = Split-Path $Path + if (!(Test-Path $targetPath)) { + Write-Debug "Base path $($targetPath) does not exist, creating" + $null = mkdir $targetPath -ErrorAction Ignore + } + elseif (Test-Path $Path) { + Write-Debug "Path '$Path' already exists" + } + + $pkg = New-Object OfficeOpenXml.ExcelPackage $Path + } + + [OfficeOpenXml.ExcelWorksheet]$ws = $pkg | Add-WorkSheet -WorkSheetname $WorkSheetname -NoClobber:$NoClobber -ClearSheet:$ClearSheet #Add worksheet doesn't take any action for -noClobber + if ($MoveToStart) {$pkg.Workbook.Worksheets.MoveToStart($worksheetName) } + elseif ($MoveToEnd) {$pkg.Workbook.Worksheets.MoveToEnd($worksheetName) } + elseif ($MoveBefore) { + if ($pkg.Workbook.Worksheets[$MoveBefore]) { + if ($MoveBefore -is [int]) { + $pkg.Workbook.Worksheets.MoveBefore($ws.Index, $MoveBefore) + } + else {$pkg.Workbook.Worksheets.MoveBefore($worksheetname, $MoveBefore)} + } + else {Write-Warning "Can't find worksheet '$MoveBefore'; worsheet '$WorkSheetname' will not be moved."} + } + elseif ($MoveAfter) { + if ($MoveAfter = "*") { + if ($WorkSheetname -lt $pkg.Workbook.Worksheets[1].Name) {$pkg.Workbook.Worksheets.MoveToStart($worksheetName)} + else { + $i = 1 + While ($i -lt $pkg.Workbook.Worksheets.Count -and $pkg.Workbook.Worksheets[$i + 1].Name -lt $worksheetname ) { $i++} + $pkg.Workbook.Worksheets.MoveAfter($ws.Index, $i) + } + } + elseif ($pkg.Workbook.Worksheets[$MoveAfter]) { + if ($MoveAfter -is [int]) { + $pkg.Workbook.Worksheets.MoveAfter($ws.Index, $MoveAfter) + } + else { + $pkg.Workbook.Worksheets.MoveAfter($worksheetname, $MoveAfter) + } + } + else {Write-Warning "Can't find worksheet '$MoveAfter'; worsheet '$WorkSheetname' will not be moved."} + } + $ws.View.TabSelected = $true + foreach ($format in $ConditionalFormat ) { + $target = "Add$($format.Formatter)" + $rule = ($ws.ConditionalFormatting).PSObject.Methods[$target].Invoke($format.Range, $format.IconType) + $rule.Reverse = $format.Reverse + } + + if ($append -and $ws.Dimension) { + $headerRange = $ws.Dimension.Address -replace "\d+$", "1" + #if there is a title or anything else above the header row, specifying StartRow will skip it. + if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} + #$script:Header = $ws.Cells[$headerrange].Value + #using a slightly odd syntax otherwise header ends up as a 2D array + $ws.Cells[$headerRange].Value | ForEach-Object -Begin {$Script:header = @()} -Process {$Script:header += $_ } + $row = $ws.Dimension.Rows + Write-Debug -Message ("Appending: headers are " + ($script:Header -join ", ") + "Start row $row") + } + elseif ($Title) { + #Can only add a title if not appending + $Row = $StartRow + $ws.Cells[$Row, $StartColumn].Value = $Title + $ws.Cells[$Row, $StartColumn].Style.Font.Size = $TitleSize + + if ($TitleBold) { + #set title to Bold if -TitleBold was specified. + #Otherwise the default will be unbolded. + $ws.Cells[$Row, $StartColumn].Style.Font.Bold = $True + } + #can only set TitleBackgroundColor if TitleFillPattern is something other than None + if ($TitleBackgroundColor -and ($TitleFillPattern -eq 'None')) { + $TitleFillPattern = [OfficeOpenXml.Style.ExcelFillStyle]::Solid + } + $ws.Cells[$Row, $StartColumn].Style.Fill.PatternType = $TitleFillPattern + + if ($TitleBackgroundColor ) { + $ws.Cells[$Row, $StartColumn].Style.Fill.BackgroundColor.SetColor($TitleBackgroundColor) + } + $Row ++ ; $startRow ++ + } + else { + $Row = $StartRow + } + $ColumnIndex = $StartColumn + $firstTimeThru = $true + $isDataTypeValueType = $false + } + Catch { + if ($AlreadyExists) { + #Is this set anywhere ? + throw "Failed exporting worksheet '$WorkSheetname' to '$Path': The worksheet '$WorkSheetname' already exists." + } + else { + throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" + } + } + } + + Process { + if ($TargetData) { + Try { + if ($firstTimeThru) { + $firstTimeThru = $false + $isDataTypeValueType = $TargetData.GetType().name -match 'string|bool|byte|char|decimal|double|float|int|long|sbyte|short|uint|ulong|ushort' + Write-Debug "DataTypeName is '$($TargetData.GetType().name)' isDataTypeValueType '$isDataTypeValueType'" + } + + if ($isDataTypeValueType) { + $ColumnIndex = $StartColumn + + Add-CellValue -TargetCell $ws.Cells[$Row, $ColumnIndex] -CellValue $TargetData + + $Row += 1 + } + else { + #region Add headers + if (-not $script:Header) { + $ColumnIndex = $StartColumn + $script:Header = $TargetData.PSObject.Properties.Name | Where-Object {$_ -notin $ExcludeProperty} + + if ($NoHeader) { + # Don't push the headers to the spread sheet + $Row -= 1 + } + else { + foreach ($Name in $script:Header) { + $ws.Cells[$Row, $ColumnIndex].Value = $Name + Write-Verbose "Cell '$Row`:$ColumnIndex' add header '$Name'" + $ColumnIndex += 1 + } + } + } + #endregion + + $Row += 1 + $ColumnIndex = $StartColumn + + foreach ($Name in $script:Header) { + #region Add non header values + Add-CellValue -TargetCell $ws.Cells[$Row, $ColumnIndex] -CellValue $TargetData.$Name + + $ColumnIndex += 1 + #endregion + } + } + } + Catch { + throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" + } + } + } + + End { + Try { + if ($AutoNameRange) { + if (-not $script:header) { + $headerRange = $ws.Dimension.Address -replace "\d+$", "1" + #if there is a title or anything else above the header row, specifying StartRow will skip it. + if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} + #using a slightly odd syntax otherwise header ends up as a 2D array + $ws.Cells[$headerRange].Value | ForEach-Object -Begin {$Script:header = @()} -Process {$Script:header += $_ } + } + $totalRows = $ws.Dimension.End.Row + $totalColumns = $ws.Dimension.Columns + foreach ($c in 0..($totalColumns - 1)) { + $targetRangeName = "$($script:Header[$c])" + $targetColumn = $c + $StartColumn + $theCell = $ws.Cells[($startrow + 1), $targetColumn, $totalRows , $targetColumn ] + if ($ws.names[$targetRangeName]) { $ws.names[$targetRangeName].Address = $theCell.FullAddressAbsolute } + else {$ws.Names.Add($targetRangeName, $theCell) | Out-Null } + + if ([OfficeOpenXml.FormulaParsing.ExcelUtilities.ExcelAddressUtil]::IsValidAddress($targetRangeName)) { + Write-Warning "AutoNameRange: Property name '$targetRangeName' is also a valid Excel address and may cause issues. Consider renaming the property name." + } + } + } + + if ($Title) { + $startAddress = $ws.Dimension.Start.address -replace "$($ws.Dimension.Start.row)`$", "$($ws.Dimension.Start.row + 1)" + } + else { + $startAddress = $ws.Dimension.Start.Address + } + + $dataRange = "{0}:{1}" -f $startAddress, $ws.Dimension.End.Address + + Write-Debug "Data Range '$dataRange'" + + if (-not [String]::IsNullOrEmpty($RangeName)) { + if ($ws.Names[$RangeName]) { $ws.Names[$rangename].Address = $ws.Cells[$dataRange].FullAddressAbsolute } + else {$ws.Names.Add($RangeName, $ws.Cells[$dataRange]) | Out-Null } + } + + if (-not [String]::IsNullOrEmpty($TableName)) { + $csr = $StartRow + + $csc = $StartColumn + $cer = $ws.Dimension.End.Row + $cec = $ws.Dimension.End.Column # was $script:Header.Count + + $targetRange = $ws.Cells[$csr, $csc, $cer, $cec] + #if we're appending data the table may already exist: + if ($ws.Tables[$TableName]) { + $ws.Tables[$TableName].TableXml.table.ref = $targetRange.Address + $ws.Tables[$TableName].TableStyle = $TableStyle + } + else { + $tbl = $ws.Tables.Add($targetRange, $TableName) + $tbl.TableStyle = $TableStyle + } + } + + if ($PivotTableDefinition) { + foreach ($item in $PivotTableDefinition.GetEnumerator()) { + $pivotTableName = $item.Key + $pivotTableDataName = $item.Key + 'PivotTableData' + if ($item.Value.PivotFilter) {$PivotTableStartCell = "A3"} else { $PivotTableStartCell = "A1"} + + #Make sure the Pivot table sheet doesn't already exist + #try { $pkg.Workbook.Worksheets.Delete( $pivotTableName) } catch {} + [OfficeOpenXml.ExcelWorksheet]$wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber + + #If it is a pivot for the default sheet and it doesn't exist - create it + if (-not $item.Value.SourceWorkSheet -and -not $wsPivot.PivotTables[$pivotTableDataName] ) { + $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) + } + #If it is a pivot for the default sheet and it exists - update the range. + elseif (-not $item.Value.SourceWorkSheet -and $wsPivot.PivotTables[$pivotTableDataName] ) { + $wsPivot.PivotTables[$pivotTableDataName].CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref = $WS.Cells[$dataRange].Address + } + #if it is a pivot for a named sheet and it doesn't exist, create it. + elseif ($item.Value.SourceWorkSheet -and -not $wsPivot.PivotTables[$pivotTableDataName] ) { + $workSheet = $pkg.Workbook.Worksheets.where( {$_.name -match $item.Value.SourceWorkSheet})[0] #removed find worksheet + if (-not $workSheet) {Write-Warning -Message "Could not find Worksheet '$($item.Value.SourceWorkSheet)' specified in pivot-table definition $($item.key)." } + else { + $targetDataRange = "{0}:{1}" -f $workSheet.Dimension.Start.Address, $workSheet.Dimension.End.Address + $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $workSheet.Cells[$targetDataRange], $pivotTableDataName) + } + } + + #if we created the pivot table, set up the rows, columns and data if we didn't, put out a message 'existed' or 'error' + if ($pivotTable) { + foreach ($Row in $item.Value.PivotRows) { + try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } + catch {Write-Warning -message "Could not add '$row' to Rows in PivotTable $pivotTableName." } + } + foreach ($Column in $item.Value.PivotColumns) { + try {$null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column])} + catch {Write-Warning -message "Could not add '$Column' to Columns in PivotTable $pivotTableName." } + } + if ($item.Value.PivotData -is [HashTable] -or $item.Value.PivotData -is [System.Collections.Specialized.OrderedDictionary]) { + $item.Value.PivotData.Keys | ForEach-Object { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) + $df.Function = $item.Value.PivotData.$_ + } + catch {Write-Warning -message "Problem adding data fields to PivotTable $pivotTableName." } + } + } + else { + foreach ($field in $item.Value.PivotData) { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$field]) + $df.Function = 'Count' + } + catch {Write-Warning -message "Problem adding data field '$field' to PivotTable $pivotTableName." } + } + } + foreach ( $pFilter in $item.Value.PivotFilter) { + try { $null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter])} + catch {Write-Warning -message "Could not add '$pFilter' to Filter/Page fields in PivotTable $pivotTableName." } + } + if ($item.Value.NoTotalsInPivot -or $NoTotalsInPivot) { $pivotTable.RowGrandTotals = $false } + if ($item.Value.PivotDataToColumn -or $PivotDataToColumn) { $pivotTable.DataOnRows = $false } + } + elseif ($wsPivot.PivotTables[$pivotTableDataName]) { + Write-Warning -Message "Pivot table defined in $($item.key) already exists." + } + else { Write-Warning -Message "Could not create the pivot table defined in $($item.key)."} + + #Create the chart if it doesn't exist, leave alone if it does. + if ($item.Value.IncludePivotChart -and -not $wsPivot.Drawings['PivotChart'] ) { + if ($item.Value.ChartType) { $ChartType = $item.Value.ChartType} # $ChartType may be passed as a parameter, has default of "Pie", over-ride that if it is in the pivot definition + [OfficeOpenXml.Drawing.Chart.ExcelChart] $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) + if (-not $item.Value.ChartHeight) {$item.Value.ChartHeight = 400 } + if (-not $item.Value.ChartWidth) {$item.Value.ChartWidth = 600 } + if (-not $item.Value.ChartRow) {$item.Value.ChartRow = 0 } + if (-not $item.Value.ChartColumn) {$item.Value.ChartColumn = 4 } + if (-not $item.Value.ChartRowOffSetPixels) {$item.Value.ChartRowOffSetPixels = 0 } + if (-not $item.Value.ChartColumnOffSetPixels) {$item.Value.ChartColumnOffSetPixels = 0 } + $chart.SetPosition($item.Value.ChartRow , $item.Value.ChartRowOffSetPixels , $item.Value.ChartColumn, $item.Value.ChartColumnOffSetPixels) + $chart.SetSize( $item.Value.ChartWidth, $item.Value.ChartHeight) + if ($chart.DataLabel) { + $chart.DataLabel.ShowCategory = [boolean]$item.Value.ShowCategory + $chart.DataLabel.ShowPercent = [boolean]$item.Value.ShowPercent + } + if ([boolean]$item.Value.NoLegend -or $NoLegend) {$chart.Legend.Remove()} + if ( $item.Value.ChartTitle) {$chart.Title.Text = $item.Value.chartTitle} + } + } + } + + if ($IncludePivotTable -or $IncludePivotChart) { + if ($PivotFilter) {$PivotTableStartCell = "A3"} else {$PivotTableStartCell = "A1"} + + $pivotTableName = $WorkSheetname + 'PivotTable' + $wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber + + $wsPivot.View.TabSelected = $true + + $pivotTableDataName = $WorkSheetname + 'PivotTableData' + if ($wsPivot.PivotTables[$pivotTableDataName] ) { + $pivotTable = $wsPivot.PivotTables[$pivotTableDataName] + $pivotTable.CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref = $WS.Cells[$dataRange].Address + Write-Warning -Message "Pivot table for $worksheetName already exists; updating the data range, but other properties will not be changed" + } + else { + $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) + + foreach ($Row in $PivotRows) { + try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } + catch {Write-Warning -message "Could not add '$row' to PivotTable Rows." } + } + + foreach ($Column in $PivotColumns) { + try {$null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column])} + catch {Write-Warning -message "Could not add '$Column' to PivotTable Columns." } + } + + if ($PivotData -is [HashTable] -or $PivotData -is [System.Collections.Specialized.OrderedDictionary]) { + $PivotData.Keys | ForEach-Object { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) + $df.Function = $PivotData.$_ + } + catch {Write-Warning "Problem adding to Pivot table data fields." } + } + } + else { + foreach ($Item in $PivotData) { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$Item]) + $df.Function = 'Count' + } + catch {Write-Warning "Problem adding '$item' to Pivot table data fields." } + } + } + + if ($PivotDataToColumn) { $pivotTable.DataOnRows = $false } + + foreach ($pFilter in $PivotFilter) { + try {$null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter]) } + catch {Write-Warning "Problem adding 'pFilter' to Pivot table page/filter fields." } + } + + if ($NoTotalsInPivot) { $pivotTable.RowGrandTotals = $false } + } + + if ($IncludePivotChart) { + if (-not $wsPivot.Drawings['PivotChart']) { + $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) + if ($chart.DataLabel) { + $chart.DataLabel.ShowCategory = $ShowCategory + $chart.DataLabel.ShowPercent = $ShowPercent + } + $chart.SetPosition(0, 26, 2, 26) # if Pivot table is rows+data only it will be 2 columns wide if has pivot columns we don't know how wide it will be + if ($NoLegend) { $chart.Legend.Remove() } + } + } + } + + if ($Password) { + $ws.Protection.SetPassword($Password) + } + + if ($AutoFilter) { + $ws.Cells[$dataRange].AutoFilter = $true + } + + if ($FreezeTopRow) { + $ws.View.FreezePanes(2, 1) + } + + if ($FreezeTopRowFirstColumn) { + $ws.View.FreezePanes(2, 2) + } + + if ($FreezeFirstColumn) { + $ws.View.FreezePanes(1, 2) + } + + if ($FreezePane) { + $freezeRow, $freezeColumn = $FreezePane + if (-not $freezeColumn -or $freezeColumn -eq 0) { + $freezeColumn = 1 + } + + if ($freezeRow -gt 1) { + $ws.View.FreezePanes($freezeRow, $freezeColumn) + } + } + + if ($BoldTopRow) { + if ($Title) { + $range = $ws.Dimension.Address -replace '\d+', '2' + } + else { + $range = $ws.Dimension.Address -replace '\d+', '1' + } + + $ws.Cells[$range].Style.Font.Bold = $true + } + + if ($AutoSize) { + $ws.Cells.AutoFitColumns() + } + + foreach ($Sheet in $HideSheet) { + $pkg.Workbook.WorkSheets[$Sheet].Hidden = 'Hidden' + } + + foreach ($chartDef in $ExcelChartDefinition) { + $ChartName = 'Chart' + (Split-Path -Leaf ([System.IO.path]::GetTempFileName())) -replace 'tmp|\.', '' + $chart = $ws.Drawings.AddChart($ChartName, $chartDef.ChartType) + $chart.Title.Text = $chartDef.Title + + if ($chartDef.NoLegend) { + $chart.Legend.Remove() + } + + if ($chart.Datalabel -ne $null) { + $chart.Datalabel.ShowCategory = $chartDef.ShowCategory + $chart.Datalabel.ShowPercent = $chartDef.ShowPercent + } + + $chart.SetPosition($chartDef.Row, $chartDef.RowOffsetPixels, $chartDef.Column, $chartDef.ColumnOffsetPixels) + $chart.SetSize($chartDef.Width, $chartDef.Height) + + $chartDefCount = @($chartDef.YRange).Count + if ($chartDefCount -eq 1) { + $Series = $chart.Series.Add($chartDef.YRange, $chartDef.XRange) + + $SeriesHeader = $chartDef.SeriesHeader + if (-not $SeriesHeader) { + $SeriesHeader = 'Series 1' + } + + $Series.Header = $SeriesHeader + } + else { + for ($idx = 0; $idx -lt $chartDefCount; $idx += 1) { + $Series = $chart.Series.Add($chartDef.YRange[$idx], $chartDef.XRange) + + if ($chartDef.SeriesHeader.Count -gt 0) { + $SeriesHeader = $chartDef.SeriesHeader[$idx] + } + + if (-not $SeriesHeader) { + $SeriesHeader = "Series $($idx)" + } + + $Series.Header = $SeriesHeader + $SeriesHeader = $null + } + } + } + + if ($ConditionalText) { + foreach ($targetConditionalText in $ConditionalText) { + $target = "Add$($targetConditionalText.ConditionalType)" + + $Range = $targetConditionalText.Range + if (-not $Range) { + $Range = $ws.Dimension.Address + } + + $rule = ($ws.Cells[$Range].ConditionalFormatting).PSObject.Methods[$target].Invoke() + + if ($targetConditionalText.Text) { + if ($targetConditionalText.ConditionalType -match 'equal|notequal|lessthan|lessthanorequal|greaterthan|greaterthanorequal') { + $rule.Formula = $targetConditionalText.Text + } + else { + $rule.Text = $targetConditionalText.Text + } + } + + $rule.Style.Font.Color.Color = $targetConditionalText.ConditionalTextColor + $rule.Style.Fill.PatternType = $targetConditionalText.PatternType + $rule.Style.Fill.BackgroundColor.Color = $targetConditionalText.BackgroundColor + } + } + + if ($CellStyleSB) { + $TotalRows = $ws.Dimension.Rows + $LastColumn = (Get-ExcelColumnName $ws.Dimension.Columns).ColumnName + & $CellStyleSB $ws $TotalRows $LastColumn + } + + if ($PassThru) { + $pkg + } + else { + if ($ReturnRange) { + $ws.Dimension.Address + } + + $pkg.Save() + + if ($ReZip) { + write-verbose "Re-Zipping $($pkg.file) using .NET ZIP library" + $zipAssembly = "System.IO.Compression.Filesystem" + try { + Add-Type -assembly $zipAssembly -ErrorAction stop + } + catch { + write-error "The -ReZip parameter requires .NET Framework 4.5 or later to be installed. Recommend to install Powershell v4+" + continue + } + + $TempZipPath = Join-Path -path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName()) + [io.compression.zipfile]::ExtractToDirectory($pkg.File, $TempZipPath) | Out-Null + Remove-Item $pkg.File -Force + [io.compression.zipfile]::CreateFromDirectory($TempZipPath, $pkg.File) | Out-Null + } + + $pkg.Dispose() + + if ($Show) { + Invoke-Item $Path + } + } + } + Catch { + throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" + } + } +} + +function New-PivotTableDefinition { + param( + [Parameter(Mandatory)] + [Alias("PivtoTableName")]#Previous typo - use alias to avoid breaking scripts + $PivotTableName, + $SourceWorkSheet, + $PivotRows, + [hashtable]$PivotData, + $PivotColumns, + $PivotFilter, + [Switch]$NoTotalsInPivot, + [Switch]$IncludePivotChart, + [String]$ChartTitle, + [int]$ChartHeight = 400 , + [int]$ChartWidth = 600, + [Int]$ChartRow = 0 , + [Int]$ChartColumn = 6, + [Int]$ChartRowOffSetPixels = 0 , + [Int]$ChartColumnOffSetPixels = 0, + [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', + [Switch]$NoLegend, + [Switch]$ShowCategory, + [Switch]$ShowPercent + ) + + $parameters = @{} + $PSBoundParameters + $parameters.Remove('PivotTableName') + + @{$PivotTableName = $parameters} +} From d74cce91b681f5f1e184cc0cf9c11b76414684fd Mon Sep 17 00:00:00 2001 From: jhoneill Date: Sun, 17 Jun 2018 03:15:47 +0100 Subject: [PATCH 02/11] Added join Worksheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved "MoveTo…" functionality into Add-Worksheet and gave it copy worksheet functionality Added create new file functionality to Open-ExcelPackage - requires a -create switch so previous "Open for read if it exists" behaviour is kept. Fixed Conditional formatting so background pattern is "None" not "Solid" by default. Tidied comments and help in Merge and Compare Added Join Worksheet Added Extra parameters, sanity check and help to New-PivotTableDefinition --- AddConditionalFormatting.ps1 | 233 +++++---- Compare-WorkSheet.ps1 | 251 ++++++++++ Export-Excel.ps1 | 229 +++++---- ImportExcel.psm1 | 106 +++- Join-Worksheet.ps1 | 164 ++++++ Merge-worksheet.ps1 | 947 ++++++++++++++++++----------------- Open-ExcelPackage.ps1 | 117 +++-- 7 files changed, 1273 insertions(+), 774 deletions(-) create mode 100644 Compare-WorkSheet.ps1 create mode 100644 Join-Worksheet.ps1 diff --git a/AddConditionalFormatting.ps1 b/AddConditionalFormatting.ps1 index 00b4c6a..2e7b4f7 100644 --- a/AddConditionalFormatting.ps1 +++ b/AddConditionalFormatting.ps1 @@ -1,118 +1,117 @@ -Function Add-ConditionalFormatting { -<# -.Synopsis - Adds contitional formatting to worksheet -.Example - $excel = $avdata | Export-Excel -Path (Join-path $FilePath "\Machines.XLSX" ) -WorksheetName "Server Anti-Virus" -AutoSize -FreezeTopRow -AutoFilter -PassThru - - Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "b":b1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "2003" - Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "i2:i1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "Disabled" - $excel.Workbook.Worksheets[1].Cells["D1:G1048576"].Style.Numberformat.Format = [cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern - $excel.Workbook.Worksheets[1].Row(1).style.font.bold = $true - $excel.Save() ; $excel.Dispose() - - Here Export-Excel is called with the -passThru parameter so the Excel Package object is stored in $Excel - The desired worksheet is selected and the then columns B and i are conditially formatted (excluding the top row) to show - Fixed formats are then applied to dates in columns D..G and the top row is formatted - Finally the workbook is saved and the Excel closed. - -#> - Param ( - #The worksheet where the format is to be applied - [Parameter(Mandatory = $true, ParameterSetName = "NamedRule")] - [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] - [OfficeOpenXml.ExcelWorksheet]$WorkSheet , - #The area of the worksheet where the format is to be applied - [Parameter(Mandatory = $true, ParameterSetName = "NamedRule")] - [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] - [OfficeOpenXml.ExcelAddress]$Range , - #One or more row(s), Column(s) and/or block(s) of cells to format - [Parameter(Mandatory = $true, ParameterSetName = "NamedRuleAddress")] - [Parameter(Mandatory = $true, ParameterSetName = "DataBarAddress")] - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSetAddress")] - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSetAddress")] - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSetAddress")] - $Address , - #One of the standard named rules - Top / Bottom / Less than / Greater than / Contains etc - [Parameter(Mandatory = $true, ParameterSetName = "NamedRule", Position = 3)] - [Parameter(Mandatory = $true, ParameterSetName = "NamedRuleAddress", Position = 3)] - [OfficeOpenXml.ConditionalFormatting.eExcelConditionalFormattingRuleType]$RuleType , - #Text colour for matching objects - [Alias("ForeGroundColour")] - [System.Drawing.Color]$ForeGroundColor, - #colour for databar type charts - [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] - [Parameter(Mandatory = $true, ParameterSetName = "DataBarAddress")] - [Alias("DataBarColour")] - [System.Drawing.Color]$DataBarColor, - #One of the three-icon set types (e.g. Traffic Lights) - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSetAddress")] - [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting3IconsSetType]$ThreeIconsSet, - #A four-icon set name - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSetAddress")] - [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting4IconsSetType]$FourIconsSet, - #A five-icon set name - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSetAddress")] - [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting5IconsSetType]$FiveIconsSet, - #A value for the condition (e.g. "2000" if the test is 'lessthan 2000') - [string]$ConditionValue, - #A second value for the conditions like between x and Y - [string]$ConditionValue2, - #Background colour for matching items - [System.Drawing.Color]$BackgroundColor, - #Background pattern for matching items - [OfficeOpenXml.Style.ExcelFillStyle]$BackgroundPattern = [OfficeOpenXml.Style.ExcelFillStyle]::Solid, - #Secondary colour when a background pattern requires it - [System.Drawing.Color]$PatternColor, - #Sets the numeric format for matching items - $NumberFormat, - #Put matching items in bold face - [switch]$Bold, - #Put matching items in italic - [switch]$Italic, - #Underline matching items - [switch]$Underline, - #Strikethrough text of matching items - [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) { - $WorkSheet = $Address.Worksheet[0] - $Range = $Address.Address - } - 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) } - else { $rule = ($WorkSheet.ConditionalFormatting)."Add$RuleType"($Range)} - - if ($ConditionValue -and $RuleType -match "Top|Botom") {$rule.Rank = $ConditionValue } - if ($ConditionValue -and $RuleType -match "StdDev") {$rule.StdDev = $ConditionValue } - if ($ConditionValue -and $RuleType -match "Than|Equal|Expression") {$rule.Formula = $ConditionValue } - if ($ConditionValue -and $RuleType -match "Text|With") {$rule.Text = $ConditionValue } - if ($ConditionValue -and - $ConditionValue2 -and $RuleType -match "Between") { - $rule.Formula = $ConditionValue - $rule.Formula2 = $ConditionValue2 - } - - if ($NumberFormat) {$rule.Style.NumberFormat.Format = $NumberFormat } - if ($Underline) {$rule.Style.Font.Underline = [OfficeOpenXml.Style.ExcelUnderLineType]::Single } - if ($Bold) {$rule.Style.Font.Bold = $true} - if ($Italic) {$rule.Style.Font.Italic = $true} - if ($StrikeThru) {$rule.Style.Font.Strike = $true} - if ($ForeGroundColor) {$rule.Style.Font.Color.color = $ForeGroundColor } - if ($BackgroundColor) {$rule.Style.Fill.BackgroundColor.color = $BackgroundColor } - if ($BackgroundPattern) {$rule.Style.Fill.PatternType = $BackgroundPattern } - if ($PatternColor) {$rule.Style.Fill.PatternColor.color = $PatternColor } +Function Add-ConditionalFormatting { +<# + .Synopsis + Adds contitional formatting to worksheet. + .Example + $excel = $avdata | Export-Excel -Path (Join-path $FilePath "\Machines.XLSX" ) -WorksheetName "Server Anti-Virus" -AutoSize -FreezeTopRow -AutoFilter -PassThru + + Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "b2:b1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "2003" + Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "i2:i1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "Disabled" + $excel.Workbook.Worksheets[1].Cells["D1:G1048576"].Style.Numberformat.Format = [cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern + $excel.Workbook.Worksheets[1].Row(1).style.font.bold = $true + $excel.Save() ; $excel.Dispose() + + Here Export-Excel is called with the -passThru parameter so the Excel Package object is stored in $Excel + The desired worksheet is selected and the then columns B and i are conditially formatted (excluding the top row) to show red text if + the columns contain "2003" or "Disabled respectively. A fixed date formats are then applied to columns D..G, and the top row is formatted. + Finally the workbook is saved and the Excel object closed. + +#> + Param ( + #The worksheet where the format is to be applied + [Parameter(Mandatory = $true, ParameterSetName = "NamedRule")] + [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] + [OfficeOpenXml.ExcelWorksheet]$WorkSheet , + #The area of the worksheet where the format is to be applied + [Parameter(Mandatory = $true, ParameterSetName = "NamedRule")] + [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] + [OfficeOpenXml.ExcelAddress]$Range , + #One or more row(s), Column(s) and/or block(s) of cells to format + [Parameter(Mandatory = $true, ParameterSetName = "NamedRuleAddress")] + [Parameter(Mandatory = $true, ParameterSetName = "DataBarAddress")] + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSetAddress")] + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSetAddress")] + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSetAddress")] + $Address , + #One of the standard named rules - Top / Bottom / Less than / Greater than / Contains etc + [Parameter(Mandatory = $true, ParameterSetName = "NamedRule", Position = 3)] + [Parameter(Mandatory = $true, ParameterSetName = "NamedRuleAddress", Position = 3)] + [OfficeOpenXml.ConditionalFormatting.eExcelConditionalFormattingRuleType]$RuleType , + #Text colour for matching objects + [Alias("ForeGroundColour")] + [System.Drawing.Color]$ForeGroundColor, + #colour for databar type charts + [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] + [Parameter(Mandatory = $true, ParameterSetName = "DataBarAddress")] + [Alias("DataBarColour")] + [System.Drawing.Color]$DataBarColor, + #One of the three-icon set types (e.g. Traffic Lights) + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSetAddress")] + [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting3IconsSetType]$ThreeIconsSet, + #A four-icon set name + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSetAddress")] + [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting4IconsSetType]$FourIconsSet, + #A five-icon set name + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSetAddress")] + [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting5IconsSetType]$FiveIconsSet, + #A value for the condition (e.g. "2000" if the test is 'lessthan 2000') + [string]$ConditionValue, + #A second value for the conditions like between x and Y + [string]$ConditionValue2, + #Background colour for matching items + [System.Drawing.Color]$BackgroundColor, + #Background pattern for matching items + [OfficeOpenXml.Style.ExcelFillStyle]$BackgroundPattern = [OfficeOpenXml.Style.ExcelFillStyle]::None , + #Secondary colour when a background pattern requires it + [System.Drawing.Color]$PatternColor, + #Sets the numeric format for matching items + $NumberFormat, + #Put matching items in bold face + [switch]$Bold, + #Put matching items in italic + [switch]$Italic, + #Underline matching items + [switch]$Underline, + #Strikethrough text of matching items + [switch]$StrikeThru + ) + #Allow 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) { + $WorkSheet = $Address.Worksheet[0] + $Range = $Address.Address + } + 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) } + else { $rule = ($WorkSheet.ConditionalFormatting)."Add$RuleType"($Range)} + + if ($ConditionValue -and $RuleType -match "Top|Botom") {$rule.Rank = $ConditionValue } + if ($ConditionValue -and $RuleType -match "StdDev") {$rule.StdDev = $ConditionValue } + if ($ConditionValue -and $RuleType -match "Than|Equal|Expression") {$rule.Formula = $ConditionValue } + if ($ConditionValue -and $RuleType -match "Text|With") {$rule.Text = $ConditionValue } + if ($ConditionValue -and + $ConditionValue2 -and $RuleType -match "Between") { + $rule.Formula = $ConditionValue + $rule.Formula2 = $ConditionValue2 + } + + if ($NumberFormat) {$rule.Style.NumberFormat.Format = $NumberFormat } + if ($Underline) {$rule.Style.Font.Underline = [OfficeOpenXml.Style.ExcelUnderLineType]::Single } + if ($Bold) {$rule.Style.Font.Bold = $true} + if ($Italic) {$rule.Style.Font.Italic = $true} + if ($StrikeThru) {$rule.Style.Font.Strike = $true} + if ($ForeGroundColor) {$rule.Style.Font.Color.color = $ForeGroundColor } + if ($BackgroundColor) {$rule.Style.Fill.BackgroundColor.color = $BackgroundColor } + if ($BackgroundPattern) {$rule.Style.Fill.PatternType = $BackgroundPattern } + if ($PatternColor) {$rule.Style.Fill.PatternColor.color = $PatternColor } } \ No newline at end of file diff --git a/Compare-WorkSheet.ps1 b/Compare-WorkSheet.ps1 new file mode 100644 index 0000000..f20eb35 --- /dev/null +++ b/Compare-WorkSheet.ps1 @@ -0,0 +1,251 @@ +Function Compare-Worksheet { +<# + .Synopsis + Compares two worksheets (usually with the same name in different files). + .Description + This command takes two file names, one or two worksheet name(s) and a name for a 'key' column. + It reads the worksheets and determines which column will be compared and builds a hashtable of the values in 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 the column names it selected). + 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 to the result. + If -BackgroundColor is specified the difference rows in the source spreadsheet will have their background changed to identify the different rows. + .Example + 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 to register a differnce if if the software was installed on a + different date or from a different place, so specify -ExcludeProperty Install* removes InstallDate and InstallSource from the comparison. + This data doesn't have a "name" column" so we specify the "IdentifyingNumber" column to be the key. + PowerShell will output the differences formatted as a table. + .Example + 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. + 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 '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), + the command 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 the command specifies a limited set of columns should be compared. + .Example + 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 for CPU, Memory, Domain, Disk and so on. + So the command is instructed to start at row 2 (skipping the title) and to name the columns: the first is "label" and the second "Value" with label acting as the key. + This time we are interested only in 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 highlights all the cells in lightgray and then sets the changed rows back to white so that + only the unchanged rows are highlighted +#> + [cmdletbinding(DefaultParameterSetName)] + Param( + #First file to compare. + [parameter(Mandatory=$true,Position=0)] + $Referencefile , + #Second file to compare. + [parameter(Mandatory=$true,Position=1)] + $Differencefile , + #Name(s) of worksheets to compare. + $WorkSheetName = "Sheet1", + #Properties to include in the DIFF - supports wildcards, default is "*". + $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 Start row . + [Parameter(ParameterSetName='B', Mandatory)] + [String[]]$Headername, + #Automatically generate property names (P1, P2, P3, ..) instead of the using the values the Start row of the sheet. + [Parameter(ParameterSetName='C', Mandatory)] + [switch]$NoHeader, + #The row from where we start to import data, all rows above the Start row are disregarded. By default this is row 1. + [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 (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 + ) + + #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} + 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 { + $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 } + + #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 = @{} ; $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"} + $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} + + #Add RowNumber, Sheetname and file name to every row. + $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 = $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} + + 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 . + [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. + 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) { + 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] + $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 ($diff -and $FontColor -and ($propList -contains $Key) ) { + $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} + 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()} + } + } + 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 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 ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" } + } + 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 ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} + else {$hash[($result.SideIndicator+$P)] =$result.$P} + } + } + [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 ($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 diff --git a/Export-Excel.ps1 b/Export-Excel.ps1 index 329174d..0e78e71 100644 --- a/Export-Excel.ps1 +++ b/Export-Excel.ps1 @@ -2,99 +2,98 @@ <# .SYNOPSIS Export data to an Excel worksheet. - .DESCRIPTION Export data to an Excel file and where possible try to convert numbers so Excel recognizes them as numbers instead of text. After all. Excel is a spreadsheet program used for number manipulation and calculations. In case the number conversion is not desired, use the parameter '-NoNumberConversion *'. .PARAMETER Path - Path to a new or existing .XLSX file + Path to a new or existing .XLSX file. .PARAMETER ExcelPackage An object representing an Excel Package - usually this is returned by specifying -Passthru allowing multiple commands to work on the same Workbook without saving and reloading each time. .PARAMETER WorkSheetName - The name of a sheet within the workbook - "Sheet1" by default + The name of a sheet within the workbook - "Sheet1" by default. .PARAMETER ClearSheet - If specified Export-Excel will remove any existing worksheet with the selected name. The Default behaviour is to overwrite cells in this sheet as needed (but leaving non-overwritten ones in place) + If specified Export-Excel will remove any existing worksheet with the selected name. The Default behaviour is to overwrite cells in this sheet as needed (but leaving non-overwritten ones in place). .PARAMETER Append If specified data will be added to the end of an existing sheet, using the same column headings. .PARAMETER TargetData Data to insert onto the worksheet - this is often provided from the pipeline. .PARAMETER ExcludeProperty - Specifies 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 + Text of a title to be placed in Cell A1. .PARAMETER TitleBold - Sets the title in boldface type + Sets the title in boldface type. .PARAMETER TitleSize - Sets the point size for the title + Sets the point size for the title. .PARAMETER TitleBackgroundColor - Sets the cell background color for the title cell + Sets the cell background color for the title cell. .PARAMETER TitleFillPattern - Sets the fill pattern for the title cell + Sets the fill pattern for the title cell. .PARAMETER Password - Sets password protection on the workbook + Sets password protection on the workbook. .PARAMETER IncludePivotTable - Adds a Pivot table using the data in the worksheet + Adds a Pivot table using the data in the worksheet. .PARAMETER PivotRows - Name(s) columns from the spreadhseet which will provide 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 provide 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 + 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, HashTable(s) with Sheet PivotTows, PivotColumns, PivotData, IncludePivotChart and ChartType values to make it easier to specify a definition or multiple Pivots. .PARAMETER IncludePivotChart, Include a chart with the Pivot table - implies Include Pivot Table. .PARAMETER NoLegend - Exclude the legend from the pivot chart + Exclude the legend from the pivot chart. .PARAMETER ShowCategory - Add category labels to the pivot chart + Add category labels to the pivot chart. .PARAMETER ShowPercent - Add Percentage labels to the pivot chart + Add Percentage labels to the pivot chart. .PARAMETER ConditionalText Applies a 'Conditional formatting rule' in Excel on all the cells. When specific conditions are met a rule is triggered. .PARAMETER NoNumberConversion By default we convert all values to numbers if possible, but this isn't always desirable. NoNumberConversion allows you to add exceptions for the conversion. Wildcards (like '*') are allowed. .PARAMETER BoldTopRow - Makes the top Row boldface + Makes the top Row boldface. .PARAMETER NoHeader - Does not put field names at the top of columns + Does not put field names at the top of columns. .PARAMETER RangeName - Makes the data in the worksheet a named range + Makes the data in the worksheet a named range. .PARAMETER TableName - Makes the data in the worksheet a table with a name applies a style to it. Name must not contain spaces + Makes the data in the worksheet a table with a name applies a style to it. Name must not contain spaces. .PARAMETER TableStyle - Selects the style for the named table - defaults to 'Medium6' + Selects the style for the named table - defaults to 'Medium6'. .PARAMETER ExcelChartDefinition - A hash table containing ChartType, Title, NoLegend, ShowCategory, ShowPercent, 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 + Name(s) of Sheet(s) to hide in the workbook. .PARAMETER MoveToStart If specified, the worksheet will be moved to the start of the workbook. - MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified + MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified. .PARAMETER MoveToEnd If specified, the worksheet will be moved to the end of the workbook. - (This is the default position for newly created sheets, but this can be used to move existing sheets) + (This is the default position for newly created sheets, but this can be used to move existing sheets.) .PARAMETER MoveBefore If specified, the worksheet will be moved before the nominated one (which can be a postion starting from 1, or a name). - MoveBefore takes precedence over MoveAfter if both are specified + MoveBefore takes precedence over MoveAfter if both are specified. .PARAMETER MoveAfter If specified, the worksheet will be moved after the nominated one (which can be a postion starting from 1, or a name or *). - If * is used, the worksheet names will be examined starting with the first one sheet placed after the last sheet which comes before it alphabetically. + If * is used, the worksheet names will be examined starting with the first one, and the sheet placed after the last sheet which comes before it alphabetically. .PARAMETER KillExcel - Closes Excel - prevents errors writing to the file because Excel has it open + 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 + 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 - Column to start adding data - 1 by default + Column to start adding data - 1 by default. .PARAMETER FreezeTopRow - Freezes headers etc. in the top row + Freezes headers etc. in the top row. .PARAMETER FreezeFirstColumn - Freezes titles etc. in the left column + Freezes titles etc. in the left column. .PARAMETER FreezeTopRowFirstColumn - Freezes top row and left column (equivalent to Freeze pane 2,2 ) + Freezes top row and left column (equivalent to Freeze pane 2,2 ). .PARAMETER FreezePane - Freezes panes at specified coordinates (in the form RowNumber , ColumnNumber) + Freezes panes at specified coordinates (in the form RowNumber , ColumnNumber). .PARAMETER AutoFilter Enables the 'Filter' in Excel on the complete header row. So users can easily sort, filter and/or search the data in the select column from within Excel. .PARAMETER AutoSize @@ -105,34 +104,34 @@ Formats all values that can be converted to a number to the format specified. Examples: - # integer (not really needed unless you need to round numbers, Excel with use default cell properties) + # integer (not really needed unless you need to round numbers, Excel will use default cell properties). '0' - # integer without displaying the number 0 in the cell + # integer without displaying the number 0 in the cell. '#' - # number with 1 decimal place + # number with 1 decimal place. '0.0' - # number with 2 decimal places + # number with 2 decimal places. '0.00' - # number with 2 decimal places and thousand separator + # number with 2 decimal places and thousand separator. '#,##0.00' - # number with 2 decimal places and thousand separator and money symbol + # number with 2 decimal places and thousand separator and money symbol. '€#,##0.00' # percentage (1 = 100%, 0.01 = 1%) '0%' - # Blue color for positive numbers and a red color for negative numbers. All numbers will proceed a dollar sign '$'. + # Blue color for positive numbers and a red color for negative numbers. All numbers will be proceeded by a dollar sign '$'. '[Blue]$#,##0.00;[Red]-$#,##0.00' .PARAMETER Show Opens the Excel file immediately after creation. Convenient for viewing the results instantly without having to search for the file first. .PARAMETER PassThru - If specified, Export-Excel returns an object representing the Excel package without saving the package first. To save it you need to call the save or Saveas method or send it back to Export-Excel + If specified, Export-Excel returns an object representing the Excel package without saving the package first. To save it you need to call the save or Saveas method or send it back to Export-Excel. .EXAMPLE Get-Process | Export-Excel .\Test.xlsx -show @@ -291,8 +290,8 @@ Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show - This example defines two pivot tables. Then it puts Service data on Sheet1 with one call to Export-Excel and Process Data on sheet2 with a second call to Export-Excel - The thrid and final call adds the two pivot tables and opens the spreadsheet in Excel + This example defines two pivot tables. Then it puts Service data on Sheet1 with one call to Export-Excel and Process Data on sheet2 with a second call to Export-Excel. + The thrid and final call adds the two pivot tables and opens the spreadsheet in Excel. .EXAMPLE @@ -305,7 +304,7 @@ $excel.Dispose() Start-Process .\test.xlsx - This example uses -passthrough - put service information into sheet1 of the work book and saves the excelPackageObject in $Excel + This example uses -passthrough - put service information into sheet1 of the work book and saves the excelPackageObject in $Excel. It then uses the package object to apply formatting, and then saves the workbook and disposes of the object before loading the document in Excel. .EXAMPLE @@ -425,8 +424,8 @@ Save a value in an Excel cell. .DESCRIPTION - DateTime objects are always converted to a DateTime format in Excel. And formulas are always - saved as formulas. + DateTime objects are always converted to a short DateTime format in Excel. When Excel loads the file, + it applies the local format for dates. And formulas are always saved as formulas. URIs are set as hyperlinks in the file. Numerical values will be converted to numbers as defined in the regional settings of the local system. In case the parameter 'NoNumberConversion' is used, we don't convert to number and leave @@ -495,11 +494,7 @@ Try { $script:Header = $null - if ($append -and $clearSheet) {throw "You can't use -Append AND -ClearSheet."} - if ($KillExcel) { - Get-Process excel -ErrorAction Ignore | Stop-Process - while (Get-Process excel -ErrorAction Ignore) {} - } + if ($Append -and $ClearSheet) {throw "You can't use -Append AND -ClearSheet."} if ($PSBoundParameters.Keys.Count -eq 0 -Or $Now) { $Path = [System.IO.Path]::GetTempFileName() -replace '\.tmp', '.xlsx' @@ -511,55 +506,16 @@ } if ($ExcelPackage) { - $pkg = $ExcelPackage - $Path = $pkg.File + $pkg = $ExcelPackage + $Path = $pkg.File } - Else { - $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + Else { $pkg = Open-ExcelPackage -Path $Path -Create -KillExcel:$KillExcel} + + $params = @{} + if ($NoClobber) {Write-Warning -Message "-NoClobber parameter is no longer used" } + foreach ($p in @("WorkSheetname","ClearSheet","MoveToStart","MoveToEnd","MoveBefore","MoveAfter")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} + $ws = $pkg | Add-WorkSheet @params - $targetPath = Split-Path $Path - if (!(Test-Path $targetPath)) { - Write-Debug "Base path $($targetPath) does not exist, creating" - $null = mkdir $targetPath -ErrorAction Ignore - } - elseif (Test-Path $Path) { - Write-Debug "Path '$Path' already exists" - } - - $pkg = New-Object OfficeOpenXml.ExcelPackage $Path - } - - [OfficeOpenXml.ExcelWorksheet]$ws = $pkg | Add-WorkSheet -WorkSheetname $WorkSheetname -NoClobber:$NoClobber -ClearSheet:$ClearSheet #Add worksheet doesn't take any action for -noClobber - if ($MoveToStart) {$pkg.Workbook.Worksheets.MoveToStart($worksheetName) } - elseif ($MoveToEnd) {$pkg.Workbook.Worksheets.MoveToEnd($worksheetName) } - elseif ($MoveBefore) { - if ($pkg.Workbook.Worksheets[$MoveBefore]) { - if ($MoveBefore -is [int]) { - $pkg.Workbook.Worksheets.MoveBefore($ws.Index, $MoveBefore) - } - else {$pkg.Workbook.Worksheets.MoveBefore($worksheetname, $MoveBefore)} - } - else {Write-Warning "Can't find worksheet '$MoveBefore'; worsheet '$WorkSheetname' will not be moved."} - } - elseif ($MoveAfter) { - if ($MoveAfter = "*") { - if ($WorkSheetname -lt $pkg.Workbook.Worksheets[1].Name) {$pkg.Workbook.Worksheets.MoveToStart($worksheetName)} - else { - $i = 1 - While ($i -lt $pkg.Workbook.Worksheets.Count -and $pkg.Workbook.Worksheets[$i + 1].Name -lt $worksheetname ) { $i++} - $pkg.Workbook.Worksheets.MoveAfter($ws.Index, $i) - } - } - elseif ($pkg.Workbook.Worksheets[$MoveAfter]) { - if ($MoveAfter -is [int]) { - $pkg.Workbook.Worksheets.MoveAfter($ws.Index, $MoveAfter) - } - else { - $pkg.Workbook.Worksheets.MoveAfter($worksheetname, $MoveAfter) - } - } - else {Write-Warning "Can't find worksheet '$MoveAfter'; worsheet '$WorkSheetname' will not be moved."} - } $ws.View.TabSelected = $true foreach ($format in $ConditionalFormat ) { $target = "Add$($format.Formatter)" @@ -578,17 +534,17 @@ Write-Debug -Message ("Appending: headers are " + ($script:Header -join ", ") + "Start row $row") } elseif ($Title) { - #Can only add a title if not appending + #Can only add a title if not appending! $Row = $StartRow $ws.Cells[$Row, $StartColumn].Value = $Title $ws.Cells[$Row, $StartColumn].Style.Font.Size = $TitleSize if ($TitleBold) { - #set title to Bold if -TitleBold was specified. + #Set title to Bold face font if -TitleBold was specified. #Otherwise the default will be unbolded. $ws.Cells[$Row, $StartColumn].Style.Font.Bold = $True } - #can only set TitleBackgroundColor if TitleFillPattern is something other than None + #Can only set TitleBackgroundColor if TitleFillPattern is something other than None. if ($TitleBackgroundColor -and ($TitleFillPattern -eq 'None')) { $TitleFillPattern = [OfficeOpenXml.Style.ExcelFillStyle]::Solid } @@ -599,9 +555,7 @@ } $Row ++ ; $startRow ++ } - else { - $Row = $StartRow - } + else { $Row = $StartRow } $ColumnIndex = $StartColumn $firstTimeThru = $true $isDataTypeValueType = $false @@ -640,7 +594,7 @@ $script:Header = $TargetData.PSObject.Properties.Name | Where-Object {$_ -notin $ExcludeProperty} if ($NoHeader) { - # Don't push the headers to the spread sheet + # Don't push the headers to the spreadsheet $Row -= 1 } else { @@ -720,7 +674,7 @@ $cec = $ws.Dimension.End.Column # was $script:Header.Count $targetRange = $ws.Cells[$csr, $csc, $cer, $cec] - #if we're appending data the table may already exist: + #if we're appending data the table may already exist. if ($ws.Tables[$TableName]) { $ws.Tables[$TableName].TableXml.table.ref = $targetRange.Address $ws.Tables[$TableName].TableStyle = $TableStyle @@ -737,7 +691,7 @@ $pivotTableDataName = $item.Key + 'PivotTableData' if ($item.Value.PivotFilter) {$PivotTableStartCell = "A3"} else { $PivotTableStartCell = "A1"} - #Make sure the Pivot table sheet doesn't already exist + #Make sure the Pivot table sheet doesn't already exist. #try { $pkg.Workbook.Worksheets.Delete( $pivotTableName) } catch {} [OfficeOpenXml.ExcelWorksheet]$wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber @@ -751,15 +705,17 @@ } #if it is a pivot for a named sheet and it doesn't exist, create it. elseif ($item.Value.SourceWorkSheet -and -not $wsPivot.PivotTables[$pivotTableDataName] ) { - $workSheet = $pkg.Workbook.Worksheets.where( {$_.name -match $item.Value.SourceWorkSheet})[0] #removed find worksheet + #find the worksheet + $workSheet = $pkg.Workbook.Worksheets.where( {$_.name -match $item.Value.SourceWorkSheet})[0] if (-not $workSheet) {Write-Warning -Message "Could not find Worksheet '$($item.Value.SourceWorkSheet)' specified in pivot-table definition $($item.key)." } else { - $targetDataRange = "{0}:{1}" -f $workSheet.Dimension.Start.Address, $workSheet.Dimension.End.Address + if ($item.Value.SourceRange) { $targetdataRange = $item.Value.SourceRange } + else { $targetDataRange = $workSheet.Dimension.Address} $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $workSheet.Cells[$targetDataRange], $pivotTableDataName) } } - #if we created the pivot table, set up the rows, columns and data if we didn't, put out a message 'existed' or 'error' + #if we created the pivot table, set up the rows, columns and data if we didn't, put out a message 'existed' or 'error' . if ($pivotTable) { foreach ($Row in $item.Value.PivotRows) { try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } @@ -1059,29 +1015,72 @@ } function New-PivotTableDefinition { +<# + .Synopsis + Creates Pivot table definitons for export excel + .Description + Export-Excel allows a single Pivot table to be defined using the parameters -IncludePivotTable, -PivotColumns -PivotRows, + =PivotData, -PivotFilter, -NoTotalsInPivot, -PivotDataToColumn, -IncludePivotChart and -ChartType. + Its -PivotTableDefintion paramater allows multiple pivot tables to be defined, with additional parameters. + New-PivotTableDefinition is a convenient way to build these definitions. + .Example + $pt = New-PivotTableDefinition -PivotTableName "PT1" -SourceWorkSheet "Sheet1" -PivotRows "Status" -PivotData @{Status='Count' } -PivotFilter 'StartType' -IncludePivotChart -ChartType BarClustered3D + $Pt += New-PivotTableDefinition -PivotTableName "PT2" -SourceWorkSheet "Sheet2" -PivotRows "Company" -PivotData @{Company='Count'} -IncludePivotChart -ChartType PieExploded3D -ShowPercent -ChartTitle "Breakdown of processes by company" + Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -AutoSize + Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' + $excel = Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show + + This is a re-work of one of the examples in Export-Excel - instead of writing out the pivot definition hash table it is built by calling New-PivotTableDefinition. +#> param( [Parameter(Mandatory)] [Alias("PivtoTableName")]#Previous typo - use alias to avoid breaking scripts $PivotTableName, + #Worksheet where the data is found $SourceWorkSheet, + #Address range in the worksheet e.g "A10:F20" - the first row must be column names: if not specified the whole sheet will be used/ + $SourceRange, + #Fields to set as rows in the Pivot table $PivotRows, + #A hash table in form "FieldName"="Function", where function is one of + #Average, Count, CountNums, Max, Min, Product, None, StdDev, StdDevP, Sum, Var, VarP [hashtable]$PivotData, + #Fields to set as columns in the Pivot table $PivotColumns, + #Fields to use to filter in the Pivot table $PivotFilter, + [Switch]$PivotDataToColumn, [Switch]$NoTotalsInPivot, + #If specified a chart Will be included. [Switch]$IncludePivotChart, + #Optional title for the pivot chart, by default the title omitted. [String]$ChartTitle, + #Height of the chart in Pixels (400 by default) [int]$ChartHeight = 400 , + #Width of the chart in Pixels (600 by default) [int]$ChartWidth = 600, + #Cell position of the top left corner of the chart, there will be this number of rows above the top edge of the chart (default is 0, chart starts at top edge of row 1). [Int]$ChartRow = 0 , - [Int]$ChartColumn = 6, + #Cell position of the top left corner of the chart, there will be this number of cells to the left of the chart (default is 4, chart starts at left edge of column E) + [Int]$ChartColumn = 4, + #Vertical offset of the chart from the cell corner. [Int]$ChartRowOffSetPixels = 0 , + #Horizontal offset of the chart from the cell corner. [Int]$ChartColumnOffSetPixels = 0, + #Type of chart [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', + #If specified hides the chart legend [Switch]$NoLegend, + #if specified attaches the category to slices in a pie chart : not supported on all chart types, this may give errors if applied to an unsupported type. [Switch]$ShowCategory, + #If specified attaches percentages to slices in a pie chart. [Switch]$ShowPercent ) + $validDataFuntions = [system.enum]::GetNames([OfficeOpenXml.Table.PivotTable.DataFieldFunctions]) + + if ($PivotData.values.Where({$_ -notin $validDataFuntions}) ) { + Write-Warning -Message ("Pivot data functions might not be valid, they should be one of " + ($validDataFuntions -join ", ") + ".") + } $parameters = @{} + $PSBoundParameters $parameters.Remove('PivotTableName') diff --git a/ImportExcel.psm1 b/ImportExcel.psm1 index 552c964..ee58bb4 100644 --- a/ImportExcel.psm1 +++ b/ImportExcel.psm1 @@ -20,6 +20,7 @@ . $PSScriptRoot\Import-Html.ps1 . $PSScriptRoot\InferData.ps1 . $PSScriptRoot\Invoke-Sum.ps1 + . $PSScriptRoot\Join-WorkSheet.ps1 . $PSScriptRoot\Merge-Worksheet.ps1 . $PSScriptRoot\New-ConditionalFormattingIconSet.ps1 . $PSScriptRoot\New-ConditionalText.ps1 @@ -53,7 +54,7 @@ Write-Warning 'PowerShell Excel is ready, except for that functionality' } #endregion -Function Import-Excel { +function Import-Excel { <# .SYNOPSIS Create custom objects from the rows in an Excel worksheet. @@ -94,6 +95,12 @@ Function Import-Excel { .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 StartColumn + The number of the first column to read data from (1 by default). + + .PARAMETER EndColumn + By default the import reads up to the last populated column, -EndColumn tells the import to stop at an earlier number. + .PARAMETER Password Accepts a string that will be used to open a password protected Excel file. @@ -359,7 +366,7 @@ Function Import-Excel { #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" + #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]) { @@ -416,38 +423,95 @@ Function Import-Excel { } function Add-WorkSheet { + [cmdletBinding()] + [OutputType([OfficeOpenXml.ExcelWorksheet])] 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 + #An object representing an Excel Package. + [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName = "Package", Position=0)] + [OfficeOpenXml.ExcelPackage]$ExcelPackage, + #An Excel workbook to which the Worksheet will be added - a package contains one workbook so you can use whichever fits at the time. + [Parameter(Mandatory=$true, ParameterSetName = "WorkBook")] + [OfficeOpenXml.ExcelWorkbook]$ExcelWorkbook, + #The name of the worksheet 'Sheet1' by default. + [string]$WorkSheetname = 'Sheet1', + #If the worksheet already exists, by default it will returned, unless -ClearSheet is specified in which case it will be deleted and re-created. + [switch]$ClearSheet, + #If specified, the worksheet will be moved to the start of the workbook. + #MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified. + [Switch]$MoveToStart, + #If specified, the worksheet will be moved to the end of the workbook. + #(This is the default position for newly created sheets, but this can be used to move existing sheets.) + [Switch]$MoveToEnd, + #If specified, the worksheet will be moved before the nominated one (which can be a postion starting from 1, or a name). + #MoveBefore takes precedence over MoveAfter if both are specified. + $MoveBefore , + # If specified, the worksheet will be moved after the nominated one (which can be a postion starting from 1, or a name or *). + # If * is used, the worksheet names will be examined starting with the first one, and the sheet placed after the last sheet which comes before it alphabetically. + $MoveAfter , + #If worksheet is provided as a copy source the new worksheet will be a copy of it. The source can be in the same workbook, or in a different file. + [OfficeOpenXml.ExcelWorksheet]$CopySource, + #Ignored but retained for backwards compatibility. + [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) - } + if ($ExcelPackage -and -not $ExcelWorkbook) {$ExcelWorkbook = $ExcelPackage.Workbook} + $ws = $ExcelWorkbook.Worksheets[$WorkSheetname] + if( $ws -and $ClearSheet) { $ExcelWorkbook.Worksheets.Delete($WorkSheetname) ; $ws = $null } + if(!$ws -and $CopySource) { + Write-Verbose -Message "Copying into worksheet '$WorkSheetname'." + $ws=$ExcelWorkbook.Worksheets.Add($WorkSheetname, $CopySource) + } + elseif(!$ws) { + Write-Verbose -Message "Adding worksheet '$WorkSheetname'." + $ws=$ExcelWorkbook.Worksheets.Add($WorkSheetname) + } + else {Write-Verbose -Message "Worksheet '$WorkSheetname' already existed."} + if ($MoveToStart) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName) } + elseif ($MoveToEnd ) {$ExcelWorkbook.Worksheets.MoveToEnd($worksheetName) } + elseif ($MoveBefore ) { + if ($ExcelWorkbook.Worksheets[$MoveBefore]) { + if ($MoveBefore -is [int]) { + $ExcelWorkbook.Worksheets.MoveBefore($ws.Index, $MoveBefore) + } + else {$ExcelWorkbook.Worksheets.MoveBefore($worksheetname, $MoveBefore)} + } + else {Write-Warning "Can't find worksheet '$MoveBefore'; worsheet '$WorkSheetname' will not be moved."} + } + elseif ($MoveAfter ) { + if ($MoveAfter = "*") { + if ($WorkSheetname -lt $ExcelWorkbook.Worksheets[1].Name) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName)} + else { + $i = 1 + While ($i -lt $ExcelWorkbook.Worksheets.Count -and $ExcelWorkbook.Worksheets[$i + 1].Name -lt $worksheetname ) { $i++} + $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $i) + } + } + elseif ($ExcelWorkbook.Worksheets[$MoveAfter]) { + if ($MoveAfter -is [int]) { + $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $MoveAfter) + } + else { + $ExcelWorkbook.Worksheets.MoveAfter($worksheetname, $MoveAfter) + } + } + else {Write-Warning "Can't find worksheet '$MoveAfter'; worsheet '$WorkSheetname' will not be moved."} + } return $ws } function ConvertFrom-ExcelSheet { <# - .Synopsis - Reads an Excel file an converts the data to a delimited text file + .Synopsis + Reads an Excel file an converts the data to a delimited text file. - .Example + .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 + Reads each sheet in TestSheets.xlsx and outputs it to the data directory as the sheet name with the extension .txt. - .Example + .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 + 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()] diff --git a/Join-Worksheet.ps1 b/Join-Worksheet.ps1 new file mode 100644 index 0000000..7c3656f --- /dev/null +++ b/Join-Worksheet.ps1 @@ -0,0 +1,164 @@ +function Join-Worksheet { + [CmdletBinding(DefaultParameterSetName = 'Default')] + <# + .SYNOPSIS + Combines data on all the sheets in an Excel worksheet onto a single sheet. + .DESCRIPTION + Join worksheet can work in two main ways: + Either Combining data which has the same layout from many pages into one, or combining pages which have nothing in common. + In the former case the header row is copied from the first sheet and, by default, each row of data is labelled with the name of the sheet it came from. + In the latter case -NoHeader is specified, and each copied block can have the sheet it came from placed above it as a title. + .EXAMPLE + foreach ($computerName in @('Server1', 'Server2', 'Server3', 'Server4')) { + Get-Service -ComputerName $computerName | Select-Object -Property Status, Name, DisplayName, StartType | + Export-Excel -Path .\test.xlsx -WorkSheetname $computerName -AutoSize + } + $ptDef =New-PivotTableDefinition -PivotTableName "Pivot1" -SourceWorkSheet "Combined" -PivotRows "Status" -PivotFilter "MachineName" -PivotData @{Status='Count'} -IncludePivotChart -ChartType BarClustered3D + Join-Worksheet -Path .\test.xlsx -WorkSheetName combined -FromLabel "MachineName" -HideSource -AutoSize -FreezeTopRow -BoldTopRow -PivotTableDefinition $pt -Show + + The foreach command gets the services running on four servers and exports each to its own page in Test.xlsx. + $PtDef= creates a defintion for a single Pivot table. + The Join-Worksheet command uses the same file and merges the results onto a sheet named "Combined". It sets a column header of "Machinename", + this column will contain the name of the sheet the data was copied from; after copying the data to the sheet "combined", the other sheets will be hidden. + Join-Worksheet finishes by calling export-Excel to AutoSize cells, freeze the top row and make it bold and add the Pivot table. + + .EXAMPLE + Get-WmiObject -Class win32_logicaldisk | select -Property DeviceId,VolumeName, Size,Freespace | + Export-Excel -Path "$env:computerName.xlsx" -WorkSheetname Volumes + Get-NetAdapter | Select-Object Name,InterfaceDescription,MacAddress,LinkSpeed | + Export-Excel -Path "$env:COMPUTERNAME.xlsx" -WorkSheetname NetAdapter + Join-Worksheet -Path "$env:COMPUTERNAME.xlsx" -WorkSheetName Summay -Title "Summary" -TitleBold -TitleSize 22 -NoHeader -LabelBlocks -AutoSize -HideSource + + The first two command get logical disk and network card information; each type is exported to its own sheet in a workbook. + The Join-worksheet command copies both onto a page named "Summary". Because the data is disimilar -NoHeader is specified, ensuring the whole of each page is copied. + Specifying -LabelBlocks causes each sheet's name to become a title on the summary page above the copied data. + The source data is hidden, a title is addded in 22 point boldface and the columns are sized to fit the data. + #> + param ( + # Path to a new or existing .XLSX file. + [Parameter(ParameterSetName = "Default", Position = 0)] + [String]$Path , + # An object representing an Excel Package - usually this is returned by specifying -Passthru allowing multiple commands to work on the same Workbook without saving and reloading each time. + [Parameter(Mandatory = $true, ParameterSetName = "Package")] + [OfficeOpenXml.ExcelPackage]$ExcelPackage, + # The name of a sheet within the workbook where the other sheets will be joined together - "Combined" by default. + $WorkSheetName = 'Combined', + # If specified any pre-existing target for the joined data will be deleted and re-created; otherwise data will be appended on this sheet. + [switch]$Clearsheet, + #Join-Worksheet assumes each sheet has identical headers and the headers should be copied to the target sheet, unless -NoHeader is specified. + [switch]$NoHeader, + #If -NoHeader is NOT specified, then rows of data will be labeled with the name of the sheet they came, FromLabel is the header for this column. If it is null or empty, the labels will be omitted. + $FromLabel = "From" , + #If specified, the copied blocks of data will have the name of the sheet they were copied from inserted above them as a title. + [switch]$LabelBlocks, + #Sizes the width of the Excel column to the maximum width needed to display all the containing data in that cell. + [Switch]$AutoSize, + #Freezes headers etc. in the top row. + [Switch]$FreezeTopRow, + #Freezes titles etc. in the left column. + [Switch]$FreezeFirstColumn, + #Freezes top row and left column (equivalent to Freeze pane 2,2 ). + [Switch]$FreezeTopRowFirstColumn, + # Freezes panes at specified coordinates (in the form RowNumber , ColumnNumber). + [Int[]]$FreezePane, + #Enables the 'Filter' in Excel on the complete header row. So users can easily sort, filter and/or search the data in the select column from within Excel. + [Switch]$AutoFilter, + #Makes the top Row boldface. + [Switch]$BoldTopRow, + #If Specified hides the sheets that the data is copied from. + [switch]$HideSource, + #Text of a title to be placed in Cell A1. + [String]$Title, + #Sets the fill pattern for the title cell. + [OfficeOpenXml.Style.ExcelFillStyle]$TitleFillPattern = 'None', + #Sets the cell background color for the title cell. + [System.Drawing.Color]$TitleBackgroundColor, + #Sets the title in boldface type. + [Switch]$TitleBold, + #Sets the point size for the title. + [Int]$TitleSize = 22, + # Hashtable(s) with Sheet PivotRows, PivotColumns, PivotData, IncludePivotChart and ChartType values to specify a definition for one or more pivot table(s). + [Hashtable]$PivotTableDefinition, + # A hashtable containing ChartType, Title, NoLegend, ShowCategory, ShowPercent, Yrange, Xrange and SeriesHeader for one or more [non-pivot] charts. + [Object[]]$ExcelChartDefinition, + #Opens the Excel file immediately after creation. Convenient for viewing the results instantly without having to search for the file first. + [switch]$Show, + #If specified, an object representing the unsaved Excel package will be returned, it then needs to be saved. + [switch]$PassThru + ) + #region get target worksheet, select it and move it to the end. + if ($Path -and -not $ExcelPackage) {$ExcelPackage = Open-ExcelPackage -path $Path } + $destinationSheet = Add-WorkSheet -ExcelPackage $ExcelPackage -WorkSheetname $WorkSheetName -ClearSheet:$Clearsheet + $destinationSheet.View.TabSelected = $true + $ExcelPackage.Workbook.Worksheets.MoveToEnd($WorkSheetName) + #row to insert at will be 1 on a blank sheet and lastrow + 1 on populated one + $row = (1 + $destinationSheet.Dimension.End.Row ) + #endregion + + #region Setup title and header rows + #Title parameters work as they do in Export-Excel . + if ($row -eq 1 -and $Title) { + $destinationSheet.Cells[1, 1].Value = $Title + $destinationSheet.Cells[1, 1].Style.Font.Size = $TitleSize + if ($TitleBold) {$destinationSheet.Cells[1, 1].Style.Font.Bold = $True } + #Can only set TitleBackgroundColor if TitleFillPattern is something other than None. + if ($TitleBackgroundColor -AND ($TitleFillPattern -ne 'None')) { + $destinationSheet.Cells[1, 1].Style.Fill.PatternType = $TitleFillPattern + $destinationSheet.Cells[1, 1].Style.Fill.BackgroundColor.SetColor($TitleBackgroundColor) + } + elseif ($TitleBackgroundColor) { Write-Warning "Title Background Color ignored. You must set the TitleFillPattern parameter to a value other than 'None'. Try 'Solid'." } + $row = 2 + } + + if (-not $noHeader) { + #Assume every row has titles in row 1, copy row 1 from first sheet to new sheet. + $destinationSheet.Select("A$row") + $ExcelPackage.Workbook.Worksheets[1].cells["1:1"].Copy($destinationSheet.SelectedRange) + if ($FromLabel ) { + #Add a column which says where the data comes from. + $fromColumn = ($destinationSheet.Dimension.Columns + 1) + $destinationSheet.Cells[$row, $fromColumn].Value = $FromLabel + } + $row += 1 + } + #endregion + + foreach ($i in 1..($ExcelPackage.Workbook.Worksheets.Count - 1) ) { + $sourceWorksheet = $ExcelPackage.Workbook.Worksheets[$i] + #Assume row one is titles, so data itself starts at A2. + if ($NoHeader) {$sourceRange = $sourceWorksheet.Dimension.Address} + else {$sourceRange = $sourceWorksheet.Dimension.Address -replace "A1:", "A2:"} + #Position insertion point/ + $destinationSheet.Select("A$row") + if ($LabelBlocks) { + $destinationSheet.Cells[$row, 1].value = $sourceWorksheet.Name + $destinationSheet.Cells[$row, 1].Style.Font.Bold = $true + $destinationSheet.Cells[$row, 1].Style.Font.Size += 2 + $row += 1 + } + $destinationSheet.Select("A$row") + + #And finally we're ready to copy the data. + $sourceWorksheet.Cells[$sourceRange].Copy($destinationSheet.SelectedRange) + #Fill in column saying where data came from. + if ($fromColumn) { $row..$destinationSheet.Dimension.Rows | ForEach-Object {$destinationSheet.Cells[$_, $fromColumn].Value = $sourceWorksheet.Name} } + #Update where next insertion will go. + $row = $destinationSheet.Dimension.Rows + 1 + if ($HideSource) {$sourceWorksheet.Hidden = [OfficeOpenXml.eWorkSheetHidden]::Hidden} + } + + #We accept a bunch of parameters work to pass on to Export-excel ( Autosize, Autofilter, boldtopRow Freeze ); if we have any of those call export-excel otherwise close the package here. + $params = @{} + $PSBoundParameters + 'Path', 'Clearsheet', 'NoHeader', 'FromLabel', 'LabelBlocks', 'HideSource', + 'Title', 'TitleFillPattern', 'TitleBackgroundColor', 'TitleBold', 'TitleSize' | ForEach-Object {[void]$params.Remove($_)} + if ($params.Keys.Count) { + $params.WorkSheetName = $WorkSheetName + $params.ExcelPackage = $ExcelPackage + Export-Excel @Params + } + else { + Close-ExcelPackage -ExcelPackage $ExcelPackage + $ExcelPackage.Dispose() + $ExcelPackage = $null + } +} \ No newline at end of file diff --git a/Merge-worksheet.ps1 b/Merge-worksheet.ps1 index 5f1da13..d4ba4c5 100644 --- a/Merge-worksheet.ps1 +++ b/Merge-worksheet.ps1 @@ -1,471 +1,476 @@ -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 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. - .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 changed size will be highlighed in the output sheet. Changes to dates or other attributes will be ignored - .Example - 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( - #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)] - [parameter(ParameterSetName='E',Position=3)] - $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 } - if ($DiffPrefix -eq "=>" ) { - $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 - } -} - -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. - [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 -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 - } - - #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") - 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) - 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 - $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) { - 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 - } -} +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 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. + .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 changed size will be highlighed in the output sheet. Changes to dates or other attributes will be ignored. + .Example + 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( + #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, + + #Object to compare if a worksheet is NOT being used. + [parameter(ParameterSetName='D',Mandatory=$true)] + [parameter(ParameterSetName='E',Mandatory=$true)] + [Alias('RefObject')] + $ReferenceObject , + #Object to compare if a worksheet is NOT being used. + [parameter(ParameterSetName='D',Mandatory=$true,Position=1)] + [Alias('DiffObject')] + $DifferenceObject , + [parameter(ParameterSetName='D',Position=2)] + [parameter(ParameterSetName='E',Position=3)] + $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 pipeline). + [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 } + if ($DiffPrefix -eq "=>" ) { + $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 + } +} + +Function Merge-MultipleSheets { + <# + .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 + Merge-MultipleSheets 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 + Merge-MultipleSheets then calls Merge-Worsheet with this result and sheet B, comparing 'Name', 'Displayname' and 'Start mode' columns on each side + which outputs _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, in this case "DisplayName" and "Start" are renamed to reflect their source, + so become "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 will not 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 if 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. + [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 -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 + } + + #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") + 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) + 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 + $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) { + 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 + } +} diff --git a/Open-ExcelPackage.ps1 b/Open-ExcelPackage.ps1 index 6789dfd..5aea5b6 100644 --- a/Open-ExcelPackage.ps1 +++ b/Open-ExcelPackage.ps1 @@ -1,50 +1,67 @@ -Function Open-ExcelPackage { -<# -.Synopsis - Returns an Excel Package Object with for the specified XLSX ile -.Example - $excel = Open-ExcelPackage -path $xlPath - $sheet1 = $excel.Workbook.Worksheets["sheet1"] - set-Format -Address $sheet1.Cells["E1:S1048576"], $sheet1.Cells["V1:V1048576"] -NFormat ([cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern) - close-ExcelPackage $excel -Show - - 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, - [switch]$KillExcel) - - if($KillExcel) { - Get-Process -Name "excel" -ErrorAction Ignore | Stop-Process - while (Get-Process -Name "excel" -ErrorAction Ignore) {} - } - - $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) - if (Test-Path $path) {New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Path } - Else {Write-Warning "Could not find $path" } - } - -Function Close-ExcelPackage { -<# -.Synopsis - Closes an Excel Package, saving, saving under a new name or abandoning changes and opening the file as required -#> - Param ( - #File to close - [parameter(Mandatory=$true, ValueFromPipeline=$true)] - [OfficeOpenXml.ExcelPackage]$ExcelPackage, - #Open the file - [switch]$Show, - #Abandon the file without saving - [Switch]$NoSave, - #Save file with a new name (ignored if -NoSaveSpecified) - $SaveAs - ) - if ( $NoSave) {$ExcelPackage.Dispose()} - else { - if ($SaveAs) {$ExcelPackage.SaveAs( $SaveAs ) } - Else {$ExcelPackage.Save(); $SaveAs = $ExcelPackage.File.FullName } - $ExcelPackage.Dispose() - if ($show) {Start-Process -FilePath $SaveAs } - } -} \ No newline at end of file +Function Open-ExcelPackage { +<# +.Synopsis + Returns an Excel Package Object with for the specified XLSX ile +.Example + $excel = Open-ExcelPackage -path $xlPath + $sheet1 = $excel.Workbook.Worksheets["sheet1"] + Set-Format -Address $sheet1.Cells["E1:S1048576"], $sheet1.Cells["V1:V1048576"] -NFormat ([cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern) + Close-ExcelPackage $excel -Show + + This will open the file at $xlPath, select sheet1 apply formatting to two blocks of the sheet and save the package, and launch it in Excel. +#> + [OutputType([OfficeOpenXml.ExcelPackage])] + Param ( + #The Path to the file to open + [Parameter(Mandatory=$true)]$Path, + #If specified, any running instances of Excel will be terminated before opening the file. + [switch]$KillExcel, + #By default open only opens an existing file; -Create instructs it to create a new file if required. + [switch]$Create + ) + + if($KillExcel) { + Get-Process -Name "excel" -ErrorAction Ignore | Stop-Process + while (Get-Process -Name "excel" -ErrorAction Ignore) {} + } + + $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + #If -Create was not specified only open the file if it exists already (send a warning if it doesn't exist). + if ($Create) { + #Create the directory if required. + $targetPath = Split-Path -Parent -Path $Path + if (!(Test-Path -Path $targetPath)) { + Write-Debug "Base path $($targetPath) does not exist, creating" + $null = New-item -ItemType Directory -Path $targetPath -ErrorAction Ignore + } + New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Path + } + elseif (Test-Path -Path $path) {New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Path } + else {Write-Warning "Could not find $path" } + } + +Function Close-ExcelPackage { +<# +.Synopsis + Closes an Excel Package, saving, saving under a new name or abandoning changes and opening the file in Excel as required. +#> + Param ( + #File to close. + [parameter(Mandatory=$true, ValueFromPipeline=$true)] + [OfficeOpenXml.ExcelPackage]$ExcelPackage, + #Open the file. + [switch]$Show, + #Abandon the file without saving. + [Switch]$NoSave, + #Save file with a new name (ignored if -NoSave Specified). + $SaveAs + ) + if ( $NoSave) {$ExcelPackage.Dispose()} + else { + if ($SaveAs) {$ExcelPackage.SaveAs( $SaveAs ) } + Else {$ExcelPackage.Save(); $SaveAs = $ExcelPackage.File.FullName } + $ExcelPackage.Dispose() + if ($show) {Start-Process -FilePath $SaveAs } + } +} + From 3508acb395dcb3c5694bcbcbc19a67359aa39bb0 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 22 Jun 2018 21:35:31 +0100 Subject: [PATCH 03/11] needed --- AddConditionalFormatting.ps1 | 232 +++++++++++++++++------------------ 1 file changed, 116 insertions(+), 116 deletions(-) diff --git a/AddConditionalFormatting.ps1 b/AddConditionalFormatting.ps1 index 2e7b4f7..2b54282 100644 --- a/AddConditionalFormatting.ps1 +++ b/AddConditionalFormatting.ps1 @@ -1,117 +1,117 @@ -Function Add-ConditionalFormatting { -<# - .Synopsis - Adds contitional formatting to worksheet. - .Example - $excel = $avdata | Export-Excel -Path (Join-path $FilePath "\Machines.XLSX" ) -WorksheetName "Server Anti-Virus" -AutoSize -FreezeTopRow -AutoFilter -PassThru - - Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "b2:b1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "2003" - Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "i2:i1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "Disabled" - $excel.Workbook.Worksheets[1].Cells["D1:G1048576"].Style.Numberformat.Format = [cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern - $excel.Workbook.Worksheets[1].Row(1).style.font.bold = $true - $excel.Save() ; $excel.Dispose() - - Here Export-Excel is called with the -passThru parameter so the Excel Package object is stored in $Excel - The desired worksheet is selected and the then columns B and i are conditially formatted (excluding the top row) to show red text if - the columns contain "2003" or "Disabled respectively. A fixed date formats are then applied to columns D..G, and the top row is formatted. - Finally the workbook is saved and the Excel object closed. - -#> - Param ( - #The worksheet where the format is to be applied - [Parameter(Mandatory = $true, ParameterSetName = "NamedRule")] - [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] - [OfficeOpenXml.ExcelWorksheet]$WorkSheet , - #The area of the worksheet where the format is to be applied - [Parameter(Mandatory = $true, ParameterSetName = "NamedRule")] - [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] - [OfficeOpenXml.ExcelAddress]$Range , - #One or more row(s), Column(s) and/or block(s) of cells to format - [Parameter(Mandatory = $true, ParameterSetName = "NamedRuleAddress")] - [Parameter(Mandatory = $true, ParameterSetName = "DataBarAddress")] - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSetAddress")] - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSetAddress")] - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSetAddress")] - $Address , - #One of the standard named rules - Top / Bottom / Less than / Greater than / Contains etc - [Parameter(Mandatory = $true, ParameterSetName = "NamedRule", Position = 3)] - [Parameter(Mandatory = $true, ParameterSetName = "NamedRuleAddress", Position = 3)] - [OfficeOpenXml.ConditionalFormatting.eExcelConditionalFormattingRuleType]$RuleType , - #Text colour for matching objects - [Alias("ForeGroundColour")] - [System.Drawing.Color]$ForeGroundColor, - #colour for databar type charts - [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] - [Parameter(Mandatory = $true, ParameterSetName = "DataBarAddress")] - [Alias("DataBarColour")] - [System.Drawing.Color]$DataBarColor, - #One of the three-icon set types (e.g. Traffic Lights) - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSetAddress")] - [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting3IconsSetType]$ThreeIconsSet, - #A four-icon set name - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FourIconSetAddress")] - [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting4IconsSetType]$FourIconsSet, - #A five-icon set name - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] - [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSetAddress")] - [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting5IconsSetType]$FiveIconsSet, - #A value for the condition (e.g. "2000" if the test is 'lessthan 2000') - [string]$ConditionValue, - #A second value for the conditions like between x and Y - [string]$ConditionValue2, - #Background colour for matching items - [System.Drawing.Color]$BackgroundColor, - #Background pattern for matching items - [OfficeOpenXml.Style.ExcelFillStyle]$BackgroundPattern = [OfficeOpenXml.Style.ExcelFillStyle]::None , - #Secondary colour when a background pattern requires it - [System.Drawing.Color]$PatternColor, - #Sets the numeric format for matching items - $NumberFormat, - #Put matching items in bold face - [switch]$Bold, - #Put matching items in italic - [switch]$Italic, - #Underline matching items - [switch]$Underline, - #Strikethrough text of matching items - [switch]$StrikeThru - ) - #Allow 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) { - $WorkSheet = $Address.Worksheet[0] - $Range = $Address.Address - } - 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) } - else { $rule = ($WorkSheet.ConditionalFormatting)."Add$RuleType"($Range)} - - if ($ConditionValue -and $RuleType -match "Top|Botom") {$rule.Rank = $ConditionValue } - if ($ConditionValue -and $RuleType -match "StdDev") {$rule.StdDev = $ConditionValue } - if ($ConditionValue -and $RuleType -match "Than|Equal|Expression") {$rule.Formula = $ConditionValue } - if ($ConditionValue -and $RuleType -match "Text|With") {$rule.Text = $ConditionValue } - if ($ConditionValue -and - $ConditionValue2 -and $RuleType -match "Between") { - $rule.Formula = $ConditionValue - $rule.Formula2 = $ConditionValue2 - } - - if ($NumberFormat) {$rule.Style.NumberFormat.Format = $NumberFormat } - if ($Underline) {$rule.Style.Font.Underline = [OfficeOpenXml.Style.ExcelUnderLineType]::Single } - if ($Bold) {$rule.Style.Font.Bold = $true} - if ($Italic) {$rule.Style.Font.Italic = $true} - if ($StrikeThru) {$rule.Style.Font.Strike = $true} - if ($ForeGroundColor) {$rule.Style.Font.Color.color = $ForeGroundColor } - if ($BackgroundColor) {$rule.Style.Fill.BackgroundColor.color = $BackgroundColor } - if ($BackgroundPattern) {$rule.Style.Fill.PatternType = $BackgroundPattern } - if ($PatternColor) {$rule.Style.Fill.PatternColor.color = $PatternColor } +Function Add-ConditionalFormatting { +<# + .Synopsis + Adds contitional formatting to worksheet. + .Example + $excel = $avdata | Export-Excel -Path (Join-path $FilePath "\Machines.XLSX" ) -WorksheetName "Server Anti-Virus" -AutoSize -FreezeTopRow -AutoFilter -PassThru + + Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "b2:b1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "2003" + Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "i2:i1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "Disabled" + $excel.Workbook.Worksheets[1].Cells["D1:G1048576"].Style.Numberformat.Format = [cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern + $excel.Workbook.Worksheets[1].Row(1).style.font.bold = $true + $excel.Save() ; $excel.Dispose() + + Here Export-Excel is called with the -passThru parameter so the Excel Package object is stored in $Excel + The desired worksheet is selected and the then columns B and i are conditially formatted (excluding the top row) to show red text if + the columns contain "2003" or "Disabled respectively. A fixed date formats are then applied to columns D..G, and the top row is formatted. + Finally the workbook is saved and the Excel object closed. + +#> + Param ( + #The worksheet where the format is to be applied + [Parameter(Mandatory = $true, ParameterSetName = "NamedRule")] + [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] + [OfficeOpenXml.ExcelWorksheet]$WorkSheet , + #The area of the worksheet where the format is to be applied + [Parameter(Mandatory = $true, ParameterSetName = "NamedRule")] + [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] + [OfficeOpenXml.ExcelAddress]$Range , + #One or more row(s), Column(s) and/or block(s) of cells to format + [Parameter(Mandatory = $true, ParameterSetName = "NamedRuleAddress")] + [Parameter(Mandatory = $true, ParameterSetName = "DataBarAddress")] + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSetAddress")] + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSetAddress")] + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSetAddress")] + $Address , + #One of the standard named rules - Top / Bottom / Less than / Greater than / Contains etc + [Parameter(Mandatory = $true, ParameterSetName = "NamedRule", Position = 3)] + [Parameter(Mandatory = $true, ParameterSetName = "NamedRuleAddress", Position = 3)] + [OfficeOpenXml.ConditionalFormatting.eExcelConditionalFormattingRuleType]$RuleType , + #Text colour for matching objects + [Alias("ForeGroundColour")] + [System.Drawing.Color]$ForeGroundColor, + #colour for databar type charts + [Parameter(Mandatory = $true, ParameterSetName = "DataBar")] + [Parameter(Mandatory = $true, ParameterSetName = "DataBarAddress")] + [Alias("DataBarColour")] + [System.Drawing.Color]$DataBarColor, + #One of the three-icon set types (e.g. Traffic Lights) + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "ThreeIconSetAddress")] + [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting3IconsSetType]$ThreeIconsSet, + #A four-icon set name + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FourIconSetAddress")] + [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting4IconsSetType]$FourIconsSet, + #A five-icon set name + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSet")] + [Parameter(Mandatory = $true, ParameterSetName = "FiveIconSetAddress")] + [OfficeOpenXml.ConditionalFormatting.eExcelconditionalFormatting5IconsSetType]$FiveIconsSet, + #A value for the condition (e.g. "2000" if the test is 'lessthan 2000') + [string]$ConditionValue, + #A second value for the conditions like between x and Y + [string]$ConditionValue2, + #Background colour for matching items + [System.Drawing.Color]$BackgroundColor, + #Background pattern for matching items + [OfficeOpenXml.Style.ExcelFillStyle]$BackgroundPattern = [OfficeOpenXml.Style.ExcelFillStyle]::None , + #Secondary colour when a background pattern requires it + [System.Drawing.Color]$PatternColor, + #Sets the numeric format for matching items + $NumberFormat, + #Put matching items in bold face + [switch]$Bold, + #Put matching items in italic + [switch]$Italic, + #Underline matching items + [switch]$Underline, + #Strikethrough text of matching items + [switch]$StrikeThru + ) + #Allow 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) { + $WorkSheet = $Address.Worksheet[0] + $Range = $Address.Address + } + 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) } + else { $rule = ($WorkSheet.ConditionalFormatting)."Add$RuleType"($Range)} + + if ($ConditionValue -and $RuleType -match "Top|Botom") {$rule.Rank = $ConditionValue } + if ($ConditionValue -and $RuleType -match "StdDev") {$rule.StdDev = $ConditionValue } + if ($ConditionValue -and $RuleType -match "Than|Equal|Expression") {$rule.Formula = $ConditionValue } + if ($ConditionValue -and $RuleType -match "Text|With") {$rule.Text = $ConditionValue } + if ($ConditionValue -and + $ConditionValue2 -and $RuleType -match "Between") { + $rule.Formula = $ConditionValue + $rule.Formula2 = $ConditionValue2 + } + + if ($NumberFormat) {$rule.Style.NumberFormat.Format = $NumberFormat } + if ($Underline) {$rule.Style.Font.Underline = [OfficeOpenXml.Style.ExcelUnderLineType]::Single } + if ($Bold) {$rule.Style.Font.Bold = $true} + if ($Italic) {$rule.Style.Font.Italic = $true} + if ($StrikeThru) {$rule.Style.Font.Strike = $true} + if ($ForeGroundColor) {$rule.Style.Font.Color.color = $ForeGroundColor } + if ($BackgroundColor) {$rule.Style.Fill.BackgroundColor.color = $BackgroundColor } + if ($BackgroundPattern) {$rule.Style.Fill.PatternType = $BackgroundPattern } + if ($PatternColor) {$rule.Style.Fill.PatternColor.color = $PatternColor } } \ No newline at end of file From f689c84d045ccde6f6a1ec7c5c51309f8e1addee Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 22 Jun 2018 21:36:46 +0100 Subject: [PATCH 04/11] needed to sync a --- Compare-WorkSheet.ps1 | 501 +++++----- Export-Excel.ps1 | 2178 ++++++++++++++++++++--------------------- ImportExcel.psm1 | 1184 +++++++++++----------- Join-Worksheet.ps1 | 326 +++--- Merge-worksheet.ps1 | 952 +++++++++--------- Open-ExcelPackage.ps1 | 134 +-- 6 files changed, 2638 insertions(+), 2637 deletions(-) diff --git a/Compare-WorkSheet.ps1 b/Compare-WorkSheet.ps1 index f20eb35..e39a898 100644 --- a/Compare-WorkSheet.ps1 +++ b/Compare-WorkSheet.ps1 @@ -1,251 +1,252 @@ -Function Compare-Worksheet { -<# - .Synopsis - Compares two worksheets (usually with the same name in different files). - .Description - This command takes two file names, one or two worksheet name(s) and a name for a 'key' column. - It reads the worksheets and determines which column will be compared and builds a hashtable of the values in 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 the column names it selected). - 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 to the result. - If -BackgroundColor is specified the difference rows in the source spreadsheet will have their background changed to identify the different rows. - .Example - 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 to register a differnce if if the software was installed on a - different date or from a different place, so specify -ExcludeProperty Install* removes InstallDate and InstallSource from the comparison. - This data doesn't have a "name" column" so we specify the "IdentifyingNumber" column to be the key. - PowerShell will output the differences formatted as a table. - .Example - 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. - 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 '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), - the command 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 the command specifies a limited set of columns should be compared. - .Example - 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 for CPU, Memory, Domain, Disk and so on. - So the command is instructed to start at row 2 (skipping the title) and to name the columns: the first is "label" and the second "Value" with label acting as the key. - This time we are interested only in 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 highlights all the cells in lightgray and then sets the changed rows back to white so that - only the unchanged rows are highlighted -#> - [cmdletbinding(DefaultParameterSetName)] - Param( - #First file to compare. - [parameter(Mandatory=$true,Position=0)] - $Referencefile , - #Second file to compare. - [parameter(Mandatory=$true,Position=1)] - $Differencefile , - #Name(s) of worksheets to compare. - $WorkSheetName = "Sheet1", - #Properties to include in the DIFF - supports wildcards, default is "*". - $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 Start row . - [Parameter(ParameterSetName='B', Mandatory)] - [String[]]$Headername, - #Automatically generate property names (P1, P2, P3, ..) instead of the using the values the Start row of the sheet. - [Parameter(ParameterSetName='C', Mandatory)] - [switch]$NoHeader, - #The row from where we start to import data, all rows above the Start row are disregarded. By default this is row 1. - [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 (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 - ) - - #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} - 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 { - $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 } - - #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 = @{} ; $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"} - $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} - - #Add RowNumber, Sheetname and file name to every row. - $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 = $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} - - 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 . - [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. - 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) { - 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] - $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 ($diff -and $FontColor -and ($propList -contains $Key) ) { - $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} - 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()} - } - } - 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 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 ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" } - } - 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 ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} - else {$hash[($result.SideIndicator+$P)] =$result.$P} - } - } - [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 ($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 } +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 '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 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 "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -GridView + This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel + 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 '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 the command specifies a limited set of columns should be used. + .Example + 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, + 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,Position=0)] + $Referencefile , + #Second file to compare + [parameter(Mandatory=$true,Position=1)] + $Differencefile , + #Name(s) of worksheets to compare. + $WorkSheetName = "Sheet1", + #Properties to include in the DIFF - supports wildcards, default is "*" + $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 = 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 (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 + ) + + #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} + 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 { + $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 } + + #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 = @{} ; $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"} + $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} + + #Add RowNumber, Sheetname and file name to every row + $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 = $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} + + 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 + [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 + 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) { + 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] + $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 ($diff -and $FontColor -and ($propList -contains $Key) ) { + $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} + 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()} + } + } + 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 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 ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" } + } + 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 ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} + else {$hash[($result.SideIndicator+$P)] =$result.$P} + } + } + [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 ($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 diff --git a/Export-Excel.ps1 b/Export-Excel.ps1 index 0e78e71..d50204a 100644 --- a/Export-Excel.ps1 +++ b/Export-Excel.ps1 @@ -1,1089 +1,1089 @@ -function Export-Excel { - <# - .SYNOPSIS - Export data to an Excel worksheet. - .DESCRIPTION - Export data to an Excel file and where possible try to convert numbers so Excel recognizes them as numbers instead of text. After all. Excel is a spreadsheet program used for number manipulation and calculations. In case the number conversion is not desired, use the parameter '-NoNumberConversion *'. - .PARAMETER Path - Path to a new or existing .XLSX file. - .PARAMETER ExcelPackage - An object representing an Excel Package - usually this is returned by specifying -Passthru allowing multiple commands to work on the same Workbook without saving and reloading each time. - .PARAMETER WorkSheetName - The name of a sheet within the workbook - "Sheet1" by default. - .PARAMETER ClearSheet - If specified Export-Excel will remove any existing worksheet with the selected name. The Default behaviour is to overwrite cells in this sheet as needed (but leaving non-overwritten ones in place). - .PARAMETER Append - If specified data will be added to the end of an existing sheet, using the same column headings. - .PARAMETER TargetData - Data to insert onto the worksheet - this is often provided from the pipeline. - .PARAMETER ExcludeProperty - 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 - Sets the title in boldface type. - .PARAMETER TitleSize - Sets the point size for the title. - .PARAMETER TitleBackgroundColor - Sets the cell background color for the title cell. - .PARAMETER TitleFillPattern - Sets the fill pattern for the title cell. - .PARAMETER Password - Sets password protection on the workbook. - .PARAMETER IncludePivotTable - Adds a Pivot table using the data in the worksheet. - .PARAMETER PivotRows - 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 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, - HashTable(s) with Sheet PivotTows, PivotColumns, PivotData, IncludePivotChart and ChartType values to make it easier to specify a definition or multiple Pivots. - .PARAMETER IncludePivotChart, - Include a chart with the Pivot table - implies Include Pivot Table. - .PARAMETER NoLegend - Exclude the legend from the pivot chart. - .PARAMETER ShowCategory - Add category labels to the pivot chart. - .PARAMETER ShowPercent - Add Percentage labels to the pivot chart. - .PARAMETER ConditionalText - Applies a 'Conditional formatting rule' in Excel on all the cells. When specific conditions are met a rule is triggered. - .PARAMETER NoNumberConversion - By default we convert all values to numbers if possible, but this isn't always desirable. NoNumberConversion allows you to add exceptions for the conversion. Wildcards (like '*') are allowed. - .PARAMETER BoldTopRow - Makes the top Row boldface. - .PARAMETER NoHeader - Does not put field names at the top of columns. - .PARAMETER RangeName - Makes the data in the worksheet a named range. - .PARAMETER TableName - Makes the data in the worksheet a table with a name applies a style to it. Name must not contain spaces. - .PARAMETER TableStyle - Selects the style for the named table - defaults to 'Medium6'. - .PARAMETER ExcelChartDefinition - 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 MoveToStart - If specified, the worksheet will be moved to the start of the workbook. - MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified. - .PARAMETER MoveToEnd - If specified, the worksheet will be moved to the end of the workbook. - (This is the default position for newly created sheets, but this can be used to move existing sheets.) - .PARAMETER MoveBefore - If specified, the worksheet will be moved before the nominated one (which can be a postion starting from 1, or a name). - MoveBefore takes precedence over MoveAfter if both are specified. - .PARAMETER MoveAfter - If specified, the worksheet will be moved after the nominated one (which can be a postion starting from 1, or a name or *). - If * is used, the worksheet names will be examined starting with the first one, and the sheet placed after the last sheet which comes before it alphabetically. - .PARAMETER KillExcel - Closes Excel - prevents errors writing to the file because Excel has it open. - .PARAMETER AutoNameRange - 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 - Column to start adding data - 1 by default. - .PARAMETER FreezeTopRow - Freezes headers etc. in the top row. - .PARAMETER FreezeFirstColumn - Freezes titles etc. in the left column. - .PARAMETER FreezeTopRowFirstColumn - Freezes top row and left column (equivalent to Freeze pane 2,2 ). - .PARAMETER FreezePane - Freezes panes at specified coordinates (in the form RowNumber , ColumnNumber). - .PARAMETER AutoFilter - Enables the 'Filter' in Excel on the complete header row. So users can easily sort, filter and/or search the data in the select column from within Excel. - .PARAMETER AutoSize - Sizes the width of the Excel column to the maximum width needed to display all the containing data in that cell. - .PARAMETER Now - The 'Now' switch is a shortcut that creates automatically a temporary file, enables 'AutoSize', 'AutoFiler' and 'Show', and opens the file immediately. - .PARAMETER NumberFormat - Formats all values that can be converted to a number to the format specified. - - Examples: - # integer (not really needed unless you need to round numbers, Excel will use default cell properties). - '0' - - # integer without displaying the number 0 in the cell. - '#' - - # number with 1 decimal place. - '0.0' - - # number with 2 decimal places. - '0.00' - - # number with 2 decimal places and thousand separator. - '#,##0.00' - - # number with 2 decimal places and thousand separator and money symbol. - '€#,##0.00' - - # percentage (1 = 100%, 0.01 = 1%) - '0%' - - # Blue color for positive numbers and a red color for negative numbers. All numbers will be proceeded by a dollar sign '$'. - '[Blue]$#,##0.00;[Red]-$#,##0.00' - - .PARAMETER Show - Opens the Excel file immediately after creation. Convenient for viewing the results instantly without having to search for the file first. - .PARAMETER PassThru - If specified, Export-Excel returns an object representing the Excel package without saving the package first. To save it you need to call the save or Saveas method or send it back to Export-Excel. - - .EXAMPLE - Get-Process | Export-Excel .\Test.xlsx -show - Export all the processes to the Excel file 'Test.xlsx' and open the file immediately. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - Write-Output -1 668 34 777 860 -0.5 119 -0.1 234 788 | - Export-Excel @ExcelParams -NumberFormat '[Blue]$#,##0.00;[Red]-$#,##0.00' - - Exports all data to the Excel file 'Excel.xslx' and colors the negative values in 'Red' and the positive values in 'Blue'. It will also add a dollar sign '$' in front of the rounded numbers to two decimal characters behind the comma. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - [PSCustOmobject][Ordered]@{ - Date = Get-Date - Formula1 = '=SUM(F2:G2)' - String1 = 'My String' - String2 = 'a' - IPAddress = '10.10.25.5' - Number1 = '07670' - Number2 = '0,26' - Number3 = '1.555,83' - Number4 = '1.2' - Number5 = '-31' - PhoneNr1 = '+32 44' - PhoneNr2 = '+32 4 4444 444' - PhoneNr3 = '+3244444444' - } | Export-Excel @ExcelParams -NoNumberConversion IPAddress, Number1 - - Exports all data to the Excel file 'Excel.xslx' and tries to convert all values to numbers where possible except for 'IPAddress' and 'Number1'. These are stored in the sheet 'as is', without being converted to a number. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - [PSCustOmobject][Ordered]@{ - Date = Get-Date - Formula1 = '=SUM(F2:G2)' - String1 = 'My String' - String2 = 'a' - IPAddress = '10.10.25.5' - Number1 = '07670' - Number2 = '0,26' - Number3 = '1.555,83' - Number4 = '1.2' - Number5 = '-31' - PhoneNr1 = '+32 44' - PhoneNr2 = '+32 4 4444 444' - PhoneNr3 = '+3244444444' - } | Export-Excel @ExcelParams -NoNumberConversion * - - Exports all data to the Excel file 'Excel.xslx' as is, no number conversion will take place. This means that Excel will show the exact same data that you handed over to the 'Export-Excel' function. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - Write-Output 489 668 299 777 860 151 119 497 234 788 | - Export-Excel @ExcelParams -ConditionalText $( - New-ConditionalText -ConditionalType GreaterThan 525 -ConditionalTextColor DarkRed -BackgroundColor LightPink - ) - - Exports data that will have a 'Conditional formatting rule' in Excel on these cells that will show the background fill color in 'LightPink' and the text color in 'DarkRed' when the value is greater then '525'. In case this condition is not met the color will be the default, black text on a white background. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - Get-Service | Select Name, Status, DisplayName, ServiceName | - Export-Excel @ExcelParams -ConditionalText $( - New-ConditionalText Stop DarkRed LightPink - New-ConditionalText Running Blue Cyan - ) - - Export all services to an Excel sheet where all cells have a 'Conditional formatting rule' in Excel that will show the background fill color in 'LightPink' and the text color in 'DarkRed' when the value contains the word 'Stop'. If the value contains the word 'Running' it will have a background fill color in 'Cyan' and a text color 'Blue'. In case none of these conditions are met the color will be the default, black text on a white background. - - .EXAMPLE - $ExcelParams = @{ - Path = $env:TEMP + '\Excel.xlsx' - Show = $true - Verbose = $true - } - Remove-Item -Path $ExcelParams.Path -Force -EA Ignore - - $Array = @() - - $Obj1 = [PSCustomObject]@{ - Member1 = 'First' - Member2 = 'Second' - } - - $Obj2 = [PSCustomObject]@{ - Member1 = 'First' - Member2 = 'Second' - Member3 = 'Third' - } - - $Obj3 = [PSCustomObject]@{ - Member1 = 'First' - Member2 = 'Second' - Member3 = 'Third' - Member4 = 'Fourth' - } - - $Array = $Obj1, $Obj2, $Obj3 - $Array | Out-GridView -Title 'Not showing Member3 and Member4' - $Array | Update-FirstObjectProperties | Export-Excel @ExcelParams -WorkSheetname Numbers - - Updates the first object of the array by adding property 'Member3' and 'Member4'. Afterwards. all objects are exported to an Excel file and all column headers are visible. - - .EXAMPLE - Get-Process | Export-Excel .\test.xlsx -WorkSheetname Processes -IncludePivotTable -Show -PivotRows Company -PivotData PM - - .EXAMPLE - Get-Process | Export-Excel .\test.xlsx -WorkSheetname Processes -ChartType PieExploded3D -IncludePivotChart -IncludePivotTable -Show -PivotRows Company -PivotData PM - - .EXAMPLE - Get-Service | Export-Excel 'c:\temp\test.xlsx' -Show -IncludePivotTable -PivotRows status -PivotData @{status='count'} - - .EXAMPLE - $pt = [ordered]@{} - $pt.pt1=@{ SourceWorkSheet = 'Sheet1'; - PivotRows = 'Status' - PivotData = @{'Status'='count'} - IncludePivotChart = $true - ChartType = 'BarClustered3D' - } - $pt.pt2=@{ SourceWorkSheet = 'Sheet2'; - PivotRows = 'Company' - PivotData = @{'Company'='count'} - IncludePivotChart = $true - ChartType = 'PieExploded3D' - } - Remove-Item -Path .\test.xlsx - Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -AutoSize - Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' - Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show - - This example defines two pivot tables. Then it puts Service data on Sheet1 with one call to Export-Excel and Process Data on sheet2 with a second call to Export-Excel. - The thrid and final call adds the two pivot tables and opens the spreadsheet in Excel. - - - .EXAMPLE - Remove-Item -Path .\test.xlsx - $excel = Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -PassThru - $excel.Workbook.Worksheets["Sheet1"].Row(1).style.font.bold = $true - $excel.Workbook.Worksheets["Sheet1"].Column(3 ).width = 29 - $excel.Workbook.Worksheets["Sheet1"].Column(3 ).Style.wraptext = $true - $excel.Save() - $excel.Dispose() - Start-Process .\test.xlsx - - This example uses -passthrough - put service information into sheet1 of the work book and saves the excelPackageObject in $Excel. - It then uses the package object to apply formatting, and then saves the workbook and disposes of the object before loading the document in Excel. - - .EXAMPLE - $excel = Get-Process | Select-Object -Property Name,Company,Handles,CPU,PM,NPM,WS | Export-Excel -Path .\test.xlsx -ClearSheet -WorkSheetname "Processes" -PassThru - $sheet = $excel.Workbook.Worksheets["Processes"] - $sheet.Column(1) | Set-Format -Bold -AutoFit - $sheet.Column(2) | Set-Format -Width 29 -WrapText - $sheet.Column(3) | Set-Format -HorizontalAlignment Right -NFormat "#,###" - Set-Format -Address $sheet.Cells["E1:H1048576"] -HorizontalAlignment Right -NFormat "#,###" - Set-Format -Address $sheet.Column(4) -HorizontalAlignment Right -NFormat "#,##0.0" -Bold - Set-Format -Address $sheet.Row(1) -Bold -HorizontalAlignment Center - Add-ConditionalFormatting -WorkSheet $sheet -Range "D2:D1048576" -DataBarColor Red - Add-ConditionalFormatting -WorkSheet $sheet -Range "G2:G1048576" -RuleType GreaterThan -ConditionValue "104857600" -ForeGroundColor Red - foreach ($c in 5..9) {Set-Format $sheet.Column($c) -AutoFit } - Export-Excel -ExcelPackage $excel -WorkSheetname "Processes" -IncludePivotChart -ChartType ColumnClustered -NoLegend -PivotRows company -PivotData @{'Name'='Count'} -Show - - This a more sophisticated version of the previous example showing different ways of using Set-Format, and also adding conditional formatting. - In the final command a Pivot chart is added and the workbook is opened in Excel. - - .LINK - https://github.com/dfinke/ImportExcel - #> - - [CmdletBinding(DefaultParameterSetName = 'Default')] - Param( - [Parameter(ParameterSetName = "Default", Position = 0)] - [Parameter(ParameterSetName = "Table" , Position = 0)] - [String]$Path, - [Parameter(Mandatory = $true, ParameterSetName = "PackageDefault")] - [Parameter(Mandatory = $true, ParameterSetName = "PackageTable")] - [OfficeOpenXml.ExcelPackage]$ExcelPackage, - [Parameter(ValueFromPipeline = $true)] - $TargetData, - [Switch]$Show, - [String]$WorkSheetname = 'Sheet1', - [String]$Password, - [switch]$ClearSheet, - [switch]$Append, - [String]$Title, - [OfficeOpenXml.Style.ExcelFillStyle]$TitleFillPattern = 'None', - [Switch]$TitleBold, - [Int]$TitleSize = 22, - [System.Drawing.Color]$TitleBackgroundColor, - [Switch]$IncludePivotTable, - [String[]]$PivotRows, - [String[]]$PivotColumns, - $PivotData, - [String[]]$PivotFilter, - [Switch]$PivotDataToColumn, - [Hashtable]$PivotTableDefinition, - [Switch]$IncludePivotChart, - [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', - [Switch]$NoLegend, - [Switch]$ShowCategory, - [Switch]$ShowPercent, - [Switch]$AutoSize, - [Switch]$NoClobber, - [Switch]$FreezeTopRow, - [Switch]$FreezeFirstColumn, - [Switch]$FreezeTopRowFirstColumn, - [Int[]]$FreezePane, - [Parameter(ParameterSetName = 'Default')] - [Parameter(ParameterSetName = 'PackageDefault')] - [Switch]$AutoFilter, - [Switch]$BoldTopRow, - [Switch]$NoHeader, - [String]$RangeName, - [ValidateScript( { - if ($_.Contains(' ')) { - throw 'Tablename has spaces.' - } - elseif (-not $_) { - throw 'Tablename is null or empty.' - } - elseif ($_[0] -notmatch '[a-z]') { - throw 'Tablename starts with an invalid character.' - } - else { - $true - } - })] - [Parameter(ParameterSetName = 'Table' , Mandatory = $true)] - [Parameter(ParameterSetName = 'PackageTable' , Mandatory = $true)] - [String]$TableName, - [Parameter(ParameterSetName = 'Table')] - [Parameter(ParameterSetName = 'PackageTable')] - [OfficeOpenXml.Table.TableStyles]$TableStyle = 'Medium6', - [Object[]]$ExcelChartDefinition, - [String[]]$HideSheet, - [Switch]$MoveToStart, - [Switch]$MoveToEnd, - $MoveBefore , - $MoveAfter , - [Switch]$KillExcel, - [Switch]$AutoNameRange, - [Int]$StartRow = 1, - [Int]$StartColumn = 1, - [Switch]$PassThru, - [String]$Numberformat = 'General', - [string[]]$ExcludeProperty, - [String[]]$NoNumberConversion, - [Object[]]$ConditionalFormat, - [Object[]]$ConditionalText, - [ScriptBlock]$CellStyleSB, - [Parameter(ParameterSetName = 'Now')] - # [Parameter(ParameterSetName = 'TableNow')] - [Switch]$Now, - [Switch]$ReturnRange, - [Switch]$NoTotalsInPivot, - [Switch]$ReZip - ) - - Begin { - function Add-CellValue { - <# - .SYNOPSIS - Save a value in an Excel cell. - - .DESCRIPTION - DateTime objects are always converted to a short DateTime format in Excel. When Excel loads the file, - it applies the local format for dates. And formulas are always saved as formulas. URIs are set as hyperlinks in the file. - - Numerical values will be converted to numbers as defined in the regional settings of the local - system. In case the parameter 'NoNumberConversion' is used, we don't convert to number and leave - the value 'as is'. In case of conversion failure, we also leave the value 'as is'. - #> - - Param ( - [Object]$TargetCell, - [Object]$CellValue - ) - - Switch ($CellValue) { - {($_ -is [String]) -and ($_.StartsWith('='))} { - #region Save an Excel formula - $TargetCell.Formula = $_ - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$_' as formula" - break - #endregion - } - { $_ -is [URI] } { - #region Save a hyperlink - $TargetCell.Value = $_.AbsoluteUri - $TargetCell.HyperLink = $_ - $TargetCell.Style.Font.Color.SetColor([System.Drawing.Color]::Blue) - $TargetCell.Style.Font.UnderLine = $true - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($_.AbsoluteUri)' as Hyperlink" - break - #endregion - } - { $_ -is [DateTime]} { - #region Save a date with an international valid format - $TargetCell.Value = $_ - $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 - } - - {(($NoNumberConversion) -and ($NoNumberConversion -contains $Name)) -or - ($NoNumberConversion -eq '*')} { - #region Save a value without converting to number - $TargetCell.Value = $_ - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' unconverted" - break - #endregion - } - - Default { - #region Save a value as a number if possible - $number = $null - if ([Double]::TryParse([String]$_, [System.Globalization.NumberStyles]::Any, - [System.Globalization.NumberFormatInfo]::CurrentInfo, [Ref]$number)) { - $TargetCell.Value = $number - $targetCell.Style.Numberformat.Format = $Numberformat - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as number converted from '$_' with format '$Numberformat'" - } - else { - $TargetCell.Value = $_ - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as string" - } - break - #endregion - } - } - } - - Try { - $script:Header = $null - if ($Append -and $ClearSheet) {throw "You can't use -Append AND -ClearSheet."} - - if ($PSBoundParameters.Keys.Count -eq 0 -Or $Now) { - $Path = [System.IO.Path]::GetTempFileName() -replace '\.tmp', '.xlsx' - $Show = $true - $AutoSize = $true - if (!$TableName) { - $AutoFilter = $true - } - } - - if ($ExcelPackage) { - $pkg = $ExcelPackage - $Path = $pkg.File - } - Else { $pkg = Open-ExcelPackage -Path $Path -Create -KillExcel:$KillExcel} - - $params = @{} - if ($NoClobber) {Write-Warning -Message "-NoClobber parameter is no longer used" } - foreach ($p in @("WorkSheetname","ClearSheet","MoveToStart","MoveToEnd","MoveBefore","MoveAfter")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} - $ws = $pkg | Add-WorkSheet @params - - $ws.View.TabSelected = $true - foreach ($format in $ConditionalFormat ) { - $target = "Add$($format.Formatter)" - $rule = ($ws.ConditionalFormatting).PSObject.Methods[$target].Invoke($format.Range, $format.IconType) - $rule.Reverse = $format.Reverse - } - - if ($append -and $ws.Dimension) { - $headerRange = $ws.Dimension.Address -replace "\d+$", "1" - #if there is a title or anything else above the header row, specifying StartRow will skip it. - if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} - #$script:Header = $ws.Cells[$headerrange].Value - #using a slightly odd syntax otherwise header ends up as a 2D array - $ws.Cells[$headerRange].Value | ForEach-Object -Begin {$Script:header = @()} -Process {$Script:header += $_ } - $row = $ws.Dimension.Rows - Write-Debug -Message ("Appending: headers are " + ($script:Header -join ", ") + "Start row $row") - } - elseif ($Title) { - #Can only add a title if not appending! - $Row = $StartRow - $ws.Cells[$Row, $StartColumn].Value = $Title - $ws.Cells[$Row, $StartColumn].Style.Font.Size = $TitleSize - - if ($TitleBold) { - #Set title to Bold face font if -TitleBold was specified. - #Otherwise the default will be unbolded. - $ws.Cells[$Row, $StartColumn].Style.Font.Bold = $True - } - #Can only set TitleBackgroundColor if TitleFillPattern is something other than None. - if ($TitleBackgroundColor -and ($TitleFillPattern -eq 'None')) { - $TitleFillPattern = [OfficeOpenXml.Style.ExcelFillStyle]::Solid - } - $ws.Cells[$Row, $StartColumn].Style.Fill.PatternType = $TitleFillPattern - - if ($TitleBackgroundColor ) { - $ws.Cells[$Row, $StartColumn].Style.Fill.BackgroundColor.SetColor($TitleBackgroundColor) - } - $Row ++ ; $startRow ++ - } - else { $Row = $StartRow } - $ColumnIndex = $StartColumn - $firstTimeThru = $true - $isDataTypeValueType = $false - } - Catch { - if ($AlreadyExists) { - #Is this set anywhere ? - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': The worksheet '$WorkSheetname' already exists." - } - else { - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" - } - } - } - - Process { - if ($TargetData) { - Try { - if ($firstTimeThru) { - $firstTimeThru = $false - $isDataTypeValueType = $TargetData.GetType().name -match 'string|bool|byte|char|decimal|double|float|int|long|sbyte|short|uint|ulong|ushort' - Write-Debug "DataTypeName is '$($TargetData.GetType().name)' isDataTypeValueType '$isDataTypeValueType'" - } - - if ($isDataTypeValueType) { - $ColumnIndex = $StartColumn - - Add-CellValue -TargetCell $ws.Cells[$Row, $ColumnIndex] -CellValue $TargetData - - $Row += 1 - } - else { - #region Add headers - if (-not $script:Header) { - $ColumnIndex = $StartColumn - $script:Header = $TargetData.PSObject.Properties.Name | Where-Object {$_ -notin $ExcludeProperty} - - if ($NoHeader) { - # Don't push the headers to the spreadsheet - $Row -= 1 - } - else { - foreach ($Name in $script:Header) { - $ws.Cells[$Row, $ColumnIndex].Value = $Name - Write-Verbose "Cell '$Row`:$ColumnIndex' add header '$Name'" - $ColumnIndex += 1 - } - } - } - #endregion - - $Row += 1 - $ColumnIndex = $StartColumn - - foreach ($Name in $script:Header) { - #region Add non header values - Add-CellValue -TargetCell $ws.Cells[$Row, $ColumnIndex] -CellValue $TargetData.$Name - - $ColumnIndex += 1 - #endregion - } - } - } - Catch { - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" - } - } - } - - End { - Try { - if ($AutoNameRange) { - if (-not $script:header) { - $headerRange = $ws.Dimension.Address -replace "\d+$", "1" - #if there is a title or anything else above the header row, specifying StartRow will skip it. - if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} - #using a slightly odd syntax otherwise header ends up as a 2D array - $ws.Cells[$headerRange].Value | ForEach-Object -Begin {$Script:header = @()} -Process {$Script:header += $_ } - } - $totalRows = $ws.Dimension.End.Row - $totalColumns = $ws.Dimension.Columns - foreach ($c in 0..($totalColumns - 1)) { - $targetRangeName = "$($script:Header[$c])" - $targetColumn = $c + $StartColumn - $theCell = $ws.Cells[($startrow + 1), $targetColumn, $totalRows , $targetColumn ] - if ($ws.names[$targetRangeName]) { $ws.names[$targetRangeName].Address = $theCell.FullAddressAbsolute } - else {$ws.Names.Add($targetRangeName, $theCell) | Out-Null } - - if ([OfficeOpenXml.FormulaParsing.ExcelUtilities.ExcelAddressUtil]::IsValidAddress($targetRangeName)) { - Write-Warning "AutoNameRange: Property name '$targetRangeName' is also a valid Excel address and may cause issues. Consider renaming the property name." - } - } - } - - if ($Title) { - $startAddress = $ws.Dimension.Start.address -replace "$($ws.Dimension.Start.row)`$", "$($ws.Dimension.Start.row + 1)" - } - else { - $startAddress = $ws.Dimension.Start.Address - } - - $dataRange = "{0}:{1}" -f $startAddress, $ws.Dimension.End.Address - - Write-Debug "Data Range '$dataRange'" - - if (-not [String]::IsNullOrEmpty($RangeName)) { - if ($ws.Names[$RangeName]) { $ws.Names[$rangename].Address = $ws.Cells[$dataRange].FullAddressAbsolute } - else {$ws.Names.Add($RangeName, $ws.Cells[$dataRange]) | Out-Null } - } - - if (-not [String]::IsNullOrEmpty($TableName)) { - $csr = $StartRow - - $csc = $StartColumn - $cer = $ws.Dimension.End.Row - $cec = $ws.Dimension.End.Column # was $script:Header.Count - - $targetRange = $ws.Cells[$csr, $csc, $cer, $cec] - #if we're appending data the table may already exist. - if ($ws.Tables[$TableName]) { - $ws.Tables[$TableName].TableXml.table.ref = $targetRange.Address - $ws.Tables[$TableName].TableStyle = $TableStyle - } - else { - $tbl = $ws.Tables.Add($targetRange, $TableName) - $tbl.TableStyle = $TableStyle - } - } - - if ($PivotTableDefinition) { - foreach ($item in $PivotTableDefinition.GetEnumerator()) { - $pivotTableName = $item.Key - $pivotTableDataName = $item.Key + 'PivotTableData' - if ($item.Value.PivotFilter) {$PivotTableStartCell = "A3"} else { $PivotTableStartCell = "A1"} - - #Make sure the Pivot table sheet doesn't already exist. - #try { $pkg.Workbook.Worksheets.Delete( $pivotTableName) } catch {} - [OfficeOpenXml.ExcelWorksheet]$wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber - - #If it is a pivot for the default sheet and it doesn't exist - create it - if (-not $item.Value.SourceWorkSheet -and -not $wsPivot.PivotTables[$pivotTableDataName] ) { - $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) - } - #If it is a pivot for the default sheet and it exists - update the range. - elseif (-not $item.Value.SourceWorkSheet -and $wsPivot.PivotTables[$pivotTableDataName] ) { - $wsPivot.PivotTables[$pivotTableDataName].CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref = $WS.Cells[$dataRange].Address - } - #if it is a pivot for a named sheet and it doesn't exist, create it. - elseif ($item.Value.SourceWorkSheet -and -not $wsPivot.PivotTables[$pivotTableDataName] ) { - #find the worksheet - $workSheet = $pkg.Workbook.Worksheets.where( {$_.name -match $item.Value.SourceWorkSheet})[0] - if (-not $workSheet) {Write-Warning -Message "Could not find Worksheet '$($item.Value.SourceWorkSheet)' specified in pivot-table definition $($item.key)." } - else { - if ($item.Value.SourceRange) { $targetdataRange = $item.Value.SourceRange } - else { $targetDataRange = $workSheet.Dimension.Address} - $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $workSheet.Cells[$targetDataRange], $pivotTableDataName) - } - } - - #if we created the pivot table, set up the rows, columns and data if we didn't, put out a message 'existed' or 'error' . - if ($pivotTable) { - foreach ($Row in $item.Value.PivotRows) { - try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } - catch {Write-Warning -message "Could not add '$row' to Rows in PivotTable $pivotTableName." } - } - foreach ($Column in $item.Value.PivotColumns) { - try {$null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column])} - catch {Write-Warning -message "Could not add '$Column' to Columns in PivotTable $pivotTableName." } - } - if ($item.Value.PivotData -is [HashTable] -or $item.Value.PivotData -is [System.Collections.Specialized.OrderedDictionary]) { - $item.Value.PivotData.Keys | ForEach-Object { - try { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) - $df.Function = $item.Value.PivotData.$_ - } - catch {Write-Warning -message "Problem adding data fields to PivotTable $pivotTableName." } - } - } - else { - foreach ($field in $item.Value.PivotData) { - try { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$field]) - $df.Function = 'Count' - } - catch {Write-Warning -message "Problem adding data field '$field' to PivotTable $pivotTableName." } - } - } - foreach ( $pFilter in $item.Value.PivotFilter) { - try { $null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter])} - catch {Write-Warning -message "Could not add '$pFilter' to Filter/Page fields in PivotTable $pivotTableName." } - } - if ($item.Value.NoTotalsInPivot -or $NoTotalsInPivot) { $pivotTable.RowGrandTotals = $false } - if ($item.Value.PivotDataToColumn -or $PivotDataToColumn) { $pivotTable.DataOnRows = $false } - } - elseif ($wsPivot.PivotTables[$pivotTableDataName]) { - Write-Warning -Message "Pivot table defined in $($item.key) already exists." - } - else { Write-Warning -Message "Could not create the pivot table defined in $($item.key)."} - - #Create the chart if it doesn't exist, leave alone if it does. - if ($item.Value.IncludePivotChart -and -not $wsPivot.Drawings['PivotChart'] ) { - if ($item.Value.ChartType) { $ChartType = $item.Value.ChartType} # $ChartType may be passed as a parameter, has default of "Pie", over-ride that if it is in the pivot definition - [OfficeOpenXml.Drawing.Chart.ExcelChart] $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) - if (-not $item.Value.ChartHeight) {$item.Value.ChartHeight = 400 } - if (-not $item.Value.ChartWidth) {$item.Value.ChartWidth = 600 } - if (-not $item.Value.ChartRow) {$item.Value.ChartRow = 0 } - if (-not $item.Value.ChartColumn) {$item.Value.ChartColumn = 4 } - if (-not $item.Value.ChartRowOffSetPixels) {$item.Value.ChartRowOffSetPixels = 0 } - if (-not $item.Value.ChartColumnOffSetPixels) {$item.Value.ChartColumnOffSetPixels = 0 } - $chart.SetPosition($item.Value.ChartRow , $item.Value.ChartRowOffSetPixels , $item.Value.ChartColumn, $item.Value.ChartColumnOffSetPixels) - $chart.SetSize( $item.Value.ChartWidth, $item.Value.ChartHeight) - if ($chart.DataLabel) { - $chart.DataLabel.ShowCategory = [boolean]$item.Value.ShowCategory - $chart.DataLabel.ShowPercent = [boolean]$item.Value.ShowPercent - } - if ([boolean]$item.Value.NoLegend -or $NoLegend) {$chart.Legend.Remove()} - if ( $item.Value.ChartTitle) {$chart.Title.Text = $item.Value.chartTitle} - } - } - } - - if ($IncludePivotTable -or $IncludePivotChart) { - if ($PivotFilter) {$PivotTableStartCell = "A3"} else {$PivotTableStartCell = "A1"} - - $pivotTableName = $WorkSheetname + 'PivotTable' - $wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber - - $wsPivot.View.TabSelected = $true - - $pivotTableDataName = $WorkSheetname + 'PivotTableData' - if ($wsPivot.PivotTables[$pivotTableDataName] ) { - $pivotTable = $wsPivot.PivotTables[$pivotTableDataName] - $pivotTable.CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref = $WS.Cells[$dataRange].Address - Write-Warning -Message "Pivot table for $worksheetName already exists; updating the data range, but other properties will not be changed" - } - else { - $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) - - foreach ($Row in $PivotRows) { - try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } - catch {Write-Warning -message "Could not add '$row' to PivotTable Rows." } - } - - foreach ($Column in $PivotColumns) { - try {$null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column])} - catch {Write-Warning -message "Could not add '$Column' to PivotTable Columns." } - } - - if ($PivotData -is [HashTable] -or $PivotData -is [System.Collections.Specialized.OrderedDictionary]) { - $PivotData.Keys | ForEach-Object { - try { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) - $df.Function = $PivotData.$_ - } - catch {Write-Warning "Problem adding to Pivot table data fields." } - } - } - else { - foreach ($Item in $PivotData) { - try { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$Item]) - $df.Function = 'Count' - } - catch {Write-Warning "Problem adding '$item' to Pivot table data fields." } - } - } - - if ($PivotDataToColumn) { $pivotTable.DataOnRows = $false } - - foreach ($pFilter in $PivotFilter) { - try {$null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter]) } - catch {Write-Warning "Problem adding 'pFilter' to Pivot table page/filter fields." } - } - - if ($NoTotalsInPivot) { $pivotTable.RowGrandTotals = $false } - } - - if ($IncludePivotChart) { - if (-not $wsPivot.Drawings['PivotChart']) { - $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) - if ($chart.DataLabel) { - $chart.DataLabel.ShowCategory = $ShowCategory - $chart.DataLabel.ShowPercent = $ShowPercent - } - $chart.SetPosition(0, 26, 2, 26) # if Pivot table is rows+data only it will be 2 columns wide if has pivot columns we don't know how wide it will be - if ($NoLegend) { $chart.Legend.Remove() } - } - } - } - - if ($Password) { - $ws.Protection.SetPassword($Password) - } - - if ($AutoFilter) { - $ws.Cells[$dataRange].AutoFilter = $true - } - - if ($FreezeTopRow) { - $ws.View.FreezePanes(2, 1) - } - - if ($FreezeTopRowFirstColumn) { - $ws.View.FreezePanes(2, 2) - } - - if ($FreezeFirstColumn) { - $ws.View.FreezePanes(1, 2) - } - - if ($FreezePane) { - $freezeRow, $freezeColumn = $FreezePane - if (-not $freezeColumn -or $freezeColumn -eq 0) { - $freezeColumn = 1 - } - - if ($freezeRow -gt 1) { - $ws.View.FreezePanes($freezeRow, $freezeColumn) - } - } - - if ($BoldTopRow) { - if ($Title) { - $range = $ws.Dimension.Address -replace '\d+', '2' - } - else { - $range = $ws.Dimension.Address -replace '\d+', '1' - } - - $ws.Cells[$range].Style.Font.Bold = $true - } - - if ($AutoSize) { - $ws.Cells.AutoFitColumns() - } - - foreach ($Sheet in $HideSheet) { - $pkg.Workbook.WorkSheets[$Sheet].Hidden = 'Hidden' - } - - foreach ($chartDef in $ExcelChartDefinition) { - $ChartName = 'Chart' + (Split-Path -Leaf ([System.IO.path]::GetTempFileName())) -replace 'tmp|\.', '' - $chart = $ws.Drawings.AddChart($ChartName, $chartDef.ChartType) - $chart.Title.Text = $chartDef.Title - - if ($chartDef.NoLegend) { - $chart.Legend.Remove() - } - - if ($chart.Datalabel -ne $null) { - $chart.Datalabel.ShowCategory = $chartDef.ShowCategory - $chart.Datalabel.ShowPercent = $chartDef.ShowPercent - } - - $chart.SetPosition($chartDef.Row, $chartDef.RowOffsetPixels, $chartDef.Column, $chartDef.ColumnOffsetPixels) - $chart.SetSize($chartDef.Width, $chartDef.Height) - - $chartDefCount = @($chartDef.YRange).Count - if ($chartDefCount -eq 1) { - $Series = $chart.Series.Add($chartDef.YRange, $chartDef.XRange) - - $SeriesHeader = $chartDef.SeriesHeader - if (-not $SeriesHeader) { - $SeriesHeader = 'Series 1' - } - - $Series.Header = $SeriesHeader - } - else { - for ($idx = 0; $idx -lt $chartDefCount; $idx += 1) { - $Series = $chart.Series.Add($chartDef.YRange[$idx], $chartDef.XRange) - - if ($chartDef.SeriesHeader.Count -gt 0) { - $SeriesHeader = $chartDef.SeriesHeader[$idx] - } - - if (-not $SeriesHeader) { - $SeriesHeader = "Series $($idx)" - } - - $Series.Header = $SeriesHeader - $SeriesHeader = $null - } - } - } - - if ($ConditionalText) { - foreach ($targetConditionalText in $ConditionalText) { - $target = "Add$($targetConditionalText.ConditionalType)" - - $Range = $targetConditionalText.Range - if (-not $Range) { - $Range = $ws.Dimension.Address - } - - $rule = ($ws.Cells[$Range].ConditionalFormatting).PSObject.Methods[$target].Invoke() - - if ($targetConditionalText.Text) { - if ($targetConditionalText.ConditionalType -match 'equal|notequal|lessthan|lessthanorequal|greaterthan|greaterthanorequal') { - $rule.Formula = $targetConditionalText.Text - } - else { - $rule.Text = $targetConditionalText.Text - } - } - - $rule.Style.Font.Color.Color = $targetConditionalText.ConditionalTextColor - $rule.Style.Fill.PatternType = $targetConditionalText.PatternType - $rule.Style.Fill.BackgroundColor.Color = $targetConditionalText.BackgroundColor - } - } - - if ($CellStyleSB) { - $TotalRows = $ws.Dimension.Rows - $LastColumn = (Get-ExcelColumnName $ws.Dimension.Columns).ColumnName - & $CellStyleSB $ws $TotalRows $LastColumn - } - - if ($PassThru) { - $pkg - } - else { - if ($ReturnRange) { - $ws.Dimension.Address - } - - $pkg.Save() - - if ($ReZip) { - write-verbose "Re-Zipping $($pkg.file) using .NET ZIP library" - $zipAssembly = "System.IO.Compression.Filesystem" - try { - Add-Type -assembly $zipAssembly -ErrorAction stop - } - catch { - write-error "The -ReZip parameter requires .NET Framework 4.5 or later to be installed. Recommend to install Powershell v4+" - continue - } - - $TempZipPath = Join-Path -path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName()) - [io.compression.zipfile]::ExtractToDirectory($pkg.File, $TempZipPath) | Out-Null - Remove-Item $pkg.File -Force - [io.compression.zipfile]::CreateFromDirectory($TempZipPath, $pkg.File) | Out-Null - } - - $pkg.Dispose() - - if ($Show) { - Invoke-Item $Path - } - } - } - Catch { - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" - } - } -} - -function New-PivotTableDefinition { -<# - .Synopsis - Creates Pivot table definitons for export excel - .Description - Export-Excel allows a single Pivot table to be defined using the parameters -IncludePivotTable, -PivotColumns -PivotRows, - =PivotData, -PivotFilter, -NoTotalsInPivot, -PivotDataToColumn, -IncludePivotChart and -ChartType. - Its -PivotTableDefintion paramater allows multiple pivot tables to be defined, with additional parameters. - New-PivotTableDefinition is a convenient way to build these definitions. - .Example - $pt = New-PivotTableDefinition -PivotTableName "PT1" -SourceWorkSheet "Sheet1" -PivotRows "Status" -PivotData @{Status='Count' } -PivotFilter 'StartType' -IncludePivotChart -ChartType BarClustered3D - $Pt += New-PivotTableDefinition -PivotTableName "PT2" -SourceWorkSheet "Sheet2" -PivotRows "Company" -PivotData @{Company='Count'} -IncludePivotChart -ChartType PieExploded3D -ShowPercent -ChartTitle "Breakdown of processes by company" - Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -AutoSize - Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' - $excel = Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show - - This is a re-work of one of the examples in Export-Excel - instead of writing out the pivot definition hash table it is built by calling New-PivotTableDefinition. -#> - param( - [Parameter(Mandatory)] - [Alias("PivtoTableName")]#Previous typo - use alias to avoid breaking scripts - $PivotTableName, - #Worksheet where the data is found - $SourceWorkSheet, - #Address range in the worksheet e.g "A10:F20" - the first row must be column names: if not specified the whole sheet will be used/ - $SourceRange, - #Fields to set as rows in the Pivot table - $PivotRows, - #A hash table in form "FieldName"="Function", where function is one of - #Average, Count, CountNums, Max, Min, Product, None, StdDev, StdDevP, Sum, Var, VarP - [hashtable]$PivotData, - #Fields to set as columns in the Pivot table - $PivotColumns, - #Fields to use to filter in the Pivot table - $PivotFilter, - [Switch]$PivotDataToColumn, - [Switch]$NoTotalsInPivot, - #If specified a chart Will be included. - [Switch]$IncludePivotChart, - #Optional title for the pivot chart, by default the title omitted. - [String]$ChartTitle, - #Height of the chart in Pixels (400 by default) - [int]$ChartHeight = 400 , - #Width of the chart in Pixels (600 by default) - [int]$ChartWidth = 600, - #Cell position of the top left corner of the chart, there will be this number of rows above the top edge of the chart (default is 0, chart starts at top edge of row 1). - [Int]$ChartRow = 0 , - #Cell position of the top left corner of the chart, there will be this number of cells to the left of the chart (default is 4, chart starts at left edge of column E) - [Int]$ChartColumn = 4, - #Vertical offset of the chart from the cell corner. - [Int]$ChartRowOffSetPixels = 0 , - #Horizontal offset of the chart from the cell corner. - [Int]$ChartColumnOffSetPixels = 0, - #Type of chart - [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', - #If specified hides the chart legend - [Switch]$NoLegend, - #if specified attaches the category to slices in a pie chart : not supported on all chart types, this may give errors if applied to an unsupported type. - [Switch]$ShowCategory, - #If specified attaches percentages to slices in a pie chart. - [Switch]$ShowPercent - ) - $validDataFuntions = [system.enum]::GetNames([OfficeOpenXml.Table.PivotTable.DataFieldFunctions]) - - if ($PivotData.values.Where({$_ -notin $validDataFuntions}) ) { - Write-Warning -Message ("Pivot data functions might not be valid, they should be one of " + ($validDataFuntions -join ", ") + ".") - } - - $parameters = @{} + $PSBoundParameters - $parameters.Remove('PivotTableName') - - @{$PivotTableName = $parameters} -} +function Export-Excel { + <# + .SYNOPSIS + Export data to an Excel worksheet. + .DESCRIPTION + Export data to an Excel file and where possible try to convert numbers so Excel recognizes them as numbers instead of text. After all. Excel is a spreadsheet program used for number manipulation and calculations. In case the number conversion is not desired, use the parameter '-NoNumberConversion *'. + .PARAMETER Path + Path to a new or existing .XLSX file. + .PARAMETER ExcelPackage + An object representing an Excel Package - usually this is returned by specifying -Passthru allowing multiple commands to work on the same Workbook without saving and reloading each time. + .PARAMETER WorkSheetName + The name of a sheet within the workbook - "Sheet1" by default. + .PARAMETER ClearSheet + If specified Export-Excel will remove any existing worksheet with the selected name. The Default behaviour is to overwrite cells in this sheet as needed (but leaving non-overwritten ones in place). + .PARAMETER Append + If specified data will be added to the end of an existing sheet, using the same column headings. + .PARAMETER TargetData + Data to insert onto the worksheet - this is often provided from the pipeline. + .PARAMETER ExcludeProperty + 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 + Sets the title in boldface type. + .PARAMETER TitleSize + Sets the point size for the title. + .PARAMETER TitleBackgroundColor + Sets the cell background color for the title cell. + .PARAMETER TitleFillPattern + Sets the fill pattern for the title cell. + .PARAMETER Password + Sets password protection on the workbook. + .PARAMETER IncludePivotTable + Adds a Pivot table using the data in the worksheet. + .PARAMETER PivotRows + 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 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, + HashTable(s) with Sheet PivotTows, PivotColumns, PivotData, IncludePivotChart and ChartType values to make it easier to specify a definition or multiple Pivots. + .PARAMETER IncludePivotChart, + Include a chart with the Pivot table - implies Include Pivot Table. + .PARAMETER NoLegend + Exclude the legend from the pivot chart. + .PARAMETER ShowCategory + Add category labels to the pivot chart. + .PARAMETER ShowPercent + Add Percentage labels to the pivot chart. + .PARAMETER ConditionalText + Applies a 'Conditional formatting rule' in Excel on all the cells. When specific conditions are met a rule is triggered. + .PARAMETER NoNumberConversion + By default we convert all values to numbers if possible, but this isn't always desirable. NoNumberConversion allows you to add exceptions for the conversion. Wildcards (like '*') are allowed. + .PARAMETER BoldTopRow + Makes the top Row boldface. + .PARAMETER NoHeader + Does not put field names at the top of columns. + .PARAMETER RangeName + Makes the data in the worksheet a named range. + .PARAMETER TableName + Makes the data in the worksheet a table with a name applies a style to it. Name must not contain spaces. + .PARAMETER TableStyle + Selects the style for the named table - defaults to 'Medium6'. + .PARAMETER ExcelChartDefinition + 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 MoveToStart + If specified, the worksheet will be moved to the start of the workbook. + MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified. + .PARAMETER MoveToEnd + If specified, the worksheet will be moved to the end of the workbook. + (This is the default position for newly created sheets, but this can be used to move existing sheets.) + .PARAMETER MoveBefore + If specified, the worksheet will be moved before the nominated one (which can be a postion starting from 1, or a name). + MoveBefore takes precedence over MoveAfter if both are specified. + .PARAMETER MoveAfter + If specified, the worksheet will be moved after the nominated one (which can be a postion starting from 1, or a name or *). + If * is used, the worksheet names will be examined starting with the first one, and the sheet placed after the last sheet which comes before it alphabetically. + .PARAMETER KillExcel + Closes Excel - prevents errors writing to the file because Excel has it open. + .PARAMETER AutoNameRange + 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 + Column to start adding data - 1 by default. + .PARAMETER FreezeTopRow + Freezes headers etc. in the top row. + .PARAMETER FreezeFirstColumn + Freezes titles etc. in the left column. + .PARAMETER FreezeTopRowFirstColumn + Freezes top row and left column (equivalent to Freeze pane 2,2 ). + .PARAMETER FreezePane + Freezes panes at specified coordinates (in the form RowNumber , ColumnNumber). + .PARAMETER AutoFilter + Enables the 'Filter' in Excel on the complete header row. So users can easily sort, filter and/or search the data in the select column from within Excel. + .PARAMETER AutoSize + Sizes the width of the Excel column to the maximum width needed to display all the containing data in that cell. + .PARAMETER Now + The 'Now' switch is a shortcut that creates automatically a temporary file, enables 'AutoSize', 'AutoFiler' and 'Show', and opens the file immediately. + .PARAMETER NumberFormat + Formats all values that can be converted to a number to the format specified. + + Examples: + # integer (not really needed unless you need to round numbers, Excel will use default cell properties). + '0' + + # integer without displaying the number 0 in the cell. + '#' + + # number with 1 decimal place. + '0.0' + + # number with 2 decimal places. + '0.00' + + # number with 2 decimal places and thousand separator. + '#,##0.00' + + # number with 2 decimal places and thousand separator and money symbol. + '€#,##0.00' + + # percentage (1 = 100%, 0.01 = 1%) + '0%' + + # Blue color for positive numbers and a red color for negative numbers. All numbers will be proceeded by a dollar sign '$'. + '[Blue]$#,##0.00;[Red]-$#,##0.00' + + .PARAMETER Show + Opens the Excel file immediately after creation. Convenient for viewing the results instantly without having to search for the file first. + .PARAMETER PassThru + If specified, Export-Excel returns an object representing the Excel package without saving the package first. To save it you need to call the save or Saveas method or send it back to Export-Excel. + + .EXAMPLE + Get-Process | Export-Excel .\Test.xlsx -show + Export all the processes to the Excel file 'Test.xlsx' and open the file immediately. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + Write-Output -1 668 34 777 860 -0.5 119 -0.1 234 788 | + Export-Excel @ExcelParams -NumberFormat '[Blue]$#,##0.00;[Red]-$#,##0.00' + + Exports all data to the Excel file 'Excel.xslx' and colors the negative values in 'Red' and the positive values in 'Blue'. It will also add a dollar sign '$' in front of the rounded numbers to two decimal characters behind the comma. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + [PSCustOmobject][Ordered]@{ + Date = Get-Date + Formula1 = '=SUM(F2:G2)' + String1 = 'My String' + String2 = 'a' + IPAddress = '10.10.25.5' + Number1 = '07670' + Number2 = '0,26' + Number3 = '1.555,83' + Number4 = '1.2' + Number5 = '-31' + PhoneNr1 = '+32 44' + PhoneNr2 = '+32 4 4444 444' + PhoneNr3 = '+3244444444' + } | Export-Excel @ExcelParams -NoNumberConversion IPAddress, Number1 + + Exports all data to the Excel file 'Excel.xslx' and tries to convert all values to numbers where possible except for 'IPAddress' and 'Number1'. These are stored in the sheet 'as is', without being converted to a number. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + [PSCustOmobject][Ordered]@{ + Date = Get-Date + Formula1 = '=SUM(F2:G2)' + String1 = 'My String' + String2 = 'a' + IPAddress = '10.10.25.5' + Number1 = '07670' + Number2 = '0,26' + Number3 = '1.555,83' + Number4 = '1.2' + Number5 = '-31' + PhoneNr1 = '+32 44' + PhoneNr2 = '+32 4 4444 444' + PhoneNr3 = '+3244444444' + } | Export-Excel @ExcelParams -NoNumberConversion * + + Exports all data to the Excel file 'Excel.xslx' as is, no number conversion will take place. This means that Excel will show the exact same data that you handed over to the 'Export-Excel' function. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + Write-Output 489 668 299 777 860 151 119 497 234 788 | + Export-Excel @ExcelParams -ConditionalText $( + New-ConditionalText -ConditionalType GreaterThan 525 -ConditionalTextColor DarkRed -BackgroundColor LightPink + ) + + Exports data that will have a 'Conditional formatting rule' in Excel on these cells that will show the background fill color in 'LightPink' and the text color in 'DarkRed' when the value is greater then '525'. In case this condition is not met the color will be the default, black text on a white background. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + Get-Service | Select Name, Status, DisplayName, ServiceName | + Export-Excel @ExcelParams -ConditionalText $( + New-ConditionalText Stop DarkRed LightPink + New-ConditionalText Running Blue Cyan + ) + + Export all services to an Excel sheet where all cells have a 'Conditional formatting rule' in Excel that will show the background fill color in 'LightPink' and the text color in 'DarkRed' when the value contains the word 'Stop'. If the value contains the word 'Running' it will have a background fill color in 'Cyan' and a text color 'Blue'. In case none of these conditions are met the color will be the default, black text on a white background. + + .EXAMPLE + $ExcelParams = @{ + Path = $env:TEMP + '\Excel.xlsx' + Show = $true + Verbose = $true + } + Remove-Item -Path $ExcelParams.Path -Force -EA Ignore + + $Array = @() + + $Obj1 = [PSCustomObject]@{ + Member1 = 'First' + Member2 = 'Second' + } + + $Obj2 = [PSCustomObject]@{ + Member1 = 'First' + Member2 = 'Second' + Member3 = 'Third' + } + + $Obj3 = [PSCustomObject]@{ + Member1 = 'First' + Member2 = 'Second' + Member3 = 'Third' + Member4 = 'Fourth' + } + + $Array = $Obj1, $Obj2, $Obj3 + $Array | Out-GridView -Title 'Not showing Member3 and Member4' + $Array | Update-FirstObjectProperties | Export-Excel @ExcelParams -WorkSheetname Numbers + + Updates the first object of the array by adding property 'Member3' and 'Member4'. Afterwards. all objects are exported to an Excel file and all column headers are visible. + + .EXAMPLE + Get-Process | Export-Excel .\test.xlsx -WorkSheetname Processes -IncludePivotTable -Show -PivotRows Company -PivotData PM + + .EXAMPLE + Get-Process | Export-Excel .\test.xlsx -WorkSheetname Processes -ChartType PieExploded3D -IncludePivotChart -IncludePivotTable -Show -PivotRows Company -PivotData PM + + .EXAMPLE + Get-Service | Export-Excel 'c:\temp\test.xlsx' -Show -IncludePivotTable -PivotRows status -PivotData @{status='count'} + + .EXAMPLE + $pt = [ordered]@{} + $pt.pt1=@{ SourceWorkSheet = 'Sheet1'; + PivotRows = 'Status' + PivotData = @{'Status'='count'} + IncludePivotChart = $true + ChartType = 'BarClustered3D' + } + $pt.pt2=@{ SourceWorkSheet = 'Sheet2'; + PivotRows = 'Company' + PivotData = @{'Company'='count'} + IncludePivotChart = $true + ChartType = 'PieExploded3D' + } + Remove-Item -Path .\test.xlsx + Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -AutoSize + Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' + Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show + + This example defines two pivot tables. Then it puts Service data on Sheet1 with one call to Export-Excel and Process Data on sheet2 with a second call to Export-Excel. + The thrid and final call adds the two pivot tables and opens the spreadsheet in Excel. + + + .EXAMPLE + Remove-Item -Path .\test.xlsx + $excel = Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -PassThru + $excel.Workbook.Worksheets["Sheet1"].Row(1).style.font.bold = $true + $excel.Workbook.Worksheets["Sheet1"].Column(3 ).width = 29 + $excel.Workbook.Worksheets["Sheet1"].Column(3 ).Style.wraptext = $true + $excel.Save() + $excel.Dispose() + Start-Process .\test.xlsx + + This example uses -passthrough - put service information into sheet1 of the work book and saves the excelPackageObject in $Excel. + It then uses the package object to apply formatting, and then saves the workbook and disposes of the object before loading the document in Excel. + + .EXAMPLE + $excel = Get-Process | Select-Object -Property Name,Company,Handles,CPU,PM,NPM,WS | Export-Excel -Path .\test.xlsx -ClearSheet -WorkSheetname "Processes" -PassThru + $sheet = $excel.Workbook.Worksheets["Processes"] + $sheet.Column(1) | Set-Format -Bold -AutoFit + $sheet.Column(2) | Set-Format -Width 29 -WrapText + $sheet.Column(3) | Set-Format -HorizontalAlignment Right -NFormat "#,###" + Set-Format -Address $sheet.Cells["E1:H1048576"] -HorizontalAlignment Right -NFormat "#,###" + Set-Format -Address $sheet.Column(4) -HorizontalAlignment Right -NFormat "#,##0.0" -Bold + Set-Format -Address $sheet.Row(1) -Bold -HorizontalAlignment Center + Add-ConditionalFormatting -WorkSheet $sheet -Range "D2:D1048576" -DataBarColor Red + Add-ConditionalFormatting -WorkSheet $sheet -Range "G2:G1048576" -RuleType GreaterThan -ConditionValue "104857600" -ForeGroundColor Red + foreach ($c in 5..9) {Set-Format $sheet.Column($c) -AutoFit } + Export-Excel -ExcelPackage $excel -WorkSheetname "Processes" -IncludePivotChart -ChartType ColumnClustered -NoLegend -PivotRows company -PivotData @{'Name'='Count'} -Show + + This a more sophisticated version of the previous example showing different ways of using Set-Format, and also adding conditional formatting. + In the final command a Pivot chart is added and the workbook is opened in Excel. + + .LINK + https://github.com/dfinke/ImportExcel + #> + + [CmdletBinding(DefaultParameterSetName = 'Default')] + Param( + [Parameter(ParameterSetName = "Default", Position = 0)] + [Parameter(ParameterSetName = "Table" , Position = 0)] + [String]$Path, + [Parameter(Mandatory = $true, ParameterSetName = "PackageDefault")] + [Parameter(Mandatory = $true, ParameterSetName = "PackageTable")] + [OfficeOpenXml.ExcelPackage]$ExcelPackage, + [Parameter(ValueFromPipeline = $true)] + $TargetData, + [Switch]$Show, + [String]$WorkSheetname = 'Sheet1', + [String]$Password, + [switch]$ClearSheet, + [switch]$Append, + [String]$Title, + [OfficeOpenXml.Style.ExcelFillStyle]$TitleFillPattern = 'None', + [Switch]$TitleBold, + [Int]$TitleSize = 22, + [System.Drawing.Color]$TitleBackgroundColor, + [Switch]$IncludePivotTable, + [String[]]$PivotRows, + [String[]]$PivotColumns, + $PivotData, + [String[]]$PivotFilter, + [Switch]$PivotDataToColumn, + [Hashtable]$PivotTableDefinition, + [Switch]$IncludePivotChart, + [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', + [Switch]$NoLegend, + [Switch]$ShowCategory, + [Switch]$ShowPercent, + [Switch]$AutoSize, + [Switch]$NoClobber, + [Switch]$FreezeTopRow, + [Switch]$FreezeFirstColumn, + [Switch]$FreezeTopRowFirstColumn, + [Int[]]$FreezePane, + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'PackageDefault')] + [Switch]$AutoFilter, + [Switch]$BoldTopRow, + [Switch]$NoHeader, + [String]$RangeName, + [ValidateScript( { + if ($_.Contains(' ')) { + throw 'Tablename has spaces.' + } + elseif (-not $_) { + throw 'Tablename is null or empty.' + } + elseif ($_[0] -notmatch '[a-z]') { + throw 'Tablename starts with an invalid character.' + } + else { + $true + } + })] + [Parameter(ParameterSetName = 'Table' , Mandatory = $true)] + [Parameter(ParameterSetName = 'PackageTable' , Mandatory = $true)] + [String]$TableName, + [Parameter(ParameterSetName = 'Table')] + [Parameter(ParameterSetName = 'PackageTable')] + [OfficeOpenXml.Table.TableStyles]$TableStyle = 'Medium6', + [Object[]]$ExcelChartDefinition, + [String[]]$HideSheet, + [Switch]$MoveToStart, + [Switch]$MoveToEnd, + $MoveBefore , + $MoveAfter , + [Switch]$KillExcel, + [Switch]$AutoNameRange, + [Int]$StartRow = 1, + [Int]$StartColumn = 1, + [Switch]$PassThru, + [String]$Numberformat = 'General', + [string[]]$ExcludeProperty, + [String[]]$NoNumberConversion, + [Object[]]$ConditionalFormat, + [Object[]]$ConditionalText, + [ScriptBlock]$CellStyleSB, + [Parameter(ParameterSetName = 'Now')] + # [Parameter(ParameterSetName = 'TableNow')] + [Switch]$Now, + [Switch]$ReturnRange, + [Switch]$NoTotalsInPivot, + [Switch]$ReZip + ) + + Begin { + function Add-CellValue { + <# + .SYNOPSIS + Save a value in an Excel cell. + + .DESCRIPTION + DateTime objects are always converted to a short DateTime format in Excel. When Excel loads the file, + it applies the local format for dates. And formulas are always saved as formulas. URIs are set as hyperlinks in the file. + + Numerical values will be converted to numbers as defined in the regional settings of the local + system. In case the parameter 'NoNumberConversion' is used, we don't convert to number and leave + the value 'as is'. In case of conversion failure, we also leave the value 'as is'. + #> + + Param ( + [Object]$TargetCell, + [Object]$CellValue + ) + + Switch ($CellValue) { + {($_ -is [String]) -and ($_.StartsWith('='))} { + #region Save an Excel formula + $TargetCell.Formula = $_ + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$_' as formula" + break + #endregion + } + { $_ -is [URI] } { + #region Save a hyperlink + $TargetCell.Value = $_.AbsoluteUri + $TargetCell.HyperLink = $_ + $TargetCell.Style.Font.Color.SetColor([System.Drawing.Color]::Blue) + $TargetCell.Style.Font.UnderLine = $true + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($_.AbsoluteUri)' as Hyperlink" + break + #endregion + } + { $_ -is [DateTime]} { + #region Save a date with an international valid format + $TargetCell.Value = $_ + $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 + } + + {(($NoNumberConversion) -and ($NoNumberConversion -contains $Name)) -or + ($NoNumberConversion -eq '*')} { + #region Save a value without converting to number + $TargetCell.Value = $_ + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' unconverted" + break + #endregion + } + + Default { + #region Save a value as a number if possible + $number = $null + if ([Double]::TryParse([String]$_, [System.Globalization.NumberStyles]::Any, + [System.Globalization.NumberFormatInfo]::CurrentInfo, [Ref]$number)) { + $TargetCell.Value = $number + $targetCell.Style.Numberformat.Format = $Numberformat + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as number converted from '$_' with format '$Numberformat'" + } + else { + $TargetCell.Value = $_ + Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as string" + } + break + #endregion + } + } + } + + Try { + $script:Header = $null + if ($Append -and $ClearSheet) {throw "You can't use -Append AND -ClearSheet."} + + if ($PSBoundParameters.Keys.Count -eq 0 -Or $Now) { + $Path = [System.IO.Path]::GetTempFileName() -replace '\.tmp', '.xlsx' + $Show = $true + $AutoSize = $true + if (!$TableName) { + $AutoFilter = $true + } + } + + if ($ExcelPackage) { + $pkg = $ExcelPackage + $Path = $pkg.File + } + Else { $pkg = Open-ExcelPackage -Path $Path -Create -KillExcel:$KillExcel} + + $params = @{} + if ($NoClobber) {Write-Warning -Message "-NoClobber parameter is no longer used" } + foreach ($p in @("WorkSheetname","ClearSheet","MoveToStart","MoveToEnd","MoveBefore","MoveAfter")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} + $ws = $pkg | Add-WorkSheet @params + + $ws.View.TabSelected = $true + foreach ($format in $ConditionalFormat ) { + $target = "Add$($format.Formatter)" + $rule = ($ws.ConditionalFormatting).PSObject.Methods[$target].Invoke($format.Range, $format.IconType) + $rule.Reverse = $format.Reverse + } + + if ($append -and $ws.Dimension) { + $headerRange = $ws.Dimension.Address -replace "\d+$", "1" + #if there is a title or anything else above the header row, specifying StartRow will skip it. + if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} + #$script:Header = $ws.Cells[$headerrange].Value + #using a slightly odd syntax otherwise header ends up as a 2D array + $ws.Cells[$headerRange].Value | ForEach-Object -Begin {$Script:header = @()} -Process {$Script:header += $_ } + $row = $ws.Dimension.Rows + Write-Debug -Message ("Appending: headers are " + ($script:Header -join ", ") + "Start row $row") + } + elseif ($Title) { + #Can only add a title if not appending! + $Row = $StartRow + $ws.Cells[$Row, $StartColumn].Value = $Title + $ws.Cells[$Row, $StartColumn].Style.Font.Size = $TitleSize + + if ($TitleBold) { + #Set title to Bold face font if -TitleBold was specified. + #Otherwise the default will be unbolded. + $ws.Cells[$Row, $StartColumn].Style.Font.Bold = $True + } + #Can only set TitleBackgroundColor if TitleFillPattern is something other than None. + if ($TitleBackgroundColor -and ($TitleFillPattern -eq 'None')) { + $TitleFillPattern = [OfficeOpenXml.Style.ExcelFillStyle]::Solid + } + $ws.Cells[$Row, $StartColumn].Style.Fill.PatternType = $TitleFillPattern + + if ($TitleBackgroundColor ) { + $ws.Cells[$Row, $StartColumn].Style.Fill.BackgroundColor.SetColor($TitleBackgroundColor) + } + $Row ++ ; $startRow ++ + } + else { $Row = $StartRow } + $ColumnIndex = $StartColumn + $firstTimeThru = $true + $isDataTypeValueType = $false + } + Catch { + if ($AlreadyExists) { + #Is this set anywhere ? + throw "Failed exporting worksheet '$WorkSheetname' to '$Path': The worksheet '$WorkSheetname' already exists." + } + else { + throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" + } + } + } + + Process { + if ($TargetData) { + Try { + if ($firstTimeThru) { + $firstTimeThru = $false + $isDataTypeValueType = $TargetData.GetType().name -match 'string|bool|byte|char|decimal|double|float|int|long|sbyte|short|uint|ulong|ushort' + Write-Debug "DataTypeName is '$($TargetData.GetType().name)' isDataTypeValueType '$isDataTypeValueType'" + } + + if ($isDataTypeValueType) { + $ColumnIndex = $StartColumn + + Add-CellValue -TargetCell $ws.Cells[$Row, $ColumnIndex] -CellValue $TargetData + + $Row += 1 + } + else { + #region Add headers + if (-not $script:Header) { + $ColumnIndex = $StartColumn + $script:Header = $TargetData.PSObject.Properties.Name | Where-Object {$_ -notin $ExcludeProperty} + + if ($NoHeader) { + # Don't push the headers to the spreadsheet + $Row -= 1 + } + else { + foreach ($Name in $script:Header) { + $ws.Cells[$Row, $ColumnIndex].Value = $Name + Write-Verbose "Cell '$Row`:$ColumnIndex' add header '$Name'" + $ColumnIndex += 1 + } + } + } + #endregion + + $Row += 1 + $ColumnIndex = $StartColumn + + foreach ($Name in $script:Header) { + #region Add non header values + Add-CellValue -TargetCell $ws.Cells[$Row, $ColumnIndex] -CellValue $TargetData.$Name + + $ColumnIndex += 1 + #endregion + } + } + } + Catch { + throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" + } + } + } + + End { + Try { + if ($AutoNameRange) { + if (-not $script:header) { + $headerRange = $ws.Dimension.Address -replace "\d+$", "1" + #if there is a title or anything else above the header row, specifying StartRow will skip it. + if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} + #using a slightly odd syntax otherwise header ends up as a 2D array + $ws.Cells[$headerRange].Value | ForEach-Object -Begin {$Script:header = @()} -Process {$Script:header += $_ } + } + $totalRows = $ws.Dimension.End.Row + $totalColumns = $ws.Dimension.Columns + foreach ($c in 0..($totalColumns - 1)) { + $targetRangeName = "$($script:Header[$c])" + $targetColumn = $c + $StartColumn + $theCell = $ws.Cells[($startrow + 1), $targetColumn, $totalRows , $targetColumn ] + if ($ws.names[$targetRangeName]) { $ws.names[$targetRangeName].Address = $theCell.FullAddressAbsolute } + else {$ws.Names.Add($targetRangeName, $theCell) | Out-Null } + + if ([OfficeOpenXml.FormulaParsing.ExcelUtilities.ExcelAddressUtil]::IsValidAddress($targetRangeName)) { + Write-Warning "AutoNameRange: Property name '$targetRangeName' is also a valid Excel address and may cause issues. Consider renaming the property name." + } + } + } + + if ($Title) { + $startAddress = $ws.Dimension.Start.address -replace "$($ws.Dimension.Start.row)`$", "$($ws.Dimension.Start.row + 1)" + } + else { + $startAddress = $ws.Dimension.Start.Address + } + + $dataRange = "{0}:{1}" -f $startAddress, $ws.Dimension.End.Address + + Write-Debug "Data Range '$dataRange'" + + if (-not [String]::IsNullOrEmpty($RangeName)) { + if ($ws.Names[$RangeName]) { $ws.Names[$rangename].Address = $ws.Cells[$dataRange].FullAddressAbsolute } + else {$ws.Names.Add($RangeName, $ws.Cells[$dataRange]) | Out-Null } + } + + if (-not [String]::IsNullOrEmpty($TableName)) { + $csr = $StartRow + + $csc = $StartColumn + $cer = $ws.Dimension.End.Row + $cec = $ws.Dimension.End.Column # was $script:Header.Count + + $targetRange = $ws.Cells[$csr, $csc, $cer, $cec] + #if we're appending data the table may already exist. + if ($ws.Tables[$TableName]) { + $ws.Tables[$TableName].TableXml.table.ref = $targetRange.Address + $ws.Tables[$TableName].TableStyle = $TableStyle + } + else { + $tbl = $ws.Tables.Add($targetRange, $TableName) + $tbl.TableStyle = $TableStyle + } + } + + if ($PivotTableDefinition) { + foreach ($item in $PivotTableDefinition.GetEnumerator()) { + $pivotTableName = $item.Key + $pivotTableDataName = $item.Key + 'PivotTableData' + if ($item.Value.PivotFilter) {$PivotTableStartCell = "A3"} else { $PivotTableStartCell = "A1"} + + #Make sure the Pivot table sheet doesn't already exist. + #try { $pkg.Workbook.Worksheets.Delete( $pivotTableName) } catch {} + [OfficeOpenXml.ExcelWorksheet]$wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber + + #If it is a pivot for the default sheet and it doesn't exist - create it + if (-not $item.Value.SourceWorkSheet -and -not $wsPivot.PivotTables[$pivotTableDataName] ) { + $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) + } + #If it is a pivot for the default sheet and it exists - update the range. + elseif (-not $item.Value.SourceWorkSheet -and $wsPivot.PivotTables[$pivotTableDataName] ) { + $wsPivot.PivotTables[$pivotTableDataName].CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref = $WS.Cells[$dataRange].Address + } + #if it is a pivot for a named sheet and it doesn't exist, create it. + elseif ($item.Value.SourceWorkSheet -and -not $wsPivot.PivotTables[$pivotTableDataName] ) { + #find the worksheet + $workSheet = $pkg.Workbook.Worksheets.where( {$_.name -match $item.Value.SourceWorkSheet})[0] + if (-not $workSheet) {Write-Warning -Message "Could not find Worksheet '$($item.Value.SourceWorkSheet)' specified in pivot-table definition $($item.key)." } + else { + if ($item.Value.SourceRange) { $targetdataRange = $item.Value.SourceRange } + else { $targetDataRange = $workSheet.Dimension.Address} + $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $workSheet.Cells[$targetDataRange], $pivotTableDataName) + } + } + + #if we created the pivot table, set up the rows, columns and data if we didn't, put out a message 'existed' or 'error' . + if ($pivotTable) { + foreach ($Row in $item.Value.PivotRows) { + try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } + catch {Write-Warning -message "Could not add '$row' to Rows in PivotTable $pivotTableName." } + } + foreach ($Column in $item.Value.PivotColumns) { + try {$null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column])} + catch {Write-Warning -message "Could not add '$Column' to Columns in PivotTable $pivotTableName." } + } + if ($item.Value.PivotData -is [HashTable] -or $item.Value.PivotData -is [System.Collections.Specialized.OrderedDictionary]) { + $item.Value.PivotData.Keys | ForEach-Object { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) + $df.Function = $item.Value.PivotData.$_ + } + catch {Write-Warning -message "Problem adding data fields to PivotTable $pivotTableName." } + } + } + else { + foreach ($field in $item.Value.PivotData) { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$field]) + $df.Function = 'Count' + } + catch {Write-Warning -message "Problem adding data field '$field' to PivotTable $pivotTableName." } + } + } + foreach ( $pFilter in $item.Value.PivotFilter) { + try { $null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter])} + catch {Write-Warning -message "Could not add '$pFilter' to Filter/Page fields in PivotTable $pivotTableName." } + } + if ($item.Value.NoTotalsInPivot -or $NoTotalsInPivot) { $pivotTable.RowGrandTotals = $false } + if ($item.Value.PivotDataToColumn -or $PivotDataToColumn) { $pivotTable.DataOnRows = $false } + } + elseif ($wsPivot.PivotTables[$pivotTableDataName]) { + Write-Warning -Message "Pivot table defined in $($item.key) already exists." + } + else { Write-Warning -Message "Could not create the pivot table defined in $($item.key)."} + + #Create the chart if it doesn't exist, leave alone if it does. + if ($item.Value.IncludePivotChart -and -not $wsPivot.Drawings['PivotChart'] ) { + if ($item.Value.ChartType) { $ChartType = $item.Value.ChartType} # $ChartType may be passed as a parameter, has default of "Pie", over-ride that if it is in the pivot definition + [OfficeOpenXml.Drawing.Chart.ExcelChart] $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) + if (-not $item.Value.ChartHeight) {$item.Value.ChartHeight = 400 } + if (-not $item.Value.ChartWidth) {$item.Value.ChartWidth = 600 } + if (-not $item.Value.ChartRow) {$item.Value.ChartRow = 0 } + if (-not $item.Value.ChartColumn) {$item.Value.ChartColumn = 4 } + if (-not $item.Value.ChartRowOffSetPixels) {$item.Value.ChartRowOffSetPixels = 0 } + if (-not $item.Value.ChartColumnOffSetPixels) {$item.Value.ChartColumnOffSetPixels = 0 } + $chart.SetPosition($item.Value.ChartRow , $item.Value.ChartRowOffSetPixels , $item.Value.ChartColumn, $item.Value.ChartColumnOffSetPixels) + $chart.SetSize( $item.Value.ChartWidth, $item.Value.ChartHeight) + if ($chart.DataLabel) { + $chart.DataLabel.ShowCategory = [boolean]$item.Value.ShowCategory + $chart.DataLabel.ShowPercent = [boolean]$item.Value.ShowPercent + } + if ([boolean]$item.Value.NoLegend -or $NoLegend) {$chart.Legend.Remove()} + if ( $item.Value.ChartTitle) {$chart.Title.Text = $item.Value.chartTitle} + } + } + } + + if ($IncludePivotTable -or $IncludePivotChart) { + if ($PivotFilter) {$PivotTableStartCell = "A3"} else {$PivotTableStartCell = "A1"} + + $pivotTableName = $WorkSheetname + 'PivotTable' + $wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber + + $wsPivot.View.TabSelected = $true + + $pivotTableDataName = $WorkSheetname + 'PivotTableData' + if ($wsPivot.PivotTables[$pivotTableDataName] ) { + $pivotTable = $wsPivot.PivotTables[$pivotTableDataName] + $pivotTable.CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref = $WS.Cells[$dataRange].Address + Write-Warning -Message "Pivot table for $worksheetName already exists; updating the data range, but other properties will not be changed" + } + else { + $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) + + foreach ($Row in $PivotRows) { + try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } + catch {Write-Warning -message "Could not add '$row' to PivotTable Rows." } + } + + foreach ($Column in $PivotColumns) { + try {$null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column])} + catch {Write-Warning -message "Could not add '$Column' to PivotTable Columns." } + } + + if ($PivotData -is [HashTable] -or $PivotData -is [System.Collections.Specialized.OrderedDictionary]) { + $PivotData.Keys | ForEach-Object { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) + $df.Function = $PivotData.$_ + } + catch {Write-Warning "Problem adding to Pivot table data fields." } + } + } + else { + foreach ($Item in $PivotData) { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$Item]) + $df.Function = 'Count' + } + catch {Write-Warning "Problem adding '$item' to Pivot table data fields." } + } + } + + if ($PivotDataToColumn) { $pivotTable.DataOnRows = $false } + + foreach ($pFilter in $PivotFilter) { + try {$null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter]) } + catch {Write-Warning "Problem adding 'pFilter' to Pivot table page/filter fields." } + } + + if ($NoTotalsInPivot) { $pivotTable.RowGrandTotals = $false } + } + + if ($IncludePivotChart) { + if (-not $wsPivot.Drawings['PivotChart']) { + $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) + if ($chart.DataLabel) { + $chart.DataLabel.ShowCategory = $ShowCategory + $chart.DataLabel.ShowPercent = $ShowPercent + } + $chart.SetPosition(0, 26, 2, 26) # if Pivot table is rows+data only it will be 2 columns wide if has pivot columns we don't know how wide it will be + if ($NoLegend) { $chart.Legend.Remove() } + } + } + } + + if ($Password) { + $ws.Protection.SetPassword($Password) + } + + if ($AutoFilter) { + $ws.Cells[$dataRange].AutoFilter = $true + } + + if ($FreezeTopRow) { + $ws.View.FreezePanes(2, 1) + } + + if ($FreezeTopRowFirstColumn) { + $ws.View.FreezePanes(2, 2) + } + + if ($FreezeFirstColumn) { + $ws.View.FreezePanes(1, 2) + } + + if ($FreezePane) { + $freezeRow, $freezeColumn = $FreezePane + if (-not $freezeColumn -or $freezeColumn -eq 0) { + $freezeColumn = 1 + } + + if ($freezeRow -gt 1) { + $ws.View.FreezePanes($freezeRow, $freezeColumn) + } + } + + if ($BoldTopRow) { + if ($Title) { + $range = $ws.Dimension.Address -replace '\d+', '2' + } + else { + $range = $ws.Dimension.Address -replace '\d+', '1' + } + + $ws.Cells[$range].Style.Font.Bold = $true + } + + if ($AutoSize) { + $ws.Cells.AutoFitColumns() + } + + foreach ($Sheet in $HideSheet) { + $pkg.Workbook.WorkSheets[$Sheet].Hidden = 'Hidden' + } + + foreach ($chartDef in $ExcelChartDefinition) { + $ChartName = 'Chart' + (Split-Path -Leaf ([System.IO.path]::GetTempFileName())) -replace 'tmp|\.', '' + $chart = $ws.Drawings.AddChart($ChartName, $chartDef.ChartType) + $chart.Title.Text = $chartDef.Title + + if ($chartDef.NoLegend) { + $chart.Legend.Remove() + } + + if ($chart.Datalabel -ne $null) { + $chart.Datalabel.ShowCategory = $chartDef.ShowCategory + $chart.Datalabel.ShowPercent = $chartDef.ShowPercent + } + + $chart.SetPosition($chartDef.Row, $chartDef.RowOffsetPixels, $chartDef.Column, $chartDef.ColumnOffsetPixels) + $chart.SetSize($chartDef.Width, $chartDef.Height) + + $chartDefCount = @($chartDef.YRange).Count + if ($chartDefCount -eq 1) { + $Series = $chart.Series.Add($chartDef.YRange, $chartDef.XRange) + + $SeriesHeader = $chartDef.SeriesHeader + if (-not $SeriesHeader) { + $SeriesHeader = 'Series 1' + } + + $Series.Header = $SeriesHeader + } + else { + for ($idx = 0; $idx -lt $chartDefCount; $idx += 1) { + $Series = $chart.Series.Add($chartDef.YRange[$idx], $chartDef.XRange) + + if ($chartDef.SeriesHeader.Count -gt 0) { + $SeriesHeader = $chartDef.SeriesHeader[$idx] + } + + if (-not $SeriesHeader) { + $SeriesHeader = "Series $($idx)" + } + + $Series.Header = $SeriesHeader + $SeriesHeader = $null + } + } + } + + if ($ConditionalText) { + foreach ($targetConditionalText in $ConditionalText) { + $target = "Add$($targetConditionalText.ConditionalType)" + + $Range = $targetConditionalText.Range + if (-not $Range) { + $Range = $ws.Dimension.Address + } + + $rule = ($ws.Cells[$Range].ConditionalFormatting).PSObject.Methods[$target].Invoke() + + if ($targetConditionalText.Text) { + if ($targetConditionalText.ConditionalType -match 'equal|notequal|lessthan|lessthanorequal|greaterthan|greaterthanorequal') { + $rule.Formula = $targetConditionalText.Text + } + else { + $rule.Text = $targetConditionalText.Text + } + } + + $rule.Style.Font.Color.Color = $targetConditionalText.ConditionalTextColor + $rule.Style.Fill.PatternType = $targetConditionalText.PatternType + $rule.Style.Fill.BackgroundColor.Color = $targetConditionalText.BackgroundColor + } + } + + if ($CellStyleSB) { + $TotalRows = $ws.Dimension.Rows + $LastColumn = (Get-ExcelColumnName $ws.Dimension.Columns).ColumnName + & $CellStyleSB $ws $TotalRows $LastColumn + } + + if ($PassThru) { + $pkg + } + else { + if ($ReturnRange) { + $ws.Dimension.Address + } + + $pkg.Save() + + if ($ReZip) { + write-verbose "Re-Zipping $($pkg.file) using .NET ZIP library" + $zipAssembly = "System.IO.Compression.Filesystem" + try { + Add-Type -assembly $zipAssembly -ErrorAction stop + } + catch { + write-error "The -ReZip parameter requires .NET Framework 4.5 or later to be installed. Recommend to install Powershell v4+" + continue + } + + $TempZipPath = Join-Path -path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName()) + [io.compression.zipfile]::ExtractToDirectory($pkg.File, $TempZipPath) | Out-Null + Remove-Item $pkg.File -Force + [io.compression.zipfile]::CreateFromDirectory($TempZipPath, $pkg.File) | Out-Null + } + + $pkg.Dispose() + + if ($Show) { + Invoke-Item $Path + } + } + } + Catch { + throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" + } + } +} + +function New-PivotTableDefinition { +<# + .Synopsis + Creates Pivot table definitons for export excel + .Description + Export-Excel allows a single Pivot table to be defined using the parameters -IncludePivotTable, -PivotColumns -PivotRows, + =PivotData, -PivotFilter, -NoTotalsInPivot, -PivotDataToColumn, -IncludePivotChart and -ChartType. + Its -PivotTableDefintion paramater allows multiple pivot tables to be defined, with additional parameters. + New-PivotTableDefinition is a convenient way to build these definitions. + .Example + $pt = New-PivotTableDefinition -PivotTableName "PT1" -SourceWorkSheet "Sheet1" -PivotRows "Status" -PivotData @{Status='Count' } -PivotFilter 'StartType' -IncludePivotChart -ChartType BarClustered3D + $Pt += New-PivotTableDefinition -PivotTableName "PT2" -SourceWorkSheet "Sheet2" -PivotRows "Company" -PivotData @{Company='Count'} -IncludePivotChart -ChartType PieExploded3D -ShowPercent -ChartTitle "Breakdown of processes by company" + Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -AutoSize + Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' + $excel = Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show + + This is a re-work of one of the examples in Export-Excel - instead of writing out the pivot definition hash table it is built by calling New-PivotTableDefinition. +#> + param( + [Parameter(Mandatory)] + [Alias("PivtoTableName")]#Previous typo - use alias to avoid breaking scripts + $PivotTableName, + #Worksheet where the data is found + $SourceWorkSheet, + #Address range in the worksheet e.g "A10:F20" - the first row must be column names: if not specified the whole sheet will be used/ + $SourceRange, + #Fields to set as rows in the Pivot table + $PivotRows, + #A hash table in form "FieldName"="Function", where function is one of + #Average, Count, CountNums, Max, Min, Product, None, StdDev, StdDevP, Sum, Var, VarP + [hashtable]$PivotData, + #Fields to set as columns in the Pivot table + $PivotColumns, + #Fields to use to filter in the Pivot table + $PivotFilter, + [Switch]$PivotDataToColumn, + [Switch]$NoTotalsInPivot, + #If specified a chart Will be included. + [Switch]$IncludePivotChart, + #Optional title for the pivot chart, by default the title omitted. + [String]$ChartTitle, + #Height of the chart in Pixels (400 by default) + [int]$ChartHeight = 400 , + #Width of the chart in Pixels (600 by default) + [int]$ChartWidth = 600, + #Cell position of the top left corner of the chart, there will be this number of rows above the top edge of the chart (default is 0, chart starts at top edge of row 1). + [Int]$ChartRow = 0 , + #Cell position of the top left corner of the chart, there will be this number of cells to the left of the chart (default is 4, chart starts at left edge of column E) + [Int]$ChartColumn = 4, + #Vertical offset of the chart from the cell corner. + [Int]$ChartRowOffSetPixels = 0 , + #Horizontal offset of the chart from the cell corner. + [Int]$ChartColumnOffSetPixels = 0, + #Type of chart + [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', + #If specified hides the chart legend + [Switch]$NoLegend, + #if specified attaches the category to slices in a pie chart : not supported on all chart types, this may give errors if applied to an unsupported type. + [Switch]$ShowCategory, + #If specified attaches percentages to slices in a pie chart. + [Switch]$ShowPercent + ) + $validDataFuntions = [system.enum]::GetNames([OfficeOpenXml.Table.PivotTable.DataFieldFunctions]) + + if ($PivotData.values.Where({$_ -notin $validDataFuntions}) ) { + Write-Warning -Message ("Pivot data functions might not be valid, they should be one of " + ($validDataFuntions -join ", ") + ".") + } + + $parameters = @{} + $PSBoundParameters + $parameters.Remove('PivotTableName') + + @{$PivotTableName = $parameters} +} diff --git a/ImportExcel.psm1 b/ImportExcel.psm1 index ee58bb4..9789da6 100644 --- a/ImportExcel.psm1 +++ b/ImportExcel.psm1 @@ -1,592 +1,592 @@ -#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\Join-WorkSheet.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 StartColumn - The number of the first column to read data from (1 by default). - - .PARAMETER EndColumn - By default the import reads up to the last populated column, -EndColumn tells the import to stop at an earlier number. - - .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 { - [cmdletBinding()] - [OutputType([OfficeOpenXml.ExcelWorksheet])] - param( - #An object representing an Excel Package. - [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName = "Package", Position=0)] - [OfficeOpenXml.ExcelPackage]$ExcelPackage, - #An Excel workbook to which the Worksheet will be added - a package contains one workbook so you can use whichever fits at the time. - [Parameter(Mandatory=$true, ParameterSetName = "WorkBook")] - [OfficeOpenXml.ExcelWorkbook]$ExcelWorkbook, - #The name of the worksheet 'Sheet1' by default. - [string]$WorkSheetname = 'Sheet1', - #If the worksheet already exists, by default it will returned, unless -ClearSheet is specified in which case it will be deleted and re-created. - [switch]$ClearSheet, - #If specified, the worksheet will be moved to the start of the workbook. - #MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified. - [Switch]$MoveToStart, - #If specified, the worksheet will be moved to the end of the workbook. - #(This is the default position for newly created sheets, but this can be used to move existing sheets.) - [Switch]$MoveToEnd, - #If specified, the worksheet will be moved before the nominated one (which can be a postion starting from 1, or a name). - #MoveBefore takes precedence over MoveAfter if both are specified. - $MoveBefore , - # If specified, the worksheet will be moved after the nominated one (which can be a postion starting from 1, or a name or *). - # If * is used, the worksheet names will be examined starting with the first one, and the sheet placed after the last sheet which comes before it alphabetically. - $MoveAfter , - #If worksheet is provided as a copy source the new worksheet will be a copy of it. The source can be in the same workbook, or in a different file. - [OfficeOpenXml.ExcelWorksheet]$CopySource, - #Ignored but retained for backwards compatibility. - [Switch] $NoClobber - ) - - if ($ExcelPackage -and -not $ExcelWorkbook) {$ExcelWorkbook = $ExcelPackage.Workbook} - - $ws = $ExcelWorkbook.Worksheets[$WorkSheetname] - if( $ws -and $ClearSheet) { $ExcelWorkbook.Worksheets.Delete($WorkSheetname) ; $ws = $null } - if(!$ws -and $CopySource) { - Write-Verbose -Message "Copying into worksheet '$WorkSheetname'." - $ws=$ExcelWorkbook.Worksheets.Add($WorkSheetname, $CopySource) - } - elseif(!$ws) { - Write-Verbose -Message "Adding worksheet '$WorkSheetname'." - $ws=$ExcelWorkbook.Worksheets.Add($WorkSheetname) - } - else {Write-Verbose -Message "Worksheet '$WorkSheetname' already existed."} - if ($MoveToStart) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName) } - elseif ($MoveToEnd ) {$ExcelWorkbook.Worksheets.MoveToEnd($worksheetName) } - elseif ($MoveBefore ) { - if ($ExcelWorkbook.Worksheets[$MoveBefore]) { - if ($MoveBefore -is [int]) { - $ExcelWorkbook.Worksheets.MoveBefore($ws.Index, $MoveBefore) - } - else {$ExcelWorkbook.Worksheets.MoveBefore($worksheetname, $MoveBefore)} - } - else {Write-Warning "Can't find worksheet '$MoveBefore'; worsheet '$WorkSheetname' will not be moved."} - } - elseif ($MoveAfter ) { - if ($MoveAfter = "*") { - if ($WorkSheetname -lt $ExcelWorkbook.Worksheets[1].Name) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName)} - else { - $i = 1 - While ($i -lt $ExcelWorkbook.Worksheets.Count -and $ExcelWorkbook.Worksheets[$i + 1].Name -lt $worksheetname ) { $i++} - $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $i) - } - } - elseif ($ExcelWorkbook.Worksheets[$MoveAfter]) { - if ($MoveAfter -is [int]) { - $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $MoveAfter) - } - else { - $ExcelWorkbook.Worksheets.MoveAfter($worksheetname, $MoveAfter) - } - } - else {Write-Warning "Can't find worksheet '$MoveAfter'; worsheet '$WorkSheetname' will not be moved."} - } - 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\Join-WorkSheet.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 StartColumn + The number of the first column to read data from (1 by default). + + .PARAMETER EndColumn + By default the import reads up to the last populated column, -EndColumn tells the import to stop at an earlier number. + + .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 { + [cmdletBinding()] + [OutputType([OfficeOpenXml.ExcelWorksheet])] + param( + #An object representing an Excel Package. + [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName = "Package", Position=0)] + [OfficeOpenXml.ExcelPackage]$ExcelPackage, + #An Excel workbook to which the Worksheet will be added - a package contains one workbook so you can use whichever fits at the time. + [Parameter(Mandatory=$true, ParameterSetName = "WorkBook")] + [OfficeOpenXml.ExcelWorkbook]$ExcelWorkbook, + #The name of the worksheet 'Sheet1' by default. + [string]$WorkSheetname = 'Sheet1', + #If the worksheet already exists, by default it will returned, unless -ClearSheet is specified in which case it will be deleted and re-created. + [switch]$ClearSheet, + #If specified, the worksheet will be moved to the start of the workbook. + #MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified. + [Switch]$MoveToStart, + #If specified, the worksheet will be moved to the end of the workbook. + #(This is the default position for newly created sheets, but this can be used to move existing sheets.) + [Switch]$MoveToEnd, + #If specified, the worksheet will be moved before the nominated one (which can be a postion starting from 1, or a name). + #MoveBefore takes precedence over MoveAfter if both are specified. + $MoveBefore , + # If specified, the worksheet will be moved after the nominated one (which can be a postion starting from 1, or a name or *). + # If * is used, the worksheet names will be examined starting with the first one, and the sheet placed after the last sheet which comes before it alphabetically. + $MoveAfter , + #If worksheet is provided as a copy source the new worksheet will be a copy of it. The source can be in the same workbook, or in a different file. + [OfficeOpenXml.ExcelWorksheet]$CopySource, + #Ignored but retained for backwards compatibility. + [Switch] $NoClobber + ) + + if ($ExcelPackage -and -not $ExcelWorkbook) {$ExcelWorkbook = $ExcelPackage.Workbook} + + $ws = $ExcelWorkbook.Worksheets[$WorkSheetname] + if( $ws -and $ClearSheet) { $ExcelWorkbook.Worksheets.Delete($WorkSheetname) ; $ws = $null } + if(!$ws -and $CopySource) { + Write-Verbose -Message "Copying into worksheet '$WorkSheetname'." + $ws=$ExcelWorkbook.Worksheets.Add($WorkSheetname, $CopySource) + } + elseif(!$ws) { + Write-Verbose -Message "Adding worksheet '$WorkSheetname'." + $ws=$ExcelWorkbook.Worksheets.Add($WorkSheetname) + } + else {Write-Verbose -Message "Worksheet '$WorkSheetname' already existed."} + if ($MoveToStart) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName) } + elseif ($MoveToEnd ) {$ExcelWorkbook.Worksheets.MoveToEnd($worksheetName) } + elseif ($MoveBefore ) { + if ($ExcelWorkbook.Worksheets[$MoveBefore]) { + if ($MoveBefore -is [int]) { + $ExcelWorkbook.Worksheets.MoveBefore($ws.Index, $MoveBefore) + } + else {$ExcelWorkbook.Worksheets.MoveBefore($worksheetname, $MoveBefore)} + } + else {Write-Warning "Can't find worksheet '$MoveBefore'; worsheet '$WorkSheetname' will not be moved."} + } + elseif ($MoveAfter ) { + if ($MoveAfter = "*") { + if ($WorkSheetname -lt $ExcelWorkbook.Worksheets[1].Name) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName)} + else { + $i = 1 + While ($i -lt $ExcelWorkbook.Worksheets.Count -and $ExcelWorkbook.Worksheets[$i + 1].Name -lt $worksheetname ) { $i++} + $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $i) + } + } + elseif ($ExcelWorkbook.Worksheets[$MoveAfter]) { + if ($MoveAfter -is [int]) { + $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $MoveAfter) + } + else { + $ExcelWorkbook.Worksheets.MoveAfter($worksheetname, $MoveAfter) + } + } + else {Write-Warning "Can't find worksheet '$MoveAfter'; worsheet '$WorkSheetname' will not be moved."} + } + 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} +} diff --git a/Join-Worksheet.ps1 b/Join-Worksheet.ps1 index 7c3656f..c0ba310 100644 --- a/Join-Worksheet.ps1 +++ b/Join-Worksheet.ps1 @@ -1,164 +1,164 @@ -function Join-Worksheet { - [CmdletBinding(DefaultParameterSetName = 'Default')] - <# - .SYNOPSIS - Combines data on all the sheets in an Excel worksheet onto a single sheet. - .DESCRIPTION - Join worksheet can work in two main ways: - Either Combining data which has the same layout from many pages into one, or combining pages which have nothing in common. - In the former case the header row is copied from the first sheet and, by default, each row of data is labelled with the name of the sheet it came from. - In the latter case -NoHeader is specified, and each copied block can have the sheet it came from placed above it as a title. - .EXAMPLE - foreach ($computerName in @('Server1', 'Server2', 'Server3', 'Server4')) { - Get-Service -ComputerName $computerName | Select-Object -Property Status, Name, DisplayName, StartType | - Export-Excel -Path .\test.xlsx -WorkSheetname $computerName -AutoSize - } - $ptDef =New-PivotTableDefinition -PivotTableName "Pivot1" -SourceWorkSheet "Combined" -PivotRows "Status" -PivotFilter "MachineName" -PivotData @{Status='Count'} -IncludePivotChart -ChartType BarClustered3D - Join-Worksheet -Path .\test.xlsx -WorkSheetName combined -FromLabel "MachineName" -HideSource -AutoSize -FreezeTopRow -BoldTopRow -PivotTableDefinition $pt -Show - - The foreach command gets the services running on four servers and exports each to its own page in Test.xlsx. - $PtDef= creates a defintion for a single Pivot table. - The Join-Worksheet command uses the same file and merges the results onto a sheet named "Combined". It sets a column header of "Machinename", - this column will contain the name of the sheet the data was copied from; after copying the data to the sheet "combined", the other sheets will be hidden. - Join-Worksheet finishes by calling export-Excel to AutoSize cells, freeze the top row and make it bold and add the Pivot table. - - .EXAMPLE - Get-WmiObject -Class win32_logicaldisk | select -Property DeviceId,VolumeName, Size,Freespace | - Export-Excel -Path "$env:computerName.xlsx" -WorkSheetname Volumes - Get-NetAdapter | Select-Object Name,InterfaceDescription,MacAddress,LinkSpeed | - Export-Excel -Path "$env:COMPUTERNAME.xlsx" -WorkSheetname NetAdapter - Join-Worksheet -Path "$env:COMPUTERNAME.xlsx" -WorkSheetName Summay -Title "Summary" -TitleBold -TitleSize 22 -NoHeader -LabelBlocks -AutoSize -HideSource - - The first two command get logical disk and network card information; each type is exported to its own sheet in a workbook. - The Join-worksheet command copies both onto a page named "Summary". Because the data is disimilar -NoHeader is specified, ensuring the whole of each page is copied. - Specifying -LabelBlocks causes each sheet's name to become a title on the summary page above the copied data. - The source data is hidden, a title is addded in 22 point boldface and the columns are sized to fit the data. - #> - param ( - # Path to a new or existing .XLSX file. - [Parameter(ParameterSetName = "Default", Position = 0)] - [String]$Path , - # An object representing an Excel Package - usually this is returned by specifying -Passthru allowing multiple commands to work on the same Workbook without saving and reloading each time. - [Parameter(Mandatory = $true, ParameterSetName = "Package")] - [OfficeOpenXml.ExcelPackage]$ExcelPackage, - # The name of a sheet within the workbook where the other sheets will be joined together - "Combined" by default. - $WorkSheetName = 'Combined', - # If specified any pre-existing target for the joined data will be deleted and re-created; otherwise data will be appended on this sheet. - [switch]$Clearsheet, - #Join-Worksheet assumes each sheet has identical headers and the headers should be copied to the target sheet, unless -NoHeader is specified. - [switch]$NoHeader, - #If -NoHeader is NOT specified, then rows of data will be labeled with the name of the sheet they came, FromLabel is the header for this column. If it is null or empty, the labels will be omitted. - $FromLabel = "From" , - #If specified, the copied blocks of data will have the name of the sheet they were copied from inserted above them as a title. - [switch]$LabelBlocks, - #Sizes the width of the Excel column to the maximum width needed to display all the containing data in that cell. - [Switch]$AutoSize, - #Freezes headers etc. in the top row. - [Switch]$FreezeTopRow, - #Freezes titles etc. in the left column. - [Switch]$FreezeFirstColumn, - #Freezes top row and left column (equivalent to Freeze pane 2,2 ). - [Switch]$FreezeTopRowFirstColumn, - # Freezes panes at specified coordinates (in the form RowNumber , ColumnNumber). - [Int[]]$FreezePane, - #Enables the 'Filter' in Excel on the complete header row. So users can easily sort, filter and/or search the data in the select column from within Excel. - [Switch]$AutoFilter, - #Makes the top Row boldface. - [Switch]$BoldTopRow, - #If Specified hides the sheets that the data is copied from. - [switch]$HideSource, - #Text of a title to be placed in Cell A1. - [String]$Title, - #Sets the fill pattern for the title cell. - [OfficeOpenXml.Style.ExcelFillStyle]$TitleFillPattern = 'None', - #Sets the cell background color for the title cell. - [System.Drawing.Color]$TitleBackgroundColor, - #Sets the title in boldface type. - [Switch]$TitleBold, - #Sets the point size for the title. - [Int]$TitleSize = 22, - # Hashtable(s) with Sheet PivotRows, PivotColumns, PivotData, IncludePivotChart and ChartType values to specify a definition for one or more pivot table(s). - [Hashtable]$PivotTableDefinition, - # A hashtable containing ChartType, Title, NoLegend, ShowCategory, ShowPercent, Yrange, Xrange and SeriesHeader for one or more [non-pivot] charts. - [Object[]]$ExcelChartDefinition, - #Opens the Excel file immediately after creation. Convenient for viewing the results instantly without having to search for the file first. - [switch]$Show, - #If specified, an object representing the unsaved Excel package will be returned, it then needs to be saved. - [switch]$PassThru - ) - #region get target worksheet, select it and move it to the end. - if ($Path -and -not $ExcelPackage) {$ExcelPackage = Open-ExcelPackage -path $Path } - $destinationSheet = Add-WorkSheet -ExcelPackage $ExcelPackage -WorkSheetname $WorkSheetName -ClearSheet:$Clearsheet - $destinationSheet.View.TabSelected = $true - $ExcelPackage.Workbook.Worksheets.MoveToEnd($WorkSheetName) - #row to insert at will be 1 on a blank sheet and lastrow + 1 on populated one - $row = (1 + $destinationSheet.Dimension.End.Row ) - #endregion - - #region Setup title and header rows - #Title parameters work as they do in Export-Excel . - if ($row -eq 1 -and $Title) { - $destinationSheet.Cells[1, 1].Value = $Title - $destinationSheet.Cells[1, 1].Style.Font.Size = $TitleSize - if ($TitleBold) {$destinationSheet.Cells[1, 1].Style.Font.Bold = $True } - #Can only set TitleBackgroundColor if TitleFillPattern is something other than None. - if ($TitleBackgroundColor -AND ($TitleFillPattern -ne 'None')) { - $destinationSheet.Cells[1, 1].Style.Fill.PatternType = $TitleFillPattern - $destinationSheet.Cells[1, 1].Style.Fill.BackgroundColor.SetColor($TitleBackgroundColor) - } - elseif ($TitleBackgroundColor) { Write-Warning "Title Background Color ignored. You must set the TitleFillPattern parameter to a value other than 'None'. Try 'Solid'." } - $row = 2 - } - - if (-not $noHeader) { - #Assume every row has titles in row 1, copy row 1 from first sheet to new sheet. - $destinationSheet.Select("A$row") - $ExcelPackage.Workbook.Worksheets[1].cells["1:1"].Copy($destinationSheet.SelectedRange) - if ($FromLabel ) { - #Add a column which says where the data comes from. - $fromColumn = ($destinationSheet.Dimension.Columns + 1) - $destinationSheet.Cells[$row, $fromColumn].Value = $FromLabel - } - $row += 1 - } - #endregion - - foreach ($i in 1..($ExcelPackage.Workbook.Worksheets.Count - 1) ) { - $sourceWorksheet = $ExcelPackage.Workbook.Worksheets[$i] - #Assume row one is titles, so data itself starts at A2. - if ($NoHeader) {$sourceRange = $sourceWorksheet.Dimension.Address} - else {$sourceRange = $sourceWorksheet.Dimension.Address -replace "A1:", "A2:"} - #Position insertion point/ - $destinationSheet.Select("A$row") - if ($LabelBlocks) { - $destinationSheet.Cells[$row, 1].value = $sourceWorksheet.Name - $destinationSheet.Cells[$row, 1].Style.Font.Bold = $true - $destinationSheet.Cells[$row, 1].Style.Font.Size += 2 - $row += 1 - } - $destinationSheet.Select("A$row") - - #And finally we're ready to copy the data. - $sourceWorksheet.Cells[$sourceRange].Copy($destinationSheet.SelectedRange) - #Fill in column saying where data came from. - if ($fromColumn) { $row..$destinationSheet.Dimension.Rows | ForEach-Object {$destinationSheet.Cells[$_, $fromColumn].Value = $sourceWorksheet.Name} } - #Update where next insertion will go. - $row = $destinationSheet.Dimension.Rows + 1 - if ($HideSource) {$sourceWorksheet.Hidden = [OfficeOpenXml.eWorkSheetHidden]::Hidden} - } - - #We accept a bunch of parameters work to pass on to Export-excel ( Autosize, Autofilter, boldtopRow Freeze ); if we have any of those call export-excel otherwise close the package here. - $params = @{} + $PSBoundParameters - 'Path', 'Clearsheet', 'NoHeader', 'FromLabel', 'LabelBlocks', 'HideSource', - 'Title', 'TitleFillPattern', 'TitleBackgroundColor', 'TitleBold', 'TitleSize' | ForEach-Object {[void]$params.Remove($_)} - if ($params.Keys.Count) { - $params.WorkSheetName = $WorkSheetName - $params.ExcelPackage = $ExcelPackage - Export-Excel @Params - } - else { - Close-ExcelPackage -ExcelPackage $ExcelPackage - $ExcelPackage.Dispose() - $ExcelPackage = $null - } +function Join-Worksheet { + [CmdletBinding(DefaultParameterSetName = 'Default')] + <# + .SYNOPSIS + Combines data on all the sheets in an Excel worksheet onto a single sheet. + .DESCRIPTION + Join worksheet can work in two main ways: + Either Combining data which has the same layout from many pages into one, or combining pages which have nothing in common. + In the former case the header row is copied from the first sheet and, by default, each row of data is labelled with the name of the sheet it came from. + In the latter case -NoHeader is specified, and each copied block can have the sheet it came from placed above it as a title. + .EXAMPLE + foreach ($computerName in @('Server1', 'Server2', 'Server3', 'Server4')) { + Get-Service -ComputerName $computerName | Select-Object -Property Status, Name, DisplayName, StartType | + Export-Excel -Path .\test.xlsx -WorkSheetname $computerName -AutoSize + } + $ptDef =New-PivotTableDefinition -PivotTableName "Pivot1" -SourceWorkSheet "Combined" -PivotRows "Status" -PivotFilter "MachineName" -PivotData @{Status='Count'} -IncludePivotChart -ChartType BarClustered3D + Join-Worksheet -Path .\test.xlsx -WorkSheetName combined -FromLabel "MachineName" -HideSource -AutoSize -FreezeTopRow -BoldTopRow -PivotTableDefinition $pt -Show + + The foreach command gets the services running on four servers and exports each to its own page in Test.xlsx. + $PtDef= creates a defintion for a single Pivot table. + The Join-Worksheet command uses the same file and merges the results onto a sheet named "Combined". It sets a column header of "Machinename", + this column will contain the name of the sheet the data was copied from; after copying the data to the sheet "combined", the other sheets will be hidden. + Join-Worksheet finishes by calling export-Excel to AutoSize cells, freeze the top row and make it bold and add the Pivot table. + + .EXAMPLE + Get-WmiObject -Class win32_logicaldisk | select -Property DeviceId,VolumeName, Size,Freespace | + Export-Excel -Path "$env:computerName.xlsx" -WorkSheetname Volumes + Get-NetAdapter | Select-Object Name,InterfaceDescription,MacAddress,LinkSpeed | + Export-Excel -Path "$env:COMPUTERNAME.xlsx" -WorkSheetname NetAdapter + Join-Worksheet -Path "$env:COMPUTERNAME.xlsx" -WorkSheetName Summay -Title "Summary" -TitleBold -TitleSize 22 -NoHeader -LabelBlocks -AutoSize -HideSource + + The first two command get logical disk and network card information; each type is exported to its own sheet in a workbook. + The Join-worksheet command copies both onto a page named "Summary". Because the data is disimilar -NoHeader is specified, ensuring the whole of each page is copied. + Specifying -LabelBlocks causes each sheet's name to become a title on the summary page above the copied data. + The source data is hidden, a title is addded in 22 point boldface and the columns are sized to fit the data. + #> + param ( + # Path to a new or existing .XLSX file. + [Parameter(ParameterSetName = "Default", Position = 0)] + [String]$Path , + # An object representing an Excel Package - usually this is returned by specifying -Passthru allowing multiple commands to work on the same Workbook without saving and reloading each time. + [Parameter(Mandatory = $true, ParameterSetName = "Package")] + [OfficeOpenXml.ExcelPackage]$ExcelPackage, + # The name of a sheet within the workbook where the other sheets will be joined together - "Combined" by default. + $WorkSheetName = 'Combined', + # If specified any pre-existing target for the joined data will be deleted and re-created; otherwise data will be appended on this sheet. + [switch]$Clearsheet, + #Join-Worksheet assumes each sheet has identical headers and the headers should be copied to the target sheet, unless -NoHeader is specified. + [switch]$NoHeader, + #If -NoHeader is NOT specified, then rows of data will be labeled with the name of the sheet they came, FromLabel is the header for this column. If it is null or empty, the labels will be omitted. + $FromLabel = "From" , + #If specified, the copied blocks of data will have the name of the sheet they were copied from inserted above them as a title. + [switch]$LabelBlocks, + #Sizes the width of the Excel column to the maximum width needed to display all the containing data in that cell. + [Switch]$AutoSize, + #Freezes headers etc. in the top row. + [Switch]$FreezeTopRow, + #Freezes titles etc. in the left column. + [Switch]$FreezeFirstColumn, + #Freezes top row and left column (equivalent to Freeze pane 2,2 ). + [Switch]$FreezeTopRowFirstColumn, + # Freezes panes at specified coordinates (in the form RowNumber , ColumnNumber). + [Int[]]$FreezePane, + #Enables the 'Filter' in Excel on the complete header row. So users can easily sort, filter and/or search the data in the select column from within Excel. + [Switch]$AutoFilter, + #Makes the top Row boldface. + [Switch]$BoldTopRow, + #If Specified hides the sheets that the data is copied from. + [switch]$HideSource, + #Text of a title to be placed in Cell A1. + [String]$Title, + #Sets the fill pattern for the title cell. + [OfficeOpenXml.Style.ExcelFillStyle]$TitleFillPattern = 'None', + #Sets the cell background color for the title cell. + [System.Drawing.Color]$TitleBackgroundColor, + #Sets the title in boldface type. + [Switch]$TitleBold, + #Sets the point size for the title. + [Int]$TitleSize = 22, + # Hashtable(s) with Sheet PivotRows, PivotColumns, PivotData, IncludePivotChart and ChartType values to specify a definition for one or more pivot table(s). + [Hashtable]$PivotTableDefinition, + # A hashtable containing ChartType, Title, NoLegend, ShowCategory, ShowPercent, Yrange, Xrange and SeriesHeader for one or more [non-pivot] charts. + [Object[]]$ExcelChartDefinition, + #Opens the Excel file immediately after creation. Convenient for viewing the results instantly without having to search for the file first. + [switch]$Show, + #If specified, an object representing the unsaved Excel package will be returned, it then needs to be saved. + [switch]$PassThru + ) + #region get target worksheet, select it and move it to the end. + if ($Path -and -not $ExcelPackage) {$ExcelPackage = Open-ExcelPackage -path $Path } + $destinationSheet = Add-WorkSheet -ExcelPackage $ExcelPackage -WorkSheetname $WorkSheetName -ClearSheet:$Clearsheet + $destinationSheet.View.TabSelected = $true + $ExcelPackage.Workbook.Worksheets.MoveToEnd($WorkSheetName) + #row to insert at will be 1 on a blank sheet and lastrow + 1 on populated one + $row = (1 + $destinationSheet.Dimension.End.Row ) + #endregion + + #region Setup title and header rows + #Title parameters work as they do in Export-Excel . + if ($row -eq 1 -and $Title) { + $destinationSheet.Cells[1, 1].Value = $Title + $destinationSheet.Cells[1, 1].Style.Font.Size = $TitleSize + if ($TitleBold) {$destinationSheet.Cells[1, 1].Style.Font.Bold = $True } + #Can only set TitleBackgroundColor if TitleFillPattern is something other than None. + if ($TitleBackgroundColor -AND ($TitleFillPattern -ne 'None')) { + $destinationSheet.Cells[1, 1].Style.Fill.PatternType = $TitleFillPattern + $destinationSheet.Cells[1, 1].Style.Fill.BackgroundColor.SetColor($TitleBackgroundColor) + } + elseif ($TitleBackgroundColor) { Write-Warning "Title Background Color ignored. You must set the TitleFillPattern parameter to a value other than 'None'. Try 'Solid'." } + $row = 2 + } + + if (-not $noHeader) { + #Assume every row has titles in row 1, copy row 1 from first sheet to new sheet. + $destinationSheet.Select("A$row") + $ExcelPackage.Workbook.Worksheets[1].cells["1:1"].Copy($destinationSheet.SelectedRange) + if ($FromLabel ) { + #Add a column which says where the data comes from. + $fromColumn = ($destinationSheet.Dimension.Columns + 1) + $destinationSheet.Cells[$row, $fromColumn].Value = $FromLabel + } + $row += 1 + } + #endregion + + foreach ($i in 1..($ExcelPackage.Workbook.Worksheets.Count - 1) ) { + $sourceWorksheet = $ExcelPackage.Workbook.Worksheets[$i] + #Assume row one is titles, so data itself starts at A2. + if ($NoHeader) {$sourceRange = $sourceWorksheet.Dimension.Address} + else {$sourceRange = $sourceWorksheet.Dimension.Address -replace "A1:", "A2:"} + #Position insertion point/ + $destinationSheet.Select("A$row") + if ($LabelBlocks) { + $destinationSheet.Cells[$row, 1].value = $sourceWorksheet.Name + $destinationSheet.Cells[$row, 1].Style.Font.Bold = $true + $destinationSheet.Cells[$row, 1].Style.Font.Size += 2 + $row += 1 + } + $destinationSheet.Select("A$row") + + #And finally we're ready to copy the data. + $sourceWorksheet.Cells[$sourceRange].Copy($destinationSheet.SelectedRange) + #Fill in column saying where data came from. + if ($fromColumn) { $row..$destinationSheet.Dimension.Rows | ForEach-Object {$destinationSheet.Cells[$_, $fromColumn].Value = $sourceWorksheet.Name} } + #Update where next insertion will go. + $row = $destinationSheet.Dimension.Rows + 1 + if ($HideSource) {$sourceWorksheet.Hidden = [OfficeOpenXml.eWorkSheetHidden]::Hidden} + } + + #We accept a bunch of parameters work to pass on to Export-excel ( Autosize, Autofilter, boldtopRow Freeze ); if we have any of those call export-excel otherwise close the package here. + $params = @{} + $PSBoundParameters + 'Path', 'Clearsheet', 'NoHeader', 'FromLabel', 'LabelBlocks', 'HideSource', + 'Title', 'TitleFillPattern', 'TitleBackgroundColor', 'TitleBold', 'TitleSize' | ForEach-Object {[void]$params.Remove($_)} + if ($params.Keys.Count) { + $params.WorkSheetName = $WorkSheetName + $params.ExcelPackage = $ExcelPackage + Export-Excel @Params + } + else { + Close-ExcelPackage -ExcelPackage $ExcelPackage + $ExcelPackage.Dispose() + $ExcelPackage = $null + } } \ No newline at end of file diff --git a/Merge-worksheet.ps1 b/Merge-worksheet.ps1 index d4ba4c5..b052731 100644 --- a/Merge-worksheet.ps1 +++ b/Merge-worksheet.ps1 @@ -1,476 +1,476 @@ -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 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. - .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 changed size will be highlighed in the output sheet. Changes to dates or other attributes will be ignored. - .Example - 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( - #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, - - #Object to compare if a worksheet is NOT being used. - [parameter(ParameterSetName='D',Mandatory=$true)] - [parameter(ParameterSetName='E',Mandatory=$true)] - [Alias('RefObject')] - $ReferenceObject , - #Object to compare if a worksheet is NOT being used. - [parameter(ParameterSetName='D',Mandatory=$true,Position=1)] - [Alias('DiffObject')] - $DifferenceObject , - [parameter(ParameterSetName='D',Position=2)] - [parameter(ParameterSetName='E',Position=3)] - $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 pipeline). - [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 } - if ($DiffPrefix -eq "=>" ) { - $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 - } -} - -Function Merge-MultipleSheets { - <# - .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 - Merge-MultipleSheets 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 - Merge-MultipleSheets then calls Merge-Worsheet with this result and sheet B, comparing 'Name', 'Displayname' and 'Start mode' columns on each side - which outputs _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, in this case "DisplayName" and "Start" are renamed to reflect their source, - so become "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 will not 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 if 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. - [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 -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 - } - - #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") - 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) - 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 - $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) { - 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 - } -} +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 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. + .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 changed size will be highlighed in the output sheet. Changes to dates or other attributes will be ignored. + .Example + 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( + #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, + + #Object to compare if a worksheet is NOT being used. + [parameter(ParameterSetName='D',Mandatory=$true)] + [parameter(ParameterSetName='E',Mandatory=$true)] + [Alias('RefObject')] + $ReferenceObject , + #Object to compare if a worksheet is NOT being used. + [parameter(ParameterSetName='D',Mandatory=$true,Position=1)] + [Alias('DiffObject')] + $DifferenceObject , + [parameter(ParameterSetName='D',Position=2)] + [parameter(ParameterSetName='E',Position=3)] + $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 pipeline). + [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 } + if ($DiffPrefix -eq "=>" ) { + $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 + } +} + +Function Merge-MultipleSheets { + <# + .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 + Merge-MultipleSheets 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 + Merge-MultipleSheets then calls Merge-Worsheet with this result and sheet B, comparing 'Name', 'Displayname' and 'Start mode' columns on each side + which outputs _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, in this case "DisplayName" and "Start" are renamed to reflect their source, + so become "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 will not 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 if 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. + [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 -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 + } + + #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") + 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) + 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 + $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) { + 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 + } +} diff --git a/Open-ExcelPackage.ps1 b/Open-ExcelPackage.ps1 index 5aea5b6..00dc7ba 100644 --- a/Open-ExcelPackage.ps1 +++ b/Open-ExcelPackage.ps1 @@ -1,67 +1,67 @@ -Function Open-ExcelPackage { -<# -.Synopsis - Returns an Excel Package Object with for the specified XLSX ile -.Example - $excel = Open-ExcelPackage -path $xlPath - $sheet1 = $excel.Workbook.Worksheets["sheet1"] - Set-Format -Address $sheet1.Cells["E1:S1048576"], $sheet1.Cells["V1:V1048576"] -NFormat ([cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern) - Close-ExcelPackage $excel -Show - - This will open the file at $xlPath, select sheet1 apply formatting to two blocks of the sheet and save the package, and launch it in Excel. -#> - [OutputType([OfficeOpenXml.ExcelPackage])] - Param ( - #The Path to the file to open - [Parameter(Mandatory=$true)]$Path, - #If specified, any running instances of Excel will be terminated before opening the file. - [switch]$KillExcel, - #By default open only opens an existing file; -Create instructs it to create a new file if required. - [switch]$Create - ) - - if($KillExcel) { - Get-Process -Name "excel" -ErrorAction Ignore | Stop-Process - while (Get-Process -Name "excel" -ErrorAction Ignore) {} - } - - $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) - #If -Create was not specified only open the file if it exists already (send a warning if it doesn't exist). - if ($Create) { - #Create the directory if required. - $targetPath = Split-Path -Parent -Path $Path - if (!(Test-Path -Path $targetPath)) { - Write-Debug "Base path $($targetPath) does not exist, creating" - $null = New-item -ItemType Directory -Path $targetPath -ErrorAction Ignore - } - New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Path - } - elseif (Test-Path -Path $path) {New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Path } - else {Write-Warning "Could not find $path" } - } - -Function Close-ExcelPackage { -<# -.Synopsis - Closes an Excel Package, saving, saving under a new name or abandoning changes and opening the file in Excel as required. -#> - Param ( - #File to close. - [parameter(Mandatory=$true, ValueFromPipeline=$true)] - [OfficeOpenXml.ExcelPackage]$ExcelPackage, - #Open the file. - [switch]$Show, - #Abandon the file without saving. - [Switch]$NoSave, - #Save file with a new name (ignored if -NoSave Specified). - $SaveAs - ) - if ( $NoSave) {$ExcelPackage.Dispose()} - else { - if ($SaveAs) {$ExcelPackage.SaveAs( $SaveAs ) } - Else {$ExcelPackage.Save(); $SaveAs = $ExcelPackage.File.FullName } - $ExcelPackage.Dispose() - if ($show) {Start-Process -FilePath $SaveAs } - } -} - +Function Open-ExcelPackage { +<# +.Synopsis + Returns an Excel Package Object with for the specified XLSX ile +.Example + $excel = Open-ExcelPackage -path $xlPath + $sheet1 = $excel.Workbook.Worksheets["sheet1"] + Set-Format -Address $sheet1.Cells["E1:S1048576"], $sheet1.Cells["V1:V1048576"] -NFormat ([cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern) + Close-ExcelPackage $excel -Show + + This will open the file at $xlPath, select sheet1 apply formatting to two blocks of the sheet and save the package, and launch it in Excel. +#> + [OutputType([OfficeOpenXml.ExcelPackage])] + Param ( + #The Path to the file to open + [Parameter(Mandatory=$true)]$Path, + #If specified, any running instances of Excel will be terminated before opening the file. + [switch]$KillExcel, + #By default open only opens an existing file; -Create instructs it to create a new file if required. + [switch]$Create + ) + + if($KillExcel) { + Get-Process -Name "excel" -ErrorAction Ignore | Stop-Process + while (Get-Process -Name "excel" -ErrorAction Ignore) {} + } + + $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + #If -Create was not specified only open the file if it exists already (send a warning if it doesn't exist). + if ($Create) { + #Create the directory if required. + $targetPath = Split-Path -Parent -Path $Path + if (!(Test-Path -Path $targetPath)) { + Write-Debug "Base path $($targetPath) does not exist, creating" + $null = New-item -ItemType Directory -Path $targetPath -ErrorAction Ignore + } + New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Path + } + elseif (Test-Path -Path $path) {New-Object -TypeName OfficeOpenXml.ExcelPackage -ArgumentList $Path } + else {Write-Warning "Could not find $path" } + } + +Function Close-ExcelPackage { +<# +.Synopsis + Closes an Excel Package, saving, saving under a new name or abandoning changes and opening the file in Excel as required. +#> + Param ( + #File to close. + [parameter(Mandatory=$true, ValueFromPipeline=$true)] + [OfficeOpenXml.ExcelPackage]$ExcelPackage, + #Open the file. + [switch]$Show, + #Abandon the file without saving. + [Switch]$NoSave, + #Save file with a new name (ignored if -NoSave Specified). + $SaveAs + ) + if ( $NoSave) {$ExcelPackage.Dispose()} + else { + if ($SaveAs) {$ExcelPackage.SaveAs( $SaveAs ) } + Else {$ExcelPackage.Save(); $SaveAs = $ExcelPackage.File.FullName } + $ExcelPackage.Dispose() + if ($show) {Start-Process -FilePath $SaveAs } + } +} + From a3693a7fafa86c59144e55ea12864c3f943f9d8b Mon Sep 17 00:00:00 2001 From: jhoneill Date: Mon, 25 Jun 2018 14:06:43 +0100 Subject: [PATCH 05/11] Many changes. Detailed on Readme.md under what's new --- AddConditionalFormatting.ps1 | 30 +- Export-Excel.Tests.ps1 | 694 ++++++++++++++++++++++++---- Export-Excel.ps1 | 866 ++++++++++++++++++++--------------- ImportExcel.psm1 | 78 ---- Old_Export-Excel.Tests.ps1 | 94 ++++ Open-ExcelPackage.ps1 | 2 +- README.md | 37 ++ SetFormat.ps1 | 2 +- 8 files changed, 1252 insertions(+), 551 deletions(-) create mode 100644 Old_Export-Excel.Tests.ps1 diff --git a/AddConditionalFormatting.ps1 b/AddConditionalFormatting.ps1 index 2b54282..6d35dbd 100644 --- a/AddConditionalFormatting.ps1 +++ b/AddConditionalFormatting.ps1 @@ -1,22 +1,22 @@ Function Add-ConditionalFormatting { -<# - .Synopsis - Adds contitional formatting to worksheet. - .Example - $excel = $avdata | Export-Excel -Path (Join-path $FilePath "\Machines.XLSX" ) -WorksheetName "Server Anti-Virus" -AutoSize -FreezeTopRow -AutoFilter -PassThru + <# + .Synopsis + Adds contitional formatting to worksheet. + .Example + $excel = $avdata | Export-Excel -Path (Join-path $FilePath "\Machines.XLSX" ) -WorksheetName "Server Anti-Virus" -AutoSize -FreezeTopRow -AutoFilter -PassThru - Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "b2:b1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "2003" - Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "i2:i1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "Disabled" - $excel.Workbook.Worksheets[1].Cells["D1:G1048576"].Style.Numberformat.Format = [cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern - $excel.Workbook.Worksheets[1].Row(1).style.font.bold = $true - $excel.Save() ; $excel.Dispose() + Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "b2:b1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "2003" + Add-ConditionalFormatting -WorkSheet $excel.Workbook.Worksheets[1] -Address "i2:i1048576" -ForeGroundColor "RED" -RuleType ContainsText -ConditionValue "Disabled" + $excel.Workbook.Worksheets[1].Cells["D1:G1048576"].Style.Numberformat.Format = [cultureinfo]::CurrentCulture.DateTimeFormat.ShortDatePattern + $excel.Workbook.Worksheets[1].Row(1).style.font.bold = $true + $excel.Save() ; $excel.Dispose() - Here Export-Excel is called with the -passThru parameter so the Excel Package object is stored in $Excel - The desired worksheet is selected and the then columns B and i are conditially formatted (excluding the top row) to show red text if - the columns contain "2003" or "Disabled respectively. A fixed date formats are then applied to columns D..G, and the top row is formatted. - Finally the workbook is saved and the Excel object closed. + Here Export-Excel is called with the -passThru parameter so the Excel Package object is stored in $Excel + The desired worksheet is selected and the then columns B and i are conditially formatted (excluding the top row) to show red text if + the columns contain "2003" or "Disabled respectively. A fixed date formats are then applied to columns D..G, and the top row is formatted. + Finally the workbook is saved and the Excel object closed. -#> + #> Param ( #The worksheet where the format is to be applied [Parameter(Mandatory = $true, ParameterSetName = "NamedRule")] diff --git a/Export-Excel.Tests.ps1 b/Export-Excel.Tests.ps1 index c59ae7f..c5a77db 100644 --- a/Export-Excel.Tests.ps1 +++ b/Export-Excel.Tests.ps1 @@ -1,94 +1,620 @@ -#Requires -Modules Pester -#Requires -Modules Assert +#Requires -Modules Pester $here = Split-Path -Parent $MyInvocation.MyCommand.Path -$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' + +Import-Module $here -Force -Import-Module $here -Force +if (Get-process -Name Excel,xlim -ErrorAction SilentlyContinue) { Write-Warning -Message "You need to close Excel before running the tests." ; return} +Describe ExportExcel { + + Context "#Example 1 # Creates and opens a file with the right number of rows and columns" { + $path = "$env:TEMP\Test.xlsx" + Remove-item -Path $path -ErrorAction SilentlyContinue + $processes = Get-Process + $propertyNames = $Processes[0].psobject.properties.name + $rowcount = $Processes.Count + $Processes | Export-Excel $path -show + + it "Created a new file " { + Test-Path -Path $path -ErrorAction SilentlyContinue | should be $true + } -$WarningPreference = 'SilentlyContinue' -$ProgressPreference = 'SilentlyContinue' + it "Started Excel to display the file " { + Get-process -Name Excel,xlim -ErrorAction SilentlyContinue | should not benullorempty + } + + Start-Sleep -Seconds 5 ; -Function Test-isNumeric { - Param ( - [Parameter(ValueFromPipeline)]$x - ) + #Open-ExcelPackage with -Create is tested in Export-Excel + #This is a test of using it with -KillExcel + #TODO Need to test opening pre-existing file with no -create switch (and graceful failure when file does not exist) somewhere else + $Excel = Open-ExcelPackage -Path $path -KillExcel + it "Killed Excel when Open-Excelpackage was told to " { + Get-process -Name Excel,xlim -ErrorAction SilentlyContinue | should benullorempty + } - Return $x -is [byte] -or $x -is [int16] -or $x -is [int32] -or $x -is [int64] ` - -or $x -is [sbyte] -or $x -is [uint16] -or $x -is [uint32] -or $x -is [uint64] ` - -or $x -is [float] -or $x -is [double] -or $x -is [decimal] -} + it "Created 1 worksheet " { + $Excel.Workbook.Worksheets.count | should be 1 + } -$fakeData = [PSCustOmobject]@{ - Property_1_Date = (Get-Date).ToString('d') # US '10/16/2017' BE '16/10/2107' - Property_2_Formula = '=SUM(G2:H2)' - Property_3_String = 'My String' - Property_4_String = 'a' - Property_5_IPAddress = '10.10.25.5' - Property_6_Number = '0' - Property_7_Number = '5' - Property_8_Number = '007' - Property_9_Number = (33).ToString('F2') # US '33.00' BE '33,00' - Property_10_Number = (5/3).ToString('F2') # US '1.67' BE '1,67' - Property_11_Number = (15999998/3).ToString('N2') # US '5,333,332.67' BE '5.333.332,67' - Property_12_Number = '1.555,83' - Property_13_PhoneNr = '+32 44' - Property_14_PhoneNr = '+32 4 4444 444' - Property_15_PhoneNr = '+3244444444' -} - -$Path = 'Test.xlsx' - -Describe 'Export-Excel' { - in $TestDrive { - Describe 'Number conversion' { - Context 'numerical values expected' { - #region Create test file - $fakeData | Export-Excel -Path $Path - - $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) - $Excel = New-Object OfficeOpenXml.ExcelPackage $Path - $Worksheet = $Excel.Workbook.WorkSheets[1] - #endregion - - it 'zero' { - $fakeData.Property_6_Number | Should -BeExactly '0' - $Worksheet.Cells[2, 6].Text | Should -BeExactly $fakeData.Property_6_Number - $Worksheet.Cells[2, 6].Value | Test-isNumeric | Should -Be $true - } - - It 'regular number' { - $fakeData.Property_7_Number | Should -BeExactly '5' - $Worksheet.Cells[2, 7].Text | Should -BeExactly $fakeData.Property_7_Number - $Worksheet.Cells[2, 7].Value | Test-isNumeric | Should -Be $true - } - - It 'number starting with zero' { - $fakeData.Property_8_Number | Should -BeExactly '007' - $Worksheet.Cells[2, 8].Text | Should -BeExactly '7' - $Worksheet.Cells[2, 8].Value | Test-isNumeric | Should -Be $true - } - - It 'decimal number' { - # US '33.00' BE '33,00' - $fakeData.Property_9_Number | Should -BeExactly (33).ToString('F2') - $Worksheet.Cells[2, 9].Text | Should -BeExactly '33' - $Worksheet.Cells[2, 9].Value | Test-isNumeric | Should -Be $true - - # US '1.67' BE '1,67' - $fakeData.Property_10_Number | Should -BeExactly (5/3).ToString('F2') - $Worksheet.Cells[2, 10].Text | Should -BeExactly $fakeData.Property_10_Number - $Worksheet.Cells[2, 10].Value | Test-isNumeric | Should -Be $true - } - - It 'thousand seperator and decimal number' { - # US '5,333,332.67' BE '5.333.332,67' - # Excel BE '5333332,67' - $fakeData.Property_11_Number | Should -BeExactly (15999998/3).ToString('N2') - $Worksheet.Cells[2, 11].Text | Should -BeExactly $fakeData.Property_11_Number - $Worksheet.Cells[2, 11].Value | Test-isNumeric | Should -Be $true - } + $ws = $Excel.Workbook.Worksheets[1] + it "Created the worksheet with the expected name, number of rows and number of columns " { + $ws.Name | should be "sheet1" + $ws.Dimension.Columns | should be $propertyNames.Count + $ws.Dimension.Rows | should be ($rowcount + 1) + } + + $headingNames = $ws.cells["1:1"].Value + it "Created the worksheet with the correct header names " { + foreach ($p in $propertyNames) { + $headingnames -contains $p | should be $true } } + + it "Formatted the process StartTime field as 'local short date' " { + $STHeader = $ws.cells["1:1"].where({$_.Value -eq "StartTime"})[0] + $STCell = $STHeader.Address -replace '1$','2' + $ws.cells[$stcell].Style.Numberformat.NumFmtID | should be 22 + } + + it "Formatted the process ID field as 'General' " { + $IDHeader = $ws.cells["1:1"].where({$_.Value -eq "ID"})[0] + $IDCell = $IDHeader.Address -replace '1$','2' + $ws.cells[$IDcell].Style.Numberformat.NumFmtID | should be 0 + } } -} \ No newline at end of file + + Context " # NoAliasOrScriptPropeties -ExcludeProperty and -DisplayPropertySet work" { + $path = "$env:TEMP\Test.xlsx" + Remove-item -Path $path -ErrorAction SilentlyContinue + $processes = Get-Process + $propertyNames = $Processes[0].psobject.properties.where( {$_.MemberType -eq 'Property'}).name + $rowcount = $Processes.Count + #TestCreating a range with a name which needs illegal chars removing + $warnVar = $null + $Processes | Export-Excel $path -NoAliasOrScriptPropeties -RangeName "No Spaces" -WarningVariable warnvar -WarningAction SilentlyContinue + + $Excel = Open-ExcelPackage -Path $path + $ws = $Excel.Workbook.Worksheets[1] + it "Created a new file with alias & Script Properties removed. " { + $ws.Name | should be "sheet1" + $ws.Dimension.Columns | should be $propertyNames.Count + $ws.Dimension.Rows | should be ($rowcount + 1 ) # +1 for the header. + } + it "Created a Range - even though the name given was invalid. " { + $ws.Names["No_spaces"] | should not beNullOrEmpty + $ws.Names["No_spaces"].End.Column | should be $propertyNames.Count + $ws.names["No_spaces"].End.Row | should be ($rowcount + 1 ) # +1 for the header. + $warnVar.Count | should be 1 + } + #This time use clearsheet instead of deleting the file + $Processes | Export-Excel $path -NoAliasOrScriptPropeties -ExcludeProperty SafeHandle, modules, MainModule, StartTime, Threads -ClearSheet + + $Excel = Open-ExcelPackage -Path $path + $ws = $Excel.Workbook.Worksheets[1] + it "Created a new file with a further 5 properties excluded and cleared the old sheet " { + $ws.Name | should be "sheet1" + $ws.Dimension.Columns | should be ($propertyNames.Count - 5) + $ws.Dimension.Rows | should be ($rowcount + 1) # +1 for the header + } + + $propertyNames = $Processes[0].psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames + Remove-item -Path $path -ErrorAction SilentlyContinue + $Processes | Export-Excel $path -DisplayPropertySet + + $Excel = Open-ExcelPackage -Path $path + $ws = $Excel.Workbook.Worksheets[1] + it "Created a new file with just the members of the Display Property Set " { + $ws.Name | should be "sheet1" + $ws.Dimension.Columns | should be $propertyNames.Count + $ws.Dimension.Rows | should be ($rowcount + 1) + } + } + + Context "#Example 2 # Exports a list of numbers and applies number format " { + + $path = "$env:TEMP\Test.xlsx" + Remove-item -Path $path -ErrorAction SilentlyContinue + #testing -ReturnRange switch + $returnedRange = Write-Output -1 668 34 777 860 -0.5 119 -0.1 234 788 | Export-Excel -NumberFormat '[Blue]$#,##0.00;[Red]-$#,##0.00' -Path $path -ReturnRange + it "Created a new file and returned the expected range " { + Test-Path -Path $path -ErrorAction SilentlyContinue | should be $true + $returnedRange | should be "A1:A10" + } + + $Excel = Open-ExcelPackage -Path $path + it "Created 1 worksheet " { + $Excel.Workbook.Worksheets.count | should be 1 + } + + $ws = $Excel.Workbook.Worksheets[1] + it "Created the worksheet with the expected name, number of rows and number of columns " { + $ws.Name | should be "sheet1" + $ws.Dimension.Columns | should be 1 + $ws.Dimension.Rows | should be 10 + } + + it "Set the default style for the sheet as expected " { + $ws.cells.Style.Numberformat.Format | should be '[Blue]$#,##0.00;[Red]-$#,##0.00' + } + + it "Set the default style and value for Cell A1 as expected " { + $ws.cells[1,1].Style.Numberformat.Format | should be '[Blue]$#,##0.00;[Red]-$#,##0.00' + $ws.cells[1,1].Value | should be -1 + } + } + + Context "#Examples 3 & 4 # Setting cells for different data types Also added test for URI type" { + + $path = "$env:TEMP\Test.xlsx" + Remove-item -Path $path -ErrorAction SilentlyContinue + [PSCustOmobject][Ordered]@{ + Date = Get-Date + Formula1 = '=SUM(F2:G2)' + String1 = 'My String' + String2 = 'a' + IPAddress = '10.10.25.5' + Number1 = '07670' + Number2 = '0,26' + Number3 = '1.555,83' + Number4 = '1.2' + Number5 = '-31' + PhoneNr1 = '+32 44' + PhoneNr2 = '+32 4 4444 444' + PhoneNr3 = '+3244444444' + Link = [uri]"https://github.com/dfinke/ImportExcel" + } | Export-Excel -NoNumberConversion IPAddress, Number1 -Path $path + it "Created a new file " { + Test-Path -Path $path -ErrorAction SilentlyContinue | should be $true + } + + $Excel = Open-ExcelPackage -Path $path + it "Created 1 worksheet " { + $Excel.Workbook.Worksheets.count | should be 1 + } + + $ws = $Excel.Workbook.Worksheets[1] + it "Created the worksheet with the expected name, number of rows and number of columns " { + $ws.Name | should be "sheet1" + $ws.Dimension.Columns | should be 14 + $ws.Dimension.Rows | should be 2 + } + + it "Set a date in Cell A2 " { + $ws.Cells[2,1].Value.Gettype().name | should be 'DateTime' + } + + it "Set a formula in Cell B2 " { + $ws.Cells[2,2].Formula | should be '=SUM(F2:G2)' + } + + it "Set strings in Cells E2 and F2 " { + $ws.Cells[2,5].Value.GetType().name | should be 'String' + $ws.Cells[2,6].Value.GetType().name | should be 'String' + } + + it "Set a number in Cell I2 " { + ($ws.Cells[2,9].Value -is [valuetype] ) | should be $true + } + + it "Set a hyperlink in Cell N2 " { + $ws.Cells[2,14].Hyperlink | should be "https://github.com/dfinke/ImportExcel" + } + } + + + Context "# # Setting cells for different data types with -noHeader" { + + $path = "$env:TEMP\Test.xlsx" + Remove-item -Path $path -ErrorAction SilentlyContinue + [PSCustOmobject][Ordered]@{ + Date = Get-Date + Formula1 = '=SUM(F1:G1)' + String1 = 'My String' + String2 = 'a' + IPAddress = '10.10.25.5' + Number1 = '07670' + Number2 = '0,26' + Number3 = '1.555,83' + Number4 = '1.2' + Number5 = '-31' + PhoneNr1 = '+32 44' + PhoneNr2 = '+32 4 4444 444' + PhoneNr3 = '+3244444444' + Link = [uri]"https://github.com/dfinke/ImportExcel" + } | Export-Excel -NoNumberConversion IPAddress, Number1 -Path $path -NoHeader + it "Created a new file " { + Test-Path -Path $path -ErrorAction SilentlyContinue | should be $true + } + + $Excel = Open-ExcelPackage -Path $path + it "Created 1 worksheet " { + $Excel.Workbook.Worksheets.count | should be 1 + } + + $ws = $Excel.Workbook.Worksheets[1] + it "Created the worksheet with the expected name, number of rows and number of columns " { + $ws.Name | should be "sheet1" + $ws.Dimension.Columns | should be 14 + $ws.Dimension.Rows | should be 1 + } + + it "Set a date in Cell A1 " { + $ws.Cells[1,1].Value.Gettype().name | should be 'DateTime' + } + + it "Set a formula in Cell B1 " { + $ws.Cells[1,2].Formula | should be '=SUM(F1:G1)' + } + + it "Set strings in Cells E1 and F1 " { + $ws.Cells[1,5].Value.GetType().name | should be 'String' + $ws.Cells[1,6].Value.GetType().name | should be 'String' + } + + it "Set a number in Cell I1 " { + ($ws.Cells[1,9].Value -is [valuetype] ) | should be $true + } + + it "Set a hyperlink in Cell N1 " { + $ws.Cells[1,14].Hyperlink | should be "https://github.com/dfinke/ImportExcel" + } + } + + Context "#Example 5 # Adding a single conditional format " { + ### TODO New-ConditionalText doesn't a lot of options in Add-ConditionalFormat. + # It would be good to pull the logic out of Export-Excel and have EE call Add-ConditionalFormat. + $ct = New-ConditionalText -ConditionalType GreaterThan 525 -ConditionalTextColor DarkRed -BackgroundColor LightPink + it "Created a Conditional format description " { + $ct.BackgroundColor -is [System.Drawing.Color] | should be $true + $ct.ConditionalTextColor -is [System.Drawing.Color] | should be $true + $ct.ConditionalType -in [enum]::GetNames( [OfficeOpenXml.ConditionalFormatting.eExcelConditionalFormattingRuleType] ) | + should be $true + } + + $path = "$env:TEMP\Test.xlsx" + Remove-item -Path $path -ErrorAction SilentlyContinue + Write-Output 489 668 299 777 860 151 119 497 234 788 | Export-Excel -Path $path -ConditionalText $ct + + it "Created a new file " { + Test-Path -Path $path -ErrorAction SilentlyContinue | should be $true + } + + #ToDo need to test applying conitional formatting to a pre-existing worksheet + $Excel = Open-ExcelPackage -Path $path + $ws = $Excel.Workbook.Worksheets[1] + + it "Added one block of conditional formating for the data range " { + $ws.ConditionalFormatting.Count | should be 1 + $ws.ConditionalFormatting[0].Address | should be ($ws.Dimension.Address) + } + + $cf = $ws.ConditionalFormatting[0] + it "Set the conditional formatting properties correctly " { + $cf.Formula | should be $ct.Text + $cf.Type.ToString() | should be $ct.ConditionalType + #$cf.Style.Fill.BackgroundColor | should be $ct.BackgroundColor + # $cf.Style.Font.Color | should be $ct.ConditionalTextColor - have to compare r.g.b + + + } + } + + Context "#Example 6 # Adding multiple conditional formats using short form syntax. " { + #this is a test of adding more than one conditional block and using the minimal syntax for new-ConditionalText = + $path = "$env:TEMP\Test.xlsx" + Remove-item -Path $path -ErrorAction SilentlyContinue + + #Testing -Passthrough + $Excel = Get-Service | Select-Object Name, Status, DisplayName, ServiceName | + Export-Excel $path -PassThru -ConditionalText $( + New-ConditionalText Stop DarkRed LightPink + New-ConditionalText Running Blue Cyan + ) + $ws = $Excel.Workbook.Worksheets[1] + it "Added two blocks of conditional formating for the data range " { + $ws.ConditionalFormatting.Count | should be 2 + $ws.ConditionalFormatting[0].Address | should be ($ws.Dimension.Address) + $ws.ConditionalFormatting[1].Address | should be ($ws.Dimension.Address) + } + it "Set the conditional formatting properties correctly " { + $ws.ConditionalFormatting[0].Text | should be "Stop" + $ws.ConditionalFormatting[1].Text | should be "Running" + $ws.ConditionalFormatting[0].Type | should be "ContainsText" + $ws.ConditionalFormatting[1].Type | should be "ContainsText" + #Add RGB Comparison + } + Close-ExcelPackage -ExcelPackage $Excel + } + + context "#Example 7 # Update-FirstObjectProperties works "{ + $Array = @() + + $Obj1 = [PSCustomObject]@{ + Member1 = 'First' + Member2 = 'Second' + } + + $Obj2 = [PSCustomObject]@{ + Member1 = 'First' + Member2 = 'Second' + Member3 = 'Third' + } + + $Obj3 = [PSCustomObject]@{ + Member1 = 'First' + Member2 = 'Second' + Member3 = 'Third' + Member4 = 'Fourth' + } + + $Array = $Obj1, $Obj2, $Obj3 + $newarray = $Array | Update-FirstObjectProperties + it "Outputs as many objects as it input " { + $newarray.Count | should be $Array.Count + } + it "Added properties to item 0 " { + $newarray[0].psobject.Properties.name.Count | should be 4 + $newarray[0].Member1 | should be 'First' + $newarray[0].Member2 | should be 'Second' + $newarray[0].Member3 | should beNullOrEmpty + $newarray[0].Member4 | should beNullOrEmpty + } + } + + Context "#Examples 8 & 9 # Adding Pivot tables and charts from parameters" { + $path = "$env:TEMP\Test.xlsx" + #This time we are not deleting the XLSX file so this should create a new, named, sheet. + $Excel = Get-Process | Select-Object -first 50 -Property Name,cpu,pm,handles,company | Export-Excel $path -WorkSheetname Processes -PassThru + #Testing -passthru and adding the Pivot as a second step. Want to save and re-open it ... + Export-Excel -ExcelPackage $Excel -WorkSheetname Processes -IncludePivotTable -PivotRows Company -PivotData PM + + $Excel = Open-ExcelPackage $path + $PTws = $Excel.Workbook.Worksheets["ProcessesPivotTable"] + $wCount = $Excel.Workbook.Worksheets.Count + it "Added the named sheet and pivot table to the workbook " { + $PTws | should not beNullOrEmpty + $PTws.PivotTables.Count | should be 1 + $Excel.Workbook.Worksheets["Processes"] | should not beNullOrEmpty + $Excel.Workbook.Worksheets.Count | should beGreaterThan 2 + $excel.Workbook.Worksheets["Processes"].Dimension.rows | should be 51 #50 data + 1 header + } + $pt = $PTws.PivotTables[0] + it "Built the expected Pivot table " { + $pt.RowFields.Count | should be 1 + $pt.RowFields[0].Name | should be "Company" + $pt.DataFields.Count | should be 1 + $pt.DataFields[0].Function | should be "Count" + $pt.DataFields[0].Field.Name | should be "PM" + $PTws.Drawings.Count | should be 0 + } + #using the already open sheet add the pivot chart + $warnvar = $null + Export-Excel -ExcelPackage $Excel -WorkSheetname Processes -IncludePivotTable -PivotRows Company -PivotData PM -IncludePivotChart -ChartType PieExploded3D -WarningAction SilentlyContinue -WarningVariable warnvar + $Excel = Open-ExcelPackage $path + it "Added a chart to the pivot table without rebuilding " { + $ws = $Excel.Workbook.Worksheets["ProcessesPivotTable"] + $Excel.Workbook.Worksheets.Count | should be $wCount + $ws.Drawings.count | should be 1 + $ws.Drawings[0].ChartType.ToString() | should be "PieExploded3D" + } + it "Generated a message on re-processing the Pivot table " { + $warnVar | Should not beNullOrEmpty + } + $warnVar = $null + Get-Process | Select-Object -Last 50 -Property Name,cpu,pm,handles,company | Export-Excel $path -WorkSheetname Processes -Append -IncludePivotTable -PivotRows Company -PivotData PM -IncludePivotChart -ChartType PieExploded3D -WarningAction SilentlyContinue -WarningVariable warnvar + $Excel = Open-ExcelPackage $path + $pt = $Excel.Workbook.Worksheets["ProcessesPivotTable"].PivotTables[0] + it "Appended to the Worksheet and Extended the Pivot table " { + $Excel.Workbook.Worksheets.Count | should be $wCount + $excel.Workbook.Worksheets["Processes"].Dimension.rows | should be 101 #appended 50 rows to the previous total + $pt.CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref | + should be "A1:E101" + } + it "Generated a message on extending the Pivot table " { + $warnVar | Should not beNullOrEmpty + } + } + + Context " # Add-Worksheet inserted sheets, moved them correctly, and copied a sheet" { + $path = "$env:TEMP\Test.xlsx" + + $Excel = Open-ExcelPackage $path + #At this point Sheets should be in the order Sheet1, Processes, ProcessesPivotTable + $null = Add-WorkSheet -ExcelPackage $Excel -WorkSheetname "Processes" -MoveToEnd # order now Sheet1, ProcessesPivotTable, Processes + $null = Add-WorkSheet -ExcelPackage $Excel -WorkSheetname "NewSheet" -MoveAfter "*" -CopySource ($excel.Workbook.Worksheets["Sheet1"]) # Now its NewSheet, Sheet1, ProcessesPivotTable, Processes + $null = Add-WorkSheet -ExcelPackage $Excel -WorkSheetname "Sheet1" -MoveAfter "*" # Now its NewSheet, ProcessesPivotTable, Processes, Sheet1 + $null = Add-WorkSheet -ExcelPackage $Excel -WorkSheetname "Another" -MoveToStart # Now its Another, NewSheet, ProcessesPivotTable, Processes, Sheet1 + $null = Add-WorkSheet -ExcelPackage $Excel -WorkSheetname "OneLast" -MoveBefore "ProcessesPivotTable" # Now its Another, NewSheet, Onelast, ProcessesPivotTable, Processes, Sheet1 + Close-ExcelPackage $Excel + + $Excel = Open-ExcelPackage $path + + it "Got the Sheets in the right order " { + $excel.Workbook.Worksheets[1].Name | should be "Another" + $excel.Workbook.Worksheets[2].Name | should be "NewSheet" + $excel.Workbook.Worksheets[3].Name | should be "Onelast" + $excel.Workbook.Worksheets[4].Name | should be "ProcessesPivotTable" + $excel.Workbook.Worksheets[5].Name | should be "Processes" + $excel.Workbook.Worksheets[6].Name | should be "Sheet1" + } + + it "Cloned 'Sheet1' to 'NewSheet' "{ + $newWs = $excel.Workbook.Worksheets["NewSheet"] + $newWs.Dimension.Address | should be ($excel.Workbook.Worksheets["Sheet1"].Dimension.Address) + $newWs.ConditionalFormatting.Count | should be ($excel.Workbook.Worksheets["Sheet1"].ConditionalFormatting.Count) + $newWs.ConditionalFormatting[0].Address.Address | should be ($excel.Workbook.Worksheets["Sheet1"].ConditionalFormatting[0].Address.Address) + $newWs.ConditionalFormatting[0].Formula | should be ($excel.Workbook.Worksheets["Sheet1"].ConditionalFormatting[0].Formula) + } + + } + + Context " # Create and append with Start row and Start Column, inc ranges and Pivot table" { + $path = "$env:TEMP\Test.xlsx" + #Catch warning + $warnVar = $null + #Test Append with no existing sheet. + get-process | Select-Object -first 10 -Property Name,cpu,pm,handles,company | export-excel -StartRow 3 -StartColumn 3 -AutoFilter -AutoNameRange -BoldTopRow -IncludePivotTable -PivotRows Company -PivotData PM -Path $path -WorkSheetname withOffset -append + get-process | Select-Object -last 10 -Property Name,cpu,pm,handles,company | export-excel -StartRow 3 -StartColumn 3 -AutoFilter -AutoNameRange -BoldTopRow -IncludePivotTable -PivotRows Company -PivotData PM -Path $path -WorkSheetname withOffset -append -WarningAction SilentlyContinue -WarningVariable warnvar + $Excel = Open-ExcelPackage $path + $dataWs = $Excel.Workbook.Worksheets["withOffset"] + $pt = $Excel.Workbook.Worksheets["withOffsetPivotTable"].PivotTables[0] + it "Created and appended to a sheet offset from the top left corner " { + $dataWs.Cells[1,1].Value | Should beNullOrEmpty + $dataWs.Cells[2,2].Value | Should beNullOrEmpty + $dataWs.Cells[3,3].Value | Should not beNullOrEmpty + $dataWs.Cells[3,3].Style.Font.Bold | Should be $true + $dataWs.Dimension.End.Row | Should be 23 + $dataWs.names[0].end.row | Should be 23 + $dataWs.names[0].name | Should be 'Name' + $dataWs.names.Count | Should be 6 + $dataWs.cells[$dataws.Dimension].AutoFilter | Should be true + $pt.CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref | + Should be "C3:G23" + } + it "Generated a message on extending the Pivot table " { + $warnVar | Should not beNullOrEmpty + } + } + + Context "#Example 11 # Create and append with title, inc ranges and Pivot table" { + $path = "$env:TEMP\Test.xlsx" + #Catch warning + $ptDef = [ordered]@{} + $ptDef += New-PivotTableDefinition -PivotTableName "PT1" -SourceWorkSheet 'Sheet1' -PivotRows "Status" -PivotData @{'Status' = 'Count'} -PivotFilter "StartType" -IncludePivotChart -ChartType BarClustered3D -ChartTitle "Services by status" -ChartHeight 512 -ChartWidth 768 -ChartRow 10 -ChartColumn 0 -NoLegend + $ptDef += New-PivotTableDefinition -PivotTableName "PT2" -SourceWorkSheet 'Sheet2' -PivotRows "Company" -PivotData @{'Company' = 'Count'} -IncludePivotChart -ChartType PieExploded3D -ShowPercent -WarningAction SilentlyContinue + + it "Built a pivot definition using New-PivotTableDefinition " { + $ptDef.PT1.SourceWorkSheet | Should be 'Sheet1' + $ptDef.PT1.PivotRows | Should be 'Status' + $ptDef.PT1.PivotData.Status | Should be 'Count' + $ptDef.PT1.PivotFilter | Should be 'StartType' + $ptDef.PT1.IncludePivotChart | Should be $true + $ptDef.PT1.ChartType.tostring() | Should be 'BarClustered3D' + } + Remove-Item -Path $path + $warnvar = $null + Get-Service | Select-Object -Property Status, Name, DisplayName, StartType | Export-Excel -Path $path -AutoSize -TableName "All Services" -TableStyle Medium1 -WarningAction SilentlyContinue -WarningVariable warnvar + Get-Process | Select-Object -Property Name, Company, Handles, CPU, VM | Export-Excel -Path $path -AutoSize -WorkSheetname 'sheet2' -TableName "Processes" -TableStyle Light1 -Title "Processes" -TitleFillPattern Solid -TitleBackgroundColor AliceBlue -TitleBold -TitleSize 22 -PivotTableDefinition $ptDef + $Excel = Open-ExcelPackage $path + $ws1 = $Excel.Workbook.Worksheets["Sheet1"] + $ws2 = $Excel.Workbook.Worksheets["Sheet2"] + + + it "Set Column widths (with autosize) " { + $ws1.Column(2).Width | Should not be $ws1.DefaultColWidth + $ws2.Column(1).width | Should not be $ws2.DefaultColWidth + } + + it "Added tables to both sheets (handling illegal chars) and a title in sheet 2 " { + $warnvar.count | Should be 1 + $ws1.tables.Count | Should be 1 + $ws2.tables.Count | Should be 1 + $ws1.Tables[0].Address.Start.Row | Should be 1 + $ws2.Tables[0].Address.Start.Row | Should be 2 #Title in row 1 + $ws1.Tables[0].Address.End.Address | Should be $ws1.Dimension.End.Address + $ws2.Tables[0].Address.End.Address | Should be $ws2.Dimension.End.Address + $ws2.Tables[0].Name | Should be "Processes" + $ws2.Tables[0].StyleName | Should be "TableStyleLight1" + $ws2.Cells["A1"].Value | Should be "Processes" + $ws2.Cells["A1"].Style.Font.Bold | Should be $true + $ws2.Cells["A1"].Style.Font.Size | Should be 22 + $ws2.Cells["A1"].Style.Fill.PatternType.tostring() | Should be "solid" + $ws2.Cells["A1"].Style.Fill.BackgroundColor.Rgb | Should be "fff0f8ff" + } + + $ptsheet1 = $Excel.Workbook.Worksheets["Pt1"] + $ptsheet2 = $Excel.Workbook.Worksheets["Pt2"] + $PT1 = $ptsheet1.PivotTables[0] + $PT2 = $ptsheet2.PivotTables[0] + $PC1 = $ptsheet1.Drawings[0] + $PC2 = $ptsheet2.Drawings[0] + it "Created the correct pivot tables and charts from the definitions. " { + + $PT1.CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref | + Should be ("A1:" + $ws1.Dimension.End.Address) + $PT2.CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref | + Should be ("A2:" + $ws2.Dimension.End.Address) #Title in row 1 + + $pt1.PageFields[0].Name | Should be 'StartType' + $pt1.RowFields[0].Name | Should be 'Status' + $pt1.DataFields[0].Field.name | Should be 'Status' + $pt1.DataFields[0].Function | Should be 'Count' + $pc1.ChartType | Should be 'BarClustered3D' + $pc1.From.Column | Should be 0 #chart 1 at 0,10 chart 2 at 4,0 (default) + $pc2.From.Column | Should be 4 + $pc1.From.Row | Should be 10 + $pc2.From.Row | Should be 0 + $pc1.Legend.Font | should beNullOrEmpty #Best check for legend removed. + $pc2.Legend.Font | should not beNullOrEmpty + $pc1.Title.Text | Should be 'Services by status' + $pc2.DataLabel.ShowPercent | should be $true + } + } + + Context "#Example 13 # Formatting and another way to do a pivot. " { + $path = "$env:TEMP\Test.xlsx" + Remove-Item $path + $excel = Get-Process | Select-Object -Property Name,Company,Handles,CPU,PM,NPM,WS | Export-Excel -Path $path -ClearSheet -WorkSheetname "Processes" -FreezeTopRowFirstColumn -PassThru + $sheet = $excel.Workbook.Worksheets["Processes"] + $sheet.Column(1) | Set-Format -Bold -AutoFit + $sheet.Column(2) | Set-Format -Width 29 -WrapText + $sheet.Column(3) | Set-Format -HorizontalAlignment Right -NFormat "#,###" + Set-Format -Address $sheet.Cells["E1:H1048576"] -HorizontalAlignment Right -NFormat "#,###" + Set-Format -Address $sheet.Column(4) -HorizontalAlignment Right -NFormat "#,##0.0" -Bold + Set-Format -Address $sheet.Row(1) -Bold -HorizontalAlignment Center + Add-ConditionalFormatting -WorkSheet $sheet -Range "D2:D1048576" -DataBarColor Red + Add-ConditionalFormatting -WorkSheet $sheet -Range "G2:G1048576" -RuleType GreaterThan -ConditionValue "104857600" -ForeGroundColor Red + foreach ($c in 5..9) {Set-Format $sheet.Column($c) -AutoFit } + Add-PivotTable -PivotTableName "PT_Procs" -ExcelPackage $excel -SourceWorkSheet "Processes" -PivotRows Company -PivotData @{'Name'='Count'} -IncludePivotChart -ChartType ColumnClustered -NoLegend + Close-ExcelPackage $excel + + $excel = Open-ExcelPackage $path + $sheet = $excel.Workbook.Worksheets["Processes"] + + it "Applied the formating" { + $sheet | should not beNullOrEmpty + $sheet.Column(1).wdith | should not be $sheet.DefaultColWidth + $sheet.Column(7).wdith | should not be $sheet.DefaultColWidth + $sheet.Column(1).style.font.bold | should be $true + $sheet.Column(2).style.wraptext | should be $true + $sheet.Column(2).width | should be 29 + $sheet.Column(3).style.horizontalalignment | should be 'right' + $sheet.Column(4).style.horizontalalignment | should be 'right' + $sheet.Cells["A1"].Style.HorizontalAlignment | should be 'Center' + $sheet.Cells['E2'].Style.HorizontalAlignment | should be 'right' + $sheet.Cells['A1'].Style.Font.Bold | should be $true + $sheet.Cells['D2'].Style.Font.Bold | should be $true + $sheet.Cells['E2'].style.numberformat.format | should be '#,###' + $sheet.Column(3).style.numberformat.format | should be '#,###' + $sheet.Column(4).style.numberformat.format | should be '#,##0.0' + $sheet.ConditionalFormatting.Count | should be 2 + $sheet.ConditionalFormatting[0].type | should be 'Databar' + $sheet.ConditionalFormatting[0].Color.name | should be 'ffff0000' + $sheet.ConditionalFormatting[0].Address.Address | should be 'D2:D1048576' + $sheet.ConditionalFormatting[1].type | should be 'GreaterThan' + $sheet.ConditionalFormatting[1].Formula | should be '104857600' + $sheet.ConditionalFormatting[1].Style.Font.Color.Color.Name | should be 'ffff0000' + } + it "Froze the panes" { + $sheet.view.Panes.Count | should be 3 + } + $ptsheet1 = $Excel.Workbook.Worksheets["Pt_procs"] + + it "Created the pivot table" { + $ptsheet1 | should not beNullOrEmpty + $ptsheet1.PivotTables[0].DataFields[0].Field.Name | should be "Name" + $ptsheet1.PivotTables[0].DataFields[0].Function | should be "Count" + $ptsheet1.PivotTables[0].RowFields[0].Name | should be "Company" + $ptsheet1.PivotTables[0].CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref | + Should be $sheet.Dimension.address + } + } + + ## To do + ## More pivot options & other FreezePanes settings ? + ## Charts + ## Style script block + ## Rezip ? + +} diff --git a/Export-Excel.ps1 b/Export-Excel.ps1 index d50204a..c6dddfe 100644 --- a/Export-Excel.ps1 +++ b/Export-Excel.ps1 @@ -18,6 +18,10 @@ Data to insert onto the worksheet - this is often provided from the pipeline. .PARAMETER ExcludeProperty Specifies properties which may exist in the target data but should not be placed on the worksheet. + .PARAMETER NoAliasOrScriptPropeties + Some objects duplicate properties with aliases, or have Script properties which take a long time to return a value and slow the export down, if specified this removes these properties + .PARAMETER DisplayPropertySet, + Many (but not all) objects have a hidden property named psStandardmembers with a child property DefaultDisplayPropertySet ; this parameter reduces the properties exported to those in this set. .PARAMETER Title Text of a title to be placed in Cell A1. .PARAMETER TitleBold @@ -329,6 +333,7 @@ #> [CmdletBinding(DefaultParameterSetName = 'Default')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] Param( [Parameter(ParameterSetName = "Default", Position = 0)] [Parameter(ParameterSetName = "Table" , Position = 0)] @@ -371,20 +376,16 @@ [Switch]$AutoFilter, [Switch]$BoldTopRow, [Switch]$NoHeader, + [ValidateScript( { + if (-not $_) { throw 'RangeName is null or empty.' } + elseif ($_[0] -notmatch '[a-z]') { throw 'RangeName starts with an invalid character.' } + else { $true } + })] [String]$RangeName, [ValidateScript( { - if ($_.Contains(' ')) { - throw 'Tablename has spaces.' - } - elseif (-not $_) { - throw 'Tablename is null or empty.' - } - elseif ($_[0] -notmatch '[a-z]') { - throw 'Tablename starts with an invalid character.' - } - else { - $true - } + if (-not $_) { throw 'Tablename is null or empty.' } + elseif ($_[0] -notmatch '[a-z]') { throw 'Tablename starts with an invalid character.' } + else { $true } })] [Parameter(ParameterSetName = 'Table' , Mandatory = $true)] [Parameter(ParameterSetName = 'PackageTable' , Mandatory = $true)] @@ -405,12 +406,13 @@ [Switch]$PassThru, [String]$Numberformat = 'General', [string[]]$ExcludeProperty, + [Switch]$NoAliasOrScriptPropeties, + [Switch]$DisplayPropertySet, [String[]]$NoNumberConversion, [Object[]]$ConditionalFormat, [Object[]]$ConditionalText, [ScriptBlock]$CellStyleSB, [Parameter(ParameterSetName = 'Now')] - # [Parameter(ParameterSetName = 'TableNow')] [Switch]$Now, [Switch]$ReturnRange, [Switch]$NoTotalsInPivot, @@ -436,58 +438,61 @@ [Object]$TargetCell, [Object]$CellValue ) - + #The write-verbose commands have been commented out below - even if verbose is silenced they cause a significiant performance impact and if it's on they will cause a flood of messages. Switch ($CellValue) { - {($_ -is [String]) -and ($_.StartsWith('='))} { - #region Save an Excel formula - $TargetCell.Formula = $_ - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$_' as formula" - break - #endregion - } - { $_ -is [URI] } { - #region Save a hyperlink - $TargetCell.Value = $_.AbsoluteUri - $TargetCell.HyperLink = $_ - $TargetCell.Style.Font.Color.SetColor([System.Drawing.Color]::Blue) - $TargetCell.Style.Font.UnderLine = $true - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($_.AbsoluteUri)' as Hyperlink" - break - #endregion - } { $_ -is [DateTime]} { - #region Save a date with an international valid format + # Save a date with an international valid format $TargetCell.Value = $_ $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" + #Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$_' as date" + break + + } + { $_ -is [System.ValueType]} { + # Save numerics, setting format if need be. + $TargetCell.Value = $_ + if ($setNumformat) {$targetCell.Style.Numberformat.Format = $Numberformat } + #Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$_' as value" break - #endregion } {(($NoNumberConversion) -and ($NoNumberConversion -contains $Name)) -or ($NoNumberConversion -eq '*')} { - #region Save a value without converting to number + #Save text without it to converting to number $TargetCell.Value = $_ - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' unconverted" + #Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' unconverted" break - #endregion } - + {($_ -is [String]) -and ($_[0] -eq '=')} { + #region Save an Excel formula + $TargetCell.Formula = $_ + #Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$_' as formula" + break + } + { $_ -is [Uri] } { + # Save a hyperlink + $TargetCell.Value = $_.AbsoluteUri + $TargetCell.HyperLink = $_ + $TargetCell.Style.Font.Color.SetColor([System.Drawing.Color]::Blue) + $TargetCell.Style.Font.UnderLine = $true + #Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($_.AbsoluteUri)' as Hyperlink" + break + } + Default { - #region Save a value as a number if possible + #Save a value as a number if possible $number = $null - if ([Double]::TryParse([String]$_, [System.Globalization.NumberStyles]::Any, - [System.Globalization.NumberFormatInfo]::CurrentInfo, [Ref]$number)) { + if ([Double]::TryParse( $_ , [ref]$number)) { + #was [Double]::TryParse([String]$_, [System.Globalization.NumberStyles]::Any,[System.Globalization.NumberFormatInfo]::CurrentInfo, [Ref]$number)) { $TargetCell.Value = $number - $targetCell.Style.Numberformat.Format = $Numberformat - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as number converted from '$_' with format '$Numberformat'" + if ($setNumformat) {$targetCell.Style.Numberformat.Format = $Numberformat } + #Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as number converted from '$_' with format '$Numberformat'" } else { $TargetCell.Value = $_ - Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as string" + #Write-Verbose "Cell '$Row`:$ColumnIndex' header '$Name' add value '$($TargetCell.Value)' as string" } break - #endregion } } } @@ -506,17 +511,16 @@ } if ($ExcelPackage) { - $pkg = $ExcelPackage - $Path = $pkg.File + $pkg = $ExcelPackage + $Path = $pkg.File } - Else { $pkg = Open-ExcelPackage -Path $Path -Create -KillExcel:$KillExcel} + Else { $pkg = Open-ExcelPackage -Path $Path -Create -KillExcel:$KillExcel} - $params = @{} + $params = @{} if ($NoClobber) {Write-Warning -Message "-NoClobber parameter is no longer used" } - foreach ($p in @("WorkSheetname","ClearSheet","MoveToStart","MoveToEnd","MoveBefore","MoveAfter")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} + foreach ($p in @("WorkSheetname", "ClearSheet", "MoveToStart", "MoveToEnd", "MoveBefore", "MoveAfter")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} $ws = $pkg | Add-WorkSheet @params - $ws.View.TabSelected = $true foreach ($format in $ConditionalFormat ) { $target = "Add$($format.Formatter)" $rule = ($ws.ConditionalFormatting).PSObject.Methods[$target].Invoke($format.Range, $format.IconType) @@ -524,14 +528,12 @@ } if ($append -and $ws.Dimension) { - $headerRange = $ws.Dimension.Address -replace "\d+$", "1" - #if there is a title or anything else above the header row, specifying StartRow will skip it. - if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} - #$script:Header = $ws.Cells[$headerrange].Value + #if there is a title or anything else above the header row, append needs to be combined wih a suitable startrow parameter + $headerRange = $ws.Dimension.Address -replace "\d+$", $StartRow #using a slightly odd syntax otherwise header ends up as a 2D array $ws.Cells[$headerRange].Value | ForEach-Object -Begin {$Script:header = @()} -Process {$Script:header += $_ } - $row = $ws.Dimension.Rows - Write-Debug -Message ("Appending: headers are " + ($script:Header -join ", ") + "Start row $row") + $row = $ws.Dimension.End.Row + Write-Debug -Message ("Appending: headers are " + ($script:Header -join ", ") + " Start row is $row") } elseif ($Title) { #Can only add a title if not appending! @@ -545,7 +547,7 @@ $ws.Cells[$Row, $StartColumn].Style.Font.Bold = $True } #Can only set TitleBackgroundColor if TitleFillPattern is something other than None. - if ($TitleBackgroundColor -and ($TitleFillPattern -eq 'None')) { + if ($TitleBackgroundColor -and ($TitleFillPattern -ne 'None')) { $TitleFillPattern = [OfficeOpenXml.Style.ExcelFillStyle]::Solid } $ws.Cells[$Row, $StartColumn].Style.Fill.PatternType = $TitleFillPattern @@ -554,9 +556,11 @@ $ws.Cells[$Row, $StartColumn].Style.Fill.BackgroundColor.SetColor($TitleBackgroundColor) } $Row ++ ; $startRow ++ - } + } else { $Row = $StartRow } $ColumnIndex = $StartColumn + $setNumformat = ($numberformat -ne $ws.Cells.Style.Numberformat.Format) + $firstTimeThru = $true $isDataTypeValueType = $false } @@ -566,7 +570,7 @@ throw "Failed exporting worksheet '$WorkSheetname' to '$Path': The worksheet '$WorkSheetname' already exists." } else { - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" + throw "Failed preparing to export to worksheet '$WorkSheetname' to '$Path': $_" } } } @@ -591,8 +595,13 @@ #region Add headers if (-not $script:Header) { $ColumnIndex = $StartColumn - $script:Header = $TargetData.PSObject.Properties.Name | Where-Object {$_ -notin $ExcludeProperty} - + if ($DisplayPropertySet -and $TargetData.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames) { + $script:Header = $TargetData.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames.Where( {$_ -notin $ExcludeProperty}) + } + else { + if ($NoAliasOrScriptPropeties) {$propType = "Property"} else {$propType = "*"} + $script:Header = $TargetData.PSObject.Properties.where( {$_.MemberType -like $propType -and $_.Name -notin $ExcludeProperty}).Name + } if ($NoHeader) { # Don't push the headers to the spreadsheet $Row -= 1 @@ -620,36 +629,46 @@ } } Catch { - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" + throw "Failed exporting data to worksheet '$WorkSheetname' to '$Path': $_" } } } End { - Try { - if ($AutoNameRange) { + if ($AutoNameRange) { + Try { if (-not $script:header) { - $headerRange = $ws.Dimension.Address -replace "\d+$", "1" - #if there is a title or anything else above the header row, specifying StartRow will skip it. - if ($StartRow -ne 1) {$headerRange = $headerRange -replace "1", "$StartRow"} + # if there aren't any headers, use the the first row of data to name the ranges: this is the last point that headers will be used. + $headerRange = $ws.Dimension.Address -replace "\d+$", $StartRow #using a slightly odd syntax otherwise header ends up as a 2D array $ws.Cells[$headerRange].Value | ForEach-Object -Begin {$Script:header = @()} -Process {$Script:header += $_ } + #if there is no header start the range at $startRow + $targetRow = $StartRow } - $totalRows = $ws.Dimension.End.Row - $totalColumns = $ws.Dimension.Columns - foreach ($c in 0..($totalColumns - 1)) { - $targetRangeName = "$($script:Header[$c])" + else { + #if there is a header, start the range and the next row down. + $targetRow = $StartRow + 1 + } + + #Dimension.start.row always seems to be one so we work out the target row + #, but start.column is the first populated one and .Columns is the count of populated ones. + # if we have 5 columns from 3 to 8, headers are numbered 0..4, so that is in the for loop and used for getting the name... + # but we have to add the start column on when referencing positions + foreach ($c in 0..($ws.Dimension.Columns - 1)) { + $targetRangeName = $script:Header[$c] -replace '\W' , '_' $targetColumn = $c + $StartColumn - $theCell = $ws.Cells[($startrow + 1), $targetColumn, $totalRows , $targetColumn ] - if ($ws.names[$targetRangeName]) { $ws.names[$targetRangeName].Address = $theCell.FullAddressAbsolute } - else {$ws.Names.Add($targetRangeName, $theCell) | Out-Null } + $theRange = $ws.Cells[$targetRow, $targetColumn, $ws.Dimension.End.Row , $targetColumn ] + if ($ws.names[$targetRangeName]) { $ws.names[$targetRangeName].Address = $theRange.FullAddressAbsolute } + else {$ws.Names.Add($targetRangeName, $theRange) | Out-Null } if ([OfficeOpenXml.FormulaParsing.ExcelUtilities.ExcelAddressUtil]::IsValidAddress($targetRangeName)) { Write-Warning "AutoNameRange: Property name '$targetRangeName' is also a valid Excel address and may cause issues. Consider renaming the property name." } } } - + Catch {Write-Warning -Message "Failed adding named ranges to worksheet '$WorkSheetname': $_" } + } + try { if ($Title) { $startAddress = $ws.Dimension.Start.address -replace "$($ws.Dimension.Start.row)`$", "$($ws.Dimension.Start.row + 1)" } @@ -662,19 +681,29 @@ Write-Debug "Data Range '$dataRange'" if (-not [String]::IsNullOrEmpty($RangeName)) { + if ($RangeName -match "\W") { + Write-Warning -Message "At least one character in $RangeName is illegal in a range name and will be replaced with '_' . " + $RangeName = $RangeName -replace '\W', '_' + } + #If named range exists, update it, else create it if ($ws.Names[$RangeName]) { $ws.Names[$rangename].Address = $ws.Cells[$dataRange].FullAddressAbsolute } else {$ws.Names.Add($RangeName, $ws.Cells[$dataRange]) | Out-Null } } - - if (-not [String]::IsNullOrEmpty($TableName)) { + } + Catch { Write-Warning -Message "Failed adding range '$RangeName' to worksheet '$WorkSheetname': $_" } + if (-not [String]::IsNullOrEmpty($TableName)) { + try { $csr = $StartRow $csc = $StartColumn $cer = $ws.Dimension.End.Row $cec = $ws.Dimension.End.Column # was $script:Header.Count - + if ($TableName -match "\W") { + Write-Warning -Message "At least one character in $TableName is illegal in a table name and will be replaced with '_' . " + $TableName = $TableName -replace '\W', '_' + } $targetRange = $ws.Cells[$csr, $csc, $cer, $cec] - #if we're appending data the table may already exist. + #if the table exists, update it. if ($ws.Tables[$TableName]) { $ws.Tables[$TableName].TableXml.table.ref = $targetRange.Address $ws.Tables[$TableName].TableStyle = $TableStyle @@ -683,187 +712,65 @@ $tbl = $ws.Tables.Add($targetRange, $TableName) $tbl.TableStyle = $TableStyle } + Write-Verbose -Message "Defined table '$TableName' at $($targetRange.Address)" } - - if ($PivotTableDefinition) { - foreach ($item in $PivotTableDefinition.GetEnumerator()) { - $pivotTableName = $item.Key - $pivotTableDataName = $item.Key + 'PivotTableData' - if ($item.Value.PivotFilter) {$PivotTableStartCell = "A3"} else { $PivotTableStartCell = "A1"} - - #Make sure the Pivot table sheet doesn't already exist. - #try { $pkg.Workbook.Worksheets.Delete( $pivotTableName) } catch {} - [OfficeOpenXml.ExcelWorksheet]$wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber - - #If it is a pivot for the default sheet and it doesn't exist - create it - if (-not $item.Value.SourceWorkSheet -and -not $wsPivot.PivotTables[$pivotTableDataName] ) { - $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) - } - #If it is a pivot for the default sheet and it exists - update the range. - elseif (-not $item.Value.SourceWorkSheet -and $wsPivot.PivotTables[$pivotTableDataName] ) { - $wsPivot.PivotTables[$pivotTableDataName].CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref = $WS.Cells[$dataRange].Address - } - #if it is a pivot for a named sheet and it doesn't exist, create it. - elseif ($item.Value.SourceWorkSheet -and -not $wsPivot.PivotTables[$pivotTableDataName] ) { - #find the worksheet - $workSheet = $pkg.Workbook.Worksheets.where( {$_.name -match $item.Value.SourceWorkSheet})[0] - if (-not $workSheet) {Write-Warning -Message "Could not find Worksheet '$($item.Value.SourceWorkSheet)' specified in pivot-table definition $($item.key)." } - else { - if ($item.Value.SourceRange) { $targetdataRange = $item.Value.SourceRange } - else { $targetDataRange = $workSheet.Dimension.Address} - $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $workSheet.Cells[$targetDataRange], $pivotTableDataName) - } - } - - #if we created the pivot table, set up the rows, columns and data if we didn't, put out a message 'existed' or 'error' . - if ($pivotTable) { - foreach ($Row in $item.Value.PivotRows) { - try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } - catch {Write-Warning -message "Could not add '$row' to Rows in PivotTable $pivotTableName." } - } - foreach ($Column in $item.Value.PivotColumns) { - try {$null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column])} - catch {Write-Warning -message "Could not add '$Column' to Columns in PivotTable $pivotTableName." } - } - if ($item.Value.PivotData -is [HashTable] -or $item.Value.PivotData -is [System.Collections.Specialized.OrderedDictionary]) { - $item.Value.PivotData.Keys | ForEach-Object { - try { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) - $df.Function = $item.Value.PivotData.$_ - } - catch {Write-Warning -message "Problem adding data fields to PivotTable $pivotTableName." } - } - } - else { - foreach ($field in $item.Value.PivotData) { - try { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$field]) - $df.Function = 'Count' - } - catch {Write-Warning -message "Problem adding data field '$field' to PivotTable $pivotTableName." } - } - } - foreach ( $pFilter in $item.Value.PivotFilter) { - try { $null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter])} - catch {Write-Warning -message "Could not add '$pFilter' to Filter/Page fields in PivotTable $pivotTableName." } - } - if ($item.Value.NoTotalsInPivot -or $NoTotalsInPivot) { $pivotTable.RowGrandTotals = $false } - if ($item.Value.PivotDataToColumn -or $PivotDataToColumn) { $pivotTable.DataOnRows = $false } - } - elseif ($wsPivot.PivotTables[$pivotTableDataName]) { - Write-Warning -Message "Pivot table defined in $($item.key) already exists." - } - else { Write-Warning -Message "Could not create the pivot table defined in $($item.key)."} - - #Create the chart if it doesn't exist, leave alone if it does. - if ($item.Value.IncludePivotChart -and -not $wsPivot.Drawings['PivotChart'] ) { - if ($item.Value.ChartType) { $ChartType = $item.Value.ChartType} # $ChartType may be passed as a parameter, has default of "Pie", over-ride that if it is in the pivot definition - [OfficeOpenXml.Drawing.Chart.ExcelChart] $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) - if (-not $item.Value.ChartHeight) {$item.Value.ChartHeight = 400 } - if (-not $item.Value.ChartWidth) {$item.Value.ChartWidth = 600 } - if (-not $item.Value.ChartRow) {$item.Value.ChartRow = 0 } - if (-not $item.Value.ChartColumn) {$item.Value.ChartColumn = 4 } - if (-not $item.Value.ChartRowOffSetPixels) {$item.Value.ChartRowOffSetPixels = 0 } - if (-not $item.Value.ChartColumnOffSetPixels) {$item.Value.ChartColumnOffSetPixels = 0 } - $chart.SetPosition($item.Value.ChartRow , $item.Value.ChartRowOffSetPixels , $item.Value.ChartColumn, $item.Value.ChartColumnOffSetPixels) - $chart.SetSize( $item.Value.ChartWidth, $item.Value.ChartHeight) - if ($chart.DataLabel) { - $chart.DataLabel.ShowCategory = [boolean]$item.Value.ShowCategory - $chart.DataLabel.ShowPercent = [boolean]$item.Value.ShowPercent - } - if ([boolean]$item.Value.NoLegend -or $NoLegend) {$chart.Legend.Remove()} - if ( $item.Value.ChartTitle) {$chart.Title.Text = $item.Value.chartTitle} - } - } + catch {Write-Warning -Message "Failed adding table '$TableName' to worksheet '$WorkSheetname': $_"} + } + if ($PivotTableDefinition) { + foreach ($item in $PivotTableDefinition.GetEnumerator()) { + $params = $item.value + if ($params.keys -notcontains "SourceRange" -and + ($params.Keys -notcontains "SourceWorkSheet" -or $params.SourceWorkSheet -eq $WorkSheetname)) {$params.SourceRange = $dataRange} + if ($params.Keys -notcontains "SourceWorkSheet") {$params.SourceWorkSheet = $ws } + if ($params.Keys -notcontains "NoTotalsInPivot" -and $NoTotalsInPivot ) {$params.NoTotalsInPivot = $true} + if ($params.Keys -notcontains "PivotDataToColumn" -and $PivotDataToColumn) {$params.PivotDataToColumn = $true} + + Add-PivotTable -ExcelPackage $pkg -PivotTableName $item.key @Params } - - if ($IncludePivotTable -or $IncludePivotChart) { - if ($PivotFilter) {$PivotTableStartCell = "A3"} else {$PivotTableStartCell = "A1"} - - $pivotTableName = $WorkSheetname + 'PivotTable' - $wsPivot = $pkg | Add-WorkSheet -WorkSheetname $pivotTableName -NoClobber:$NoClobber - - $wsPivot.View.TabSelected = $true - - $pivotTableDataName = $WorkSheetname + 'PivotTableData' - if ($wsPivot.PivotTables[$pivotTableDataName] ) { - $pivotTable = $wsPivot.PivotTables[$pivotTableDataName] - $pivotTable.CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref = $WS.Cells[$dataRange].Address - Write-Warning -Message "Pivot table for $worksheetName already exists; updating the data range, but other properties will not be changed" - } - else { - $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $ws.Cells[$dataRange], $pivotTableDataName) - - foreach ($Row in $PivotRows) { - try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } - catch {Write-Warning -message "Could not add '$row' to PivotTable Rows." } - } - - foreach ($Column in $PivotColumns) { - try {$null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column])} - catch {Write-Warning -message "Could not add '$Column' to PivotTable Columns." } - } - - if ($PivotData -is [HashTable] -or $PivotData -is [System.Collections.Specialized.OrderedDictionary]) { - $PivotData.Keys | ForEach-Object { - try { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) - $df.Function = $PivotData.$_ - } - catch {Write-Warning "Problem adding to Pivot table data fields." } - } - } - else { - foreach ($Item in $PivotData) { - try { - $df = $pivotTable.DataFields.Add($pivotTable.Fields[$Item]) - $df.Function = 'Count' - } - catch {Write-Warning "Problem adding '$item' to Pivot table data fields." } - } - } - - if ($PivotDataToColumn) { $pivotTable.DataOnRows = $false } - - foreach ($pFilter in $PivotFilter) { - try {$null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter]) } - catch {Write-Warning "Problem adding 'pFilter' to Pivot table page/filter fields." } - } - - if ($NoTotalsInPivot) { $pivotTable.RowGrandTotals = $false } - } - - if ($IncludePivotChart) { - if (-not $wsPivot.Drawings['PivotChart']) { - $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) - if ($chart.DataLabel) { - $chart.DataLabel.ShowCategory = $ShowCategory - $chart.DataLabel.ShowPercent = $ShowPercent - } - $chart.SetPosition(0, 26, 2, 26) # if Pivot table is rows+data only it will be 2 columns wide if has pivot columns we don't know how wide it will be - if ($NoLegend) { $chart.Legend.Remove() } - } - } + } + if ($IncludePivotTable -or $IncludePivotChart) { + $params = @{ + "PivotTableName" = ($WorkSheetname + 'PivotTable') ; + "SourceRange" = $dataRange } - - if ($Password) { - $ws.Protection.SetPassword($Password) + if ($PivotFilter) {$params.PivotFilter = $PivotFilter} + if ($PivotRows) {$params.PivotRows = $PivotRows} + if ($PivotColumns) {$Params.PivotColumns = $PivotColumns} + if ($PivotData) {$Params.PivotData = $PivotData} + if ($NoTotalsInPivot) {$params.NoTotalsInPivot = $true} + if ($PivotDataToColumn) {$params.PivotDataToColumn = $true} + if ($IncludePivotChart) { + $params.IncludePivotChart = $true + $Params.ChartType = $ChartType + if ($ShowCategory) {$params.ShowCategory = $true} + if ($ShowPercent) {$params.ShowPercent = $true} + if ($NoLegend) {$params.NoLegend = $true} } + Add-PivotTable -ExcelPackage $pkg -SourceWorkSheet $ws @params + } - if ($AutoFilter) { - $ws.Cells[$dataRange].AutoFilter = $true + if ($AutoFilter) { + try { + $ws.Cells[$dataRange].AutoFilter = $true + Write-Verbose -Message "Enabeld autofilter. " } + catch {Write-Warning -Message "Failed adding autofilter to worksheet '$WorkSheetname': $_"} + } + try { if ($FreezeTopRow) { - $ws.View.FreezePanes(2, 1) + $ws.View.FreezePanes(2, 1) + Write-Verbose -Message "Froze top row" } if ($FreezeTopRowFirstColumn) { - $ws.View.FreezePanes(2, 2) + $ws.View.FreezePanes(2, 2) + Write-Verbose -Message "Froze top row and first column" } if ($FreezeFirstColumn) { - $ws.View.FreezePanes(1, 2) + $ws.View.FreezePanes(1, 2) + Write-Verbose -Message "Froze first column" } if ($FreezePane) { @@ -874,164 +781,128 @@ if ($freezeRow -gt 1) { $ws.View.FreezePanes($freezeRow, $freezeColumn) + Write-Verbose -Message "Froze pandes at row $freezeRow and column $FreezeColumn" } } - - if ($BoldTopRow) { + } + catch {Write-Warning -Message "Failed adding Freezing the panes in worksheet '$WorkSheetname': $_"} + + if ($BoldTopRow) { + try { if ($Title) { - $range = $ws.Dimension.Address -replace '\d+', '2' + $range = $ws.Dimension.Address -replace '\d+', ($StartRow + 1) } else { - $range = $ws.Dimension.Address -replace '\d+', '1' + $range = $ws.Dimension.Address -replace '\d+', $StartRow } - $ws.Cells[$range].Style.Font.Bold = $true + Write-Verbose -Message "Set $range font style to bold." } - - if ($AutoSize) { - $ws.Cells.AutoFitColumns() + catch {Write-Warning -Message "Failed setting the top row to bold in worksheet '$WorkSheetname': $_"} + } + if ($AutoSize) { + try { + $ws.Cells.AutoFitColumns() + Write-Verbose -Message "Auto-sized columns" } + catch { Write-Warning -Message "Failed autosizing columns of worksheet '$WorkSheetname': $_"} + } - foreach ($Sheet in $HideSheet) { + foreach ($Sheet in $HideSheet) { + try { $pkg.Workbook.WorkSheets[$Sheet].Hidden = 'Hidden' + Write-verbose -Message "Sheet '$sheet' Hidden." } + catch {Write-Warning -Message "Failed hiding worksheet '$sheet': $_"} + } - foreach ($chartDef in $ExcelChartDefinition) { - $ChartName = 'Chart' + (Split-Path -Leaf ([System.IO.path]::GetTempFileName())) -replace 'tmp|\.', '' - $chart = $ws.Drawings.AddChart($ChartName, $chartDef.ChartType) - $chart.Title.Text = $chartDef.Title - - if ($chartDef.NoLegend) { - $chart.Legend.Remove() - } - - if ($chart.Datalabel -ne $null) { - $chart.Datalabel.ShowCategory = $chartDef.ShowCategory - $chart.Datalabel.ShowPercent = $chartDef.ShowPercent - } - - $chart.SetPosition($chartDef.Row, $chartDef.RowOffsetPixels, $chartDef.Column, $chartDef.ColumnOffsetPixels) - $chart.SetSize($chartDef.Width, $chartDef.Height) - - $chartDefCount = @($chartDef.YRange).Count - if ($chartDefCount -eq 1) { - $Series = $chart.Series.Add($chartDef.YRange, $chartDef.XRange) - - $SeriesHeader = $chartDef.SeriesHeader - if (-not $SeriesHeader) { - $SeriesHeader = 'Series 1' - } - - $Series.Header = $SeriesHeader - } - else { - for ($idx = 0; $idx -lt $chartDefCount; $idx += 1) { - $Series = $chart.Series.Add($chartDef.YRange[$idx], $chartDef.XRange) - - if ($chartDef.SeriesHeader.Count -gt 0) { - $SeriesHeader = $chartDef.SeriesHeader[$idx] - } - - if (-not $SeriesHeader) { - $SeriesHeader = "Series $($idx)" - } - - $Series.Header = $SeriesHeader - $SeriesHeader = $null - } + foreach ($chartDef in $ExcelChartDefinition) { + $params = @{} + $chardef.PSObject.Properties | ForEach-Object {if ($_.value -ne $null) {$params[$_.name] = $_.value}} + Add-ExcelChart $params + } + + foreach ($ct in $ConditionalText) { + try { + $cfParams = @{RuleType = $ct.ConditionalType; ConditionValue = $ct.text ; + BackgroundColor = $ct.BackgroundColor; BackgroundPattern = $ct.PatternType ; + ForeGroundColor = $ct.ConditionalTextColor } + if ($ct.Range) {$cfParams.range = $ct.range} else { $cfParams.Range = $ws.Dimension.Address } + Add-ConditionalFormatting -WorkSheet $ws @cfParams + Write-Verbose -Message "Added conditional formatting to range $($ct.range)" } + catch {Write-Warning -Message "Failed adding conditional formatting to worksheet '$WorkSheetname': $_"} + } - if ($ConditionalText) { - foreach ($targetConditionalText in $ConditionalText) { - $target = "Add$($targetConditionalText.ConditionalType)" - - $Range = $targetConditionalText.Range - if (-not $Range) { - $Range = $ws.Dimension.Address - } - - $rule = ($ws.Cells[$Range].ConditionalFormatting).PSObject.Methods[$target].Invoke() - - if ($targetConditionalText.Text) { - if ($targetConditionalText.ConditionalType -match 'equal|notequal|lessthan|lessthanorequal|greaterthan|greaterthanorequal') { - $rule.Formula = $targetConditionalText.Text - } - else { - $rule.Text = $targetConditionalText.Text - } - } - - $rule.Style.Font.Color.Color = $targetConditionalText.ConditionalTextColor - $rule.Style.Fill.PatternType = $targetConditionalText.PatternType - $rule.Style.Fill.BackgroundColor.Color = $targetConditionalText.BackgroundColor - } - } - - if ($CellStyleSB) { + if ($CellStyleSB) { + try { $TotalRows = $ws.Dimension.Rows $LastColumn = (Get-ExcelColumnName $ws.Dimension.Columns).ColumnName & $CellStyleSB $ws $TotalRows $LastColumn } + catch {Write-Warning -Message "Failed processing CellStyleSB in worksheet '$WorkSheetname': $_"} + } - if ($PassThru) { - $pkg + if ($Password) { + try { + $ws.Protection.SetPassword($Password) + Write-Verbose -Message "Set password on workbook" } - else { - if ($ReturnRange) { - $ws.Dimension.Address + + catch {throw "Failed setting password for worksheet '$WorkSheetname': $_"} + } + + if ($PassThru) { $pkg } + else { + if ($ReturnRange) {$ws.Dimension.Address } + + $pkg.Save() + Write-Verbose -Message "Saved workbook $($pkg.File)" + if ($ReZip) { + Write-Verbose -Message "Re-Zipping $($pkg.file) using .NET ZIP library" + try { + Add-Type -AssemblyName "System.IO.Compression.Filesystem" -ErrorAction stop + } + catch { + Write-Error "The -ReZip parameter requires .NET Framework 4.5 or later to be installed. Recommend to install Powershell v4+" + continue } - - $pkg.Save() - - if ($ReZip) { - write-verbose "Re-Zipping $($pkg.file) using .NET ZIP library" - $zipAssembly = "System.IO.Compression.Filesystem" - try { - Add-Type -assembly $zipAssembly -ErrorAction stop - } - catch { - write-error "The -ReZip parameter requires .NET Framework 4.5 or later to be installed. Recommend to install Powershell v4+" - continue - } - - $TempZipPath = Join-Path -path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName()) - [io.compression.zipfile]::ExtractToDirectory($pkg.File, $TempZipPath) | Out-Null + try { + $TempZipPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName()) + [io.compression.zipfile]::ExtractToDirectory($pkg.File, $TempZipPath) | Out-Null Remove-Item $pkg.File -Force [io.compression.zipfile]::CreateFromDirectory($TempZipPath, $pkg.File) | Out-Null } - - $pkg.Dispose() - - if ($Show) { - Invoke-Item $Path - } + catch {throw "Error resizipping $path : $_"} } + + $pkg.Dispose() + + if ($Show) { Invoke-Item $Path } } - Catch { - throw "Failed exporting worksheet '$WorkSheetname' to '$Path': $_" - } + } } function New-PivotTableDefinition { -<# - .Synopsis - Creates Pivot table definitons for export excel - .Description - Export-Excel allows a single Pivot table to be defined using the parameters -IncludePivotTable, -PivotColumns -PivotRows, - =PivotData, -PivotFilter, -NoTotalsInPivot, -PivotDataToColumn, -IncludePivotChart and -ChartType. - Its -PivotTableDefintion paramater allows multiple pivot tables to be defined, with additional parameters. - New-PivotTableDefinition is a convenient way to build these definitions. - .Example - $pt = New-PivotTableDefinition -PivotTableName "PT1" -SourceWorkSheet "Sheet1" -PivotRows "Status" -PivotData @{Status='Count' } -PivotFilter 'StartType' -IncludePivotChart -ChartType BarClustered3D - $Pt += New-PivotTableDefinition -PivotTableName "PT2" -SourceWorkSheet "Sheet2" -PivotRows "Company" -PivotData @{Company='Count'} -IncludePivotChart -ChartType PieExploded3D -ShowPercent -ChartTitle "Breakdown of processes by company" - Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -AutoSize - Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' - $excel = Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show + <# + .Synopsis + Creates Pivot table definitons for export excel + .Description + Export-Excel allows a single Pivot table to be defined using the parameters -IncludePivotTable, -PivotColumns -PivotRows, + =PivotData, -PivotFilter, -NoTotalsInPivot, -PivotDataToColumn, -IncludePivotChart and -ChartType. + Its -PivotTableDefintion paramater allows multiple pivot tables to be defined, with additional parameters. + New-PivotTableDefinition is a convenient way to build these definitions. + .Example + $pt = New-PivotTableDefinition -PivotTableName "PT1" -SourceWorkSheet "Sheet1" -PivotRows "Status" -PivotData @{Status='Count' } -PivotFilter 'StartType' -IncludePivotChart -ChartType BarClustered3D + $Pt += New-PivotTableDefinition -PivotTableName "PT2" -SourceWorkSheet "Sheet2" -PivotRows "Company" -PivotData @{Company='Count'} -IncludePivotChart -ChartType PieExploded3D -ShowPercent -ChartTitle "Breakdown of processes by company" + Get-Service | Select-Object -Property Status,Name,DisplayName,StartType | Export-Excel -Path .\test.xlsx -AutoSize + Get-Process | Select-Object -Property Name,Company,Handles,CPU,VM | Export-Excel -Path .\test.xlsx -AutoSize -WorkSheetname 'sheet2' + $excel = Export-Excel -Path .\test.xlsx -PivotTableDefinition $pt -Show - This is a re-work of one of the examples in Export-Excel - instead of writing out the pivot definition hash table it is built by calling New-PivotTableDefinition. -#> + This is a re-work of one of the examples in Export-Excel - instead of writing out the pivot definition hash table it is built by calling New-PivotTableDefinition. + #> param( [Parameter(Mandatory)] [Alias("PivtoTableName")]#Previous typo - use alias to avoid breaking scripts @@ -1087,3 +958,254 @@ function New-PivotTableDefinition { @{$PivotTableName = $parameters} } +function Add-WorkSheet { + [cmdletBinding()] + [OutputType([OfficeOpenXml.ExcelWorksheet])] + param( + #An object representing an Excel Package. + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Package", Position = 0)] + [OfficeOpenXml.ExcelPackage]$ExcelPackage, + #An Excel workbook to which the Worksheet will be added - a package contains one workbook so you can use whichever fits at the time. + [Parameter(Mandatory = $true, ParameterSetName = "WorkBook")] + [OfficeOpenXml.ExcelWorkbook]$ExcelWorkbook, + #The name of the worksheet 'Sheet1' by default. + [string]$WorkSheetname = 'Sheet1', + #If the worksheet already exists, by default it will returned, unless -ClearSheet is specified in which case it will be deleted and re-created. + [switch]$ClearSheet, + #If specified, the worksheet will be moved to the start of the workbook. + #MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified. + [Switch]$MoveToStart, + #If specified, the worksheet will be moved to the end of the workbook. + #(This is the default position for newly created sheets, but this can be used to move existing sheets.) + [Switch]$MoveToEnd, + #If specified, the worksheet will be moved before the nominated one (which can be a postion starting from 1, or a name). + #MoveBefore takes precedence over MoveAfter if both are specified. + $MoveBefore , + # If specified, the worksheet will be moved after the nominated one (which can be a postion starting from 1, or a name or *). + # If * is used, the worksheet names will be examined starting with the first one, and the sheet placed after the last sheet which comes before it alphabetically. + $MoveAfter , + #If worksheet is provided as a copy source the new worksheet will be a copy of it. The source can be in the same workbook, or in a different file. + [OfficeOpenXml.ExcelWorksheet]$CopySource, + #Ignored but retained for backwards compatibility. + [Switch] $NoClobber + ) + + if ($ExcelPackage -and -not $ExcelWorkbook) {$ExcelWorkbook = $ExcelPackage.Workbook} + + $ws = $ExcelWorkbook.Worksheets[$WorkSheetname] + if ( $ws -and $ClearSheet) { $ExcelWorkbook.Worksheets.Delete($WorkSheetname) ; $ws = $null } + if (!$ws -and $CopySource) { + Write-Verbose -Message "Copying into worksheet '$WorkSheetname'." + $ws = $ExcelWorkbook.Worksheets.Add($WorkSheetname, $CopySource) + } + elseif (!$ws) { + Write-Verbose -Message "Adding worksheet '$WorkSheetname'." + $ws = $ExcelWorkbook.Worksheets.Add($WorkSheetname) + } + else {Write-Verbose -Message "Worksheet '$WorkSheetname' already existed."} + if ($MoveToStart) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName) } + elseif ($MoveToEnd ) {$ExcelWorkbook.Worksheets.MoveToEnd($worksheetName) } + elseif ($MoveBefore ) { + if ($ExcelWorkbook.Worksheets[$MoveBefore]) { + if ($MoveBefore -is [int]) { + $ExcelWorkbook.Worksheets.MoveBefore($ws.Index, $MoveBefore) + } + else {$ExcelWorkbook.Worksheets.MoveBefore($worksheetname, $MoveBefore)} + } + else {Write-Warning "Can't find worksheet '$MoveBefore'; worsheet '$WorkSheetname' will not be moved."} + } + elseif ($MoveAfter ) { + if ($MoveAfter = "*") { + if ($WorkSheetname -lt $ExcelWorkbook.Worksheets[1].Name) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName)} + else { + $i = 1 + While ($i -lt $ExcelWorkbook.Worksheets.Count -and ($ExcelWorkbook.Worksheets[$i + 1].Name -le $worksheetname) ) { $i++} + $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $i) + } + } + elseif ($ExcelWorkbook.Worksheets[$MoveAfter]) { + if ($MoveAfter -is [int]) { + $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $MoveAfter) + } + else { + $ExcelWorkbook.Worksheets.MoveAfter($worksheetname, $MoveAfter) + } + } + else {Write-Warning "Can't find worksheet '$MoveAfter'; worsheet '$WorkSheetname' will not be moved."} + } + return $ws +} +function Add-PivotTable { + + param ( + # Parameter help description + [Parameter(Mandatory = $true)] + $PivotTableName, + $ExcelPackage, + #Worksheet where the data is found + $SourceWorkSheet, + #Address range in the worksheet e.g "A10:F20" - the first row must be column names: if not specified the whole sheet will be used/ + $SourceRange, + #Fields to set as rows in the Pivot table + $PivotRows, + #A hash table in form "FieldName"="Function", where function is one of + #Average, Count, CountNums, Max, Min, Product, None, StdDev, StdDevP, Sum, Var, VarP + $PivotData, + #Fields to set as columns in the Pivot table + $PivotColumns, + #Fields to use to filter in the Pivot table + $PivotFilter, + [Switch]$PivotDataToColumn, + [Switch]$NoTotalsInPivot, + #If specified a chart Will be included. + [Switch]$IncludePivotChart, + #Optional title for the pivot chart, by default the title omitted. + [String]$ChartTitle, + #Height of the chart in Pixels (400 by default) + [int]$ChartHeight = 400 , + #Width of the chart in Pixels (600 by default) + [int]$ChartWidth = 600, + #Cell position of the top left corner of the chart, there will be this number of rows above the top edge of the chart (default is 0, chart starts at top edge of row 1). + [Int]$ChartRow = 0 , + #Cell position of the top left corner of the chart, there will be this number of cells to the left of the chart (default is 4, chart starts at left edge of column E) + [Int]$ChartColumn = 4, + #Vertical offset of the chart from the cell corner. + [Int]$ChartRowOffSetPixels = 0 , + #Horizontal offset of the chart from the cell corner. + [Int]$ChartColumnOffSetPixels = 0, + #Type of chart + [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = 'Pie', + #If specified hides the chart legend + [Switch]$NoLegend, + #if specified attaches the category to slices in a pie chart : not supported on all chart types, this may give errors if applied to an unsupported type. + [Switch]$ShowCategory, + #If specified attaches percentages to slices in a pie chart. + [Switch]$ShowPercent + ) + + $pivotTableDataName = $pivotTableName + 'PivotTableData' + [OfficeOpenXml.ExcelWorksheet]$wsPivot = Add-WorkSheet -ExcelPackage $ExcelPackage -WorkSheetname $pivotTableName + # $wsPivot.View.TabSelected = $true + + #if the pivot doesn't exist, create it. + if (-not $wsPivot.PivotTables[$pivotTableDataName] ) { + try { + #Accept a string or a worksheet object as $Source Worksheet. + if ($SourceWorkSheet -is [string]) { + $SourceWorkSheet = $ExcelPackage.Workbook.Worksheets.where( {$_.name -match $SourceWorkSheet})[0] + } + if (-not ($SourceWorkSheet -is [OfficeOpenXml.ExcelWorksheet])) {Write-Warning -Message "Could not find source Worksheet for pivot-table '$pivotTableName'." } + else { + if ($PivotFilter) {$PivotTableStartCell = "A3"} else { $PivotTableStartCell = "A1"} + if (-not $SourceRange) { $SourceRange = $SourceWorkSheet.Dimension.Address} + $pivotTable = $wsPivot.PivotTables.Add($wsPivot.Cells[$PivotTableStartCell], $SourceWorkSheet.Cells[ $SourceRange], $pivotTableDataName) + } + foreach ($Row in $PivotRows) { + try {$null = $pivotTable.RowFields.Add($pivotTable.Fields[$Row]) } + catch {Write-Warning -message "Could not add '$row' to Rows in PivotTable $pivotTableName." } + } + foreach ($Column in $PivotColumns) { + try {$null = $pivotTable.ColumnFields.Add($pivotTable.Fields[$Column])} + catch {Write-Warning -message "Could not add '$Column' to Columns in PivotTable $pivotTableName." } + } + if ($PivotData -is [HashTable] -or $PivotData -is [System.Collections.Specialized.OrderedDictionary]) { + $PivotData.Keys | ForEach-Object { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$_]) + $df.Function = $PivotData.$_ + } + catch {Write-Warning -message "Problem adding data fields to PivotTable $pivotTableName." } + } + } + else { + foreach ($field in $PivotData) { + try { + $df = $pivotTable.DataFields.Add($pivotTable.Fields[$field]) + $df.Function = 'Count' + } + catch {Write-Warning -message "Problem adding data field '$field' to PivotTable $pivotTableName." } + } + } + foreach ( $pFilter in $PivotFilter) { + try { $null = $pivotTable.PageFields.Add($pivotTable.Fields[$pFilter])} + catch {Write-Warning -message "Could not add '$pFilter' to Filter/Page fields in PivotTable $pivotTableName." } + } + if ($NoTotalsInPivot) { $pivotTable.RowGrandTotals = $false } + if ($PivotDataToColumn ) { $pivotTable.DataOnRows = $false } + } + catch {Write-Warning -Message "Failed adding PivotTable '$pivotTableName': $_"} + } + else { + Write-Warning -Message "Pivot table defined in $($pivotTableName) already exists, only the data range will be changed." + $pivotTable = $wsPivot.PivotTables[$pivotTableDataName] + $pivotTable.CacheDefinition.CacheDefinitionXml.pivotCacheDefinition.cacheSource.worksheetSource.ref = $SourceRange + } + + #Create the chart if it doesn't exist, leave alone if it does. + if ($IncludePivotChart -and -not $wsPivot.Drawings['PivotChart'] ) { + try { + [OfficeOpenXml.Drawing.Chart.ExcelChart] $chart = $wsPivot.Drawings.AddChart('PivotChart', $ChartType, $pivotTable) + $chart.SetPosition($ChartRow , $ChartRowOffSetPixels , $ChartColumn, $ChartColumnOffSetPixels) + $chart.SetSize( $ChartWidth, $ChartHeight) + if ($chart.DataLabel) { + $chart.DataLabel.ShowCategory = [boolean]$ShowCategory + $chart.DataLabel.ShowPercent = [boolean]$ShowPercent + } + if ($NoLegend) { $chart.Legend.Remove()} + if ($ChartTitle) {$chart.Title.Text = $ChartTitle} + } + catch { catch {Write-Warning -Message "Failed adding chart for pivotable '$pivotTableName': $_"} + } + + } +} +function Add-ExcelChart { + param( + $Worksheet, + $Title = "Chart Title", + $Header, + [OfficeOpenXml.Drawing.Chart.eChartType]$ChartType = "ColumnStacked", + $XRange, + $YRange, + $Width = 500, + $Height = 350, + $Row = 0, + $RowOffSetPixels = 10, + $Column = 6, + $ColumnOffSetPixels = 5, + [Switch]$NoLegend, + [Switch]$ShowCategory, + [Switch]$ShowPercent, + $SeriesHeader + ) + try { + $ChartName = 'Chart' + (Split-Path -Leaf ([System.IO.path]::GetTempFileName())) -replace 'tmp|\.', '' + $chart = $Worksheet.Drawings.AddChart($ChartName, $ChartType) + $chart.Title.Text = $Title + + if ($NoLegend) { $chart.Legend.Remove() } + + if ($chart.Datalabel -ne $null) { + $chart.Datalabel.ShowCategory = [boolean]$ShowCategory + $chart.Datalabel.ShowPercent = [boolean]$ShowPercent + } + + $chart.SetPosition($Row, $RowOffsetPixels, $Column, $ColumnOffsetPixels) + $chart.SetSize($Width, $Height) + + $chartDefCount = @($YRange).Count + if ($chartDefCount -eq 1) { + $Series = $chart.Series.Add($YRange, $XRange) + if ($SeriesHeader) { $Series.Header = $SeriesHeader} + else { $Series.Header = 'Series 1'} + } + else { + for ($idx = 0; $idx -lt $chartDefCount; $idx += 1) { + $Series = $chart.Series.Add($YRange[$idx], $XRange) + if ($SeriesHeader.Count -gt 0) { $Series.Header = $SeriesHeader[$idx] } + else { $Series.Header = "Series $($idx)"} + } + } + } + catch {Write-Warning -Message "Failed adding Chart to worksheet '$($WorkSheet).name': $_"} +} \ No newline at end of file diff --git a/ImportExcel.psm1 b/ImportExcel.psm1 index 9789da6..8db1908 100644 --- a/ImportExcel.psm1 +++ b/ImportExcel.psm1 @@ -422,84 +422,6 @@ function Import-Excel { } } -function Add-WorkSheet { - [cmdletBinding()] - [OutputType([OfficeOpenXml.ExcelWorksheet])] - param( - #An object representing an Excel Package. - [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName = "Package", Position=0)] - [OfficeOpenXml.ExcelPackage]$ExcelPackage, - #An Excel workbook to which the Worksheet will be added - a package contains one workbook so you can use whichever fits at the time. - [Parameter(Mandatory=$true, ParameterSetName = "WorkBook")] - [OfficeOpenXml.ExcelWorkbook]$ExcelWorkbook, - #The name of the worksheet 'Sheet1' by default. - [string]$WorkSheetname = 'Sheet1', - #If the worksheet already exists, by default it will returned, unless -ClearSheet is specified in which case it will be deleted and re-created. - [switch]$ClearSheet, - #If specified, the worksheet will be moved to the start of the workbook. - #MoveToStart takes precedence over MoveToEnd, Movebefore and MoveAfter if more than one is specified. - [Switch]$MoveToStart, - #If specified, the worksheet will be moved to the end of the workbook. - #(This is the default position for newly created sheets, but this can be used to move existing sheets.) - [Switch]$MoveToEnd, - #If specified, the worksheet will be moved before the nominated one (which can be a postion starting from 1, or a name). - #MoveBefore takes precedence over MoveAfter if both are specified. - $MoveBefore , - # If specified, the worksheet will be moved after the nominated one (which can be a postion starting from 1, or a name or *). - # If * is used, the worksheet names will be examined starting with the first one, and the sheet placed after the last sheet which comes before it alphabetically. - $MoveAfter , - #If worksheet is provided as a copy source the new worksheet will be a copy of it. The source can be in the same workbook, or in a different file. - [OfficeOpenXml.ExcelWorksheet]$CopySource, - #Ignored but retained for backwards compatibility. - [Switch] $NoClobber - ) - - if ($ExcelPackage -and -not $ExcelWorkbook) {$ExcelWorkbook = $ExcelPackage.Workbook} - - $ws = $ExcelWorkbook.Worksheets[$WorkSheetname] - if( $ws -and $ClearSheet) { $ExcelWorkbook.Worksheets.Delete($WorkSheetname) ; $ws = $null } - if(!$ws -and $CopySource) { - Write-Verbose -Message "Copying into worksheet '$WorkSheetname'." - $ws=$ExcelWorkbook.Worksheets.Add($WorkSheetname, $CopySource) - } - elseif(!$ws) { - Write-Verbose -Message "Adding worksheet '$WorkSheetname'." - $ws=$ExcelWorkbook.Worksheets.Add($WorkSheetname) - } - else {Write-Verbose -Message "Worksheet '$WorkSheetname' already existed."} - if ($MoveToStart) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName) } - elseif ($MoveToEnd ) {$ExcelWorkbook.Worksheets.MoveToEnd($worksheetName) } - elseif ($MoveBefore ) { - if ($ExcelWorkbook.Worksheets[$MoveBefore]) { - if ($MoveBefore -is [int]) { - $ExcelWorkbook.Worksheets.MoveBefore($ws.Index, $MoveBefore) - } - else {$ExcelWorkbook.Worksheets.MoveBefore($worksheetname, $MoveBefore)} - } - else {Write-Warning "Can't find worksheet '$MoveBefore'; worsheet '$WorkSheetname' will not be moved."} - } - elseif ($MoveAfter ) { - if ($MoveAfter = "*") { - if ($WorkSheetname -lt $ExcelWorkbook.Worksheets[1].Name) {$ExcelWorkbook.Worksheets.MoveToStart($worksheetName)} - else { - $i = 1 - While ($i -lt $ExcelWorkbook.Worksheets.Count -and $ExcelWorkbook.Worksheets[$i + 1].Name -lt $worksheetname ) { $i++} - $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $i) - } - } - elseif ($ExcelWorkbook.Worksheets[$MoveAfter]) { - if ($MoveAfter -is [int]) { - $ExcelWorkbook.Worksheets.MoveAfter($ws.Index, $MoveAfter) - } - else { - $ExcelWorkbook.Worksheets.MoveAfter($worksheetname, $MoveAfter) - } - } - else {Write-Warning "Can't find worksheet '$MoveAfter'; worsheet '$WorkSheetname' will not be moved."} - } - return $ws -} - function ConvertFrom-ExcelSheet { <# .Synopsis diff --git a/Old_Export-Excel.Tests.ps1 b/Old_Export-Excel.Tests.ps1 new file mode 100644 index 0000000..62db210 --- /dev/null +++ b/Old_Export-Excel.Tests.ps1 @@ -0,0 +1,94 @@ +#Requires -Modules Pester + + +$here = Split-Path -Parent $MyInvocation.MyCommand.Path + + +Import-Module $here -Force + +$WarningPreference = 'SilentlyContinue' +$ProgressPreference = 'SilentlyContinue' + +Function Test-isNumeric { + Param ( + [Parameter(ValueFromPipeline)]$x + ) + + Return $x -is [byte] -or $x -is [int16] -or $x -is [int32] -or $x -is [int64] ` + -or $x -is [sbyte] -or $x -is [uint16] -or $x -is [uint32] -or $x -is [uint64] ` + -or $x -is [float] -or $x -is [double] -or $x -is [decimal] +} + +$fakeData = [PSCustOmobject]@{ + Property_1_Date = (Get-Date).ToString('d') # US '10/16/2017' BE '16/10/2107' + Property_2_Formula = '=SUM(G2:H2)' + Property_3_String = 'My String' + Property_4_String = 'a' + Property_5_IPAddress = '10.10.25.5' + Property_6_Number = '0' + Property_7_Number = '5' + Property_8_Number = '007' + Property_9_Number = (33).ToString('F2') # US '33.00' BE '33,00' + Property_10_Number = (5/3).ToString('F2') # US '1.67' BE '1,67' + Property_11_Number = (15999998/3).ToString('N2') # US '5,333,332.67' BE '5.333.332,67' + Property_12_Number = '1.555,83' + Property_13_PhoneNr = '+32 44' + Property_14_PhoneNr = '+32 4 4444 444' + Property_15_PhoneNr = '+3244444444' +} + +$Path = 'Test.xlsx' + +Describe 'Export-Excel' { + in $TestDrive { + Describe 'Number conversion' { + Context 'numerical values expected' { + #region Create test file + $fakeData | Export-Excel -Path $Path + + $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + $Excel = New-Object OfficeOpenXml.ExcelPackage $Path + $Worksheet = $Excel.Workbook.WorkSheets[1] + #endregion + + it 'zero' { + $fakeData.Property_6_Number | Should -BeExactly '0' + $Worksheet.Cells[2, 6].Text | Should -BeExactly $fakeData.Property_6_Number + $Worksheet.Cells[2, 6].Value | Test-isNumeric | Should -Be $true + } + + It 'regular number' { + $fakeData.Property_7_Number | Should -BeExactly '5' + $Worksheet.Cells[2, 7].Text | Should -BeExactly $fakeData.Property_7_Number + $Worksheet.Cells[2, 7].Value | Test-isNumeric | Should -Be $true + } + + It 'number starting with zero' { + $fakeData.Property_8_Number | Should -BeExactly '007' + $Worksheet.Cells[2, 8].Text | Should -BeExactly '7' + $Worksheet.Cells[2, 8].Value | Test-isNumeric | Should -Be $true + } + + It 'decimal number' { + # US '33.00' BE '33,00' + $fakeData.Property_9_Number | Should -BeExactly (33).ToString('F2') + $Worksheet.Cells[2, 9].Text | Should -BeExactly '33' + $Worksheet.Cells[2, 9].Value | Test-isNumeric | Should -Be $true + + # US '1.67' BE '1,67' + $fakeData.Property_10_Number | Should -BeExactly (5/3).ToString('F2') + $Worksheet.Cells[2, 10].Text | Should -BeExactly $fakeData.Property_10_Number + $Worksheet.Cells[2, 10].Value | Test-isNumeric | Should -Be $true + } + + It 'thousand seperator and decimal number' { + # US '5,333,332.67' BE '5.333.332,67' + # Excel BE '5333332,67' + $fakeData.Property_11_Number | Should -BeExactly (15999998/3).ToString('N2') + $Worksheet.Cells[2, 11].Text | Should -BeExactly $fakeData.Property_11_Number + $Worksheet.Cells[2, 11].Value | Test-isNumeric | Should -Be $true + } + } + } + } +} \ No newline at end of file diff --git a/Open-ExcelPackage.ps1 b/Open-ExcelPackage.ps1 index 00dc7ba..49a77f2 100644 --- a/Open-ExcelPackage.ps1 +++ b/Open-ExcelPackage.ps1 @@ -24,7 +24,7 @@ Get-Process -Name "excel" -ErrorAction Ignore | Stop-Process while (Get-Process -Name "excel" -ErrorAction Ignore) {} } - + $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) #If -Create was not specified only open the file if it exists already (send a warning if it doesn't exist). if ($Create) { diff --git a/README.md b/README.md index aefb734..4eb9bc3 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,43 @@ iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/dfi # What's new +- New commands - Diff , Merge and Join + - `Compare-Worksheet` (introduced in 5.0) uses the built in `Compare-object` command, to output a command-line DIFF and/or colour the worksheet to show differences. For example, if my sheets are Windows services the *extra* rows or rows where the startup status has changed get highlighted + - `Merge-Worksheet` (also introduced in 5.0) joins two lumps, side by highlighting the differences. So now I can have server A's services and Server Bs Services on the same page. I figured out a way to do multiple sheets. So I can have Server A,B,C,D on one page :-) that is `Merge-MultpleSheets` + For this release I've fixed heaven only knows how many typos and proof reading errors in the help for these two, but the code is unchanged - although correcting the spelling of Merge-MultipleSheets is potentially a breaking change (and it is still plural!) + - `Join-Worksheet` is **new** for ths release. At it's simplest it copies all the data in Worksheet A to the end of Worksheet B +- Add-Worksheet + - I have moved this from ImportExcel.psm1 to ExportExcel.ps1 and it now can move a new worksheet to the right place, and can copy an existing worksheet (from the same or a different workbook) to a new one, and I set the Set return-type to aid intellisense +- New-PivotTableDefinition + - Now Supports `-PivotFilter` and `-PivotDataToColumn`, `-ChartHeight/width` `-ChartRow/Column`, `-ChartRow/ColumnPixelOffset` parameters +- Set-Format + - Fixed a bug where the `-address` parameter had to be named, although the examples in `export-excel` help showed it working by position (which works now. ) +- Export-Excel + - I've done some re-factoring + 1. I "flattened out" small "called-once" functions , add-title, convert-toNumber and Stop-ExcelProcess. + 2. It now uses Add-Worksheet, Open-ExcelPackage and Add-ConditionalFormat instead of duplicating their functionality. + 3. I've moved the PivotTable functionality (which was doubled up) out to a new function "Add-PivotTable" which supports some extra parameters PivotFilter and PivotDataToColumn, ChartHeight/width ChartRow/Column, ChartRow/ColumnPixelOffsets. + 4. I've made the try{} catch{} blocks cover smaller blocks of code to give a better idea where a failure happend, some of these now Warn instead of throwing - I'd rather save the data with warnings than throw it away because we can't add a chart. Along with this I've added some extra write-verbose messages + - Bad column-names specified for Pivots now generate warnings instead of throwing. + - Fixed issues when pivottables / charts already exist and an export tries to create them again. + - Fixed issue where AutoNamedRange, NamedRange, and TableName do not work when appending to a sheet which already contains the range(s) / table + - Fixed issue where AutoNamedRange may try to create ranges with an illegal name. + - Added check for illegal characters in RangeName or Table Name (replace them with "_"), changed tablename validation to allow spaces and applied same validation to RangeName + - Fixed a bug where BoldTopRow is always bolds row 1 even if the export is told to start at a lower row. + - Fixed a bug where titles throw pivot table creation out of alignment. + - Fixed a bug where Append can overwrite the last rows of data if the initial export had blank rows at the top of the sheet. + - Removed the need to specify a fill type when specifying a title background color + - Added MoveToStart, MoveToEnd, MoveBefore and MoveAfter Parameters - these go straight through to Add worksheet + - Added "NoScriptOrAliasProperties" "DisplayPropertySet" switches (names subject to change) - combined with ExcludeProperty these are a quick way to reduce the data exported (and speed things up) + - Add-CellValue now understands URI item properties. If a property is of type URI it is created as a hyperlink to speed up Add-CellValue + - Commented out the write verbose statements even if verbose is silenced they cause a significiant performance impact and if it's on they will cause a flood of messages. + - Re-ordered the choices in the switch and added an option to say "If it is numeric already post it as is" + - Added an option to only set the number format if doesn't match the default for the sheet. +-Export-Excel Pester Tests + - I have converted examples 1-9, 11 and 13 from Export-Excel help into tests and have added some additional tests, and extra parameters to the example command to ge better test coverage. The test so far has 184 "should" conditions grouped as 58 "IT" statements; but is still a work in progress. +--- + + - [James O'Neill](https://twitter.com/jamesoneill) added `Compare-Worksheet` - Compares two worksheets with the same name in different files. diff --git a/SetFormat.ps1 b/SetFormat.ps1 index 3c24aa4..d4a1928 100644 --- a/SetFormat.ps1 +++ b/SetFormat.ps1 @@ -12,7 +12,7 @@ #> Param ( #One or more row(s), Column(s) and/or block(s) of cells to format - [Parameter(ValueFromPipeline = $true,ParameterSetName="Address",Mandatory=$True)] + [Parameter(ValueFromPipeline = $true,ParameterSetName="Address",Mandatory=$True,Position=0)] $Address , #The worksheet where the format is to be applied [Parameter(ParameterSetName="SheetAndRange",Mandatory=$True)] From 6c24db9593625829b287f47950ee04aac12bc7b1 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Thu, 28 Jun 2018 12:14:13 +0100 Subject: [PATCH 06/11] Added -PivotTableName to export excel (see also Readme.md) --- Export-Excel.Tests.ps1 | 24 ++++++++++++------------ Export-Excel.ps1 | 22 ++++++++++++---------- README.md | 3 ++- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/Export-Excel.Tests.ps1 b/Export-Excel.Tests.ps1 index c5a77db..1a19d6b 100644 --- a/Export-Excel.Tests.ps1 +++ b/Export-Excel.Tests.ps1 @@ -2,7 +2,7 @@ $here = Split-Path -Parent $MyInvocation.MyCommand.Path -Import-Module $here -Force +Import-Module $here -Force -Verbose if (Get-process -Name Excel,xlim -ErrorAction SilentlyContinue) { Write-Warning -Message "You need to close Excel before running the tests." ; return} Describe ExportExcel { @@ -441,10 +441,10 @@ Describe ExportExcel { it "Cloned 'Sheet1' to 'NewSheet' "{ $newWs = $excel.Workbook.Worksheets["NewSheet"] - $newWs.Dimension.Address | should be ($excel.Workbook.Worksheets["Sheet1"].Dimension.Address) - $newWs.ConditionalFormatting.Count | should be ($excel.Workbook.Worksheets["Sheet1"].ConditionalFormatting.Count) + $newWs.Dimension.Address | should be ($excel.Workbook.Worksheets["Sheet1"].Dimension.Address) + $newWs.ConditionalFormatting.Count | should be ($excel.Workbook.Worksheets["Sheet1"].ConditionalFormatting.Count) $newWs.ConditionalFormatting[0].Address.Address | should be ($excel.Workbook.Worksheets["Sheet1"].ConditionalFormatting[0].Address.Address) - $newWs.ConditionalFormatting[0].Formula | should be ($excel.Workbook.Worksheets["Sheet1"].ConditionalFormatting[0].Formula) + $newWs.ConditionalFormatting[0].Formula | should be ($excel.Workbook.Worksheets["Sheet1"].ConditionalFormatting[0].Formula) } } @@ -453,12 +453,12 @@ Describe ExportExcel { $path = "$env:TEMP\Test.xlsx" #Catch warning $warnVar = $null - #Test Append with no existing sheet. - get-process | Select-Object -first 10 -Property Name,cpu,pm,handles,company | export-excel -StartRow 3 -StartColumn 3 -AutoFilter -AutoNameRange -BoldTopRow -IncludePivotTable -PivotRows Company -PivotData PM -Path $path -WorkSheetname withOffset -append - get-process | Select-Object -last 10 -Property Name,cpu,pm,handles,company | export-excel -StartRow 3 -StartColumn 3 -AutoFilter -AutoNameRange -BoldTopRow -IncludePivotTable -PivotRows Company -PivotData PM -Path $path -WorkSheetname withOffset -append -WarningAction SilentlyContinue -WarningVariable warnvar + #Test Append with no existing sheet. Test adding a named pivot table from a command line parameter + get-process | Select-Object -first 10 -Property Name,cpu,pm,handles,company | export-excel -StartRow 3 -StartColumn 3 -AutoFilter -AutoNameRange -BoldTopRow -IncludePivotTable -PivotRows Company -PivotData PM -PivotTableName 'PTOffset' -Path $path -WorkSheetname withOffset -append + get-process | Select-Object -last 10 -Property Name,cpu,pm,handles,company | export-excel -StartRow 3 -StartColumn 3 -AutoFilter -AutoNameRange -BoldTopRow -IncludePivotTable -PivotRows Company -PivotData PM -PivotTableName 'PTOffset' -Path $path -WorkSheetname withOffset -append -WarningAction SilentlyContinue -WarningVariable warnvar $Excel = Open-ExcelPackage $path $dataWs = $Excel.Workbook.Worksheets["withOffset"] - $pt = $Excel.Workbook.Worksheets["withOffsetPivotTable"].PivotTables[0] + $pt = $Excel.Workbook.Worksheets["PTOffset"].PivotTables[0] it "Created and appended to a sheet offset from the top left corner " { $dataWs.Cells[1,1].Value | Should beNullOrEmpty $dataWs.Cells[2,2].Value | Should beNullOrEmpty @@ -479,10 +479,9 @@ Describe ExportExcel { Context "#Example 11 # Create and append with title, inc ranges and Pivot table" { $path = "$env:TEMP\Test.xlsx" - #Catch warning $ptDef = [ordered]@{} - $ptDef += New-PivotTableDefinition -PivotTableName "PT1" -SourceWorkSheet 'Sheet1' -PivotRows "Status" -PivotData @{'Status' = 'Count'} -PivotFilter "StartType" -IncludePivotChart -ChartType BarClustered3D -ChartTitle "Services by status" -ChartHeight 512 -ChartWidth 768 -ChartRow 10 -ChartColumn 0 -NoLegend - $ptDef += New-PivotTableDefinition -PivotTableName "PT2" -SourceWorkSheet 'Sheet2' -PivotRows "Company" -PivotData @{'Company' = 'Count'} -IncludePivotChart -ChartType PieExploded3D -ShowPercent -WarningAction SilentlyContinue + $ptDef += New-PivotTableDefinition -PivotTableName "PT1" -SourceWorkSheet 'Sheet1' -PivotRows "Status" -PivotData @{'Status' = 'Count'} -PivotFilter "StartType" -IncludePivotChart -ChartType BarClustered3D -ChartTitle "Services by status" -ChartHeight 512 -ChartWidth 768 -ChartRow 10 -ChartColumn 0 -NoLegend + $ptDef += New-PivotTableDefinition -PivotTableName "PT2" -SourceWorkSheet 'Sheet2' -PivotRows "Company" -PivotData @{'Company' = 'Count'} -IncludePivotChart -ChartType PieExploded3D -ShowPercent -WarningAction SilentlyContinue it "Built a pivot definition using New-PivotTableDefinition " { $ptDef.PT1.SourceWorkSheet | Should be 'Sheet1' @@ -493,9 +492,10 @@ Describe ExportExcel { $ptDef.PT1.ChartType.tostring() | Should be 'BarClustered3D' } Remove-Item -Path $path + #Catch warning $warnvar = $null Get-Service | Select-Object -Property Status, Name, DisplayName, StartType | Export-Excel -Path $path -AutoSize -TableName "All Services" -TableStyle Medium1 -WarningAction SilentlyContinue -WarningVariable warnvar - Get-Process | Select-Object -Property Name, Company, Handles, CPU, VM | Export-Excel -Path $path -AutoSize -WorkSheetname 'sheet2' -TableName "Processes" -TableStyle Light1 -Title "Processes" -TitleFillPattern Solid -TitleBackgroundColor AliceBlue -TitleBold -TitleSize 22 -PivotTableDefinition $ptDef + Get-Process | Select-Object -Property Name, Company, Handles, CPU, VM | Export-Excel -Path $path -AutoSize -WorkSheetname 'sheet2' -TableName "Processes" -TableStyle Light1 -Title "Processes" -TitleFillPattern Solid -TitleBackgroundColor AliceBlue -TitleBold -TitleSize 22 -PivotTableDefinition $ptDef $Excel = Open-ExcelPackage $path $ws1 = $Excel.Workbook.Worksheets["Sheet1"] $ws2 = $Excel.Workbook.Worksheets["Sheet2"] diff --git a/Export-Excel.ps1 b/Export-Excel.ps1 index c6dddfe..913d239 100644 --- a/Export-Excel.ps1 +++ b/Export-Excel.ps1 @@ -354,6 +354,7 @@ [Int]$TitleSize = 22, [System.Drawing.Color]$TitleBackgroundColor, [Switch]$IncludePivotTable, + [String]$PivotTableName, [String[]]$PivotRows, [String[]]$PivotColumns, $PivotData, @@ -730,21 +731,22 @@ } if ($IncludePivotTable -or $IncludePivotChart) { $params = @{ - "PivotTableName" = ($WorkSheetname + 'PivotTable') ; "SourceRange" = $dataRange } - if ($PivotFilter) {$params.PivotFilter = $PivotFilter} - if ($PivotRows) {$params.PivotRows = $PivotRows} - if ($PivotColumns) {$Params.PivotColumns = $PivotColumns} - if ($PivotData) {$Params.PivotData = $PivotData} - if ($NoTotalsInPivot) {$params.NoTotalsInPivot = $true} + if ($PivotTableName) {$params.PivotTableName = $PivotTableName} + else {$params.PivotTableName = $WorkSheetname + 'PivotTable'} + if ($PivotFilter) {$params.PivotFilter = $PivotFilter} + if ($PivotRows) {$params.PivotRows = $PivotRows} + if ($PivotColumns) {$Params.PivotColumns = $PivotColumns} + if ($PivotData) {$Params.PivotData = $PivotData} + if ($NoTotalsInPivot) {$params.NoTotalsInPivot = $true} if ($PivotDataToColumn) {$params.PivotDataToColumn = $true} if ($IncludePivotChart) { $params.IncludePivotChart = $true - $Params.ChartType = $ChartType - if ($ShowCategory) {$params.ShowCategory = $true} - if ($ShowPercent) {$params.ShowPercent = $true} - if ($NoLegend) {$params.NoLegend = $true} + $Params.ChartType = $ChartType + if ($ShowCategory) {$params.ShowCategory = $true} + if ($ShowPercent) {$params.ShowPercent = $true} + if ($NoLegend) {$params.NoLegend = $true} } Add-PivotTable -ExcelPackage $pkg -SourceWorkSheet $ws @params } diff --git a/README.md b/README.md index 4eb9bc3..6e3663a 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/dfi - Fixed a bug where Append can overwrite the last rows of data if the initial export had blank rows at the top of the sheet. - Removed the need to specify a fill type when specifying a title background color - Added MoveToStart, MoveToEnd, MoveBefore and MoveAfter Parameters - these go straight through to Add worksheet - - Added "NoScriptOrAliasProperties" "DisplayPropertySet" switches (names subject to change) - combined with ExcludeProperty these are a quick way to reduce the data exported (and speed things up) + - Added "NoScriptOrAliasProperties" "DisplayPropertySet" switches (names subject to change) - combined with ExcludeProperty these are a quick way to reduce the data exported (and speed things up) + - Added PivotTableName Switch (in line with 5.0.1 release) - Add-CellValue now understands URI item properties. If a property is of type URI it is created as a hyperlink to speed up Add-CellValue - Commented out the write verbose statements even if verbose is silenced they cause a significiant performance impact and if it's on they will cause a flood of messages. - Re-ordered the choices in the switch and added an option to say "If it is numeric already post it as is" From acd66607d3f6f992306608f2618182878bba5616 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 29 Jun 2018 16:01:47 +0100 Subject: [PATCH 07/11] Compare worksheet test and bugfix Bug when worksheets had different names and were in different files, sometimes colour was not set --- Compare-WorkSheet.ps1 | 12 ++- Compare-WorkSheet.tests.ps1 | 182 ++++++++++++++++++++++++++++++++++++ README.md | 3 + compare-WorkSheet.ps1 | 12 ++- 4 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 Compare-WorkSheet.tests.ps1 diff --git a/Compare-WorkSheet.ps1 b/Compare-WorkSheet.ps1 index e39a898..6613251 100644 --- a/Compare-WorkSheet.ps1 +++ b/Compare-WorkSheet.ps1 @@ -175,9 +175,17 @@ else {$xl2 = Open-ExcelPackage -path $Differencefile } foreach ($u in $updates) { foreach ($p in $propList) { + if ($u.group[0]._file -eq $Referencefile) { + $ws1 = $xl1.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl2.Workbook.Worksheets[$u.Group[1]._sheet] + } + else { + $ws1 = $xl2.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl1.Workbook.Worksheets[$u.Group[1]._sheet] + } 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 + Set-Format -WorkSheet $ws1 -Range ($Columns[$p] + $u.Group[0]._Row) -FontColor $FontColor + Set-Format -WorkSheet $ws1 -Range ($Columns[$p] + $u.Group[1]._Row) -FontColor $FontColor } } } diff --git a/Compare-WorkSheet.tests.ps1 b/Compare-WorkSheet.tests.ps1 new file mode 100644 index 0000000..a972d15 --- /dev/null +++ b/Compare-WorkSheet.tests.ps1 @@ -0,0 +1,182 @@ +describe "Compare Worksheet" { + + del "$env:temp\server*.xlsx" + [System.Collections.ArrayList]$s = get-service | Select-Object -Property * + + $s | Export-Excel -Path $env:temp\server1.xlsx + + #$s is a zero based array, excel rows are 1 based and excel has a header row so Excel rows will be 2 + index in $s + $row4Displayname = $s[2].DisplayName + $s[2].DisplayName = "Changed from the orginal" + + $d = $s[-1] | Select-Object -Property * + $d.DisplayName = "Dummy Service" + $d.Name = "Dummy" + $s.Insert(3,$d) + + $row6Name = $s[5].name + $s.RemoveAt(5) + + $s | Export-Excel -Path $env:temp\server2.xlsx + #Assume default worksheet name, (sheet1) and column header for key ("name") + $comp = compare-WorkSheet "$env:temp\Server1.xlsx" "$env:temp\Server2.xlsx" + + Context "Simple comparison output" { + it "Found the right number of differences " { + $comp | should not beNullOrEmpty + $comp.Count | should be 4 + } + it "Found the data row with a changed property " { + $comp | should not beNullOrEmpty + $comp[0]._Side | should be '=>' + $comp[1]._Side | should be '<=' + $comp[0]._Row | should be 4 + $comp[1]._Row | should be 4 + $comp[1].Name | should be $comp[0].Name + $comp[1].DisplayName | should be $row4Displayname + $comp[0].DisplayName | should be "Changed from the orginal" + } + it "Found the inserted data row " { + $comp | should not beNullOrEmpty + $comp[2]._Side | should be '=>' + $comp[2]._Row | should be 5 + $comp[2].Name | should be "Dummy" + } + it "Found the deleted data row " { + $comp | should not beNullOrEmpty + $comp[3]._Side | should be '<=' + $comp[3]._Row | should be 6 + $comp[3].Name | should be $row6Name + } + } + + $null = compare-WorkSheet "$env:temp\Server1.xlsx" "$env:temp\Server2.xlsx" -BackgroundColor LightGreen + $xl1 = Open-ExcelPackage -Path "$env:temp\Server1.xlsx" + $xl2 = Open-ExcelPackage -Path "$env:temp\Server2.xlsx" + $s1Sheet = $xl1.Workbook.Worksheets[1] + $s2Sheet = $xl2.Workbook.Worksheets[1] + + Context "Setting the background to highlight different rows" { + it "set the background on the right rows " { + $s1Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s1Sheet.Cells["6:6"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s2Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s2Sheet.Cells["5:5"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + } + it "Didn't set other cells " { + $s1Sheet.Cells["3:3"].Style.Fill.BackgroundColor.Rgb | should not be "FF90EE90" + $s1Sheet.Cells["F4"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["F4"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["3:3"].Style.Fill.BackgroundColor.Rgb | should not be "FF90EE90" + } + } + + Close-ExcelPackage -ExcelPackage $xl1 -NoSave + Close-ExcelPackage -ExcelPackage $xl2 -NoSave + $null = compare-WorkSheet "$env:temp\Server1.xlsx" "$env:temp\Server2.xlsx" -AllDataBackgroundColor white -BackgroundColor LightGreen -FontColor DarkRed + $xl1 = Open-ExcelPackage -Path "$env:temp\Server1.xlsx" + $xl2 = Open-ExcelPackage -Path "$env:temp\Server2.xlsx" + $s1Sheet = $xl1.Workbook.Worksheets[1] + $s2Sheet = $xl2.Workbook.Worksheets[1] + + Context "Setting the forgound to highlight changed properties" { + it "Added foreground colour to the right cells " { + $s1Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s1Sheet.Cells["6:6"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s2Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s2Sheet.Cells["5:5"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s1Sheet.Cells["F4"].Style.Font.Color.Rgb | should be "FF8B0000" + $s2Sheet.Cells["F4"].Style.Font.Color.Rgb | should be "FF8B0000" + } + it "Didn't set the foreground on other cells " { + $s1Sheet.Cells["F5"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["F5"].Style.Font.Color.Rgb | should beNullOrEmpty + $s1Sheet.Cells["G4"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["G4"].Style.Font.Color.Rgb | should beNullOrEmpty + + } + } + + Close-ExcelPackage -ExcelPackage $xl1 -NoSave + Close-ExcelPackage -ExcelPackage $xl2 -NoSave + + [System.Collections.ArrayList]$s = get-service | Select-Object -Property * -ExcludeProperty Name + + $s | Export-Excel -Path $env:temp\server1.xlsx -WorkSheetname Server1 + + #$s is a zero based array, excel rows are 1 based and excel has a header row so Excel rows will be 2 + index in $s + $row4Displayname = $s[2].DisplayName + $s[2].DisplayName = "Changed from the orginal" + + $d = $s[-1] | Select-Object -Property * + $d.DisplayName = "Dummy Service" + $d.ServiceName = "Dummy" + $s.Insert(3,$d) + + $row6Name = $s[5].ServiceName + $s.RemoveAt(5) + + $s[10].ServiceType = "Changed should not matter" + + $s | Select-Object -Property ServiceName, DisplayName, StartType, ServiceType | Export-Excel -Path $env:temp\server2.xlsx -WorkSheetname server2 + #Assume default worksheet name, (sheet1) and column header for key ("name") + $comp = compare-WorkSheet "$env:temp\Server1.xlsx" "$env:temp\Server2.xlsx" -WorkSheetName Server1,Server2 -Key ServiceName -Property DisplayName,StartType -AllDataBackgroundColor AliceBlue -BackgroundColor White -FontColor Red + + $xl1 = Open-ExcelPackage -Path "$env:temp\Server1.xlsx" + $xl2 = Open-ExcelPackage -Path "$env:temp\Server2.xlsx" + + $s1Sheet = $xl1.Workbook.Worksheets["server1"] + $s2Sheet = $xl2.Workbook.Worksheets["server2"] + Context "More complex comparison output etc different worksheet names " { + it "Found the right number of differences " { + $comp | should not beNullOrEmpty + $comp.Count | should be 4 + } + it "Found the data row with a changed property " { + $comp | should not beNullOrEmpty + $comp[0]._Side | should be '=>' + $comp[1]._Side | should be '<=' + $comp[0]._Row | should be 4 + $comp[1]._Row | should be 4 + $comp[1].ServiceName | should be $comp[0].ServiceName + $comp[1].DisplayName | should be $row4Displayname + $comp[0].DisplayName | should be "Changed from the orginal" + } + it "Found the inserted data row " { + $comp | should not beNullOrEmpty + $comp[2]._Side | should be '=>' + $comp[2]._Row | should be 5 + $comp[2].ServiceName | should be "Dummy" + } + it "Found the deleted data row " { + $comp | should not beNullOrEmpty + $comp[3]._Side | should be '<=' + $comp[3]._Row | should be 6 + $comp[3].ServiceName | should be $row6Name + } + + it "set the background on the right rows " { + $s1Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FFFFFFFF" + $s1Sheet.Cells["6:6"].Style.Fill.BackgroundColor.Rgb | should be "FFFFFFFF" + $s2Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FFFFFFFF" + $s2Sheet.Cells["5:5"].Style.Fill.BackgroundColor.Rgb | should be "FFFFFFFF" + + $s1Sheet.Cells["E4"].Style.Font.Color.Rgb | should be "FFFF0000" + $s2Sheet.Cells["E4"].Style.Font.Color.Rgb | should be "FFFF0000" + } + it "Didn't set other cells " { + $s1Sheet.Cells["3:3"].Style.Fill.BackgroundColor.Rgb | should not be "FFFFFFFF" + $s2Sheet.Cells["3:3"].Style.Fill.BackgroundColor.Rgb | should not be "FFFFFFFF" + $s1Sheet.Cells["E5"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["E5"].Style.Font.Color.Rgb | should beNullOrEmpty + $s1Sheet.Cells["F4"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["F4"].Style.Font.Color.Rgb | should beNullOrEmpty + } + + } + Close-ExcelPackage -ExcelPackage $xl1 -NoSave -Show + Close-ExcelPackage -ExcelPackage $xl2 -NoSave -Show + + +} + diff --git a/README.md b/README.md index 6e3663a..f776796 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/dfi - `Compare-Worksheet` (introduced in 5.0) uses the built in `Compare-object` command, to output a command-line DIFF and/or colour the worksheet to show differences. For example, if my sheets are Windows services the *extra* rows or rows where the startup status has changed get highlighted - `Merge-Worksheet` (also introduced in 5.0) joins two lumps, side by highlighting the differences. So now I can have server A's services and Server Bs Services on the same page. I figured out a way to do multiple sheets. So I can have Server A,B,C,D on one page :-) that is `Merge-MultpleSheets` For this release I've fixed heaven only knows how many typos and proof reading errors in the help for these two, but the code is unchanged - although correcting the spelling of Merge-MultipleSheets is potentially a breaking change (and it is still plural!) + also fixed a bug in compare worksheet where color might not be applied correctly when the worksheets came from different files and had different name. - `Join-Worksheet` is **new** for ths release. At it's simplest it copies all the data in Worksheet A to the end of Worksheet B - Add-Worksheet - I have moved this from ImportExcel.psm1 to ExportExcel.ps1 and it now can move a new worksheet to the right place, and can copy an existing worksheet (from the same or a different workbook) to a new one, and I set the Set return-type to aid intellisense @@ -68,6 +69,8 @@ iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/dfi - Added an option to only set the number format if doesn't match the default for the sheet. -Export-Excel Pester Tests - I have converted examples 1-9, 11 and 13 from Export-Excel help into tests and have added some additional tests, and extra parameters to the example command to ge better test coverage. The test so far has 184 "should" conditions grouped as 58 "IT" statements; but is still a work in progress. +-Compare-Worksheet pester tests + --- diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index e39a898..6613251 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -175,9 +175,17 @@ else {$xl2 = Open-ExcelPackage -path $Differencefile } foreach ($u in $updates) { foreach ($p in $propList) { + if ($u.group[0]._file -eq $Referencefile) { + $ws1 = $xl1.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl2.Workbook.Worksheets[$u.Group[1]._sheet] + } + else { + $ws1 = $xl2.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl1.Workbook.Worksheets[$u.Group[1]._sheet] + } 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 + Set-Format -WorkSheet $ws1 -Range ($Columns[$p] + $u.Group[0]._Row) -FontColor $FontColor + Set-Format -WorkSheet $ws1 -Range ($Columns[$p] + $u.Group[1]._Row) -FontColor $FontColor } } } From 06c22109516d4ec4a48b3aef9c0814a84bb32e74 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 29 Jun 2018 16:04:15 +0100 Subject: [PATCH 08/11] Delete Compare-WorkSheet.ps1 Trying to get the case consistent I create a duplicate file --- Compare-WorkSheet.ps1 | 252 ------------------------------------------ 1 file changed, 252 deletions(-) delete mode 100644 Compare-WorkSheet.ps1 diff --git a/Compare-WorkSheet.ps1 b/Compare-WorkSheet.ps1 deleted file mode 100644 index e39a898..0000000 --- a/Compare-WorkSheet.ps1 +++ /dev/null @@ -1,252 +0,0 @@ -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 '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 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 "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -GridView - This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel - 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 '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 the command specifies a limited set of columns should be used. - .Example - 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, - 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,Position=0)] - $Referencefile , - #Second file to compare - [parameter(Mandatory=$true,Position=1)] - $Differencefile , - #Name(s) of worksheets to compare. - $WorkSheetName = "Sheet1", - #Properties to include in the DIFF - supports wildcards, default is "*" - $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 = 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 (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 - ) - - #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} - 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 { - $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 } - - #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 = @{} ; $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"} - $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} - - #Add RowNumber, Sheetname and file name to every row - $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 = $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} - - 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 - [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 - 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) { - 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] - $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 ($diff -and $FontColor -and ($propList -contains $Key) ) { - $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} - 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()} - } - } - 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 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 ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" } - } - 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 ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} - else {$hash[($result.SideIndicator+$P)] =$result.$P} - } - } - [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 ($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 36fc9501eb8edc45ef6a4f8cf76621efe1318708 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 29 Jun 2018 16:08:00 +0100 Subject: [PATCH 09/11] Bug fix in compare worksheet --- compare-WorkSheet.ps1 | 513 +++++++++++++++++++++--------------------- 1 file changed, 261 insertions(+), 252 deletions(-) diff --git a/compare-WorkSheet.ps1 b/compare-WorkSheet.ps1 index e39a898..c26844e 100644 --- a/compare-WorkSheet.ps1 +++ b/compare-WorkSheet.ps1 @@ -1,252 +1,261 @@ -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 '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 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 "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -GridView - This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel - 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 '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 the command specifies a limited set of columns should be used. - .Example - 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, - 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,Position=0)] - $Referencefile , - #Second file to compare - [parameter(Mandatory=$true,Position=1)] - $Differencefile , - #Name(s) of worksheets to compare. - $WorkSheetName = "Sheet1", - #Properties to include in the DIFF - supports wildcards, default is "*" - $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 = 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 (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 - ) - - #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} - 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 { - $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 } - - #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 = @{} ; $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"} - $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} - - #Add RowNumber, Sheetname and file name to every row - $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 = $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} - - 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 - [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 - 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) { - 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] - $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 ($diff -and $FontColor -and ($propList -contains $Key) ) { - $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} - 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()} - } - } - 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 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 ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" } - } - 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 ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} - else {$hash[($result.SideIndicator+$P)] =$result.$P} - } - } - [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 ($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 +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 '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 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 "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -GridView + This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel + 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 '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 the command specifies a limited set of columns should be used. + .Example + 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, + 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,Position=0)] + $Referencefile , + #Second file to compare + [parameter(Mandatory=$true,Position=1)] + $Differencefile , + #Name(s) of worksheets to compare. + $WorkSheetName = "Sheet1", + #Properties to include in the DIFF - supports wildcards, default is "*" + $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 = 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 (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 + ) + + #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} + 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 { + $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 } + + #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 = @{} ; $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"} + $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} + + #Add RowNumber, Sheetname and file name to every row + $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 = $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} + + 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 + [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 + 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) { + 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] + $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 ($diff -and $FontColor -and ($propList -contains $Key) ) { + $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} + else {$xl2 = Open-ExcelPackage -path $Differencefile } + foreach ($u in $updates) { + foreach ($p in $propList) { + if ($u.group[0]._file -eq $Referencefile) { + $ws1 = $xl1.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl2.Workbook.Worksheets[$u.Group[1]._sheet] + } + else { + $ws1 = $xl2.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl1.Workbook.Worksheets[$u.Group[1]._sheet] + } + if($u.Group[0].$p -ne $u.Group[1].$p ) { + Set-Format -WorkSheet $ws1 -Range ($Columns[$p] + $u.Group[0]._Row) -FontColor $FontColor + Set-Format -WorkSheet $ws1 -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()} + } + } + 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 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 ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" } + } + 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 ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} + else {$hash[($result.SideIndicator+$P)] =$result.$P} + } + } + [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 ($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 } +} + From 9f7884f991c80448091ef56853027f64d98b6cc7 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 29 Jun 2018 16:13:46 +0100 Subject: [PATCH 10/11] Compare worksheet: tests, small bug fix, updated readme New tests for compare worksheet, fixed a bug when the worksheets have different names and are not in the same file sometimes, you can't assume an order when they are diffed and the results grouped. --- Compare-WorkSheet.tests.ps1 | 182 ++++ README.md | 1660 ++++++++++++++++++----------------- 2 files changed, 1014 insertions(+), 828 deletions(-) create mode 100644 Compare-WorkSheet.tests.ps1 diff --git a/Compare-WorkSheet.tests.ps1 b/Compare-WorkSheet.tests.ps1 new file mode 100644 index 0000000..a75b584 --- /dev/null +++ b/Compare-WorkSheet.tests.ps1 @@ -0,0 +1,182 @@ +describe "Compare Worksheet" { + + del "$env:temp\server*.xlsx" + [System.Collections.ArrayList]$s = get-service | Select-Object -Property * + + $s | Export-Excel -Path $env:temp\server1.xlsx + + #$s is a zero based array, excel rows are 1 based and excel has a header row so Excel rows will be 2 + index in $s + $row4Displayname = $s[2].DisplayName + $s[2].DisplayName = "Changed from the orginal" + + $d = $s[-1] | Select-Object -Property * + $d.DisplayName = "Dummy Service" + $d.Name = "Dummy" + $s.Insert(3,$d) + + $row6Name = $s[5].name + $s.RemoveAt(5) + + $s | Export-Excel -Path $env:temp\server2.xlsx + #Assume default worksheet name, (sheet1) and column header for key ("name") + $comp = compare-WorkSheet "$env:temp\Server1.xlsx" "$env:temp\Server2.xlsx" + + Context "Simple comparison output" { + it "Found the right number of differences " { + $comp | should not beNullOrEmpty + $comp.Count | should be 4 + } + it "Found the data row with a changed property " { + $comp | should not beNullOrEmpty + $comp[0]._Side | should be '=>' + $comp[1]._Side | should be '<=' + $comp[0]._Row | should be 4 + $comp[1]._Row | should be 4 + $comp[1].Name | should be $comp[0].Name + $comp[1].DisplayName | should be $row4Displayname + $comp[0].DisplayName | should be "Changed from the orginal" + } + it "Found the inserted data row " { + $comp | should not beNullOrEmpty + $comp[2]._Side | should be '=>' + $comp[2]._Row | should be 5 + $comp[2].Name | should be "Dummy" + } + it "Found the deleted data row " { + $comp | should not beNullOrEmpty + $comp[3]._Side | should be '<=' + $comp[3]._Row | should be 6 + $comp[3].Name | should be $row6Name + } + } + + $null = compare-WorkSheet "$env:temp\Server1.xlsx" "$env:temp\Server2.xlsx" -BackgroundColor LightGreen + $xl1 = Open-ExcelPackage -Path "$env:temp\Server1.xlsx" + $xl2 = Open-ExcelPackage -Path "$env:temp\Server2.xlsx" + $s1Sheet = $xl1.Workbook.Worksheets[1] + $s2Sheet = $xl2.Workbook.Worksheets[1] + + Context "Setting the background to highlight different rows" { + it "set the background on the right rows " { + $s1Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s1Sheet.Cells["6:6"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s2Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s2Sheet.Cells["5:5"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + } + it "Didn't set other cells " { + $s1Sheet.Cells["3:3"].Style.Fill.BackgroundColor.Rgb | should not be "FF90EE90" + $s1Sheet.Cells["F4"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["F4"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["3:3"].Style.Fill.BackgroundColor.Rgb | should not be "FF90EE90" + } + } + + Close-ExcelPackage -ExcelPackage $xl1 -NoSave + Close-ExcelPackage -ExcelPackage $xl2 -NoSave + $null = compare-WorkSheet "$env:temp\Server1.xlsx" "$env:temp\Server2.xlsx" -AllDataBackgroundColor white -BackgroundColor LightGreen -FontColor DarkRed + $xl1 = Open-ExcelPackage -Path "$env:temp\Server1.xlsx" + $xl2 = Open-ExcelPackage -Path "$env:temp\Server2.xlsx" + $s1Sheet = $xl1.Workbook.Worksheets[1] + $s2Sheet = $xl2.Workbook.Worksheets[1] + + Context "Setting the forgound to highlight changed properties" { + it "Added foreground colour to the right cells " { + $s1Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s1Sheet.Cells["6:6"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s2Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s2Sheet.Cells["5:5"].Style.Fill.BackgroundColor.Rgb | should be "FF90EE90" + $s1Sheet.Cells["F4"].Style.Font.Color.Rgb | should be "FF8B0000" + $s2Sheet.Cells["F4"].Style.Font.Color.Rgb | should be "FF8B0000" + } + it "Didn't set the foreground on other cells " { + $s1Sheet.Cells["F5"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["F5"].Style.Font.Color.Rgb | should beNullOrEmpty + $s1Sheet.Cells["G4"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["G4"].Style.Font.Color.Rgb | should beNullOrEmpty + + } + } + + Close-ExcelPackage -ExcelPackage $xl1 -NoSave + Close-ExcelPackage -ExcelPackage $xl2 -NoSave + + [System.Collections.ArrayList]$s = get-service | Select-Object -Property * -ExcludeProperty Name + + $s | Export-Excel -Path $env:temp\server1.xlsx -WorkSheetname Server1 + + #$s is a zero based array, excel rows are 1 based and excel has a header row so Excel rows will be 2 + index in $s + $row4Displayname = $s[2].DisplayName + $s[2].DisplayName = "Changed from the orginal" + + $d = $s[-1] | Select-Object -Property * + $d.DisplayName = "Dummy Service" + $d.ServiceName = "Dummy" + $s.Insert(3,$d) + + $row6Name = $s[5].ServiceName + $s.RemoveAt(5) + + $s[10].ServiceType = "Changed should not matter" + + $s | Select-Object -Property ServiceName, DisplayName, StartType, ServiceType | Export-Excel -Path $env:temp\server2.xlsx -WorkSheetname server2 + #Assume default worksheet name, (sheet1) and column header for key ("name") + $comp = compare-WorkSheet "$env:temp\Server1.xlsx" "$env:temp\Server2.xlsx" -WorkSheetName Server1,Server2 -Key ServiceName -Property DisplayName,StartType -AllDataBackgroundColor AliceBlue -BackgroundColor White -FontColor Red + + $xl1 = Open-ExcelPackage -Path "$env:temp\Server1.xlsx" + $xl2 = Open-ExcelPackage -Path "$env:temp\Server2.xlsx" + + $s1Sheet = $xl1.Workbook.Worksheets["server1"] + $s2Sheet = $xl2.Workbook.Worksheets["server2"] + Context "More complex comparison output etc different worksheet names " { + it "Found the right number of differences " { + $comp | should not beNullOrEmpty + $comp.Count | should be 4 + } + it "Found the data row with a changed property " { + $comp | should not beNullOrEmpty + $comp[0]._Side | should be '=>' + $comp[1]._Side | should be '<=' + $comp[0]._Row | should be 4 + $comp[1]._Row | should be 4 + $comp[1].ServiceName | should be $comp[0].ServiceName + $comp[1].DisplayName | should be $row4Displayname + $comp[0].DisplayName | should be "Changed from the orginal" + } + it "Found the inserted data row " { + $comp | should not beNullOrEmpty + $comp[2]._Side | should be '=>' + $comp[2]._Row | should be 5 + $comp[2].ServiceName | should be "Dummy" + } + it "Found the deleted data row " { + $comp | should not beNullOrEmpty + $comp[3]._Side | should be '<=' + $comp[3]._Row | should be 6 + $comp[3].ServiceName | should be $row6Name + } + + it "set the background on the right rows " { + $s1Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FFFFFFFF" + $s1Sheet.Cells["6:6"].Style.Fill.BackgroundColor.Rgb | should be "FFFFFFFF" + $s2Sheet.Cells["4:4"].Style.Fill.BackgroundColor.Rgb | should be "FFFFFFFF" + $s2Sheet.Cells["5:5"].Style.Fill.BackgroundColor.Rgb | should be "FFFFFFFF" + + $s1Sheet.Cells["E4"].Style.Font.Color.Rgb | should be "FFFF0000" + $s2Sheet.Cells["E4"].Style.Font.Color.Rgb | should be "FFFF0000" + } + it "Didn't set other cells " { + $s1Sheet.Cells["3:3"].Style.Fill.BackgroundColor.Rgb | should not be "FFFFFFFF" + $s2Sheet.Cells["3:3"].Style.Fill.BackgroundColor.Rgb | should not be "FFFFFFFF" + $s1Sheet.Cells["E5"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["E5"].Style.Font.Color.Rgb | should beNullOrEmpty + $s1Sheet.Cells["F4"].Style.Font.Color.Rgb | should beNullOrEmpty + $s2Sheet.Cells["F4"].Style.Font.Color.Rgb | should beNullOrEmpty + } + + } + Close-ExcelPackage -ExcelPackage $xl1 -NoSave -Show + Close-ExcelPackage -ExcelPackage $xl2 -NoSave -Show + + +} + diff --git a/README.md b/README.md index 4eb9bc3..3c74090 100644 --- a/README.md +++ b/README.md @@ -1,828 +1,832 @@ -PowerShell Import-Excel -- - -Install from the [PowerShell Gallery](https://www.powershellgallery.com/packages/ImportExcel/). - -This PowerShell Module allows you to read and write Excel files without installing Microsoft Excel on your system. No need to bother with the cumbersome Excel COM-object. Creating Tables, Pivot Tables, Charts and much more has just become a lot easier. - -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/testimonial.png) - -# How to Vidoes -* [PowerShell Excel Module - ImportExcel](https://www.youtube.com/watch?v=U3Ne_yX4tYo&list=PL5uoqS92stXioZw-u-ze_NtvSo0k0K0kq) - -Installation -- -#### [PowerShell V5](https://www.microsoft.com/en-us/download/details.aspx?id=50395) and Later -You can install the `ImportExcel` module directly from the PowerShell Gallery - -* [Recommended] Install to your personal PowerShell Modules folder -```PowerShell -Install-Module ImportExcel -scope CurrentUser -``` -* [Requires Elevation] Install for Everyone (computer PowerShell Modules folder) -```PowerShell -Install-Module ImportExcel -``` - -#### PowerShell V4 and Earlier -To install to your personal modules folder (e.g. ~\Documents\WindowsPowerShell\Modules), run: - -```PowerShell -iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/dfinke/ImportExcel/master/Install.ps1') -``` - -# What's new - -- New commands - Diff , Merge and Join - - `Compare-Worksheet` (introduced in 5.0) uses the built in `Compare-object` command, to output a command-line DIFF and/or colour the worksheet to show differences. For example, if my sheets are Windows services the *extra* rows or rows where the startup status has changed get highlighted - - `Merge-Worksheet` (also introduced in 5.0) joins two lumps, side by highlighting the differences. So now I can have server A's services and Server Bs Services on the same page. I figured out a way to do multiple sheets. So I can have Server A,B,C,D on one page :-) that is `Merge-MultpleSheets` - For this release I've fixed heaven only knows how many typos and proof reading errors in the help for these two, but the code is unchanged - although correcting the spelling of Merge-MultipleSheets is potentially a breaking change (and it is still plural!) - - `Join-Worksheet` is **new** for ths release. At it's simplest it copies all the data in Worksheet A to the end of Worksheet B -- Add-Worksheet - - I have moved this from ImportExcel.psm1 to ExportExcel.ps1 and it now can move a new worksheet to the right place, and can copy an existing worksheet (from the same or a different workbook) to a new one, and I set the Set return-type to aid intellisense -- New-PivotTableDefinition - - Now Supports `-PivotFilter` and `-PivotDataToColumn`, `-ChartHeight/width` `-ChartRow/Column`, `-ChartRow/ColumnPixelOffset` parameters -- Set-Format - - Fixed a bug where the `-address` parameter had to be named, although the examples in `export-excel` help showed it working by position (which works now. ) -- Export-Excel - - I've done some re-factoring - 1. I "flattened out" small "called-once" functions , add-title, convert-toNumber and Stop-ExcelProcess. - 2. It now uses Add-Worksheet, Open-ExcelPackage and Add-ConditionalFormat instead of duplicating their functionality. - 3. I've moved the PivotTable functionality (which was doubled up) out to a new function "Add-PivotTable" which supports some extra parameters PivotFilter and PivotDataToColumn, ChartHeight/width ChartRow/Column, ChartRow/ColumnPixelOffsets. - 4. I've made the try{} catch{} blocks cover smaller blocks of code to give a better idea where a failure happend, some of these now Warn instead of throwing - I'd rather save the data with warnings than throw it away because we can't add a chart. Along with this I've added some extra write-verbose messages - - Bad column-names specified for Pivots now generate warnings instead of throwing. - - Fixed issues when pivottables / charts already exist and an export tries to create them again. - - Fixed issue where AutoNamedRange, NamedRange, and TableName do not work when appending to a sheet which already contains the range(s) / table - - Fixed issue where AutoNamedRange may try to create ranges with an illegal name. - - Added check for illegal characters in RangeName or Table Name (replace them with "_"), changed tablename validation to allow spaces and applied same validation to RangeName - - Fixed a bug where BoldTopRow is always bolds row 1 even if the export is told to start at a lower row. - - Fixed a bug where titles throw pivot table creation out of alignment. - - Fixed a bug where Append can overwrite the last rows of data if the initial export had blank rows at the top of the sheet. - - Removed the need to specify a fill type when specifying a title background color - - Added MoveToStart, MoveToEnd, MoveBefore and MoveAfter Parameters - these go straight through to Add worksheet - - Added "NoScriptOrAliasProperties" "DisplayPropertySet" switches (names subject to change) - combined with ExcludeProperty these are a quick way to reduce the data exported (and speed things up) - - Add-CellValue now understands URI item properties. If a property is of type URI it is created as a hyperlink to speed up Add-CellValue - - Commented out the write verbose statements even if verbose is silenced they cause a significiant performance impact and if it's on they will cause a flood of messages. - - Re-ordered the choices in the switch and added an option to say "If it is numeric already post it as is" - - Added an option to only set the number format if doesn't match the default for the sheet. --Export-Excel Pester Tests - - I have converted examples 1-9, 11 and 13 from Export-Excel help into tests and have added some additional tests, and extra parameters to the example command to ge better test coverage. The test so far has 184 "should" conditions grouped as 58 "IT" statements; but is still a work in progress. ---- - - -- [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 - - Removed `[PSPlot]` as OutputType. Fixes it throwing an error -- [Nasir Zubair](https://github.com/nzubair) added `ConvertEmptyStringsToNull` to the function `ConvertFrom-ExcelToSQLInsert` - - If specified, cells without any data are replaced with NULL, instead of an empty string. This is to address behviors in certain DBMS where an empty string is insert as 0 for INT column, instead of a NULL value. - - -#### 4/10/2018 --New parameter `-ReZip`. It ReZips the xlsx so it can be imported to PowerBI - -Thanks to [Justin Grote](https://github.com/JustinGrote) for finding and fixing the error that Excel files created do not import to PowerBI online. Plus, thank you to [CrashM](https://github.com/CrashM) for confirming the fix. - -Super helpful! - -#### 3/31/2018 -- Updated `Set-Format` - * Added parameters to set borders for cells, including top, bottm, left and right - * Added parameters to set `value` and `formula` - -```powershell -$data = @" -From,To,RDollars,RPercent,MDollars,MPercent,Revenue,Margin -Atlanta,New York,3602000,.0809,955000,.09,245,65 -New York,Washington,4674000,.105,336000,.03,222,16 -Chicago,New York,4674000,.0804,1536000,.14,550,43 -New York,Philadelphia,12180000,.1427,-716000,-.07,321,-25 -New York,San Francisco,3221000,.0629,1088000,.04,436,21 -New York,Phoneix,2782000,.0723,467000,.10,674,33 -"@ -``` - -![](https://github.com/dfinke/ImportExcel/blob/master/images/CustomReport.png?raw=true) - - -- Added `-PivotFilter` parameter, allows you to set up a filter so you can drill down into a subset of the overall dataset. - -```powershell -$data =@" -Region,Area,Product,Units,Cost -North,A1,Apple,100,.5 -South,A2,Pear,120,1.5 -East,A3,Grape,140,2.5 -West,A4,Banana,160,3.5 -North,A1,Pear,120,1.5 -North,A1,Grape,140,2.5 -"@ -``` - -![](https://github.com/dfinke/ImportExcel/blob/master/images/PivotTableFilter.png?raw=true) - - -#### 3/14/2018 -- Thank you to [James O'Neill](https://twitter.com/jamesoneill), fixed bugs with ChangeDatabase parameter which would prevent it working - -#### -* Added -Force to New-Alias -* Add example to set the background color of a column -* Supports excluding Row Grand Totals for PivotTables -* Allow xlsm files to be read -* Fix `Set-Column.ps1`, `Set-Row.ps1`, `SetFormat.ps1`, `formatting.ps1` **$falsee** and **$BorderRound** -#### 1/1/2018 -* Added switch `[Switch]$NoTotalsInPivot`. Allows hiding of the row totals in the pivot table. -Thanks you to [jameseholt](https://github.com/jameseholt) for the request. - -```powershell - get-process | where Company | select Company, Handles, WorkingSet | - export-excel C:\temp\testColumnGrand.xlsx ` - -Show -ClearSheet -KillExcel ` - -IncludePivotTable -PivotRows Company -PivotData @{"Handles"="average"} -NoTotalsInPivot -``` - -* Fixed when using certain a `ChartType` for the Pivot Table Chart, would throw an error -* Fixed - when you specify a file, and the directory does not exit, it now creates it - -#### 11/23/2017 -More great additions and thanks to [James O'Neill](https://twitter.com/jamesoneill) - -* Added `Convert-XlRangeToImage` Gets the specified part of an Excel file and exports it as an image -* Fixed a typo in the message at line 373. -* Now catch an attempt to both clear the sheet and append to it. -* Fixed some issues when appending to sheets where the header isn't in row 1 or the data doesn't start in column 1. -* Added support for more settings when creating a pivot chart. -* Corrected a typo PivotTableName was PivtoTableName in definition of New-PivotTableDefinition -* Add-ConditionalFormat and Set-Format added to the parameters so each has the choice of working more like the other. -* Added Set-Row and Set-Column - fill a formula down or across. -* Added Send-SQLDataToExcel. Insert a rowset and then call Export-Excel for ranges, charts, pivots etc - -#### 10/30/2017 -Huge thanks to [James O'Neill](https://twitter.com/jamesoneill). PowerShell aficionado. He always brings a flare when working with PowerShell. This is no exception. - -(Check out the examples `help Export-Excel -Examples`) - -* New parameter `Package` allows an ExcelPackage object returned by `-passThru` to be passed in -* New parameter `ExcludeProperty` to remove unwanted properties without needing to go through `select-object` -* New parameter `Append` code to read the existing headers and move the insertion point below the current data -* New parameter `ClearSheet` which removes the worksheet and any past data - -* Remove any existing Pivot table before trying to [re]create it -* Check for inserting a pivot table so if `-InsertPivotChart` is specified it implies `-InsertPivotTable` - -(Check out the examples `help Export-Excel -Examples`) - -* New function `Export-Charts` (requires Excel to be installed) - Export Excel charts out as JPG files -* New function `Add-ConditionalFormatting` Adds contitional formatting to worksheet -* New function `Set-Format` Applies Number, font, alignment and colour formatting to a range of Excel Cells -* `ColorCompletion` an argument completer for `Colors` for params across functions - -I also worked out the parameters so you can do this, which is the same as passing `-Now`. It creates an Excel file name for you, does an auto fit and sets up filters. - -`ps | select Company, Handles | Export-Excel` - -#### 10/13/2017 -Added `New-PivotTableDefinition`. You can create and wire up a PivotTable to a WorkSheet. You can also create as many PivotTable Worksheets to point a one Worksheet. Or, you create many Worksheets and many corresponding PivotTable Worksheets. - -Here you can create a WorkSheet with the data from `Get-Service`. Then create four PivotTables, pointing to the data each pivoting on a differnt dimension and showing a differnet chart - -```powershell -$base = @{ - SourceWorkSheet = 'gsv' - PivotData = @{'Status' = 'count'} - IncludePivotChart = $true -} - -$ptd = [ordered]@{} - -$ptd += New-PivotTableDefinition @base servicetype -PivotRows servicetype -ChartType Area3D -$ptd += New-PivotTableDefinition @base status -PivotRows status -ChartType PieExploded3D -$ptd += New-PivotTableDefinition @base starttype -PivotRows starttype -ChartType BarClustered3D -$ptd += New-PivotTableDefinition @base canstop -PivotRows canstop -ChartType ConeColStacked - -Get-Service | Export-Excel -path $file -WorkSheetname gsv -Show -PivotTableDefinition $ptd -``` - -#### 10/4/2017 -Thanks to https://github.com/ili101 : -- Fix Bug, Unable to find type [PSPlot] -- Fix Bug, AutoFilter with TableName create corrupted Excel file. - -#### 10/2/2017 -Thanks to [Jeremy Brun](https://github.com/jeremytbrun) -Fixed issues related to use of -Title parameter combined with column formatting parameters. -- [Issue #182](https://github.com/dfinke/ImportExcel/issues/182) -- [Issue #89](https://github.com/dfinke/ImportExcel/issues/89) - -#### 9/28/2017 (Version 4.0.1) -- Added a new parameter called `Password` to import password protected files -- Added even more `Pester` tests for a more robust and bug free module -- Renamed parameter 'TopRow' to 'StartRow' - This allows us to be more concise when new parameters ('StartColumn', ..) will be added in the future Your code will not break after the update, because we added an alias for backward compatibility - -Special thanks to [robinmalik](https://github.com/robinmalik) for providing us with [the code](https://github.com/dfinke/ImportExcel/issues/174) to implement this new feature. A high five to [DarkLite1](https://github.com/DarkLite1) for the implementation. - -#### 9/12/2017 (Version 4.0.0) - -Super thanks and hat tip to [DarkLite1](https://github.com/DarkLite1). There is now a new and improved `Import-Excel`, not only in functionality, but also improved readability, examples and more. Not only that, he's been running it in production in his company for a number of weeks! - -*Added* `Update-FirstObjectProperties` Updates the first object to contain all the properties of the object with the most properties in the array. Check out the help. - - -***Breaking Changes***: Due to a big portion of the code that is rewritten some slightly different behavior can be expected from the `Import-Excel` function. This is especially true for importing empty Excel files with or without using the `TopRow` parameter. To make sure that your code is still valid, please check the examples in the help or the accompanying `Pester` test file. - - -Moving forward, we are planning to include automatic testing with the help of `Pester`, `Appveyor` and `Travis`. From now on any changes in the module will have to be accompanied by the corresponding `Pester` tests to avoid breakages of code and functionality. This is in preparation for new features coming down the road. - -#### 7/3/2017 -Thanks to [Mikkel Nordberg](https://www.linkedin.com/in/mikkelnordberg). He contributed a `ConvertTo-ExcelXlsx`. To use it, Excel needs to be installed. The function converts the older Excel file format ending in `.xls` to the new format ending in `.xlsx`. - -#### 6/15/2017 -Huge thank you to [DarkLite1](https://github.com/DarkLite1)! Refactoring of code, adding help, adding features, fixing bugs. Specifically this long outstanding one: - -[Export-Excel: Numeric values not correct](https://github.com/dfinke/ImportExcel/issues/168) - -It is fantastic to work with people like `DarkLite1` in the community, to help make the module so much better. A hat to you. - -Another shout out to [Damian Reeves](https://twitter.com/DamReev)! His questions turn into great features. He asked if it was possible to import an Excel worksheet and transform the data into SQL `INSERT` statements. We can now answer that question with a big YES! - -```PowerShell -ConvertFrom-ExcelToSQLInsert People .\testSQLGen.xlsx -``` - -``` -INSERT INTO People ('First', 'Last', 'The Zip') Values('John', 'Doe', '12345'); -INSERT INTO People ('First', 'Last', 'The Zip') Values('Jim', 'Doe', '12345'); -INSERT INTO People ('First', 'Last', 'The Zip') Values('Tom', 'Doe', '12345'); -INSERT INTO People ('First', 'Last', 'The Zip') Values('Harry', 'Doe', '12345'); -INSERT INTO People ('First', 'Last', 'The Zip') Values('Jane', 'Doe', '12345'); -``` -## Bonus Points -Use the underlying `ConvertFrom-ExcelData` function and you can use a scriptblock to format the data however you want. - -```PowerShell -ConvertFrom-ExcelData .\testSQLGen.xlsx { - param($propertyNames, $record) - - $reportRecord = @() - foreach ($pn in $propertyNames) { - $reportRecord += "{0}: {1}" -f $pn, $record.$pn - } - $reportRecord +="" - $reportRecord -join "`r`n" -} -``` -Generates - -``` -First: John -Last: Doe -The Zip: 12345 - -First: Jim -Last: Doe -The Zip: 12345 - -First: Tom -Last: Doe -The Zip: 12345 - -First: Harry -Last: Doe -The Zip: 12345 - -First: Jane -Last: Doe -The Zip: 12345 -``` - -#### 2/2/2017 -Thank you to [DarkLite1](https://github.com/DarkLite1) for more updates -* TableName with parameter validation, throws an error when the TableName: - - Starts with something else then a letter - - Is NULL or empty - - Contains spaces -- Numeric parsing now uses `CurrentInfo` to use the system settings - -#### 2/14/2017 -Big thanks to [DarkLite1](https://github.com/DarkLite1) for some great updates -* `-DataOnly` switch added to `Import-Excel`. When used it will only generate objects for rows that contain text values, not for empty rows or columns. - -* `Get-ExcelWorkBookInfo` - retrieves information of an Excel workbook. -``` - Get-ExcelWorkbookInfo .\Test.xlsx - - CorePropertiesXml : #document - Title : - Subject : - Author : Konica Minolta User - Comments : - Keywords : - LastModifiedBy : Bond, James (London) GBR - LastPrinted : 2017-01-21T12:36:11Z - Created : 17/01/2017 13:51:32 - Category : - Status : - ExtendedPropertiesXml : #document - Application : Microsoft Excel - HyperlinkBase : - AppVersion : 14.0300 - Company : Secret Service - Manager : - Modified : 10/02/2017 12:45:37 - CustomPropertiesXml : #document -``` - -#### 12/22/2016 -- Added `-Now` switch. This short cuts the process, automatically creating a temp file and enables the `-Show`, `-AutoFilter`, `-AutoSize` switches. - -```PowerShell -Get-Process | Select Company, Handles | Export-Excel -Now -``` - -- Added ScriptBlocks for coloring cells. Check out [Examples](https://github.com/dfinke/ImportExcel/tree/master/Examples/FormatCellStyles) - -```PowerShell -Get-Process | - Select-Object Company,Handles,PM, NPM| - Export-Excel $xlfile -Show -AutoSize -CellStyleSB { - param( - $workSheet, - $totalRows, - $lastColumn - ) - - Set-CellStyle $workSheet 1 $LastColumn Solid Cyan - - foreach($row in (2..$totalRows | Where-Object {$_ % 2 -eq 0})) { - Set-CellStyle $workSheet $row $LastColumn Solid Gray - } - - foreach($row in (2..$totalRows | Where-Object {$_ % 2 -eq 1})) { - Set-CellStyle $workSheet $row $LastColumn Solid LightGray - } - } -``` -![](https://github.com/dfinke/ImportExcel/blob/master/images/CellFormatting.png?raw=true) - -#### 9/28/2016 -[Fixed](https://github.com/dfinke/ImportExcel/pull/126) PowerShell 3.0 compatibility. Thanks to [headsphere](https://github.com/headsphere). He used `$obj.PSObject.Methods[$target]` snytax to make it backward compatible. PS v4.0 and later allow `$obj.$target`. - -Thank you to [xelsirko](https://github.com/xelsirko) for fixing - *Import-module importexcel gives version warning if started inside background job* - -#### 8/12/2016 -[Fixed](https://github.com/dfinke/ImportExcel/issues/115) reading the headers from cells, moved from using `Text` property to `Value` property. - -#### 7/30/2016 -* Added `Copy-ExcelWorksheet`. Let's you copy a work sheet from one Excel workbook to another. - -#### 7/21/2016 -* Fixes `Import-Excel` #68 - -#### 7/7/2016 -[Attila Mihalicz](https://github.com/attilamihalicz) fixed two issues - -* Removing extra spaces after the backtick -* Uninitialized variable $idx leaks into the pipeline when `-TableName` parameter is used - -Thanks Attila. - - -#### 7/1/2016 -* Pushed 2.2.7 fixed resolve path in Get-ExcelSheetInfo -* Fixed [Casting Error in Export-Excel](https://github.com/dfinke/ImportExcel/issues/108) -* For `Import-Excel` change Resolve-Path to return ProviderPath for use with UNC - -#### 6/01/2016 -* Added -UseDefaultCredentials to both `Import-Html` and `Get-HtmlTable` -* New functions, `Import-UPS` and `Import-USPS`. Pass in a valid tracking # and it scrapes the page for the delivery details - -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/Tracking.gif) - -#### 4/30/2016 -Huge thank you to [Willie Möller](https://github.com/W1M0R) - -* He added a version check so the PowerShell Classes don't cause issues for downlevel version of PowerShell -* He also contributed the first Pester tests for the module. Super! Check them out, they'll be the way tests will be implemented going forward - -#### 4/18/2016 -Thanks to [Paul Williams](https://github.com/pauldalewilliams) for this feature. Now data can be transposed to columns for better charting. - -```PowerShell -$file = "C:\Temp\ps.xlsx" -rm $file -ErrorAction Ignore - -ps | - where company | - select Company,PagedMemorySize,PeakPagedMemorySize | - Export-Excel $file -Show -AutoSize ` - -IncludePivotTable ` - -IncludePivotChart ` - -ChartType ColumnClustered ` - -PivotRows Company ` - -PivotData @{PagedMemorySize='sum';PeakPagedMemorySize='sum'} -``` -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PivotAsRows.png) - - -Add `-PivotDataToColumn` - -```PowerShell -$file = "C:\Temp\ps.xlsx" -rm $file -ErrorAction Ignore - -ps | - where company | - select Company,PagedMemorySize,PeakPagedMemorySize | - Export-Excel $file -Show -AutoSize ` - -IncludePivotTable ` - -IncludePivotChart ` - -ChartType ColumnClustered ` - -PivotRows Company ` - -PivotData @{PagedMemorySize='sum';PeakPagedMemorySize='sum'} ` - -PivotDataToColumn -``` -And here is the new chart view -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PivotAsColumns.png) -#### 4/7/2016 -Made more methods fluent -``` -$t=Get-Range 0 5 .2 - -$t2=$t|%{$_*$_} -$t3=$t|%{$_*$_*$_} - -(New-Plot). - Plot($t,$t, $t,$t2, $t,$t3). - SetChartPosition("i"). - SetChartSize(500,500). - Title("Hello World"). - Show() -``` -#### 3/31/2016 -* Thanks to [redoz](https://github.com/redoz) Multi Series Charts are now working - -Also check out how you can create a table and then with Excel notation, index into the data for charting `"Impressions[A]"` - -``` -$data = @" -A,B,C,Date -2,1,1,2016-03-29 -5,10,1,2016-03-29 -"@ | ConvertFrom-Csv - -$c = New-ExcelChart -Title Impressions ` - -ChartType Line -Header "Something" ` - -XRange "Impressions[Date]" ` - -YRange @("Impressions[B]","Impressions[A]") - -$data | - Export-Excel temp.xlsx -AutoSize -TableName Impressions -Show -ExcelChartDefinition $c -``` -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/MultiSeries.gif) - -#### 3/26/2016 -* Added `NumberFormat` parameter - -``` -$data | - Export-Excel -Path $file -Show -NumberFormat '[Blue]$#,##0.00;[Red]-$#,##0.00' -``` -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/Formatting.png) - - -#### 3/18/2016 -* Added `Get-Range`, `New-Plot` and Plot Cos example -* Updated EPPlus DLL. Allows markers to be changed and colored -* Handles and warns if auto name range names are also valid Excel ranges - -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PSPlot.gif) - -#### 3/7/2016 -* Added `Header` and `FirstDataRow` for `Import-Html` - -#### 3/2/2016 -* Added `GreaterThan`, `GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual` to `New-ConditionalText` - -```PowerShell -echo 489 668 299 777 860 151 119 497 234 788 | - Export-Excel c:\temp\test.xlsx -Show ` - -ConditionalText (New-ConditionalText -ConditionalType GreaterThan 525) -``` -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/GTConditional.png) - -#### 2/22/2016 -* `Import-Html` using Lee Holmes [Extracting Tables from PowerShell’s Invoke-WebRequest](http://www.leeholmes.com/blog/2015/01/05/extracting-tables-from-PowerShells-invoke-webrequest/) - -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/ImportHtml.gif) - -#### 2/17/2016 -* Added Conditional Text types of `Equal` and `NotEqual` -* Phone #'s like '+33 011 234 34' will be now be handled correctly - -## Try *PassThru* - -```PowerShell -$file = "C:\Temp\passthru.xlsx" -rm $file -ErrorAction Ignore - -$xlPkg = $( - New-PSItem north 10 - New-PSItem east 20 - New-PSItem west 30 - New-PSItem south 40 -) | Export-Excel $file -PassThru - -$ws=$xlPkg.Workbook.Worksheets[1] - -$ws.Cells["A3"].Value = "Hello World" -$ws.Cells["B3"].Value = "Updating cells" -$ws.Cells["D1:D5"].Value = "Data" - -$ws.Cells.AutoFitColumns() - -$xlPkg.Save() -$xlPkg.Dispose() - -Invoke-Item $file -``` - -## Result -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PassThru.png) - -#### 1/18/2016 - -* Added `Conditional Text Formatting`. [Boe Prox](https://twitter.com/proxb) posted about [HTML Reporting, Part 2: Take Your Reporting a Step Further](https://mcpmag.com/articles/2016/01/14/html-reporting-part-2.aspx) and colorized cells. Great idea, now part of the PowerShell Excel module. - -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/ConditionalText2.gif) - -#### 1/7/2016 -* Added `Get-ExcelSheetInfo` - Great contribution from *Johan Åkerström* check him out on [GitHub](https://github.com/CosmosKey) and [Twitter](https://twitter.com/neptune443) - -![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/GetExcelSheetInfo.png) - -#### 12/26/2015 - -* Added `NoLegend`, `Show-Category`, `ShowPercent` for all charts including Pivot Charts -* Updated PieChart, BarChart, ColumnChart and Line chart to work with the pipeline and added `NoLegend`, `Show-Category`, `ShowPercent` - -#### 12/17/2015 - -These new features open the door for really sophisticated work sheet creation. - -Stay tuned for a [blog post](http://www.dougfinke.com/blog/) and examples. - -***Quick List*** -* StartRow, StartColumn for placing data anywhere in a sheet -* New-ExcelChart - Add charts to a sheet, multiple series for a chart, locate the chart anywhere on the sheet -* AutoNameRange, Use functions and/or calculations in a cell -* Quick charting using PieChart, BarChart, ColumnChart and more - -![](https://raw.githubusercontent.com/dfinke/GifCam/master/JustCharts.gif) - -#### 10/20/2015 - -Big bug fix for version 3.0 PowerShell folks! - -This technique fails in 3.0 and works in 4.0 and later. -```PowerShell -$m="substring" -"hello".$m(2,1) -``` - -Adding `.invoke` works in 3.0 and later. - -```PowerShell -$m="substring" -"hello".$m.invoke(2,1) -``` - -A ***big thank you*** to [DarkLite1](https://github.com/DarkLite1) for adding the help to Export-Excel. - -Added `-HeaderRow` parameter. Sometimes the heading does not start in Row 1. - - -#### 10/16/2015 - -Fixes [Export-Excel generates corrupt Excel file](https://github.com/dfinke/ImportExcel/issues/46) - -#### 10/15/2015 - -`Import-Excel` has a new parameter `NoHeader`. If data in the sheet does not have headers and you don't want to supply your own, `Import-Excel` will generate the property name. - -`Import-Excel` now returns `.Value` rather than `.Text` - - -#### 10/1/2015 - -Merged ValidateSet for Encoding and Extension. Thank you [Irwin Strachan](https://github.com/irwins). - -#### 9/30/2015 - -Export-Excel can now handle data that is **not** an object - - echo a b c 1 $true 2.1 1/1/2015 | Export-Excel c:\temp\test.xlsx -Show -Or - - dir -Name | Export-Excel c:\temp\test.xlsx -Show - -#### 9/25/2015 - -**Hide worksheets** -Got a great request from [forensicsguy20012004](https://github.com/forensicsguy20012004) to hide worksheets. You create a few pivotables, generate charts and then pivotable worksheets don't need to be visible. - -`Export-Excel` now has a `-HideSheet` parameter that takes and array of worksheet names and hides them. - -##### Example -Here, you create four worksheets named `PM`,`Handles`,`Services` and `Files`. - -The last line creates the `Files` sheet and then hides the `Handles`,`Services` sheets. - - $p = Get-Process - - $p|select company, pm | Export-Excel $xlFile -WorkSheetname PM - $p|select company, handles| Export-Excel $xlFile -WorkSheetname Handles - Get-Service| Export-Excel $xlFile -WorkSheetname Services - - dir -File | Export-Excel $xlFile -WorkSheetname Files -Show -HideSheet Handles, Services - - -**Note** There is a bug in EPPlus that does not let you hide the first worksheet created. Hopefully it'll resolved soon. - -#### 9/11/2015 - -Added Conditional formatting. See [TryConditional.ps1](https://github.com/dfinke/ImportExcel/blob/master/TryConditional.ps1) as an example. - -Or, check out the short ***"How To"*** video. - -[![image](http://www.dougfinke.com/videos/excelpsmodule/ExcelPSModule_First_Frame.png)](http://www.dougfinke.com/videos/excelpsmodule/excelpsmodule.mp4) - - -#### 8/21/2015 -* Now import Excel sheets even if the file is open in Excel. Thank you [Francois Lachance-Guillemette](https://github.com/francoislg) - -#### 7/09/2015 -* For -PivotRows you can pass a `hashtable` with the name of the property and the type of calculation. `Sum`, `Average`, `Max`, `Min`, `Product`, `StdDev`, `StdDevp`, `Var`, `Varp` - -```PowerShell -Get-Service | - Export-Excel "c:\temp\test.xlsx" ` - -Show ` - -IncludePivotTable ` - -PivotRows status ` - -PivotData @{status='count'} -``` - -#### 6/16/2015 (Thanks [Justin](https://github.com/zippy1981)) -* Improvements to PivotTable overwriting -* Added two parameters to Export-Excel - * RangeName - Turns the data piped to Export-Excel into a named range. - * TableName - Turns the data piped to Export-Excel into an excel table. - -Examples - - Get-Process|Export-Excel foo.xlsx -Verbose -IncludePivotTable -TableName "Processes" -Show - Get-Process|Export-Excel foo.xlsx -Verbose -IncludePivotTable -RangeName "Processes" -Show - - -#### 5/25/2015 -* Fixed null header problem - -#### 5/17/2015 -* Added three parameters: - * FreezeTopRow - Freezes the first row of the data - * AutoFilter - Enables filtering for the data in the sheet - * BoldTopRow - Bolds the top row of data, the column headers - -Example - - Get-CimInstance win32_service | - select state, accept*, start*, caption | - Export-Excel test.xlsx -Show -BoldTopRow -AutoFilter -FreezeTopRow -AutoSize - -![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/FilterFreezeBold.gif) - - -#### 5/4/2015 -* Published to PowerShell Gallery. In PowerShell v5 use `Find-Module importexcel` then `Find-Module importexcel | Install-Module` - - -#### 4/27/2015 -* datetime properties were displaying as ints, now are formatted - -#### 4/25/2015 -* Now you can create multiple Pivot tables in one pass - * Thanks to [pscookiemonster](https://twitter.com/pscookiemonster), he submitted a repro case to the EPPlus CodePlex project and got it fixed - -#### Example - - $ps = ps - - $ps | - Export-Excel .\testExport.xlsx -WorkSheetname memory ` - -IncludePivotTable -PivotRows Company -PivotData PM ` - -IncludePivotChart -ChartType PieExploded3D - $ps | - Export-Excel .\testExport.xlsx -WorkSheetname handles ` - -IncludePivotTable -PivotRows Company -PivotData Handles ` - -IncludePivotChart -ChartType PieExploded3D -Show - -![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/MultiplePivotTables.png) - -#### 4/20/2015 -* Included and embellished [Claus Nielsen](https://github.com/Claustn) function to take all sheets in an Excel file workbook and create a text file for each `ConvertFrom-ExcelSheet` -* Renamed `Export-MultipleExcelSheets` to `ConvertFrom-ExcelSheet` - -#### 4/13/2015 -* You can add a title to the Excel "Report" `Title`, `TitleFillPattern`, `TitleBold`, `TitleSize`, `TitleBackgroundColor` - * Thanks to [Irwin Strachan](http://pshirwin.wordpress.com) for this and other great suggestions, testing and more - - -#### 4/10/2015 -* Renamed `AutoFitColumns` to `AutoSize` -* Implemented `Export-MultipleExcelSheets` -* Implemented `-Password` for a worksheet -* Replaced `-Force` switch with `-NoClobber` switch -* Added examples for `Get-Help` -* If Pivot table is requested, that sheet becomes the tab selected - -#### 4/8/2015 -* Implemented exporting data to **named sheets** via the -WorkSheetname parameter. - -Examples -- -`gsv | Export-Excel .\test.xlsx -WorkSheetname Services` - -`dir -file | Export-Excel .\test.xlsx -WorkSheetname Files` - -`ps | Export-Excel .\test.xlsx -WorkSheetname Processes -IncludePivotTable -Show -PivotRows Company -PivotData PM` - -#### Convert (All or Some) Excel Sheets to Text files - -Reads each sheet in TestSheets.xlsx and outputs it to the data directory as the sheet name with the extension .txt - - ConvertFrom-ExcelSheet .\TestSheets.xlsx .\data - -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 - - ConvertFrom-ExcelSheet .\TestSheets.xlsx .\data sheet?0 - -#### Example Adding a Title -You can set the pattern, size and of if the title is bold. - - $p=@{ - Title = "Process Report as of $(Get-Date)" - TitleFillPattern = "LightTrellis" - TitleSize = 18 - TitleBold = $true - - Path = "$pwd\testExport.xlsx" - Show = $true - AutoSize = $true - } - - Get-Process | - Where Company | Select Company, PM | - Export-Excel @p - -![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/Title.png) - -#### Example Export-MultipleExcelSheets -![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/ExportMultiple.gif) - - $p = Get-Process - - $DataToGather = @{ - PM = {$p|select company, pm} - Handles = {$p|select company, handles} - Services = {gsv} - Files = {dir -File} - Albums = {(Invoke-RestMethod http://www.dougfinke.com/PowerShellfordevelopers/albums.js)} - } - - Export-MultipleExcelSheets -Show -AutoSize .\testExport.xlsx $DataToGather - - - -***NOTE*** If the sheet exists when using *-WorkSheetname* parameter, it will be deleted and then added with the new data. - -## Get-Process Exported to Excel - -### Total Physical Memory Grouped By Company -![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PivotTablesAndCharts.png) - -## Importing data from an Excel spreadsheet - -![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/TryImportExcel.gif) - -You can also find EPPLus on [Nuget](https://www.nuget.org/packages/EPPlus/). - -## Known Issues - -* Using `-IncludePivotTable`, if that pivot table name exists, you'll get an error. - * Investigating a solution - * *Workaround* delete the Excel file first, then do the export +PowerShell Import-Excel +- + +Install from the [PowerShell Gallery](https://www.powershellgallery.com/packages/ImportExcel/). + +This PowerShell Module allows you to read and write Excel files without installing Microsoft Excel on your system. No need to bother with the cumbersome Excel COM-object. Creating Tables, Pivot Tables, Charts and much more has just become a lot easier. + +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/testimonial.png) + +# How to Vidoes +* [PowerShell Excel Module - ImportExcel](https://www.youtube.com/watch?v=U3Ne_yX4tYo&list=PL5uoqS92stXioZw-u-ze_NtvSo0k0K0kq) + +Installation +- +#### [PowerShell V5](https://www.microsoft.com/en-us/download/details.aspx?id=50395) and Later +You can install the `ImportExcel` module directly from the PowerShell Gallery + +* [Recommended] Install to your personal PowerShell Modules folder +```PowerShell +Install-Module ImportExcel -scope CurrentUser +``` +* [Requires Elevation] Install for Everyone (computer PowerShell Modules folder) +```PowerShell +Install-Module ImportExcel +``` + +#### PowerShell V4 and Earlier +To install to your personal modules folder (e.g. ~\Documents\WindowsPowerShell\Modules), run: + +```PowerShell +iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/dfinke/ImportExcel/master/Install.ps1') +``` + +# What's new + +- New commands - Diff , Merge and Join + - `Compare-Worksheet` (introduced in 5.0) uses the built in `Compare-object` command, to output a command-line DIFF and/or colour the worksheet to show differences. For example, if my sheets are Windows services the *extra* rows or rows where the startup status has changed get highlighted + - `Merge-Worksheet` (also introduced in 5.0) joins two lumps, side by highlighting the differences. So now I can have server A's services and Server Bs Services on the same page. I figured out a way to do multiple sheets. So I can have Server A,B,C,D on one page :-) that is `Merge-MultpleSheets` + For this release I've fixed heaven only knows how many typos and proof reading errors in the help for these two, but the code is unchanged - although correcting the spelling of Merge-MultipleSheets is potentially a breaking change (and it is still plural!) + also fixed a bug in compare worksheet where color might not be applied correctly when the worksheets came from different files and had different name. + - `Join-Worksheet` is **new** for ths release. At it's simplest it copies all the data in Worksheet A to the end of Worksheet B +- Add-Worksheet + - I have moved this from ImportExcel.psm1 to ExportExcel.ps1 and it now can move a new worksheet to the right place, and can copy an existing worksheet (from the same or a different workbook) to a new one, and I set the Set return-type to aid intellisense +- New-PivotTableDefinition + - Now Supports `-PivotFilter` and `-PivotDataToColumn`, `-ChartHeight/width` `-ChartRow/Column`, `-ChartRow/ColumnPixelOffset` parameters +- Set-Format + - Fixed a bug where the `-address` parameter had to be named, although the examples in `export-excel` help showed it working by position (which works now. ) +- Export-Excel + - I've done some re-factoring + 1. I "flattened out" small "called-once" functions , add-title, convert-toNumber and Stop-ExcelProcess. + 2. It now uses Add-Worksheet, Open-ExcelPackage and Add-ConditionalFormat instead of duplicating their functionality. + 3. I've moved the PivotTable functionality (which was doubled up) out to a new function "Add-PivotTable" which supports some extra parameters PivotFilter and PivotDataToColumn, ChartHeight/width ChartRow/Column, ChartRow/ColumnPixelOffsets. + 4. I've made the try{} catch{} blocks cover smaller blocks of code to give a better idea where a failure happend, some of these now Warn instead of throwing - I'd rather save the data with warnings than throw it away because we can't add a chart. Along with this I've added some extra write-verbose messages + - Bad column-names specified for Pivots now generate warnings instead of throwing. + - Fixed issues when pivottables / charts already exist and an export tries to create them again. + - Fixed issue where AutoNamedRange, NamedRange, and TableName do not work when appending to a sheet which already contains the range(s) / table + - Fixed issue where AutoNamedRange may try to create ranges with an illegal name. + - Added check for illegal characters in RangeName or Table Name (replace them with "_"), changed tablename validation to allow spaces and applied same validation to RangeName + - Fixed a bug where BoldTopRow is always bolds row 1 even if the export is told to start at a lower row. + - Fixed a bug where titles throw pivot table creation out of alignment. + - Fixed a bug where Append can overwrite the last rows of data if the initial export had blank rows at the top of the sheet. + - Removed the need to specify a fill type when specifying a title background color + - Added MoveToStart, MoveToEnd, MoveBefore and MoveAfter Parameters - these go straight through to Add worksheet + - Added "NoScriptOrAliasProperties" "DisplayPropertySet" switches (names subject to change) - combined with ExcludeProperty these are a quick way to reduce the data exported (and speed things up) + - Added PivotTableName Switch (in line with 5.0.1 release) + - Add-CellValue now understands URI item properties. If a property is of type URI it is created as a hyperlink to speed up Add-CellValue + - Commented out the write verbose statements even if verbose is silenced they cause a significiant performance impact and if it's on they will cause a flood of messages. + - Re-ordered the choices in the switch and added an option to say "If it is numeric already post it as is" + - Added an option to only set the number format if doesn't match the default for the sheet. +-Export-Excel Pester Tests + - I have converted examples 1-9, 11 and 13 from Export-Excel help into tests and have added some additional tests, and extra parameters to the example command to ge better test coverage. The test so far has 184 "should" conditions grouped as 58 "IT" statements; but is still a work in progress. +-Compare-Worksheet pester tests + +--- + + +- [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 + - Removed `[PSPlot]` as OutputType. Fixes it throwing an error +- [Nasir Zubair](https://github.com/nzubair) added `ConvertEmptyStringsToNull` to the function `ConvertFrom-ExcelToSQLInsert` + - If specified, cells without any data are replaced with NULL, instead of an empty string. This is to address behviors in certain DBMS where an empty string is insert as 0 for INT column, instead of a NULL value. + + +#### 4/10/2018 +-New parameter `-ReZip`. It ReZips the xlsx so it can be imported to PowerBI + +Thanks to [Justin Grote](https://github.com/JustinGrote) for finding and fixing the error that Excel files created do not import to PowerBI online. Plus, thank you to [CrashM](https://github.com/CrashM) for confirming the fix. + +Super helpful! + +#### 3/31/2018 +- Updated `Set-Format` + * Added parameters to set borders for cells, including top, bottm, left and right + * Added parameters to set `value` and `formula` + +```powershell +$data = @" +From,To,RDollars,RPercent,MDollars,MPercent,Revenue,Margin +Atlanta,New York,3602000,.0809,955000,.09,245,65 +New York,Washington,4674000,.105,336000,.03,222,16 +Chicago,New York,4674000,.0804,1536000,.14,550,43 +New York,Philadelphia,12180000,.1427,-716000,-.07,321,-25 +New York,San Francisco,3221000,.0629,1088000,.04,436,21 +New York,Phoneix,2782000,.0723,467000,.10,674,33 +"@ +``` + +![](https://github.com/dfinke/ImportExcel/blob/master/images/CustomReport.png?raw=true) + + +- Added `-PivotFilter` parameter, allows you to set up a filter so you can drill down into a subset of the overall dataset. + +```powershell +$data =@" +Region,Area,Product,Units,Cost +North,A1,Apple,100,.5 +South,A2,Pear,120,1.5 +East,A3,Grape,140,2.5 +West,A4,Banana,160,3.5 +North,A1,Pear,120,1.5 +North,A1,Grape,140,2.5 +"@ +``` + +![](https://github.com/dfinke/ImportExcel/blob/master/images/PivotTableFilter.png?raw=true) + + +#### 3/14/2018 +- Thank you to [James O'Neill](https://twitter.com/jamesoneill), fixed bugs with ChangeDatabase parameter which would prevent it working + +#### +* Added -Force to New-Alias +* Add example to set the background color of a column +* Supports excluding Row Grand Totals for PivotTables +* Allow xlsm files to be read +* Fix `Set-Column.ps1`, `Set-Row.ps1`, `SetFormat.ps1`, `formatting.ps1` **$falsee** and **$BorderRound** +#### 1/1/2018 +* Added switch `[Switch]$NoTotalsInPivot`. Allows hiding of the row totals in the pivot table. +Thanks you to [jameseholt](https://github.com/jameseholt) for the request. + +```powershell + get-process | where Company | select Company, Handles, WorkingSet | + export-excel C:\temp\testColumnGrand.xlsx ` + -Show -ClearSheet -KillExcel ` + -IncludePivotTable -PivotRows Company -PivotData @{"Handles"="average"} -NoTotalsInPivot +``` + +* Fixed when using certain a `ChartType` for the Pivot Table Chart, would throw an error +* Fixed - when you specify a file, and the directory does not exit, it now creates it + +#### 11/23/2017 +More great additions and thanks to [James O'Neill](https://twitter.com/jamesoneill) + +* Added `Convert-XlRangeToImage` Gets the specified part of an Excel file and exports it as an image +* Fixed a typo in the message at line 373. +* Now catch an attempt to both clear the sheet and append to it. +* Fixed some issues when appending to sheets where the header isn't in row 1 or the data doesn't start in column 1. +* Added support for more settings when creating a pivot chart. +* Corrected a typo PivotTableName was PivtoTableName in definition of New-PivotTableDefinition +* Add-ConditionalFormat and Set-Format added to the parameters so each has the choice of working more like the other. +* Added Set-Row and Set-Column - fill a formula down or across. +* Added Send-SQLDataToExcel. Insert a rowset and then call Export-Excel for ranges, charts, pivots etc + +#### 10/30/2017 +Huge thanks to [James O'Neill](https://twitter.com/jamesoneill). PowerShell aficionado. He always brings a flare when working with PowerShell. This is no exception. + +(Check out the examples `help Export-Excel -Examples`) + +* New parameter `Package` allows an ExcelPackage object returned by `-passThru` to be passed in +* New parameter `ExcludeProperty` to remove unwanted properties without needing to go through `select-object` +* New parameter `Append` code to read the existing headers and move the insertion point below the current data +* New parameter `ClearSheet` which removes the worksheet and any past data + +* Remove any existing Pivot table before trying to [re]create it +* Check for inserting a pivot table so if `-InsertPivotChart` is specified it implies `-InsertPivotTable` + +(Check out the examples `help Export-Excel -Examples`) + +* New function `Export-Charts` (requires Excel to be installed) - Export Excel charts out as JPG files +* New function `Add-ConditionalFormatting` Adds contitional formatting to worksheet +* New function `Set-Format` Applies Number, font, alignment and colour formatting to a range of Excel Cells +* `ColorCompletion` an argument completer for `Colors` for params across functions + +I also worked out the parameters so you can do this, which is the same as passing `-Now`. It creates an Excel file name for you, does an auto fit and sets up filters. + +`ps | select Company, Handles | Export-Excel` + +#### 10/13/2017 +Added `New-PivotTableDefinition`. You can create and wire up a PivotTable to a WorkSheet. You can also create as many PivotTable Worksheets to point a one Worksheet. Or, you create many Worksheets and many corresponding PivotTable Worksheets. + +Here you can create a WorkSheet with the data from `Get-Service`. Then create four PivotTables, pointing to the data each pivoting on a differnt dimension and showing a differnet chart + +```powershell +$base = @{ + SourceWorkSheet = 'gsv' + PivotData = @{'Status' = 'count'} + IncludePivotChart = $true +} + +$ptd = [ordered]@{} + +$ptd += New-PivotTableDefinition @base servicetype -PivotRows servicetype -ChartType Area3D +$ptd += New-PivotTableDefinition @base status -PivotRows status -ChartType PieExploded3D +$ptd += New-PivotTableDefinition @base starttype -PivotRows starttype -ChartType BarClustered3D +$ptd += New-PivotTableDefinition @base canstop -PivotRows canstop -ChartType ConeColStacked + +Get-Service | Export-Excel -path $file -WorkSheetname gsv -Show -PivotTableDefinition $ptd +``` + +#### 10/4/2017 +Thanks to https://github.com/ili101 : +- Fix Bug, Unable to find type [PSPlot] +- Fix Bug, AutoFilter with TableName create corrupted Excel file. + +#### 10/2/2017 +Thanks to [Jeremy Brun](https://github.com/jeremytbrun) +Fixed issues related to use of -Title parameter combined with column formatting parameters. +- [Issue #182](https://github.com/dfinke/ImportExcel/issues/182) +- [Issue #89](https://github.com/dfinke/ImportExcel/issues/89) + +#### 9/28/2017 (Version 4.0.1) +- Added a new parameter called `Password` to import password protected files +- Added even more `Pester` tests for a more robust and bug free module +- Renamed parameter 'TopRow' to 'StartRow' + This allows us to be more concise when new parameters ('StartColumn', ..) will be added in the future Your code will not break after the update, because we added an alias for backward compatibility + +Special thanks to [robinmalik](https://github.com/robinmalik) for providing us with [the code](https://github.com/dfinke/ImportExcel/issues/174) to implement this new feature. A high five to [DarkLite1](https://github.com/DarkLite1) for the implementation. + +#### 9/12/2017 (Version 4.0.0) + +Super thanks and hat tip to [DarkLite1](https://github.com/DarkLite1). There is now a new and improved `Import-Excel`, not only in functionality, but also improved readability, examples and more. Not only that, he's been running it in production in his company for a number of weeks! + +*Added* `Update-FirstObjectProperties` Updates the first object to contain all the properties of the object with the most properties in the array. Check out the help. + + +***Breaking Changes***: Due to a big portion of the code that is rewritten some slightly different behavior can be expected from the `Import-Excel` function. This is especially true for importing empty Excel files with or without using the `TopRow` parameter. To make sure that your code is still valid, please check the examples in the help or the accompanying `Pester` test file. + + +Moving forward, we are planning to include automatic testing with the help of `Pester`, `Appveyor` and `Travis`. From now on any changes in the module will have to be accompanied by the corresponding `Pester` tests to avoid breakages of code and functionality. This is in preparation for new features coming down the road. + +#### 7/3/2017 +Thanks to [Mikkel Nordberg](https://www.linkedin.com/in/mikkelnordberg). He contributed a `ConvertTo-ExcelXlsx`. To use it, Excel needs to be installed. The function converts the older Excel file format ending in `.xls` to the new format ending in `.xlsx`. + +#### 6/15/2017 +Huge thank you to [DarkLite1](https://github.com/DarkLite1)! Refactoring of code, adding help, adding features, fixing bugs. Specifically this long outstanding one: + +[Export-Excel: Numeric values not correct](https://github.com/dfinke/ImportExcel/issues/168) + +It is fantastic to work with people like `DarkLite1` in the community, to help make the module so much better. A hat to you. + +Another shout out to [Damian Reeves](https://twitter.com/DamReev)! His questions turn into great features. He asked if it was possible to import an Excel worksheet and transform the data into SQL `INSERT` statements. We can now answer that question with a big YES! + +```PowerShell +ConvertFrom-ExcelToSQLInsert People .\testSQLGen.xlsx +``` + +``` +INSERT INTO People ('First', 'Last', 'The Zip') Values('John', 'Doe', '12345'); +INSERT INTO People ('First', 'Last', 'The Zip') Values('Jim', 'Doe', '12345'); +INSERT INTO People ('First', 'Last', 'The Zip') Values('Tom', 'Doe', '12345'); +INSERT INTO People ('First', 'Last', 'The Zip') Values('Harry', 'Doe', '12345'); +INSERT INTO People ('First', 'Last', 'The Zip') Values('Jane', 'Doe', '12345'); +``` +## Bonus Points +Use the underlying `ConvertFrom-ExcelData` function and you can use a scriptblock to format the data however you want. + +```PowerShell +ConvertFrom-ExcelData .\testSQLGen.xlsx { + param($propertyNames, $record) + + $reportRecord = @() + foreach ($pn in $propertyNames) { + $reportRecord += "{0}: {1}" -f $pn, $record.$pn + } + $reportRecord +="" + $reportRecord -join "`r`n" +} +``` +Generates + +``` +First: John +Last: Doe +The Zip: 12345 + +First: Jim +Last: Doe +The Zip: 12345 + +First: Tom +Last: Doe +The Zip: 12345 + +First: Harry +Last: Doe +The Zip: 12345 + +First: Jane +Last: Doe +The Zip: 12345 +``` + +#### 2/2/2017 +Thank you to [DarkLite1](https://github.com/DarkLite1) for more updates +* TableName with parameter validation, throws an error when the TableName: + - Starts with something else then a letter + - Is NULL or empty + - Contains spaces +- Numeric parsing now uses `CurrentInfo` to use the system settings + +#### 2/14/2017 +Big thanks to [DarkLite1](https://github.com/DarkLite1) for some great updates +* `-DataOnly` switch added to `Import-Excel`. When used it will only generate objects for rows that contain text values, not for empty rows or columns. + +* `Get-ExcelWorkBookInfo` - retrieves information of an Excel workbook. +``` + Get-ExcelWorkbookInfo .\Test.xlsx + + CorePropertiesXml : #document + Title : + Subject : + Author : Konica Minolta User + Comments : + Keywords : + LastModifiedBy : Bond, James (London) GBR + LastPrinted : 2017-01-21T12:36:11Z + Created : 17/01/2017 13:51:32 + Category : + Status : + ExtendedPropertiesXml : #document + Application : Microsoft Excel + HyperlinkBase : + AppVersion : 14.0300 + Company : Secret Service + Manager : + Modified : 10/02/2017 12:45:37 + CustomPropertiesXml : #document +``` + +#### 12/22/2016 +- Added `-Now` switch. This short cuts the process, automatically creating a temp file and enables the `-Show`, `-AutoFilter`, `-AutoSize` switches. + +```PowerShell +Get-Process | Select Company, Handles | Export-Excel -Now +``` + +- Added ScriptBlocks for coloring cells. Check out [Examples](https://github.com/dfinke/ImportExcel/tree/master/Examples/FormatCellStyles) + +```PowerShell +Get-Process | + Select-Object Company,Handles,PM, NPM| + Export-Excel $xlfile -Show -AutoSize -CellStyleSB { + param( + $workSheet, + $totalRows, + $lastColumn + ) + + Set-CellStyle $workSheet 1 $LastColumn Solid Cyan + + foreach($row in (2..$totalRows | Where-Object {$_ % 2 -eq 0})) { + Set-CellStyle $workSheet $row $LastColumn Solid Gray + } + + foreach($row in (2..$totalRows | Where-Object {$_ % 2 -eq 1})) { + Set-CellStyle $workSheet $row $LastColumn Solid LightGray + } + } +``` +![](https://github.com/dfinke/ImportExcel/blob/master/images/CellFormatting.png?raw=true) + +#### 9/28/2016 +[Fixed](https://github.com/dfinke/ImportExcel/pull/126) PowerShell 3.0 compatibility. Thanks to [headsphere](https://github.com/headsphere). He used `$obj.PSObject.Methods[$target]` snytax to make it backward compatible. PS v4.0 and later allow `$obj.$target`. + +Thank you to [xelsirko](https://github.com/xelsirko) for fixing - *Import-module importexcel gives version warning if started inside background job* + +#### 8/12/2016 +[Fixed](https://github.com/dfinke/ImportExcel/issues/115) reading the headers from cells, moved from using `Text` property to `Value` property. + +#### 7/30/2016 +* Added `Copy-ExcelWorksheet`. Let's you copy a work sheet from one Excel workbook to another. + +#### 7/21/2016 +* Fixes `Import-Excel` #68 + +#### 7/7/2016 +[Attila Mihalicz](https://github.com/attilamihalicz) fixed two issues + +* Removing extra spaces after the backtick +* Uninitialized variable $idx leaks into the pipeline when `-TableName` parameter is used + +Thanks Attila. + + +#### 7/1/2016 +* Pushed 2.2.7 fixed resolve path in Get-ExcelSheetInfo +* Fixed [Casting Error in Export-Excel](https://github.com/dfinke/ImportExcel/issues/108) +* For `Import-Excel` change Resolve-Path to return ProviderPath for use with UNC + +#### 6/01/2016 +* Added -UseDefaultCredentials to both `Import-Html` and `Get-HtmlTable` +* New functions, `Import-UPS` and `Import-USPS`. Pass in a valid tracking # and it scrapes the page for the delivery details + +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/Tracking.gif) + +#### 4/30/2016 +Huge thank you to [Willie Möller](https://github.com/W1M0R) + +* He added a version check so the PowerShell Classes don't cause issues for downlevel version of PowerShell +* He also contributed the first Pester tests for the module. Super! Check them out, they'll be the way tests will be implemented going forward + +#### 4/18/2016 +Thanks to [Paul Williams](https://github.com/pauldalewilliams) for this feature. Now data can be transposed to columns for better charting. + +```PowerShell +$file = "C:\Temp\ps.xlsx" +rm $file -ErrorAction Ignore + +ps | + where company | + select Company,PagedMemorySize,PeakPagedMemorySize | + Export-Excel $file -Show -AutoSize ` + -IncludePivotTable ` + -IncludePivotChart ` + -ChartType ColumnClustered ` + -PivotRows Company ` + -PivotData @{PagedMemorySize='sum';PeakPagedMemorySize='sum'} +``` +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PivotAsRows.png) + + +Add `-PivotDataToColumn` + +```PowerShell +$file = "C:\Temp\ps.xlsx" +rm $file -ErrorAction Ignore + +ps | + where company | + select Company,PagedMemorySize,PeakPagedMemorySize | + Export-Excel $file -Show -AutoSize ` + -IncludePivotTable ` + -IncludePivotChart ` + -ChartType ColumnClustered ` + -PivotRows Company ` + -PivotData @{PagedMemorySize='sum';PeakPagedMemorySize='sum'} ` + -PivotDataToColumn +``` +And here is the new chart view +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PivotAsColumns.png) +#### 4/7/2016 +Made more methods fluent +``` +$t=Get-Range 0 5 .2 + +$t2=$t|%{$_*$_} +$t3=$t|%{$_*$_*$_} + +(New-Plot). + Plot($t,$t, $t,$t2, $t,$t3). + SetChartPosition("i"). + SetChartSize(500,500). + Title("Hello World"). + Show() +``` +#### 3/31/2016 +* Thanks to [redoz](https://github.com/redoz) Multi Series Charts are now working + +Also check out how you can create a table and then with Excel notation, index into the data for charting `"Impressions[A]"` + +``` +$data = @" +A,B,C,Date +2,1,1,2016-03-29 +5,10,1,2016-03-29 +"@ | ConvertFrom-Csv + +$c = New-ExcelChart -Title Impressions ` + -ChartType Line -Header "Something" ` + -XRange "Impressions[Date]" ` + -YRange @("Impressions[B]","Impressions[A]") + +$data | + Export-Excel temp.xlsx -AutoSize -TableName Impressions -Show -ExcelChartDefinition $c +``` +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/MultiSeries.gif) + +#### 3/26/2016 +* Added `NumberFormat` parameter + +``` +$data | + Export-Excel -Path $file -Show -NumberFormat '[Blue]$#,##0.00;[Red]-$#,##0.00' +``` +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/Formatting.png) + + +#### 3/18/2016 +* Added `Get-Range`, `New-Plot` and Plot Cos example +* Updated EPPlus DLL. Allows markers to be changed and colored +* Handles and warns if auto name range names are also valid Excel ranges + +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PSPlot.gif) + +#### 3/7/2016 +* Added `Header` and `FirstDataRow` for `Import-Html` + +#### 3/2/2016 +* Added `GreaterThan`, `GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual` to `New-ConditionalText` + +```PowerShell +echo 489 668 299 777 860 151 119 497 234 788 | + Export-Excel c:\temp\test.xlsx -Show ` + -ConditionalText (New-ConditionalText -ConditionalType GreaterThan 525) +``` +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/GTConditional.png) + +#### 2/22/2016 +* `Import-Html` using Lee Holmes [Extracting Tables from PowerShell’s Invoke-WebRequest](http://www.leeholmes.com/blog/2015/01/05/extracting-tables-from-PowerShells-invoke-webrequest/) + +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/ImportHtml.gif) + +#### 2/17/2016 +* Added Conditional Text types of `Equal` and `NotEqual` +* Phone #'s like '+33 011 234 34' will be now be handled correctly + +## Try *PassThru* + +```PowerShell +$file = "C:\Temp\passthru.xlsx" +rm $file -ErrorAction Ignore + +$xlPkg = $( + New-PSItem north 10 + New-PSItem east 20 + New-PSItem west 30 + New-PSItem south 40 +) | Export-Excel $file -PassThru + +$ws=$xlPkg.Workbook.Worksheets[1] + +$ws.Cells["A3"].Value = "Hello World" +$ws.Cells["B3"].Value = "Updating cells" +$ws.Cells["D1:D5"].Value = "Data" + +$ws.Cells.AutoFitColumns() + +$xlPkg.Save() +$xlPkg.Dispose() + +Invoke-Item $file +``` + +## Result +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PassThru.png) + +#### 1/18/2016 + +* Added `Conditional Text Formatting`. [Boe Prox](https://twitter.com/proxb) posted about [HTML Reporting, Part 2: Take Your Reporting a Step Further](https://mcpmag.com/articles/2016/01/14/html-reporting-part-2.aspx) and colorized cells. Great idea, now part of the PowerShell Excel module. + +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/ConditionalText2.gif) + +#### 1/7/2016 +* Added `Get-ExcelSheetInfo` - Great contribution from *Johan Åkerström* check him out on [GitHub](https://github.com/CosmosKey) and [Twitter](https://twitter.com/neptune443) + +![](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/GetExcelSheetInfo.png) + +#### 12/26/2015 + +* Added `NoLegend`, `Show-Category`, `ShowPercent` for all charts including Pivot Charts +* Updated PieChart, BarChart, ColumnChart and Line chart to work with the pipeline and added `NoLegend`, `Show-Category`, `ShowPercent` + +#### 12/17/2015 + +These new features open the door for really sophisticated work sheet creation. + +Stay tuned for a [blog post](http://www.dougfinke.com/blog/) and examples. + +***Quick List*** +* StartRow, StartColumn for placing data anywhere in a sheet +* New-ExcelChart - Add charts to a sheet, multiple series for a chart, locate the chart anywhere on the sheet +* AutoNameRange, Use functions and/or calculations in a cell +* Quick charting using PieChart, BarChart, ColumnChart and more + +![](https://raw.githubusercontent.com/dfinke/GifCam/master/JustCharts.gif) + +#### 10/20/2015 + +Big bug fix for version 3.0 PowerShell folks! + +This technique fails in 3.0 and works in 4.0 and later. +```PowerShell +$m="substring" +"hello".$m(2,1) +``` + +Adding `.invoke` works in 3.0 and later. + +```PowerShell +$m="substring" +"hello".$m.invoke(2,1) +``` + +A ***big thank you*** to [DarkLite1](https://github.com/DarkLite1) for adding the help to Export-Excel. + +Added `-HeaderRow` parameter. Sometimes the heading does not start in Row 1. + + +#### 10/16/2015 + +Fixes [Export-Excel generates corrupt Excel file](https://github.com/dfinke/ImportExcel/issues/46) + +#### 10/15/2015 + +`Import-Excel` has a new parameter `NoHeader`. If data in the sheet does not have headers and you don't want to supply your own, `Import-Excel` will generate the property name. + +`Import-Excel` now returns `.Value` rather than `.Text` + + +#### 10/1/2015 + +Merged ValidateSet for Encoding and Extension. Thank you [Irwin Strachan](https://github.com/irwins). + +#### 9/30/2015 + +Export-Excel can now handle data that is **not** an object + + echo a b c 1 $true 2.1 1/1/2015 | Export-Excel c:\temp\test.xlsx -Show +Or + + dir -Name | Export-Excel c:\temp\test.xlsx -Show + +#### 9/25/2015 + +**Hide worksheets** +Got a great request from [forensicsguy20012004](https://github.com/forensicsguy20012004) to hide worksheets. You create a few pivotables, generate charts and then pivotable worksheets don't need to be visible. + +`Export-Excel` now has a `-HideSheet` parameter that takes and array of worksheet names and hides them. + +##### Example +Here, you create four worksheets named `PM`,`Handles`,`Services` and `Files`. + +The last line creates the `Files` sheet and then hides the `Handles`,`Services` sheets. + + $p = Get-Process + + $p|select company, pm | Export-Excel $xlFile -WorkSheetname PM + $p|select company, handles| Export-Excel $xlFile -WorkSheetname Handles + Get-Service| Export-Excel $xlFile -WorkSheetname Services + + dir -File | Export-Excel $xlFile -WorkSheetname Files -Show -HideSheet Handles, Services + + +**Note** There is a bug in EPPlus that does not let you hide the first worksheet created. Hopefully it'll resolved soon. + +#### 9/11/2015 + +Added Conditional formatting. See [TryConditional.ps1](https://github.com/dfinke/ImportExcel/blob/master/TryConditional.ps1) as an example. + +Or, check out the short ***"How To"*** video. + +[![image](http://www.dougfinke.com/videos/excelpsmodule/ExcelPSModule_First_Frame.png)](http://www.dougfinke.com/videos/excelpsmodule/excelpsmodule.mp4) + + +#### 8/21/2015 +* Now import Excel sheets even if the file is open in Excel. Thank you [Francois Lachance-Guillemette](https://github.com/francoislg) + +#### 7/09/2015 +* For -PivotRows you can pass a `hashtable` with the name of the property and the type of calculation. `Sum`, `Average`, `Max`, `Min`, `Product`, `StdDev`, `StdDevp`, `Var`, `Varp` + +```PowerShell +Get-Service | + Export-Excel "c:\temp\test.xlsx" ` + -Show ` + -IncludePivotTable ` + -PivotRows status ` + -PivotData @{status='count'} +``` + +#### 6/16/2015 (Thanks [Justin](https://github.com/zippy1981)) +* Improvements to PivotTable overwriting +* Added two parameters to Export-Excel + * RangeName - Turns the data piped to Export-Excel into a named range. + * TableName - Turns the data piped to Export-Excel into an excel table. + +Examples + + Get-Process|Export-Excel foo.xlsx -Verbose -IncludePivotTable -TableName "Processes" -Show + Get-Process|Export-Excel foo.xlsx -Verbose -IncludePivotTable -RangeName "Processes" -Show + + +#### 5/25/2015 +* Fixed null header problem + +#### 5/17/2015 +* Added three parameters: + * FreezeTopRow - Freezes the first row of the data + * AutoFilter - Enables filtering for the data in the sheet + * BoldTopRow - Bolds the top row of data, the column headers + +Example + + Get-CimInstance win32_service | + select state, accept*, start*, caption | + Export-Excel test.xlsx -Show -BoldTopRow -AutoFilter -FreezeTopRow -AutoSize + +![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/FilterFreezeBold.gif) + + +#### 5/4/2015 +* Published to PowerShell Gallery. In PowerShell v5 use `Find-Module importexcel` then `Find-Module importexcel | Install-Module` + + +#### 4/27/2015 +* datetime properties were displaying as ints, now are formatted + +#### 4/25/2015 +* Now you can create multiple Pivot tables in one pass + * Thanks to [pscookiemonster](https://twitter.com/pscookiemonster), he submitted a repro case to the EPPlus CodePlex project and got it fixed + +#### Example + + $ps = ps + + $ps | + Export-Excel .\testExport.xlsx -WorkSheetname memory ` + -IncludePivotTable -PivotRows Company -PivotData PM ` + -IncludePivotChart -ChartType PieExploded3D + $ps | + Export-Excel .\testExport.xlsx -WorkSheetname handles ` + -IncludePivotTable -PivotRows Company -PivotData Handles ` + -IncludePivotChart -ChartType PieExploded3D -Show + +![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/MultiplePivotTables.png) + +#### 4/20/2015 +* Included and embellished [Claus Nielsen](https://github.com/Claustn) function to take all sheets in an Excel file workbook and create a text file for each `ConvertFrom-ExcelSheet` +* Renamed `Export-MultipleExcelSheets` to `ConvertFrom-ExcelSheet` + +#### 4/13/2015 +* You can add a title to the Excel "Report" `Title`, `TitleFillPattern`, `TitleBold`, `TitleSize`, `TitleBackgroundColor` + * Thanks to [Irwin Strachan](http://pshirwin.wordpress.com) for this and other great suggestions, testing and more + + +#### 4/10/2015 +* Renamed `AutoFitColumns` to `AutoSize` +* Implemented `Export-MultipleExcelSheets` +* Implemented `-Password` for a worksheet +* Replaced `-Force` switch with `-NoClobber` switch +* Added examples for `Get-Help` +* If Pivot table is requested, that sheet becomes the tab selected + +#### 4/8/2015 +* Implemented exporting data to **named sheets** via the -WorkSheetname parameter. + +Examples +- +`gsv | Export-Excel .\test.xlsx -WorkSheetname Services` + +`dir -file | Export-Excel .\test.xlsx -WorkSheetname Files` + +`ps | Export-Excel .\test.xlsx -WorkSheetname Processes -IncludePivotTable -Show -PivotRows Company -PivotData PM` + +#### Convert (All or Some) Excel Sheets to Text files + +Reads each sheet in TestSheets.xlsx and outputs it to the data directory as the sheet name with the extension .txt + + ConvertFrom-ExcelSheet .\TestSheets.xlsx .\data + +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 + + ConvertFrom-ExcelSheet .\TestSheets.xlsx .\data sheet?0 + +#### Example Adding a Title +You can set the pattern, size and of if the title is bold. + + $p=@{ + Title = "Process Report as of $(Get-Date)" + TitleFillPattern = "LightTrellis" + TitleSize = 18 + TitleBold = $true + + Path = "$pwd\testExport.xlsx" + Show = $true + AutoSize = $true + } + + Get-Process | + Where Company | Select Company, PM | + Export-Excel @p + +![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/Title.png) + +#### Example Export-MultipleExcelSheets +![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/ExportMultiple.gif) + + $p = Get-Process + + $DataToGather = @{ + PM = {$p|select company, pm} + Handles = {$p|select company, handles} + Services = {gsv} + Files = {dir -File} + Albums = {(Invoke-RestMethod http://www.dougfinke.com/PowerShellfordevelopers/albums.js)} + } + + Export-MultipleExcelSheets -Show -AutoSize .\testExport.xlsx $DataToGather + + + +***NOTE*** If the sheet exists when using *-WorkSheetname* parameter, it will be deleted and then added with the new data. + +## Get-Process Exported to Excel + +### Total Physical Memory Grouped By Company +![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/PivotTablesAndCharts.png) + +## Importing data from an Excel spreadsheet + +![image](https://raw.githubusercontent.com/dfinke/ImportExcel/master/images/TryImportExcel.gif) + +You can also find EPPLus on [Nuget](https://www.nuget.org/packages/EPPlus/). + +## Known Issues + +* Using `-IncludePivotTable`, if that pivot table name exists, you'll get an error. + * Investigating a solution + * *Workaround* delete the Excel file first, then do the export From 94659d3fc2c15df09d2f5143835bff765c3298f4 Mon Sep 17 00:00:00 2001 From: jhoneill Date: Fri, 29 Jun 2018 16:29:45 +0100 Subject: [PATCH 11/11] fix --- compare-worksheet.ps1 | 523 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 compare-worksheet.ps1 diff --git a/compare-worksheet.ps1 b/compare-worksheet.ps1 new file mode 100644 index 0000000..1c448cc --- /dev/null +++ b/compare-worksheet.ps1 @@ -0,0 +1,523 @@ +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 '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 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 "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -GridView + This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel + 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 '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 the command specifies a limited set of columns should be used. + .Example + 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, + 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,Position=0)] + $Referencefile , + #Second file to compare + [parameter(Mandatory=$true,Position=1)] + $Differencefile , + #Name(s) of worksheets to compare. + $WorkSheetName = "Sheet1", + #Properties to include in the DIFF - supports wildcards, default is "*" + $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 = 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 (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 + ) + + #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} + 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 { + $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 } + + #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 = @{} ; $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"} + $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} + + #Add RowNumber, Sheetname and file name to every row + $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 = $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} + + 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 + [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 + 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) { + 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] + $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 ($diff -and $FontColor -and ($propList -contains $Key) ) { + $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} + else {$xl2 = Open-ExcelPackage -path $Differencefile } + foreach ($u in $updates) { + foreach ($p in $propList) { + if ($u.group[0]._file -eq $Referencefile) { + $ws1 = $xl1.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl2.Workbook.Worksheets[$u.Group[1]._sheet] + } + else { + $ws1 = $xl2.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl1.Workbook.Worksheets[$u.Group[1]._sheet] + } + if($u.Group[0].$p -ne $u.Group[1].$p ) { + Set-Format -WorkSheet $ws1 -Range ($Columns[$p] + $u.Group[0]._Row) -FontColor $FontColor + Set-Format -WorkSheet $ws1 -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()} + } + } + 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 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 ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" } + } + 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 ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} + else {$hash[($result.SideIndicator+$P)] =$result.$P} + } + } + [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 ($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 } +} +======= +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 '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 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 "Server54.xlsx" "Server55.xlsx" -WorkSheetName services -GridView + This time two workbooks contain the result of redirecting Get-WmiObject -Class win32_service to Export-Excel + 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 '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 the command specifies a limited set of columns should be used. + .Example + 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, + 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,Position=0)] + $Referencefile , + #Second file to compare + [parameter(Mandatory=$true,Position=1)] + $Differencefile , + #Name(s) of worksheets to compare. + $WorkSheetName = "Sheet1", + #Properties to include in the DIFF - supports wildcards, default is "*" + $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 = 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 (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 + ) + + #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} + 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 { + $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 } + + #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 = @{} ; $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"} + $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} + + #Add RowNumber, Sheetname and file name to every row + $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 = $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} + + 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 + [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 + 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) { + 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] + $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 ($diff -and $FontColor -and ($propList -contains $Key) ) { + $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} + else {$xl2 = Open-ExcelPackage -path $Differencefile } + foreach ($u in $updates) { + foreach ($p in $propList) { + if ($u.group[0]._file -eq $Referencefile) { + $ws1 = $xl1.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl2.Workbook.Worksheets[$u.Group[1]._sheet] + } + else { + $ws1 = $xl2.Workbook.Worksheets[$u.Group[0]._sheet] + $ws2 = $xl1.Workbook.Worksheets[$u.Group[1]._sheet] + } + if($u.Group[0].$p -ne $u.Group[1].$p ) { + Set-Format -WorkSheet $ws1 -Range ($Columns[$p] + $u.Group[0]._Row) -FontColor $FontColor + Set-Format -WorkSheet $ws1 -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()} + } + } + 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 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 ($GridView) { Write-Warning -Message "-GridView is ignored when -Show is specified" } + } + 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 ($result.SideIndicator -eq "==") {$hash[("=>$P")] = $hash[("<=$P")] =$result.$P} + else {$hash[($result.SideIndicator+$P)] =$result.$P} + } + } + [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 ($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 } +} + +>>>>>>> 9f7884f991c80448091ef56853027f64d98b6cc7