diff --git a/Examples/Excel/Export/Example-Chart.ps1 b/Examples/Excel/Export/Example-Chart.ps1 new file mode 100644 index 0000000..ba2351c --- /dev/null +++ b/Examples/Excel/Export/Example-Chart.ps1 @@ -0,0 +1,4 @@ +$data = 1..5 | ForEach-Object { [PSCustomObject]@{ Value = $_ } } +$chart = @{ Title = 'Chart1'; Range = 'A1:B6' } +$path = Join-Path $PSScriptRoot 'Chart.xlsx' +$data | Export-OfficeExcel -FilePath $path -WorksheetName 'Data' -Charts $chart -Show diff --git a/Examples/Excel/Export/Example-Formulas.ps1 b/Examples/Excel/Export/Example-Formulas.ps1 new file mode 100644 index 0000000..7f2a9df --- /dev/null +++ b/Examples/Excel/Export/Example-Formulas.ps1 @@ -0,0 +1,7 @@ +$data = @( + [PSCustomObject]@{ A = 1; B = 2 } + [PSCustomObject]@{ A = 3; B = 4 } +) +$formulas = @{ 'C2' = '=A2+B2'; 'C3' = '=A3+B3' } +$path = Join-Path $PSScriptRoot 'Formulas.xlsx' +$data | Export-OfficeExcel -FilePath $path -WorksheetName 'Data' -Formulas $formulas -Show diff --git a/Examples/Excel/Export/Example-PivotTable.ps1 b/Examples/Excel/Export/Example-PivotTable.ps1 new file mode 100644 index 0000000..42af612 --- /dev/null +++ b/Examples/Excel/Export/Example-PivotTable.ps1 @@ -0,0 +1,8 @@ +$data = @( + [PSCustomObject]@{ Category = 'A'; Value = 1 } + [PSCustomObject]@{ Category = 'A'; Value = 2 } + [PSCustomObject]@{ Category = 'B'; Value = 3 } +) +$pivot = @{ Name = 'Pivot1'; SourceRange = 'A1:B4'; TargetCell = 'D2'; RowFields = @('Category'); Values = @{ Value = 'Sum' } } +$path = Join-Path $PSScriptRoot 'Pivot.xlsx' +$data | Export-OfficeExcel -FilePath $path -WorksheetName 'Data' -PivotTables $pivot -Show diff --git a/Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs b/Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs index b7291a6..1c86fcb 100644 --- a/Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs +++ b/Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -74,6 +75,15 @@ public class ExportOfficeExcelCommand : PSCmdlet [Parameter] public XLTableTheme Theme { get; set; } = XLTableTheme.None; + [Parameter] + public Hashtable? Formulas { get; set; } + + [Parameter] + public PSObject[] PivotTables { get; set; } = Array.Empty(); + + [Parameter] + public PSObject[] Charts { get; set; } = Array.Empty(); + private readonly List _data = new(); protected override void ProcessRecord() @@ -235,6 +245,57 @@ protected override void EndProcessing() Transpose); } + if (Formulas != null && Formulas.Count > 0) + { + var formulas = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (DictionaryEntry entry in Formulas) + { + if (entry.Key is string key && entry.Value != null) + { + formulas[key] = entry.Value.ToString()!; + } + } + ExcelDocumentService.ApplyFormulas(worksheet, formulas); + } + + if (PivotTables != null && PivotTables.Length > 0) + { + foreach (var item in PivotTables) + { + var dict = item.BaseObject as IDictionary ?? item.Properties.ToDictionary(p => p.Name, p => p.Value, StringComparer.OrdinalIgnoreCase); + var name = dict.TryGetValue("Name", out var n) ? n?.ToString() ?? "PivotTable1" : "PivotTable1"; + var sourceRange = dict.TryGetValue("SourceRange", out var sr) ? sr?.ToString() ?? string.Empty : string.Empty; + var targetCell = dict.TryGetValue("TargetCell", out var tc) ? tc?.ToString() ?? "A1" : "A1"; + + IEnumerable? rowFields = null; + if (dict.TryGetValue("RowFields", out var rf) && rf is IEnumerable rfEnum) + { + rowFields = rfEnum.Select(o => o?.ToString() ?? string.Empty).Where(s => !string.IsNullOrEmpty(s)).ToList(); + } + + IEnumerable? columnFields = null; + if (dict.TryGetValue("ColumnFields", out var cf) && cf is IEnumerable cfEnum) + { + columnFields = cfEnum.Select(o => o?.ToString() ?? string.Empty).Where(s => !string.IsNullOrEmpty(s)).ToList(); + } + + IDictionary? values = null; + if (dict.TryGetValue("Values", out var v) && v is IDictionary vDict) + { + values = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (DictionaryEntry entry in vDict) + { + var summary = Enum.TryParse(entry.Value?.ToString(), true, out var res) + ? res + : XLPivotSummary.Sum; + values[entry.Key.ToString()!] = summary; + } + } + + ExcelDocumentService.AddPivotTable(worksheet, name, sourceRange, targetCell, rowFields, columnFields, values); + } + } + if (AutoSize) { ExcelDocumentService.AutoSizeColumns(worksheet); @@ -251,6 +312,16 @@ protected override void EndProcessing() } ExcelDocumentService.SaveWorkbook(workbook, FilePath, Show); + + if (Charts != null && Charts.Length > 0) + { + foreach (var chart in Charts) + { + var dict = chart.BaseObject as IDictionary ?? chart.Properties.ToDictionary(p => p.Name, p => p.Value, StringComparer.OrdinalIgnoreCase); + var title = dict.TryGetValue("Title", out var t) ? t?.ToString() ?? string.Empty : string.Empty; + ExcelDocumentService.AddChart(FilePath, WorksheetName, title); + } + } } catch (Exception ex) { diff --git a/Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Chart.cs b/Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Chart.cs new file mode 100644 index 0000000..3bbca74 --- /dev/null +++ b/Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Chart.cs @@ -0,0 +1,56 @@ +using System.Linq; +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Spreadsheet; +using A = DocumentFormat.OpenXml.Drawing; +using C = DocumentFormat.OpenXml.Drawing.Charts; + +namespace PSWriteOffice.Services.Excel; + +public static partial class ExcelDocumentService +{ + public static void AddChart(string filePath, string worksheetName, string chartTitle) + { + using var document = SpreadsheetDocument.Open(filePath, true); + var workbookPart = document.WorkbookPart; + if (workbookPart == null) + { + return; + } + + var sheet = workbookPart.Workbook.Sheets?.Elements().FirstOrDefault(s => s.Name == worksheetName); + if (sheet == null) + { + return; + } + + var worksheetPart = (WorksheetPart)workbookPart.GetPartById(sheet.Id!); + var drawingsPart = worksheetPart.DrawingsPart ?? worksheetPart.AddNewPart(); + if (!worksheetPart.Worksheet.Elements().Any()) + { + worksheetPart.Worksheet.Append(new Drawing { Id = worksheetPart.GetIdOfPart(drawingsPart) }); + worksheetPart.Worksheet.Save(); + } + + var chartPart = drawingsPart.AddNewPart(); + var chartSpace = new C.ChartSpace(); + chartSpace.Append(new C.EditingLanguage { Val = "en-US" }); + var chart = chartSpace.AppendChild(new C.Chart()); + + if (!string.IsNullOrEmpty(chartTitle)) + { + var title = new C.Title + { + ChartText = new C.ChartText + { + RichText = new C.RichText(new A.Paragraph(new A.Run(new A.Text(chartTitle)))) + } + }; + chart.AppendChild(title); + } + + chart.AppendChild(new C.PlotArea(new C.Layout())); + chartPart.ChartSpace = chartSpace; + chartPart.ChartSpace.Save(); + } +} diff --git a/Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Formula.cs b/Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Formula.cs new file mode 100644 index 0000000..62b6f15 --- /dev/null +++ b/Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Formula.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using ClosedXML.Excel; + +namespace PSWriteOffice.Services.Excel; + +public static partial class ExcelDocumentService +{ + public static void ApplyFormulas(IXLWorksheet worksheet, IDictionary formulas) + { + foreach (var kvp in formulas) + { + worksheet.Cell(kvp.Key).FormulaA1 = kvp.Value; + } + } +} diff --git a/Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Pivot.cs b/Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Pivot.cs new file mode 100644 index 0000000..c04585d --- /dev/null +++ b/Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Pivot.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using ClosedXML.Excel; + +namespace PSWriteOffice.Services.Excel; + +public static partial class ExcelDocumentService +{ + public static IXLPivotTable AddPivotTable( + IXLWorksheet worksheet, + string name, + string sourceRange, + string targetCell, + IEnumerable? rowLabels, + IEnumerable? columnLabels, + IDictionary? values) + { + var pivot = worksheet.PivotTables.Add(name, worksheet.Cell(targetCell), worksheet.Range(sourceRange)); + + if (rowLabels != null) + { + foreach (var label in rowLabels) + { + pivot.RowLabels.Add(label); + } + } + + if (columnLabels != null) + { + foreach (var label in columnLabels) + { + pivot.ColumnLabels.Add(label); + } + } + + if (values != null) + { + foreach (var kvp in values) + { + pivot.Values.Add(kvp.Key).SetSummaryFormula(kvp.Value); + } + } + + return pivot; + } +} diff --git a/Tests/ExportOfficeExcel.Tests.ps1 b/Tests/ExportOfficeExcel.Tests.ps1 index 096b1f0..603edd5 100644 --- a/Tests/ExportOfficeExcel.Tests.ps1 +++ b/Tests/ExportOfficeExcel.Tests.ps1 @@ -53,4 +53,37 @@ Describe 'Export-OfficeExcel cmdlet' { $data | Export-OfficeExcel -FilePath $path Test-Path $path | Should -BeTrue } + + It 'creates a pivot table when definition is provided' { + $path = Join-Path $TestDrive 'pivot.xlsx' + $data = @( + [PSCustomObject]@{ Category = 'A'; Value = 1 } + [PSCustomObject]@{ Category = 'A'; Value = 2 } + [PSCustomObject]@{ Category = 'B'; Value = 3 } + ) + + $pivot = @{ Name = 'Pivot1'; SourceRange = 'A1:B4'; TargetCell = 'D2'; RowFields = @('Category'); Values = @{ Value = 'Sum' } } + $data | Export-OfficeExcel -FilePath $path -WorksheetName 'Data' -PivotTables $pivot + + $dll = Join-Path $PSScriptRoot '..' 'Sources' 'PSWriteOffice' 'bin' 'Debug' 'net8.0' 'ClosedXML.dll' + Add-Type -Path $dll + $wb = [ClosedXML.Excel.XLWorkbook]::new($path) + $ws = $wb.Worksheet('Data') + $ws.PivotTables.Count | Should -BeGreaterThan 0 + } + + It 'adds a chart when chart specification is provided' { + $path = Join-Path $TestDrive 'chart.xlsx' + $data = 1..3 | ForEach-Object { [PSCustomObject]@{ Value = $_ } } + $chart = @{ Title = 'Chart1'; Range = 'A1:B3' } + $data | Export-OfficeExcel -FilePath $path -WorksheetName 'Data' -Charts $chart + + $openXml = Join-Path $PSScriptRoot '..' 'Sources' 'PSWriteOffice' 'bin' 'Debug' 'net8.0' 'DocumentFormat.OpenXml.dll' + Add-Type -Path $openXml + $doc = [DocumentFormat.OpenXml.Packaging.SpreadsheetDocument]::Open($path, $false) + $sheet = $doc.WorkbookPart.Workbook.Sheets.ChildElements | Where-Object { $_.Name -eq 'Data' } + $wsPart = $doc.WorkbookPart.GetPartById($sheet.Id) + $wsPart.DrawingsPart.ChartParts.Count | Should -BeGreaterThan 0 + $doc.Close() + } }