diff --git a/ImportExcel.psm1 b/ImportExcel.psm1 index 7baf5b0..b8c428e 100644 --- a/ImportExcel.psm1 +++ b/ImportExcel.psm1 @@ -4,8 +4,8 @@ Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" . $PSScriptRoot\AddDataValidation.ps1 . $PSScriptRoot\Charting.ps1 . $PSScriptRoot\ColorCompletion.ps1 -. $PSScriptRoot\ConvertExcelToImageFile.ps1 . $PSScriptRoot\Compare-WorkSheet.ps1 +. $PSScriptRoot\ConvertExcelToImageFile.ps1 . $PSScriptRoot\ConvertFromExcelData.ps1 . $PSScriptRoot\ConvertFromExcelToSQLInsert.ps1 . $PSScriptRoot\ConvertToExcelXlsx.ps1 @@ -23,7 +23,7 @@ Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" . $PSScriptRoot\InferData.ps1 . $PSScriptRoot\Invoke-Sum.ps1 . $PSScriptRoot\Join-Worksheet.ps1 -. $PSScriptRoot\Merge-worksheet.ps1 +. $PSScriptRoot\Merge-Worksheet.ps1 . $PSScriptRoot\New-ConditionalFormattingIconSet.ps1 . $PSScriptRoot\New-ConditionalText.ps1 . $PSScriptRoot\New-ExcelChart.ps1 @@ -31,8 +31,9 @@ Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" . $PSScriptRoot\Open-ExcelPackage.ps1 . $PSScriptRoot\Pivot.ps1 . $PSScriptRoot\PivotTable.ps1 +#. $PSScriptRoot\Plot.ps1 . $PSScriptRoot\RemoveWorksheet.ps1 -. $PSScriptRoot\Send-SqlDataToExcel.ps1 +. $PSScriptRoot\Send-SQLDataToExcel.ps1 . $PSScriptRoot\Set-CellStyle.ps1 . $PSScriptRoot\Set-Column.ps1 . $PSScriptRoot\Set-Row.ps1 @@ -45,7 +46,7 @@ Add-Type -Path "$($PSScriptRoot)\EPPlus.dll" New-Alias -Name Use-ExcelData -Value "ConvertFrom-ExcelData" -Force if ($PSVersionTable.PSVersion.Major -ge 5) { - . $PSScriptRoot\plot.ps1 + . $PSScriptRoot\Plot.ps1 Function New-Plot { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'New-Plot does not change system state')] diff --git a/Install.ps1 b/Install.ps1 index 1c96619..ae5e08a 100644 --- a/Install.ps1 +++ b/Install.ps1 @@ -25,8 +25,47 @@ $IncludeFiles = @( '*.dll', '*.psd1', '*.psm1', - '*.ps1', - 'BinFolder*' + 'AddConditionalFormatting.ps1', + 'AddDataValidation.ps1', + 'Charting.ps1', + 'ColorCompletion.ps1', + 'Compare-WorkSheet.ps1', + 'ConvertExcelToImageFile.ps1', + 'ConvertFromExcelData.ps1', + 'ConvertFromExcelToSQLInsert.ps1', + 'ConvertToExcelXlsx.ps1', + 'Copy-ExcelWorkSheet.ps1', + 'Export-Excel.ps1', + 'Export-ExcelSheet.ps1', + 'Export-StocksToExcel.ps1', + 'Get-ExcelColumnName.ps1', + 'Get-ExcelSheetInfo.ps1', + 'Get-ExcelWorkbookInfo.ps1', + 'Get-HtmlTable.ps1', + 'Get-Range.ps1', + 'Get-XYRange.ps1', + 'Import-Html.ps1', + 'InferData.ps1', + 'Invoke-Sum.ps1', + 'Join-Worksheet.ps1', + 'Merge-Worksheet.ps1', + 'New-ConditionalFormattingIconSet.ps1', + 'New-ConditionalText.ps1', + 'New-ExcelChart.ps1', + 'New-PSItem.ps1', + 'Open-ExcelPackage.ps1', + 'Pivot.ps1', + 'PivotTable.ps1', + 'Plot.ps1', + 'RemoveWorksheet.ps1', + 'Send-SQLDataToExcel.ps1', + 'Set-CellStyle.ps1', + 'Set-Column.ps1', + 'Set-Row.ps1', + 'Set-WorkSheetProtection.ps1', + 'SetFormat.ps1', + 'TrackingUtils.ps1', + 'Update-FirstObjectProperties.ps1' ) $ExcludeFiles = @( 'Install.ps1' diff --git a/Merge-Worksheet.ps1 b/Merge-Worksheet.ps1 new file mode 100644 index 0000000..01ae37e --- /dev/null +++ b/Merge-Worksheet.ps1 @@ -0,0 +1,538 @@ +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* a 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 sheet contains + a list of services. This command creates a worksheet named "54-55" in a + workbook named "services.xlsx" which shows all the services and their + differences, and opens the new workbook 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 can be"OutSheet"; DifferenceObject & ReferenceObject can be + DiffObject & RefObject respectively). + #> + [cmdletbinding(SupportsShouldProcess=$true)] + Param( + #First Excel file to compare. You can compare two Excel files or two other objects or a reference obhct against a difference file, but not a reference file against an object. + [parameter(ParameterSetName='A',Mandatory=$true,Position=0)] #A = Compare two files default headers + [parameter(ParameterSetName='B',Mandatory=$true,Position=0)] #B = Compare two files user supplied headers + [parameter(ParameterSetName='C',Mandatory=$true,Position=0)] #C = Compare two files headers P1, P2, P3 etc + $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)] #D Compare two objects; E = Compare one object one file that uses default headers + [parameter(ParameterSetName='F',Mandatory=$true,Position=1)] #F = Compare one object one file that uses user supplied headers + [parameter(ParameterSetName='G',Mandatory=$true,Position=1)] #G Compare one object one file that uses headers P1, P2, P3 etc + $Differencefile , + + #Name(s) of Worksheets to compare. + [parameter(ParameterSetName='A',Position=2)] #Applies to all sets EXCEPT D which is two objects (no sheets) + [parameter(ParameterSetName='B',Position=2)] + [parameter(ParameterSetName='C',Position=2)] + [parameter(ParameterSetName='E',Position=2)] + [parameter(ParameterSetName='F',Position=2)] + [parameter(ParameterSetName='G',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')] #Applies to all sets EXCEPT D which is two objects (no sheets, so no start row ) + [parameter(ParameterSetName='B')] + [parameter(ParameterSetName='C')] + [parameter(ParameterSetName='E')] + [parameter(ParameterSetName='F')] + [parameter(ParameterSetName='G')] + [int]$Startrow = 1, + + #Specifies custom property names to use, instead of the values defined in the column headers of the Start ROw. + [Parameter(ParameterSetName='B',Mandatory=$true)] #Compare object + sheet or 2 sheets with user supplied headers + [Parameter(ParameterSetName='F',Mandatory=$true)] + [String[]]$Headername, + + #Automatically generate property names (P1, P2, P3, ..) instead of using the values the top row of the sheet. + [Parameter(ParameterSetName='C',Mandatory=$true)] #Compare object + sheet or 2 sheets with headers of P1, P2, P3 ... + [Parameter(ParameterSetName='G',Mandatory=$true)] + [switch]$NoHeader, + + #Reference object to compare if a Worksheet is NOT being used. Reference object can combine with a difference sheet or difference object + [parameter(ParameterSetName='D',Mandatory=$true)] + [parameter(ParameterSetName='E',Mandatory=$true)] + [parameter(ParameterSetName='F',Mandatory=$true)] + [parameter(ParameterSetName='G',Mandatory=$true)] + [Alias('RefObject')] + $ReferenceObject , + #Difference object to compare if a Worksheet is NOT being used for either half. Can't have a reference sheet and difference object. + [parameter(ParameterSetName='D',Mandatory=$true,Position=1)] + [Alias('DiffObject')] + $DifferenceObject , + [parameter(ParameterSetName='D',Position=2)] + [parameter(ParameterSetName='E',Position=2)] + [parameter(ParameterSetName='F',Position=2)] + [parameter(ParameterSetName='G',Position=2)] + #If there isn't a filename to use to label data from the "Difference" side, DiffPrefix is used, it defaults to "=>" + $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. + $KeyFontColor = [System.Drawing.Color]::DarkRed , + #Sets the background color for changed rows. + $ChangeBackgroundColor = [System.Drawing.Color]::Orange, + #Sets the background color for rows in the reference but deleted from the difference sheet. + $DeleteBackgroundColor = [System.Drawing.Color]::LightPink, + #Sets the background color for rows not in the reference but added to the difference sheet. + $AddBackgroundColor = [System.Drawing.Color]::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 the command only outputs to the pipeline). + [switch]$Passthru , + #If specified, opens the output workbook. + [Switch]$Show + ) + + #region Read Excel data + if ($Differencefile -is [System.IO.FileInfo]) {$Differencefile = $Differencefile.FullName} + if ($Referencefile -is [System.IO.FileInfo]) {$Referencefile = $Referencefile.FullName} + 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 } + foreach ($p in @("HeaderName","NoHeader","StartRow")) {if ($PSBoundParameters[$p]) {$params[$p] = $PSBoundParameters[$p]}} + 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" } + $lastRefColNo = $proplist.count + $FirstDiffColNo = $lastRefColNo + 1 + + 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 + $lastDiffColNo = (2 * $proplist.count) - 1 + } + 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 + $lastDiffColNo = (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 ($null -eq $row._row) {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 ($DifferenceObject.count -gt $Rowhash.Keys.Count) { + Write-Warning -Message "Difference object has $($DifferenceObject.Count) rows; but only $($Rowhash.keys.count) unique keys" + } + if ($Key -eq '*') {$key = "_ALL"} + #endregion + #We need to know all the properties we've met on the objects we've diffed + $eDiffProps = [ordered]@{} + #When we do a compare object changes will result in two rows so we group them and join them together. + $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. + $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, this must be the second record, so set side to indicate "changed"; if we got two "Same" indicators we may have a classh of keys + if ($hash.Side) { + if ($hash.Side -eq $result.SideIndicator) {Write-Warning -Message "'$keyval' may be a duplicate."} + $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 in 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} + } + } + + foreach ($k in $hash.keys) {$eDiffProps[$k] = $true} + [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 + $eDiffProps.keys.where({$_ -notin ($outputProps + @("_row","side","SideIndicator","_ALL" ))}) + + if ($PassThru -or -not $OutputFile) {return ($expandedDiff | Select-Object -Property $allprops | Sort-Object -Property "_row", "$DiffPrefix Row" ) } + 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-ExcelRange -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-ExcelRange -Worksheet $ws -Range $range -BackgroundColor $ChangeBackgroundColor + } + elseif ( $expandedDiff[$i].side -eq "<=" ) { + $rangeR1C1 = "R[{0}]C[1]:R[{0}]C[{1}]" -f ($i + 2 ) , $lastRefColNo + $range = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1($rangeR1C1,0,0) + Set-ExcelRange -Worksheet $ws -Range $range -BackgroundColor $DeleteBackgroundColor + } + elseif ( $expandedDiff[$i].side -eq "=>" ) { + if ($propList.count -gt 1) { + $rangeR1C1 = "R[{0}]C[{1}]:R[{0}]C[{2}]" -f ($i + 2 ) , $FirstDiffColNo , $lastDiffColNo + $range = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1($rangeR1C1,0,0) + Set-ExcelRange -Worksheet $ws -Range $range -BackgroundColor $AddBackgroundColor + } + Set-ExcelRange -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 two sheets. Merge-MultipleSheets is + designed to merge more than two. 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 "Startmode" from sheets A and C; the result has + column headings "_Row", "Name", "DisplayName", "Startmode", "C-DisplayName", + "C-StartMode", "C-Is" and "C-Row". + Merge-MultipleSheets then calls Merge-Worksheet again passing it the + intermediate result and sheet B, comparing "Name", "Displayname" and + "Start mode" columns on each side, and gets a result with columns "_Row", + "Name", "DisplayName", "Startmode", "B-DisplayName", "B-StartMode", "B-Is", + "B-Row", "C-DisplayName", "C-StartMode", "C-Is" and "C-Row". Any columns on + the "reference" side which are not used in the comparison are added on the + right, which is why we compare the sheets in reverse order. + + The "Is" columns hold "Same", "Added", "Removed" or "Changed" and is used for + conditional formatting in the output sheet (these columns are 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 + Here 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 hotfix information to their own + worksheets in a shared Excel workbook named "Hotfixes.xlsx" (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. + #> + [cmdletbinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification="False positives when initializing variable in begin block")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification="MultipleSheet would be incorrect")] + #[Alias("Merge-MulipleSheets")] #There was a spelling error in the first release. This was there to ensure things didn't break but intelisense gave the alias first. + param ( + #Paths to the files to be merged. Files are also accepted + [Parameter(Mandatory=$true,ValueFromPipeline=$true)] + $Path , + #The row from where we start to import data, all rows above the Start row 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 Start row. + [String[]]$Headername, + + #If specified, property names will be automatically generated (P1, P2, P3, ..) instead of using the values from the start row. + [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 comparison - supports wildcards, default is "*". + $Property = "*" , + #Properties to exclude from the the comparison - supports wildcards. + $ExcludeProperty , + #Name of a column which is unique used to pair up rows from the reference and difference sides, 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. + $KeyFontColor = [System.Drawing.Color]::Red, + #Sets the background color for changed rows. + $ChangeBackgroundColor = [System.Drawing.Color]::Orange, + #Sets the background color for rows in the reference but deleted from the difference sheet. + $DeleteBackgroundColor = [System.Drawing.Color]::LightPink, + #Sets the background color for rows not in the reference but added to the difference sheet. + $AddBackgroundColor = [System.Drawing.Color]::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 pipeline). + [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 identically 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 '$WorksheetName' in $($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 '$WorksheetName' in $($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 } + + $orderByProperties = $merged[0].psobject.properties.where({$_.name -match "row$"}).name + Write-Progress -Activity "Merging sheets" -CurrentOperation "creating output sheet '$OutputSheetName' in $OutputFile" + $excel = $merged | Sort-Object -Property $orderByProperties | + 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='Solid'; Worksheet=$sheet; StopIfTrue=$true; 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='Solid'; Worksheet=$sheet; StopIfTrue=$true; 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 + Add-ConditionalFormatting @condFormattingParams -ConditionValue ("AND(" +(($sameChecks -join ",") -replace '<>"Same"','="Changed"') +")" ) -BackgroundColor $ChangeBackgroundColor + } + #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 + } + } + if ($Passthru) {$excel} + else {Close-ExcelPackage -ExcelPackage $excel -Show:$Show} + Write-Progress -Activity "Merging sheets" -Completed + } +} diff --git a/Plot.ps1 b/Plot.ps1 new file mode 100644 index 0000000..4a54ea1 --- /dev/null +++ b/Plot.ps1 @@ -0,0 +1,218 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification="False positives")] +param() +class PSPlot { + hidden $path + hidden $pkg + hidden $ws + hidden $chart + + PSPlot() { + $this.path=[System.IO.Path]::GetTempFileName() -replace "\.tmp", ".xlsx" + $this.pkg = New-Object OfficeOpenXml.ExcelPackage $this.path + $this.ws=$this.pkg.Workbook.Worksheets.Add("plot") + } + + [PSPlot] Plot($yValues) { + + $this.NewChart() + + $xValues = 0..$yValues.Count + + $xCol = 'A' + $yCol = 'B' + + $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) + $this.AddSeries($xCol,$yCol,$yValues) + $this.SetChartPosition($yCol) + + return $this + } + + [PSPlot] Plot($yValues,[string]$options) { + $this.NewChart() + + $xValues = 0..$yValues.Count + + $xCol = 'A' + $yCol = 'B' + + $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) + $this.AddSeries($xCol,$yCol,$yValues) + + $this.SetMarkerInfo($options) + $this.SetChartPosition($yCol) + + return $this + } + + [PSPlot] Plot($xValues,$yValues) { + + $this.NewChart() + + $xCol = 'A' + $yCol = 'B' + + $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) + $this.AddSeries($xCol,$yCol,$yValues) + + $this.SetChartPosition($yCol) + + return $this + } + + [PSPlot] Plot($xValues,$yValues,[string]$options) { + $this.NewChart() + + $xCol = 'A' + $yCol = 'B' + + $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) + $this.AddSeries($xCol,$yCol,$yValues) + + $this.SetMarkerInfo($options) + + $this.SetChartPosition($yCol) + + return $this + } + + [PSPlot] Plot($xValues,$yValues,$x1Values,$y1Values) { + + $this.NewChart() + + $xCol = 'A' + $yCol = 'B' + + $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) + $this.AddSeries($xCol,$yCol,$yValues) + + $xCol=$this.GetNextColumnName($yCol) + $yCol=$this.GetNextColumnName($xCol) + + $this.AddDataToSheet($xCol,$yCol,'x1','y1',$x1Values,$y1Values) + $this.AddSeries($xCol,$yCol,$y1Values) + + $this.SetChartPosition($yCol) + + return $this + } + + [PSPlot] Plot($xValues,$yValues,$x1Values,$y1Values,$x2Values,$y2Values) { + + $this.NewChart() + + $xCol = 'A' + $yCol = 'B' + + $this.AddDataToSheet($xCol,$yCol,'x','y',$xValues,$yValues) + $this.AddSeries($xCol,$yCol,$yValues) + + $xCol=$this.GetNextColumnName($yCol) + $yCol=$this.GetNextColumnName($xCol) + + $this.AddDataToSheet($xCol,$yCol,'x1','y1',$x1Values,$y1Values) + $this.AddSeries($xCol,$yCol,$y1Values) + + $xCol=$this.GetNextColumnName($yCol) + $yCol=$this.GetNextColumnName($xCol) + + $this.AddDataToSheet($xCol,$yCol,'x2','y2',$x2Values,$y2Values) + $this.AddSeries($xCol,$yCol,$y2Values) + + $this.SetChartPosition($yCol) + + return $this + } + + [PSPLot] SetChartPosition($yCol) { + $columnNumber = $this.GetColumnNumber($yCol)+1 + $this.chart.SetPosition(1,0,$columnNumber,0) + + return $this + } + + AddSeries($xCol,$yCol,$yValues) { + $yRange = "{0}2:{0}{1}" -f $yCol,($yValues.Count+1) + $xRange = "{0}2:{0}{1}" -f $xCol,($yValues.Count+1) + $Series=$this.chart.Series.Add($yRange,$xRange) + } + + hidden SetMarkerInfo([string]$options) { + $c=$options.Substring(0,1) + $m=$options.Substring(1) + + $cmap=@{r='red';g='green';b='blue';i='indigo';v='violet';c='cyan'} + $mmap=@{Ci='Circle';Da='Dash';di='diamond';do='dot';pl='plus';sq='square';tr='triangle'} + + $this.chart.Series[0].Marker = $mmap.$m + $this.chart.Series[0].MarkerColor = $cmap.$c + $this.chart.Series[0].MarkerLineColor = $cmap.$c + } + + hidden [string]GetNextColumnName($columnName) { + return $this.GetColumnName($this.GetColumnNumber($columnName)+1) + } + + hidden [int]GetColumnNumber($columnName) { + $sum=0 + + $columnName.ToCharArray() | + ForEach-Object { + $sum*=26 + $sum+=[char]$_.tostring().toupper()-[char]'A'+1 + } + + return $sum + } + + hidden [string]GetColumnName($columnNumber) { + $dividend = $columnNumber + $columnName = @() + while($dividend -gt 0) { + $modulo = ($dividend - 1) % 26 + $columnName += [char](65 + $modulo) + $dividend = [int](($dividend -$modulo)/26) + } + + return ($columnName -join '') + } + + hidden AddDataToSheet($xColumn,$yColumn,$xHeader,$yHeader,$xValues,$yValues) { + $count=$yValues.Count + $this.ws.Cells["$($xColumn)1"].Value=$xHeader + $this.ws.Cells["$($yColumn)1"].Value=$yHeader + + for ($idx= 0; $idx-lt $count; $idx++) { + $row=$idx+2 + $this.ws.Cells["$($xColumn)$($row)"].Value=$xValues[$idx] + $this.ws.Cells["$($yColumn)$($row)"].Value=$yValues[$idx] + } + } + + hidden NewChart() { + $chartType="XYScatter" + #$chartType="line" + $this.chart=$this.ws.Drawings.AddChart("plot", $chartType) + $this.chart.Title.Text = 'Plot' + $this.chart.Legend.Remove() + $this.SetChartSize(300,300) + } + + [PSPlot] SetChartSize([int]$width,[int]$height){ + $this.chart.SetSize($width, $height) + + return $this + } + + [PSPlot] Title($title) { + $this.chart.Title.Text = $title + + return $this + } + + Show() { + $this.pkg.Save() + $this.pkg.Dispose() + Invoke-Item $this.path + } +} \ No newline at end of file diff --git a/PublishToGallery.ps1 b/PublishToGallery.ps1 deleted file mode 100644 index cc352c2..0000000 --- a/PublishToGallery.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -$p = @{ - Name = "ImportExcel" - NuGetApiKey = $NuGetApiKey - ReleaseNote = "Add NumberFormat parameter" -} - -Publish-Module @p \ No newline at end of file diff --git a/Send-SQLDataToExcel.ps1 b/Send-SQLDataToExcel.ps1 new file mode 100644 index 0000000..874ee87 --- /dev/null +++ b/Send-SQLDataToExcel.ps1 @@ -0,0 +1,175 @@ +Function Send-SQLDataToExcel { + <# + .SYNOPSIS + Inserts a DataTable - returned by a SQL query - into an ExcelSheet + .DESCRIPTION + This command takes a SQL statement and run it against a database connection; for the connection it accepts either + * an object representing a session with a SQL server or ODBC database, or + * a connection string to make a session (if -MSSQLServer is specified it uses the SQL Native client, + and -Connection can be a server name instead of a detailed connection string. Without this switch it uses ODBC) + The command takes all the parameters of Export-Excel, except for -InputObject (alias TargetData); after + fetching the data it calls Export-Excel with the data as the value of InputParameter and whichever of + Export-Excel's parameters it was passed; for details of these parameters see the help for Export-Excel. + .PARAMETER Session + An active ODBC Connection or SQL connection object representing a session with a database which will be queried to get the data . + .PARAMETER Connection + A database connection string to be used to create a database session; either + * A Data source name written in the form DSN=ODBC_Data_Source_Name, or + * A full ODBC or SQL Native Client Connection string, or + * The name of a SQL server. + .PARAMETER MSSQLServer + Specifies the connection string is for SQL server, not ODBC. + .PARAMETER SQL + The SQL query to run against the session which was passed in -Session or set up from -Connection. + .PARAMETER Database + Switches to a specific database on a SQL server. + .PARAMETER QueryTimeout + Override the default query time of 30 seconds. + .PARAMETER DataTable + A System.Data.DataTable object containing the data to be inserted into the spreadsheet without running a query. + This remains supported to avoid breaking older scripts, but if you have a DataTable object you can pass the it + into Export-Excel using -InputObject. + .EXAMPLE + C:\> Send-SQLDataToExcel -MsSQLserver -Connection localhost -SQL "select name,type,type_desc from [master].[sys].[all_objects]" -Path .\temp.xlsx -WorkSheetname master -AutoSize -FreezeTopRow -AutoFilter -BoldTopRow + + Connects to the local SQL server and selects 3 columns from [Sys].[all_objects] and exports then to a sheet named master with some basic header management + .EXAMPLE + C:\> $dbPath = 'C:\Users\James\Documents\Database1.accdb' + C:\> $Connection = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=$dbPath;" + C:\> $SQL="SELECT top 25 Name,Length From TestData ORDER BY Length DESC" + + C:\> Send-SQLDataToExcel -Connection $connection -SQL $sql -path .\demo1.xlsx -WorkSheetname "Sizes" -AutoSize + + This creates an ODBC connection string to read from an Access file and a SQL Statement to extracts data from it, + and sends the resulting data to a new worksheet + + .EXAMPLE + C:\> $dbPath = 'C:\users\James\Documents\f1Results.xlsx' + C:\> $Connection = "Driver={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};Dbq=$dbPath;" + C:\> $SQL="SELECT top 25 DriverName, Count(RaceDate) as Races, Count(Win) as Wins, Count(Pole) as Poles, Count(FastestLap) as Fastlaps " + + " FROM Results GROUP BY DriverName ORDER BY (count(win)) DESC" + + C:\> Send-SQLDataToExcel -Connection $connection -SQL $sql -path .\demo2.xlsx -WorkSheetname "Winners" -AutoSize -AutoNameRange -ConditionalFormat @{DataBarColor="Blue"; Range="Wins"} + + Similar to the previous example this creates a connection string, this time for an Excel file, and runs + a SQL statement to get a list of motor-racing results, outputting the resulting data to a new spreadsheet. + The spreadsheet is formatted and a data bar added to show make the drivers' wins clearer. + (the F1 results database is available from https://1drv.ms/x/s!AhfYu7-CJv4ehNdZWxJE9LMAX_N5sg ) + .EXAMPLE + C:\> $dbPath = 'C:\users\James\Documents\f1Results.xlsx' + C:\> $SQL = "SELECT top 25 DriverName, Count(RaceDate) as Races, Count(Win) as Wins, Count(Pole) as Poles, Count(FastestLap) as Fastlaps " + + " FROM Results GROUP BY DriverName ORDER BY (count(win)) DESC" + C:\> $null = Get-SQL -Session F1 -excel -Connection $dbPath -sql $sql -OutputVariable Table + + C:\> Send-SQLDataToExcel -DataTable $Table -Path ".\demo3.xlsx" -WorkSheetname Gpwinners -autosize -TableName winners -TableStyle Light6 -show + + This uses Get-SQL (at least V1.1 - download from the PowerShell gallery with Install-Module -Name GetSQL - + note the function is Get-SQL the module is GetSQL without the "-" ) + Get-SQL simplify making database connections and building /submitting SQL statements. + Here Get-SQL uses the same SQL statement as before; -OutputVariable leaves a System.Data.DataTable object in $table + and Send-SQLDataToExcel puts $table into the worksheet and sets it as an Excel table. + The command is equivalent to running + C:\> Export-Excel -inputObject $Table -Path ".\demo3.xlsx" -WorkSheetname Gpwinners -autosize -TableName winners -TableStyle Light6 -show + This is quicker than using + C:\> Get-SQL | export-excel -ExcludeProperty rowerror,rowstate,table,itemarray,haserrors + (the F1 results database is available from https://1drv.ms/x/s!AhfYu7-CJv4ehNdZWxJE9LMAX_N5sg ) + .EXAMPLE + C:\> $SQL = "SELECT top 25 DriverName, Count(Win) as Wins FROM Results GROUP BY DriverName ORDER BY (count(win)) DESC" + C:\> Send-SQLDataToExcel -Session $DbSessions["f1"] -SQL $sql -Path ".\demo3.xlsx" -WorkSheetname Gpwinners -ClearSheet -autosize -ColumnChart + + Like the previous example, this uses Get-SQL (download from the gallery with Install-Module -Name GetSQL). + It uses the database session which Get-SQL created, rather than an ODBC connection string. + The Session parameter can either be a object (as shown here), or the name used by Get-SQL ("F1" in this case). + Here the data is presented as a quick chart. + .EXAMPLE + C:\> Send-SQLDataToExcel -path .\demo4.xlsx -WorkSheetname "LR" -Connection "DSN=LR" -sql "SELECT name AS CollectionName FROM AgLibraryCollection Collection ORDER BY CollectionName" + + This example uses an Existing ODBC datasource name "LR" which maps to an adobe lightroom database and gets a list of collection names into a worksheet + + .Link + Export-Excel + #> + [CmdletBinding(DefaultParameterSetName="none")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification="Allowed to use DBSessions Global variable from GETSQL Module")] + + param ( + [Parameter(ParameterSetName="SQLConnection", Mandatory=$true)] + [Parameter(ParameterSetName="ODBCConnection", Mandatory=$true)] + $Connection, + [Parameter(ParameterSetName="ExistingSession", Mandatory=$true)] + $Session, + [Parameter(ParameterSetName="SQLConnection", Mandatory=$true)] + [switch]$MsSQLserver, + [Parameter(ParameterSetName="SQLConnection")] + [String]$DataBase, + [Parameter(ParameterSetName="SQLConnection", Mandatory=$true)] + [Parameter(ParameterSetName="ODBCConnection", Mandatory=$true)] + [Parameter(ParameterSetName="ExistingSession", Mandatory=$true)] + [string]$SQL, + [int]$QueryTimeout, + [Parameter(ParameterSetName="Pre-FetchedData", Mandatory=$true)] + [System.Data.DataTable]$DataTable + ) +#Import the parameters from Export-Excel, we will pass InputObject, and we have the common parameters so exclude those, +#and re-write the [Parmameter] attribute on each one to avoid parameterSetName here competing with the settings in Export excel. +#The down side of this that impossible parameter combinations won't be filtered out and need to be caught later. + DynamicParam { + $ParameterAttribute = "System.Management.Automation.ParameterAttribute" + $RuntimeDefinedParam = "System.Management.Automation.RuntimeDefinedParameter" + $paramDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary + $attributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute] + $attributeCollection.Add((New-Object -TypeName $ParameterAttribute -Property @{ ParameterSetName = "__AllParameterSets" ;Mandatory = $false})) + foreach ($P in (Get-Command -Name Export-Excel).Parameters.values.where({$_.name -notmatch 'Verbose|Debug|Action$|Variable$|Buffer$|TargetData$|InputObject$'})) { + $paramDictionary.Add($p.Name, (New-Object -TypeName $RuntimeDefinedParam -ArgumentList $p.name, $p.ParameterType, $attributeCollection ) ) + } + return $paramDictionary + } + process { + #Dynamic params mean we can get passed parameter combination Export-Excel will reject, so throw here, rather than get data and then have Export-Excel error. + if ($PSBoundParameters.Path -and $PSBoundParameters.ExcelPackage) { + throw 'Parameter error: you cannot specify both a path and an Excel Package.' + return + } + if ($PSBoundParameters.AutoFilter -and ($PSBoundParameters.TableName -or $PSBoundParameters.TableStyle)) { + Write-Warning "Tables are automatically auto-filtered, -AutoFilter will be ignored" + $null = $PSBoundParameters.Remove('AutoFilter') + } + #We were either given a session object or a connection string (with, optionally a MSSQLServer parameter) + #If we got -MSSQLServer, create a SQL connection, if we didn't but we got -Connection create an ODBC connection + if ($MsSQLserver -and $Connection) { + if ($Connection -notmatch '=') {$Connection = "server=$Connection;trusted_connection=true;timeout=60"} + $Session = New-Object -TypeName System.Data.SqlClient.SqlConnection -ArgumentList $Connection + if ($Session.State -ne 'Open') {$Session.Open()} + if ($DataBase) {$Session.ChangeDatabase($DataBase) } + } + elseif ($Connection) { + $Session = New-Object -TypeName System.Data.Odbc.OdbcConnection -ArgumentList $Connection ; $Session.ConnectionTimeout = 30 + } + if ($Session) { + #A session was either passed in or just created. If it's a SQL one make a SQL DataAdapter, otherwise make an ODBC one + if ($Session -is [String] -and $Global:DbSessions[$Session]) {$Session = $Global:DbSessions[$Session]} + if ($Session.GetType().name -match "SqlConnection") { + $dataAdapter = New-Object -TypeName System.Data.SqlClient.SqlDataAdapter -ArgumentList ( + New-Object -TypeName System.Data.SqlClient.SqlCommand -ArgumentList $SQL, $Session) + } + else { + $dataAdapter = New-Object -TypeName System.Data.Odbc.OdbcDataAdapter -ArgumentList ( + New-Object -TypeName System.Data.Odbc.OdbcCommand -ArgumentList $SQL, $Session ) + } + if ($QueryTimeout) {$dataAdapter.SelectCommand.CommandTimeout = $QueryTimeout} + + #Both adapter types output the same kind of table, create one and fill it from the adapter + $DataTable = New-Object -TypeName System.Data.DataTable + $rowCount = $dataAdapter.fill($dataTable) + Write-Verbose -Message "Query returned $rowCount row(s)" + } + if ($DataTable.Rows.Count) { + #Call export-excel removing parameters which relate to the SQL query, and keeping the rest. + 'Connection' , 'Database' , 'Session' , 'MsSQLserver' , 'SQL' , 'DataTable' , 'QueryTimeout' | ForEach-Object {$null = $PSBoundParameters.Remove($_) } + Export-Excel @PSBoundParameters -InputObject $DataTable + } + else {Write-Warning -Message ' No Data to insert.' } + #If we were passed a connection and opened a session, close that session. + if ($Connection) {$Session.close() } + } +} \ No newline at end of file diff --git a/filelist.txt b/filelist.txt index ebe3d59..e6bd0dc 100644 --- a/filelist.txt +++ b/filelist.txt @@ -2,17 +2,18 @@ *.psd1 *.psm1 AddConditionalFormatting.ps1 +AddDataValidation.ps1 Charting.ps1 ColorCompletion.ps1 -Compare-Worksheet.ps1 +Compare-WorkSheet.ps1 ConvertExcelToImageFile.ps1 ConvertFromExcelData.ps1 ConvertFromExcelToSQLInsert.ps1 ConvertToExcelXlsx.ps1 Copy-ExcelWorkSheet.ps1 -Export-Charts.ps1 Export-Excel.ps1 Export-ExcelSheet.ps1 +Export-StocksToExcel.ps1 Get-ExcelColumnName.ps1 Get-ExcelSheetInfo.ps1 Get-ExcelWorkbookInfo.ps1 @@ -32,10 +33,12 @@ Open-ExcelPackage.ps1 Pivot.ps1 PivotTable.ps1 Plot.ps1 +RemoveWorksheet.ps1 Send-SQLDataToExcel.ps1 Set-CellStyle.ps1 Set-Column.ps1 Set-Row.ps1 +Set-WorkSheetProtection.ps1 SetFormat.ps1 TrackingUtils.ps1 Update-FirstObjectProperties.ps1 \ No newline at end of file