Add-Subtotals.ps1

Function Add-Subtotals {
    param(
        [Parameter(Mandatory=$true, Position=0)]
        $ChangeColumnName ,              # = "Location"

        [Parameter(Mandatory=$true, Position=1)]
        [hashtable]$AggregateColumn  ,  #= @{"Sales" = "SUM" }

        [Parameter(Position=2)]
        $ExcelPath           = ([System.IO.Path]::GetTempFileName() -replace "\.tmp", ".xlsx")         ,

        [Parameter(Position=3)]
        $WorksheetName      = "Sheet1",

        [Parameter(ValueFromPipeline=$true)]
        $InputObject,  #$DataToPivot | Sort location, product

        [switch]$HideSingleRows,
        [switch]$NoSort,
        [switch]$NoOutLine,
        [switch]$Show

    )
    begin {
        if (-not $PSBoundParameters.ContainsKey('ExcelPath')) {$Show = $true}
        $data               = @()
        $aggFunctions       = [ordered]@{
                "AVERAGE"   = 1; "COUNT"     = 2;  "COUNTA"    = 3  #(non empty cells) f
                "MAX"       = 4; "MIN"       = 5;  "PRODUCT"   = 6; "STDEV"     = 7 # (sample)
                "STDEVP"    = 8 # (whole population);
                "SUM"       = 9;  "VAR"       = 10 # (Variance sample)
                "VARP"      = 11 # (whole population) #add 100 to ignore hidden cells
        }
    }
    process {
        $data               += $InputObject
    }
    end {
        if (-not $NoSort)  {$data = $data | Sort-Object  $changeColumnName}
        $Header             = $data[0].PSObject.Properties.Name
        #region turn each entry in $AggregateColumn "=SUBTOTAL(a,x{0}}:x{1})" where a is the aggregate function number and x is the column letter
        $aggFormulas        = @{}
        foreach ($k in $AggregateColumn.Keys)  {
            $columnNo       = 0 ;
            while ($columnNo -lt $header.count  -and $header[$columnNo] -ne $k) {$columnNo ++}
            if    ($columnNo -eq $header.count) {
                    throw "'$k' isn't a property of the first row of data."; return
            }
            if ($AggregateColumn[$k] -is [string]) {
                $aggfn      = $aggFunctions[$AggregateColumn[$k]]
                if (-not $aggfn) {
                    throw   "$($AggregateColumn[$k]) is not a valid aggregation function - these are $($aggFunctions.keys -join ', ')" ; return
                }
            }
            else {$aggfn = $AggregateColumn[$k]}
            $aggFormulas[$k] =  "=SUBTOTAL({0},{1}{{0}}:{1}{{1}})"  -f $aggfn , (Get-ExcelColumnName ($columnNo+1) ).ColumnName
        }
        if ($aggformulas.count -lt 1) {throw "We didn't get any aggregation formulas"}
        $aggFormulas | out-string -Stream | Write-Verbose -Verbose
        #endregion
        $insertedRows       = @()
        $singleRows         = @()
        $previousValue      = $data[0].$changeColumnName
        $currentRow         = $lastChangeRow  = 2
        #region insert subtotals and send to excel:
        #each time there is a change in the column we're intetersted in.
        #either Add a row with the value and subtotal(s) function(s) if there is more than one row to total
        #or note the row if there was only one row with that value (we may hide it later.)
        $excel              = $data |
            ForEach-Object -process {
                if ($_.$changeColumnName -ne $previousValue) {
                    if ($lastChangeRow -lt ($currentrow - 1)) {
                        $NewObj = @{$changeColumnName = $previousValue}
                        foreach    ($k in $aggFormulas.Keys) {
                            $newobj[$k] = $aggformulas[$k] -f  $lastChangeRow,  ($currentRow - 1)
                        }
                        $insertedRows  += $currentRow
                        [pscustomobject]$newobj
                        $currentRow    += 1
                    }
                    else {$singleRows  += $currentRow  }
                    $lastChangeRow      = $currentRow
                    $previousValue      = $_.$changeColumnName
                }
                $_
                $currentRow += 1
                } -end { # the process block won't output the last row
                    if ($lastChangeRow -lt ($currentrow - 1)) {
                        $NewObj = @{$changeColumnName = $previousValue}
                        foreach    ($k in $aggFormulas.Keys) {
                            $newobj[$k] = $aggformulas[$k] -f  $lastChangeRow,  ($currentRow - 1)
                        }
                        $insertedRows  += $currentRow
                        [pscustomobject]$newobj
                    }
                    else {$singleRows  += $currentRow  }
            }  |   Export-Excel  -Path $ExcelPath  -PassThru  -AutoSize -AutoFilter -AutoNameRange -BoldTopRow -WorksheetName $WorksheetName -Activate -ClearSheet #-MaxAutoSizeRows 10000
        #endregion
        #Put the subtotal rows in bold optionally hide rows where only one has the value of interest.
        $ws                 = $excel.$WorksheetName
        #We kept lists of the total rows Since 1 rows won't get expand/collapse we can hide them.
        foreach ($r in $insertedrows)   {$ws.Row($r).style.font.bold = $true }
        if ($HideSingleRows)  {
            foreach ($r in $hideRows)   { $ws.Row($r).hidden = $true}
        }
        $range                 = $ws.Dimension.Address
        $ExcelPath             = $excel.File.FullName
        $SheetIndex            = $ws.index
        if ($NoOutline)  {
            Close-ExcelPackage $excel -show:$Show
            return
        }
        else {
            Close-ExcelPackage $excel

            try   { $excelApp       = New-Object -ComObject "Excel.Application" }
            catch { Write-Warning "Could not start Excel application - which usually means it is not installed."  ; return }

            try   { $excelWorkBook  = $excelApp.Workbooks.Open($ExcelPath) }
            catch { Write-Warning -Message "Could not Open $ExcelPath."  ; return }
            $ws   = $excelWorkBook.Worksheets.item($SheetIndex)
            $null = $ws.Range($range).Select()
            $null = $excelapp.ActiveCell.AutoOutline()
            $null = $ws.Outline.ShowLevels(1,$null)
            $excelWorkBook.Save()
            if ($show) {$excelApp.Visible = $true}
            else       {
                        [void]$excelWorkBook.close()
                        $excelapp.Quit()
            }
        }
    }
}