GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
ExcelHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Linq;
20using ClosedXML.Excel;
21
22namespace GenerallyPositive
23{
24 internal class ExcelHelper
25 {
26 // Existing methods unchanged
27 internal static void TokenizeExcelFile(GPALFile gPALFile)
28 {
29 foreach (string filename in gPALFile.Filenames)
30 {
31 try
32 {
33 IGPALGrid<string> data = GPAL.Grid.ToGPALObject();
34
35 using (var workbook = new XLWorkbook(filename))
36 {
37 var worksheet = workbook.Worksheet(1);
38
39 foreach (var row in worksheet.RowsUsed())
40 {
41 var rowData = new List<string>();
42 foreach (var cell in row.CellsUsed())
43 {
44 rowData.Add(cell.GetString());
45 }
46 data.AddRow(rowData);
47 }
48 ((IGPALFileInternal)gPALFile).FileGrid.Add(data);
49 }
50 }
51 catch (Exception ex)
52 {
53 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Unable to tokenize file [{filename}]. Continuing.", gPALFile, Enums.GPALObjectType.GPALFile, ex);
54 }
55 }
56 }
57
58 internal static IGPALGrid<string> TokenizeExcelFile(string filename)
59 {
60 IGPALGrid<string> data = GPAL.Grid.ToGPALObject();
61
62 try
63 {
64 using (var workbook = new XLWorkbook(filename))
65 {
66 foreach (IXLWorksheet worksheet in workbook.Worksheets)
67 {
68 var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 0;
69 var lastCol = worksheet.LastColumnUsed()?.ColumnNumber() ?? 0;
70
71 for (int r = 1; r <= lastRow; r++)
72 {
73 var rowData = new List<string>();
74 var row = worksheet.Row(r);
75
76 for (int c = 1; c <= lastCol; c++)
77 {
78 rowData.Add(row.Cell(c).GetString());
79 }
80
81 data.AddRow(rowData);
82 }
83 }
84 }
85 }
86 catch (Exception ex)
87 {
88 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION,
89 $"Unable to tokenize file [{filename}]. Continuing.", null,
90 Enums.GPALObjectType.None, ex);
91 }
92
93 return data;
94 }
95
96
102 internal static void CopyWorksheet(IXLWorksheet src, IXLWorksheet dest)
103 {
104 try
105 {
106 // Clear the destination worksheet to avoid overlapping data
107 dest.Clear();
108
109 // Get all used cells from the source worksheet
110 foreach (var row in src.RowsUsed())
111 {
112 int rowIndex = row.RowNumber();
113 foreach (var cell in row.CellsUsed())
114 {
115 int colIndex = cell.Address.ColumnNumber;
116 // Copy the cell value to the same position in the destination worksheet
117 dest.Cell(rowIndex, colIndex).Value = cell.GetString();
118 }
119 }
120 }
121 catch (Exception ex)
122 {
123 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to copy worksheet", null, Enums.GPALObjectType.None, ex);
124 }
125 }
132 internal static void WriteRange(IXLWorksheet worksheet, string range, IGPALGrid<string> data)
133 {
134 try
135 {
136 var rangeObj = worksheet.Range(range);
137 var firstCell = rangeObj.FirstCell();
138
139 int startRow = firstCell.Address.RowNumber;
140 int startColumn = firstCell.Address.ColumnNumber;
141
142 for (int rowIndex = 0; rowIndex < data.Count(); rowIndex++)
143 {
144 var row = data[rowIndex];
145 for (int colIndex = 0; colIndex < row.Count; colIndex++)
146 {
147 worksheet.Cell(startRow + rowIndex, startColumn + colIndex).Value = row[colIndex];
148 }
149 }
150 }
151 catch (Exception ex)
152 {
153 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to write range [{range}] in worksheet", null, Enums.GPALObjectType.None, ex);
154 }
155 }
156
157 // New concrete class for comparison results
158 public class CellDifference
159 {
160 public string CellAddress { get; set; }
161 public string OldValue { get; set; }
162 public string NewValue { get; set; }
163 public string SourceRange { get; set; } // New: Tracks the source range
164 public string TargetRange { get; set; } // New: Tracks the target range
165 }
166 // New methods using XLWorkbook and concrete objects
167
174 internal static List<CellDifference> CompareRanges(IGPALGrid<string> data1, IGPALGrid<string> data2)
175 {
176 var differences = new List<CellDifference>();
177
178 try
179 {
180 int maxRows = Math.Max(data1.Count(), data2.Count());
181 int maxCols = Math.Max(data1.Any() ? data1[0].Count : 0, data2.Any() ? data2[0].Count : 0);
182
183 for (int i = 0; i < maxRows; i++)
184 {
185 for (int j = 0; j < maxCols; j++)
186 {
187 string value1 = i < data1.Count() && j < data1[i].Count ? data1[i][j] : null;
188 string value2 = i < data2.Count() && j < data2[i].Count ? data2[i][j] : null;
189
190 if (value1 != value2)
191 {
192 string cellAddress = $"{(char)('A' + j)}{i + 1}";
193 differences.Add(new CellDifference
194 {
195 CellAddress = cellAddress,
196 OldValue = value1 ?? "",
197 NewValue = value2 ?? ""
198 });
199 }
200 }
201 }
202 }
203 catch (Exception ex)
204 {
205 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to compare ranges", null, Enums.GPALObjectType.None, ex);
206 }
207
208 return differences;
209 }
210
211
212 internal static IGPALGrid<string> ReadRange(IXLWorksheet worksheet, string range)
213 {
214 IGPALGrid<string> data = GPAL.Grid.ToGPALObject();
215
216 try
217 {
218 var xlRange = worksheet.Range(range);
219
220 foreach (var row in xlRange.RowsUsed())
221 {
222 var rowData = new List<string>();
223 foreach (var cell in row.CellsUsed())
224 {
225 rowData.Add(cell.GetString());
226 }
227 data.AddRow(rowData);
228 }
229 }
230 catch (Exception ex)
231 {
232 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to extract range [{range}]", null, Enums.GPALObjectType.None, ex);
233 }
234
235 return data;
236 }
237
238
239
240 internal static List<string> GetColumnValues(IXLWorksheet worksheet, string column)
241 {
242 var values = new List<string>();
243
244 try
245 {
246 var col = worksheet.Column(column);
247 foreach (var cell in col.CellsUsed())
248 {
249 values.Add(cell.GetString());
250 }
251 }
252 catch (Exception ex)
253 {
254 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to get values for column [{column}]", null, Enums.GPALObjectType.None, ex);
255 }
256
257 return values;
258 }
259
260 internal static List<string> GetRowValues(IXLWorksheet worksheet, int rowNumber)
261 {
262 var values = new List<string>();
263
264 try
265 {
266 var row = worksheet.Row(rowNumber);
267 foreach (var cell in row.CellsUsed())
268 {
269 values.Add(cell.GetString());
270 }
271 }
272 catch (Exception ex)
273 {
274 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to get values for row [{rowNumber}]", null, Enums.GPALObjectType.None, ex);
275 }
276
277 return values;
278 }
279
289 internal static List<(string Address, int Row, int Column)> SearchValue(IXLWorksheet worksheet, string range, string searchValue)
290 {
291 List<(string Address, int Row, int Column)> retVal = new List<(string Address, int Row, int Column)>();
292
293 try
294 {
295 foreach (var cell in worksheet.Range(range).CellsUsed())
296 if (cell.GetString() == searchValue)
297 retVal.Add((cell.Address.ToString(), cell.Address.RowNumber, cell.Address.ColumnNumber));
298 }
299 catch (Exception ex)
300 {
301 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to search value in range [{range}]", null, Enums.GPALObjectType.None, ex);
302 }
303
304 return retVal;
305 }
306
307 internal static void SearchAndReplaceByValue(IXLWorksheet worksheet, string range, string searchValue, string replaceValue)
308 {
309 try
310 {
311 var xlRange = worksheet.Range(range);
312 foreach (var cell in xlRange.CellsUsed())
313 {
314 if (cell.GetString() == searchValue)
315 {
316 cell.Value = replaceValue;
317 }
318 }
319 }
320 catch (Exception ex)
321 {
322 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to search and replace in range [{range}]", null, Enums.GPALObjectType.None, ex);
323 }
324 }
325
326
327
328
329
330
331
341 public static void WriteXlsxFromTokenizedData(
342 string outputPath,
343 IGPALGrid<string> grid,
344 List<int> rowsPerSheet,
345 IGPALGrid<string> columnNamesPerSheet,
346 List<string> sheetNames,
347 bool writeHeaders = true)
348 {
349 if (grid == null) throw new ArgumentNullException(nameof(grid));
350 if (rowsPerSheet == null) throw new ArgumentNullException(nameof(rowsPerSheet));
351 if (columnNamesPerSheet == null) throw new ArgumentNullException(nameof(columnNamesPerSheet));
352
353 using var workbook = new XLWorkbook();
354
355 // a grid that never came from a workbook carries no sheet layout, so the whole of it is one sheet
356 // and its first row names the columns. without this there is no sheet to write and saving throws
357 if (0 == rowsPerSheet.Count && 0 < grid.Rows)
358 {
359 rowsPerSheet = new List<int> { grid.Rows };
360
361 if (0 == columnNamesPerSheet.Rows)
362 columnNamesPerSheet.AddRow(new List<string>(grid[0]));
363 }
364
365 int currentRowInGrid = 0;
366
367 for (int sheetIdx = 0; sheetIdx < rowsPerSheet.Count; sheetIdx++)
368 {
369 int totalRowsThisSheet = rowsPerSheet[sheetIdx];
370 if (totalRowsThisSheet == 0) continue;
371
372 var columnNames = columnNamesPerSheet[sheetIdx];
373
374 string sheetName = sheetNames != null && sheetIdx < sheetNames.Count
375 ? sheetNames[sheetIdx]
376 : $"Sheet{sheetIdx + 1}";
377
378 var worksheet = workbook.Worksheets.Add(sheetName);
379
380 int destRow = 1;
381
382 // Header
383 if (writeHeaders)
384 {
385 for (int col = 0; col < columnNames.Count; col++)
386 worksheet.Cell(1, col + 1).Value = columnNames[col];
387
388 worksheet.Row(1).Style.Font.Bold = true;
389 destRow = 2;
390 }
391
392 int startOffset = writeHeaders ? 1 : 0;
393 int dataRowsToWrite = totalRowsThisSheet - startOffset;
394
395 // === THIS IS THE ONLY IMPORTANT CHANGE ===
396 for (int i = 0; i < dataRowsToWrite; i++)
397 {
398 var sourceRow = grid[currentRowInGrid + startOffset + i];
399
400 // Always write ALL columns defined by the header, even if sourceRow has fewer items
401 for (int col = 0; col < columnNames.Count; col++)
402 {
403 string value = col < sourceRow.Count ? sourceRow[col] ?? string.Empty : string.Empty;
404 worksheet.Cell(destRow, col + 1).Value = value;
405 }
406
407 destRow++;
408 }
409
410 // Keep empty columns visible
411 if (columnNames.Count > 0)
412 worksheet.Columns(1, columnNames.Count).AdjustToContents();
413
414 currentRowInGrid += totalRowsThisSheet;
415 }
416
417 workbook.SaveAs(outputPath);
418 }
419 }
420}