Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Examples/Excel/Export/Example-Chart.ps1
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions Examples/Excel/Export/Example-Formulas.ps1
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions Examples/Excel/Export/Example-PivotTable.ps1
Original file line number Diff line number Diff line change
@@ -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
71 changes: 71 additions & 0 deletions Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -74,6 +75,15 @@
[Parameter]
public XLTableTheme Theme { get; set; } = XLTableTheme.None;

[Parameter]
public Hashtable? Formulas { get; set; }

[Parameter]
public PSObject[] PivotTables { get; set; } = Array.Empty<PSObject>();

[Parameter]
public PSObject[] Charts { get; set; } = Array.Empty<PSObject>();

private readonly List<PSObject> _data = new();

protected override void ProcessRecord()
Expand Down Expand Up @@ -235,6 +245,57 @@
Transpose);
}

if (Formulas != null && Formulas.Count > 0)
{
var formulas = new Dictionary<string, string>(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<string, object?> ?? item.Properties.ToDictionary(p => p.Name, p => p.Value, StringComparer.OrdinalIgnoreCase);

Check warning on line 265 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / macOS

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 265 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / Ubuntu

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 265 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / Windows

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 265 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / macOS PowerShell 7

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 265 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / Ubuntu PowerShell 7

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 265 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / Windows PowerShell 7

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.
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<string>? rowFields = null;
if (dict.TryGetValue("RowFields", out var rf) && rf is IEnumerable<object?> rfEnum)
{
rowFields = rfEnum.Select(o => o?.ToString() ?? string.Empty).Where(s => !string.IsNullOrEmpty(s)).ToList();
}

IEnumerable<string>? columnFields = null;
if (dict.TryGetValue("ColumnFields", out var cf) && cf is IEnumerable<object?> cfEnum)
{
columnFields = cfEnum.Select(o => o?.ToString() ?? string.Empty).Where(s => !string.IsNullOrEmpty(s)).ToList();
}

IDictionary<string, XLPivotSummary>? values = null;
if (dict.TryGetValue("Values", out var v) && v is IDictionary vDict)
{
values = new Dictionary<string, XLPivotSummary>(StringComparer.OrdinalIgnoreCase);
foreach (DictionaryEntry entry in vDict)
{
var summary = Enum.TryParse<XLPivotSummary>(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);
Expand All @@ -251,6 +312,16 @@
}

ExcelDocumentService.SaveWorkbook(workbook, FilePath, Show);

if (Charts != null && Charts.Length > 0)
{
foreach (var chart in Charts)
{
var dict = chart.BaseObject as IDictionary<string, object?> ?? chart.Properties.ToDictionary(p => p.Name, p => p.Value, StringComparer.OrdinalIgnoreCase);

Check warning on line 320 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / macOS

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 320 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / Ubuntu

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 320 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / Windows

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 320 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / macOS PowerShell 7

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 320 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / Ubuntu PowerShell 7

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.

Check warning on line 320 in Sources/PSWriteOffice/Cmdlets/Excel/ExportOfficeExcelCommand.cs

View workflow job for this annotation

GitHub Actions / Windows PowerShell 7

Nullability of reference types in value of type 'Dictionary<string, object>' doesn't match target type 'IDictionary<string, object?>'.
var title = dict.TryGetValue("Title", out var t) ? t?.ToString() ?? string.Empty : string.Empty;
ExcelDocumentService.AddChart(FilePath, WorksheetName, title);
}
}
}
catch (Exception ex)
{
Expand Down
56 changes: 56 additions & 0 deletions Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Chart.cs
Original file line number Diff line number Diff line change
@@ -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<Sheet>().FirstOrDefault(s => s.Name == worksheetName);
if (sheet == null)
{
return;
}

var worksheetPart = (WorksheetPart)workbookPart.GetPartById(sheet.Id!);
var drawingsPart = worksheetPart.DrawingsPart ?? worksheetPart.AddNewPart<DrawingsPart>();
if (!worksheetPart.Worksheet.Elements<Drawing>().Any())
{
worksheetPart.Worksheet.Append(new Drawing { Id = worksheetPart.GetIdOfPart(drawingsPart) });
worksheetPart.Worksheet.Save();
}

var chartPart = drawingsPart.AddNewPart<ChartPart>();
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();
}
}
Original file line number Diff line number Diff line change
@@ -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<string, string> formulas)
{
foreach (var kvp in formulas)
{
worksheet.Cell(kvp.Key).FormulaA1 = kvp.Value;
}
}
}
45 changes: 45 additions & 0 deletions Sources/PSWriteOffice/Services/Excel/ExcelDocumentService.Pivot.cs
Original file line number Diff line number Diff line change
@@ -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<string>? rowLabels,
IEnumerable<string>? columnLabels,
IDictionary<string, XLPivotSummary>? 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;
}
}
33 changes: 33 additions & 0 deletions Tests/ExportOfficeExcel.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Loading