diff --git a/Examples/CommunityContributions/MultipleWorksheets.ps1 b/Examples/CommunityContributions/MultipleWorksheets.ps1 new file mode 100644 index 0000000..c7b2f6b --- /dev/null +++ b/Examples/CommunityContributions/MultipleWorksheets.ps1 @@ -0,0 +1,27 @@ +<# + To see this written up with example screenshots, head over to the IT Splat blog + URL: http://bit.ly/2SxieeM +#> + +## Create an Excel file with multiple worksheets +# Get a list of processes on the system +$processes = Get-Process | Sort-Object -Property ProcessName | Group-Object -Property ProcessName | Where-Object {$_.Count -gt 2} + +# Export the processes to Excel, each process on its own sheet +$processes | ForEach-Object { $_.Group | Export-Excel -Path MultiSheetExample.xlsx -WorksheetName $_.Name -AutoSize -AutoFilter } + +# Show the completed file +Invoke-Item .\MultiSheetExample.xlsx + +## Add an additional sheet to the new workbook +# Use Open-ExcelPackage to open the workbook +$excelPackage = Open-ExcelPackage -Path .\MultiSheetExample.xlsx + +# Create a new worksheet and give it a name, set MoveToStart to make it the first sheet +$ws = Add-Worksheet -ExcelPackage $excelPackage -WorksheetName 'All Services' -MoveToStart + +# Get all the running services on the system +Get-Service | Export-Excel -ExcelPackage $excelPackage -WorksheetName $ws -AutoSize -AutoFilter + +# Close the package and show the final result +Close-ExcelPackage -ExcelPackage $excelPackage -Show diff --git a/Examples/ExcelBuiltIns/DSUM.png b/Examples/ExcelBuiltIns/DSUM.png new file mode 100644 index 0000000..b9631fe Binary files /dev/null and b/Examples/ExcelBuiltIns/DSUM.png differ diff --git a/Examples/ExcelBuiltIns/DSUM.ps1 b/Examples/ExcelBuiltIns/DSUM.ps1 new file mode 100644 index 0000000..26f3839 --- /dev/null +++ b/Examples/ExcelBuiltIns/DSUM.ps1 @@ -0,0 +1,31 @@ +# DSUM +# Adds the numbers in a field (column) of records in a list or database that match conditions that you specify. + +$xlfile = "$env:TEMP\test.xlsx" +Remove-Item $xlfile -ErrorAction SilentlyContinue + +$data = ConvertFrom-Csv @" +Color,Date,Sales +Red,1/15/2018,250 +Blue,1/15/2018,200 +Red,1/16/2018,175 +Blue,1/16/2018,325 +Red,1/17/2018,150 +Blue,1/17/2018,300 +"@ + +$xl = Export-Excel -InputObject $data -Path $xlfile -AutoSize -AutoFilter -TableName SalesInfo -AutoNameRange -PassThru + +$databaseAddress = $xl.Sheet1.Dimension.Address +Set-Format -Worksheet $xl.Sheet1 -Range C:C -NumberFormat '$##0' + +Set-Format -Worksheet $xl.Sheet1 -Range E1 -Value Color +Set-Format -Worksheet $xl.Sheet1 -Range F1 -Value Date +Set-Format -Worksheet $xl.Sheet1 -Range G1 -Value Sales + +Set-Format -Worksheet $xl.Sheet1 -Range E2 -Value Red + +Set-Format -Worksheet $xl.Sheet1 -Range E4 -Value Sales +Set-Format -Worksheet $xl.Sheet1 -Range F4 -Formula ('=DSUM({0},"Sales",E1:G2)' -f $databaseAddress) -NumberFormat '$##0' + +Close-ExcelPackage $xl -Show \ No newline at end of file diff --git a/Examples/ExcelBuiltIns/VLOOKUP.ps1 b/Examples/ExcelBuiltIns/VLOOKUP.ps1 new file mode 100644 index 0000000..a04fecb --- /dev/null +++ b/Examples/ExcelBuiltIns/VLOOKUP.ps1 @@ -0,0 +1,19 @@ +$xlfile = "$env:TEMP\test.xlsx" +Remove-Item $xlfile -ErrorAction SilentlyContinue + +$data = ConvertFrom-Csv @" +Fruit,Amount +Apples,50 +Oranges,20 +Bananas,60 +Lemons,40 +"@ + +$xl = Export-Excel -InputObject $data -Path $xlfile -PassThru -AutoSize + +Set-ExcelRange -Worksheet $xl.Sheet1 -Range D2 -BackgroundColor LightBlue -Value Apples + +$Rows = $xl.Sheet1.Dimension.Rows +Set-ExcelRange -Worksheet $xl.Sheet1 -Range E2 -Formula "=VLookup(D2,A2:B$($Rows),2,FALSE)" + +Close-ExcelPackage $xl -Show \ No newline at end of file diff --git a/Examples/ExcelBuiltIns/VLookUp.png b/Examples/ExcelBuiltIns/VLookUp.png new file mode 100644 index 0000000..421775f Binary files /dev/null and b/Examples/ExcelBuiltIns/VLookUp.png differ diff --git a/Public/Import-Excel.ps1 b/Public/Import-Excel.ps1 index e5e6ce7..5fe1234 100644 --- a/Public/Import-Excel.ps1 +++ b/Public/Import-Excel.ps1 @@ -1,8 +1,8 @@ function Import-Excel { - [CmdLetBinding()] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectUsageOfAssignmentOperator', '', Justification = 'Intentional')] - param ( + [CmdLetBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectUsageOfAssignmentOperator', '', Justification = 'Intentional')] + param ( [Alias('FullName')] [Parameter(ParameterSetName = "PathA", Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0 )] [Parameter(ParameterSetName = "PathB", Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0 )] @@ -37,182 +37,171 @@ [ValidateNotNullOrEmpty()] [String]$Password ) - end { - $sw = [System.Diagnostics.Stopwatch]::StartNew() - if ($input) { - $Paths = $input - } - elseif ($Path) { - $Paths = $Path - } - else { - $Paths = '' - } - function Get-PropertyNames { - <# + end { + $sw = [System.Diagnostics.Stopwatch]::StartNew() + if ($input) { + $Paths = $input + } + elseif ($Path) { + $Paths = $Path + } + else { + $Paths = '' + } + function Get-PropertyNames { + <# .SYNOPSIS Create objects containing the column number and the column name for each of the different header types. #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "Name would be incorrect, and command is not exported")] - param( - [Parameter(Mandatory)] - [Int[]]$Columns, - [Parameter(Mandatory)] - [Int]$StartRow - ) + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "Name would be incorrect, and command is not exported")] + param( + [Parameter(Mandatory)] + [Int[]]$Columns, + [Parameter(Mandatory)] + [Int]$StartRow + ) - try { - if ($HeaderName) { - $i = 0 - foreach ($H in $HeaderName) { - $H | Select-Object @{N = 'Column'; E = { $Columns[$i] } }, @{N = 'Value'; E = { $H } } - $i++ + try { + if ($HeaderName) { + $i = 0 + foreach ($H in $HeaderName) { + $H | Select-Object @{N = 'Column'; E = { $Columns[$i] } }, @{N = 'Value'; E = { $H } } + $i++ + } } - } - elseif ($NoHeader) { - $i = 0 - foreach ($C in $Columns) { - $i++ - $C | Select-Object @{N = 'Column'; E = { $_ } }, @{N = 'Value'; E = { 'P' + $i } } + elseif ($NoHeader) { + $i = 0 + foreach ($C in $Columns) { + $i++ + $C | Select-Object @{N = 'Column'; E = { $_ } }, @{N = 'Value'; E = { 'P' + $i } } + } } - } - else { - if ($StartRow -lt 1) { - throw 'The top row can never be less than 1 when we need to retrieve headers from the worksheet.' ; return - } + else { + if ($StartRow -lt 1) { + throw 'The top row can never be less than 1 when we need to retrieve headers from the worksheet.' ; return + } foreach ($C in $Columns) { #allow "False" or "0" to be column headings $Worksheet.Cells[$StartRow, $C] | Where-Object {-not [string]::IsNullOrEmpty($_.Value) } | Select-Object @{N = 'Column'; E = { $C } }, Value } } + catch { + throw "Failed creating property names: $_" ; return + } } - catch { - throw "Failed creating property names: $_" ; return - } - } - foreach ($Path in $Paths) { - if ($path) { - $extension = [System.IO.Path]::GetExtension($Path) - if ($extension -notmatch '.xlsx$|.xlsm$') { - throw "Import-Excel does not support reading this extension type $($extension)" - } - - $resolvedPath = (Resolve-Path $Path -ErrorAction SilentlyContinue) - if ($resolvedPath) { - $Path = $resolvedPath.ProviderPath - } - else { - throw "'$($Path)' file not found" - } - - $stream = New-Object -TypeName System.IO.FileStream -ArgumentList $Path, 'Open', 'Read', 'ReadWrite' - $ExcelPackage = New-Object -TypeName OfficeOpenXml.ExcelPackage - if ($Password) { $ExcelPackage.Load($stream, $Password) } - else { $ExcelPackage.Load($stream) } - } - try { - #Select worksheet - if (-not $WorksheetName) { $Worksheet = $ExcelPackage.Workbook.Worksheets[1] } - elseif (-not ($Worksheet = $ExcelPackage.Workbook.Worksheets[$WorkSheetName])) { - throw "Worksheet '$WorksheetName' not found, the workbook only contains the worksheets '$($ExcelPackage.Workbook.Worksheets)'. If you only wish to select the first worksheet, please remove the '-WorksheetName' parameter." ; return - } - - 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 header-row 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 ($null -ne $cell.Value ) { $colHash[$cell.Start.Column] = 1; $rowHash[$cell.Start.row] = 1 } + foreach ($Path in $Paths) { + if ($path) { + $extension = [System.IO.Path]::GetExtension($Path) + if ($extension -notmatch '.xlsx$|.xlsm$') { + throw "Import-Excel does not support reading this extension type $($extension)" } - $rows = ( $StartRow..$EndRow ).Where( { $rowHash[$_] }) - $columns = ($StartColumn..$EndColumn).Where( { $colHash[$_] }) + + $resolvedPath = (Resolve-Path $Path -ErrorAction SilentlyContinue) + if ($resolvedPath) { + $Path = $resolvedPath.ProviderPath + } + else { + throw "'$($Path)' file not found" + } + + $stream = New-Object -TypeName System.IO.FileStream -ArgumentList $Path, 'Open', 'Read', 'ReadWrite' + $ExcelPackage = New-Object -TypeName OfficeOpenXml.ExcelPackage + if ($Password) { $ExcelPackage.Load($stream, $Password) } + else { $ExcelPackage.Load($stream) } } - 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." } } - elseif ($HeaderName) { $Rows = $StartRow..$EndRow } - 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."; return - } - 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."; return - } - #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 - if ($AsText -or $AsDate) { - <#join items in AsText together with ~~~ . Escape any regex special characters... + try { + #Select worksheet + if (-not $WorksheetName) { $Worksheet = $ExcelPackage.Workbook.Worksheets[1] } + elseif (-not ($Worksheet = $ExcelPackage.Workbook.Worksheets[$WorkSheetName])) { + throw "Worksheet '$WorksheetName' not found, the workbook only contains the worksheets '$($ExcelPackage.Workbook.Worksheets)'. If you only wish to select the first worksheet, please remove the '-WorksheetName' parameter." ; return + } + + #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 header-row 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 ($null -ne $cell.Value ) { $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." } } + elseif ($HeaderName) { $Rows = $StartRow..$EndRow } + else { + $Rows = (1 + $StartRow)..$EndRow + if ($StartRow -eq 1 -and $EndRow -eq 1) { + $Rows = 0 + } + } + + # ; 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."; return + } + 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."; return + } + #endregion + if (-not $Rows) { + Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' contains no data in the rows after top row '$StartRow'" + } + else { + #region Create one object per row + if ($AsText) { + <#join items in AsText together with ~~~ . Escape any regex special characters... # which turns "*" into "\*" make it ".*". Convert ~~~ to $|^ and top and tail with ^%; So if we get "Week", "[Time]" and "*date*" ; make the expression ^week$|^\[Time\]$|^.*Date.*$ $make a regex for this which is case insensitive (option 1) and compiled (option 8) #> - $TextColExpression = '' - if ($AsText) { - $TextColExpression += '(?^' + [regex]::Escape($AsText -join '~~~').replace('\*', '.*').replace('~~~', '$|^') + '$)' + $TextColExpression = "^" + [regex]::Escape($AsText -join "~~~").replace("\*", ".*").replace("~~~", "$|^") + "$" + $TextColRegEx = New-Object -TypeName regex -ArgumentList $TextColExpression , 9 } - if ($AsText -and $AsDate) { - $TextColExpression += "|" - } - if ($AsDate) { - $TextColExpression += '(?^' + [regex]::Escape($AsDate -join '~~~').replace('\*', '.*').replace('~~~', '$|^') + '$)' - } - $TextColRegEx = New-Object -TypeName regex -ArgumentList $TextColExpression , 9 - } - else {$TextColRegEx = $null} - foreach ($R in $Rows) { - #Disabled write-verbose for speed - # Write-Verbose "Import row '$R'" - $NewRow = [Ordered]@{ } - if ($TextColRegEx) { - foreach ($P in $PropertyNames) { - $MatchTest = $TextColRegEx.Match($P.value) - if ($MatchTest.groups.name -eq "astext") { + foreach ($R in $Rows) { + #Disabled write-verbose for speed + # Write-Verbose "Import row '$R'" + $NewRow = [Ordered]@{ } + if ($TextColRegEx) { + foreach ($P in $PropertyNames) { + if ($TextColRegEx.IsMatch($P.Value)) { $NewRow[$P.Value] = $Worksheet.Cells[$R, $P.Column].Text + } + else { $NewRow[$P.Value] = $Worksheet.Cells[$R, $P.Column].Value } } - elseif ($MatchTest.groups.name -eq "asdate" -and $Worksheet.Cells[$R, $P.Column].Value -is [System.ValueType]) { - $NewRow[$P.Value] = [datetime]::FromOADate(($Worksheet.Cells[$R, $P.Column].Value)) + } + else { + 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)'." } - else { $NewRow[$P.Value] = $Worksheet.Cells[$R, $P.Column].Value } } + [PSCustomObject]$NewRow } - else { - 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 } - #endregion } - Write-Debug $sw.Elapsed.TotalMilliseconds - } - catch { throw "Failed importing the Excel workbook '$Path' with worksheet '$Worksheetname': $_"; return } - finally { - if ($Path) { $stream.close(); $ExcelPackage.Dispose() } + catch { throw "Failed importing the Excel workbook '$Path' with worksheet '$Worksheetname': $_"; return } + finally { + if ($Path) { $stream.close(); $ExcelPackage.Dispose() } + } } } - } -} \ No newline at end of file +} diff --git a/__tests__/ImportExcelHeaderName.tests.ps1 b/__tests__/ImportExcelHeaderName.tests.ps1 index 1d83429..17073b4 100644 --- a/__tests__/ImportExcelHeaderName.tests.ps1 +++ b/__tests__/ImportExcelHeaderName.tests.ps1 @@ -1,4 +1,5 @@ $xlfile = "TestDrive:\testImportExcel.xlsx" +$xlfileHeaderOnly = "TestDrive:\testImportExcelHeaderOnly.xlsx" Describe "Import-Excel on a sheet with no headings" { BeforeAll { @@ -18,6 +19,15 @@ Describe "Import-Excel on a sheet with no headings" { Set-ExcelRange -Worksheet $xl.Sheet1 -Range C3 -Value 'I' Close-ExcelPackage $xl + + # crate $xlfileHeaderOnly + $xl = "" | Export-excel $xlfileHeaderOnly -PassThru + + Set-ExcelRange -Worksheet $xl.Sheet1 -Range A1 -Value 'A' + Set-ExcelRange -Worksheet $xl.Sheet1 -Range B1 -Value 'B' + Set-ExcelRange -Worksheet $xl.Sheet1 -Range C1 -Value 'C' + + Close-ExcelPackage $xl } It "Import-Excel should have this shape" { @@ -193,4 +203,25 @@ Describe "Import-Excel on a sheet with no headings" { # $actual[0].City | Should -BeExactly 'Brussels' } + It "Should handle data correctly if there is only a single row" { + $actual = Import-Excel $xlfileHeaderOnly + $names = $actual.psobject.properties.Name + $names | should be $null + $actual.Count | should be 0 + } + + It "Should handle data correctly if there is only a single row and using -NoHeader " { + $actual = @(Import-Excel $xlfileHeaderOnly -WorksheetName Sheet1 -NoHeader) + + $names = $actual[0].psobject.properties.Name + $names.count | should be 3 + $names[0] | should be 'P1' + $names[1] | should be 'P2' + $names[2] | should be 'P3' + + $actual.Count | should be 1 + $actual[0].P1 | should be 'A' + $actual[0].P2 | should be 'B' + $actual[0].P3 | should be 'C' + } } \ No newline at end of file diff --git a/mdHelp/en/New-PivotTableDefinition.md b/mdHelp/en/New-PivotTableDefinition.md index 2fbf56a..ea885c7 100644 --- a/mdHelp/en/New-PivotTableDefinition.md +++ b/mdHelp/en/New-PivotTableDefinition.md @@ -194,7 +194,7 @@ Accept wildcard characters: False ``` ### -GroupDateRow -The name of a row field which should be grouped by parts of the date/time (ignored if GroupDateRow is not specified) +The name of a row field which should be grouped by parts of the date/time (ignored if GroupDatePart is not specified) ```yaml Type: String