Merge remote-tracking branch 'upstream/master'

This commit is contained in:
jhoneill
2020-03-16 11:43:10 +00:00
8 changed files with 251 additions and 154 deletions

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,8 +1,8 @@
function Import-Excel { function Import-Excel {
[CmdLetBinding()] [CmdLetBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectUsageOfAssignmentOperator', '', Justification = 'Intentional')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectUsageOfAssignmentOperator', '', Justification = 'Intentional')]
param ( param (
[Alias('FullName')] [Alias('FullName')]
[Parameter(ParameterSetName = "PathA", Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0 )] [Parameter(ParameterSetName = "PathA", Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0 )]
[Parameter(ParameterSetName = "PathB", Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0 )] [Parameter(ParameterSetName = "PathB", Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0 )]
@@ -37,182 +37,171 @@
[ValidateNotNullOrEmpty()] [ValidateNotNullOrEmpty()]
[String]$Password [String]$Password
) )
end { end {
$sw = [System.Diagnostics.Stopwatch]::StartNew() $sw = [System.Diagnostics.Stopwatch]::StartNew()
if ($input) { if ($input) {
$Paths = $input $Paths = $input
} }
elseif ($Path) { elseif ($Path) {
$Paths = $Path $Paths = $Path
} }
else { else {
$Paths = '' $Paths = ''
} }
function Get-PropertyNames { function Get-PropertyNames {
<# <#
.SYNOPSIS .SYNOPSIS
Create objects containing the column number and the column name for each of the different header types. 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")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = "Name would be incorrect, and command is not exported")]
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[Int[]]$Columns, [Int[]]$Columns,
[Parameter(Mandatory)] [Parameter(Mandatory)]
[Int]$StartRow [Int]$StartRow
) )
try { try {
if ($HeaderName) { if ($HeaderName) {
$i = 0 $i = 0
foreach ($H in $HeaderName) { foreach ($H in $HeaderName) {
$H | Select-Object @{N = 'Column'; E = { $Columns[$i] } }, @{N = 'Value'; E = { $H } } $H | Select-Object @{N = 'Column'; E = { $Columns[$i] } }, @{N = 'Value'; E = { $H } }
$i++ $i++
}
} }
} elseif ($NoHeader) {
elseif ($NoHeader) { $i = 0
$i = 0 foreach ($C in $Columns) {
foreach ($C in $Columns) { $i++
$i++ $C | Select-Object @{N = 'Column'; E = { $_ } }, @{N = 'Value'; E = { 'P' + $i } }
$C | Select-Object @{N = 'Column'; E = { $_ } }, @{N = 'Value'; E = { 'P' + $i } } }
} }
}
else { else {
if ($StartRow -lt 1) { if ($StartRow -lt 1) {
throw 'The top row can never be less than 1 when we need to retrieve headers from the worksheet.' ; return throw 'The top row can never be less than 1 when we need to retrieve headers from the worksheet.' ; return
} }
foreach ($C in $Columns) { foreach ($C in $Columns) {
#allow "False" or "0" to be column headings #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 $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 { foreach ($Path in $Paths) {
throw "Failed creating property names: $_" ; return if ($path) {
} $extension = [System.IO.Path]::GetExtension($Path)
} if ($extension -notmatch '.xlsx$|.xlsm$') {
foreach ($Path in $Paths) { throw "Import-Excel does not support reading this extension type $($extension)"
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 }
} }
$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 { try {
$Columns = $StartColumn .. $EndColumn ; if ($StartColumn -gt $EndColumn) { Write-Warning -Message "Selecting columns $StartColumn to $EndColumn might give odd results." } #Select worksheet
if ($NoHeader) { $Rows = $StartRow..$EndRow ; if ($StartRow -gt $EndRow) { Write-Warning -Message "Selecting rows $StartRow to $EndRow might give odd results." } } if (-not $WorksheetName) { $Worksheet = $ExcelPackage.Workbook.Worksheets[1] }
elseif ($HeaderName) { $Rows = $StartRow..$EndRow } elseif (-not ($Worksheet = $ExcelPackage.Workbook.Worksheets[$WorkSheetName])) {
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." } } 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
} }
#endregion
#region Create property names #region Get rows and columns
if ((-not $Columns) -or (-not ($PropertyNames = Get-PropertyNames -Columns $Columns -StartRow $StartRow))) { #If we are doing dataonly it is quicker to work out which rows to ignore before processing the cells.
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 (-not $EndRow ) { $EndRow = $Worksheet.Dimension.End.Row }
} if (-not $EndColumn) { $EndColumn = $Worksheet.Dimension.End.Column }
if ($Duplicates = $PropertyNames | Group-Object Value | Where-Object Count -GE 2) { $endAddress = [OfficeOpenXml.ExcelAddress]::TranslateFromR1C1("R[$EndRow]C[$EndColumn]", 0, 0)
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 if ($DataOnly) {
} #If we are using headers startrow will be the header-row so examine data from startRow + 1,
#endregion if ($NoHeader) { $range = "A" + ($StartRow ) + ":" + $endAddress }
Write-Debug $sw.Elapsed.TotalMilliseconds else { $range = "A" + ($StartRow + 1 ) + ":" + $endAddress }
if (-not $Rows) { #We're going to look at every cell and build 2 hash tables holding rows & columns which contain data.
Write-Warning "Worksheet '$WorksheetName' in workbook '$Path' contains no data in the rows after top row '$StartRow'" #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,
else { #using Hash tables: "we've seen it" is all we need, no need to worry about "seen it before" / "Seen it many times".
#region Create one object per row $colHash = @{ }
if ($AsText -or $AsDate) { $rowHash = @{ }
<#join items in AsText together with ~~~ . Escape any regex special characters... 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 ^%; # 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.*$ 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) $make a regex for this which is case insensitive (option 1) and compiled (option 8)
#> #>
$TextColExpression = '' $TextColExpression = "^" + [regex]::Escape($AsText -join "~~~").replace("\*", ".*").replace("~~~", "$|^") + "$"
if ($AsText) { $TextColRegEx = New-Object -TypeName regex -ArgumentList $TextColExpression , 9
$TextColExpression += '(?<astext>^' + [regex]::Escape($AsText -join '~~~').replace('\*', '.*').replace('~~~', '$|^') + '$)'
} }
if ($AsText -and $AsDate) { foreach ($R in $Rows) {
$TextColExpression += "|" #Disabled write-verbose for speed
} # Write-Verbose "Import row '$R'"
if ($AsDate) { $NewRow = [Ordered]@{ }
$TextColExpression += '(?<asDate>^' + [regex]::Escape($AsDate -join '~~~').replace('\*', '.*').replace('~~~', '$|^') + '$)' if ($TextColRegEx) {
} foreach ($P in $PropertyNames) {
$TextColRegEx = New-Object -TypeName regex -ArgumentList $TextColExpression , 9 if ($TextColRegEx.IsMatch($P.Value)) {
}
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") {
$NewRow[$P.Value] = $Worksheet.Cells[$R, $P.Column].Text $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 { #endregion
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': $_"; return }
} finally {
catch { throw "Failed importing the Excel workbook '$Path' with worksheet '$Worksheetname': $_"; return } if ($Path) { $stream.close(); $ExcelPackage.Dispose() }
finally { }
if ($Path) { $stream.close(); $ExcelPackage.Dispose() }
} }
} }
}
} }

View File

@@ -1,4 +1,5 @@
$xlfile = "TestDrive:\testImportExcel.xlsx" $xlfile = "TestDrive:\testImportExcel.xlsx"
$xlfileHeaderOnly = "TestDrive:\testImportExcelHeaderOnly.xlsx"
Describe "Import-Excel on a sheet with no headings" { Describe "Import-Excel on a sheet with no headings" {
BeforeAll { BeforeAll {
@@ -18,6 +19,15 @@ Describe "Import-Excel on a sheet with no headings" {
Set-ExcelRange -Worksheet $xl.Sheet1 -Range C3 -Value 'I' Set-ExcelRange -Worksheet $xl.Sheet1 -Range C3 -Value 'I'
Close-ExcelPackage $xl 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" { 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' # $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'
}
} }

View File

@@ -194,7 +194,7 @@ Accept wildcard characters: False
``` ```
### -GroupDateRow ### -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 ```yaml
Type: String Type: String