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 internal static string SaveToExcelFile(string fileName, IGPALGrid<string> data)
97 {
98 using (var workbook = new XLWorkbook())
99 {
100 var worksheet = workbook.Worksheets.Add("Sheet1");
101
102 for (int rowIndex = 0; rowIndex < data.Count(); rowIndex++)
103 {
104 var row = data[rowIndex];
105 for (int colIndex = 0; colIndex < row.Count; colIndex++)
106 {
107 try
108 {
109 worksheet.Cell(rowIndex + 1, colIndex + 1).Value = row[colIndex];
110 }
111 catch (Exception ex)
112 {
113 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Error saving to Excel [{fileName}]", data, Enums.GPALObjectType.Other, ex);
114 }
115 }
116 }
117
118 workbook.SaveAs(fileName);
119 }
120
121 return fileName;
122 }
128 internal static void CopyWorksheet(IXLWorksheet src, IXLWorksheet dest)
129 {
130 try
131 {
132 // Clear the destination worksheet to avoid overlapping data
133 dest.Clear();
134
135 // Get all used cells from the source worksheet
136 foreach (var row in src.RowsUsed())
137 {
138 int rowIndex = row.RowNumber();
139 foreach (var cell in row.CellsUsed())
140 {
141 int colIndex = cell.Address.ColumnNumber;
142 // Copy the cell value to the same position in the destination worksheet
143 dest.Cell(rowIndex, colIndex).Value = cell.GetString();
144 }
145 }
146 }
147 catch (Exception ex)
148 {
149 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to copy worksheet", null, Enums.GPALObjectType.None, ex);
150 }
151 }
158 internal static void WriteRange(IXLWorksheet worksheet, string range, IGPALGrid<string> data)
159 {
160 try
161 {
162 var rangeObj = worksheet.Range(range);
163 var firstCell = rangeObj.FirstCell();
164
165 int startRow = firstCell.Address.RowNumber;
166 int startColumn = firstCell.Address.ColumnNumber;
167
168 for (int rowIndex = 0; rowIndex < data.Count(); rowIndex++)
169 {
170 var row = data[rowIndex];
171 for (int colIndex = 0; colIndex < row.Count; colIndex++)
172 {
173 worksheet.Cell(startRow + rowIndex, startColumn + colIndex).Value = row[colIndex];
174 }
175 }
176 }
177 catch (Exception ex)
178 {
179 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to write range [{range}] in worksheet", null, Enums.GPALObjectType.None, ex);
180 }
181 }
182
183 // New concrete class for comparison results
184 public class CellDifference
185 {
186 public string CellAddress { get; set; }
187 public string OldValue { get; set; }
188 public string NewValue { get; set; }
189 public string SourceRange { get; set; } // New: Tracks the source range
190 public string TargetRange { get; set; } // New: Tracks the target range
191 }
192 // New methods using XLWorkbook and concrete objects
193 internal static List<CellDifference> CompareExcelFiles(XLWorkbook workbook1, XLWorkbook workbook2)
194 {
195 var differences = new List<CellDifference>();
196
197 try
198 {
199 var worksheet1 = workbook1.Worksheet(1);
200 var worksheet2 = workbook2.Worksheet(1);
201 var range1 = worksheet1.RangeUsed();
202 var range2 = worksheet2.RangeUsed();
203 int maxRows = Math.Max(range1?.RowCount() ?? 0, range2?.RowCount() ?? 0);
204 int maxCols = Math.Max(range1?.ColumnCount() ?? 0, range2?.ColumnCount() ?? 0);
205
206 for (int row = 1; row <= maxRows; row++)
207 {
208 for (int col = 1; col <= maxCols; col++)
209 {
210 string value1 = worksheet1.Cell(row, col).GetString();
211 string value2 = worksheet2.Cell(row, col).GetString();
212 if (value1 != value2)
213 {
214 differences.Add(new CellDifference
215 {
216 CellAddress = $"{worksheet1.Column(col).ColumnLetter()}{row}",
217 OldValue = value1,
218 NewValue = value2
219 });
220 }
221 }
222 }
223 }
224 catch (Exception ex)
225 {
226 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to compare workbooks", null, Enums.GPALObjectType.None, ex);
227 }
228
229 return differences;
230 }
237 internal static List<CellDifference> CompareRanges(IGPALGrid<string> data1, IGPALGrid<string> data2)
238 {
239 var differences = new List<CellDifference>();
240
241 try
242 {
243 int maxRows = Math.Max(data1.Count(), data2.Count());
244 int maxCols = Math.Max(data1.Any() ? data1[0].Count : 0, data2.Any() ? data2[0].Count : 0);
245
246 for (int i = 0; i < maxRows; i++)
247 {
248 for (int j = 0; j < maxCols; j++)
249 {
250 string value1 = i < data1.Count() && j < data1[i].Count ? data1[i][j] : null;
251 string value2 = i < data2.Count() && j < data2[i].Count ? data2[i][j] : null;
252
253 if (value1 != value2)
254 {
255 string cellAddress = $"{(char)('A' + j)}{i + 1}";
256 differences.Add(new CellDifference
257 {
258 CellAddress = cellAddress,
259 OldValue = value1 ?? "",
260 NewValue = value2 ?? ""
261 });
262 }
263 }
264 }
265 }
266 catch (Exception ex)
267 {
268 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to compare ranges", null, Enums.GPALObjectType.None, ex);
269 }
270
271 return differences;
272 }
273 internal static string GetCellValue(XLWorkbook workbook, string cellAddress)
274 {
275 try
276 {
277 return workbook.Worksheet(1).Cell(cellAddress).GetString();
278 }
279 catch (Exception ex)
280 {
281 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to get cell value [{cellAddress}]", null, Enums.GPALObjectType.None, ex);
282 return string.Empty;
283 }
284 }
285
286 internal static IGPALGrid<string> ReadRange(IXLWorksheet worksheet, string range)
287 {
288 IGPALGrid<string> data = GPAL.Grid.ToGPALObject();
289
290 try
291 {
292 var xlRange = worksheet.Range(range);
293
294 foreach (var row in xlRange.RowsUsed())
295 {
296 var rowData = new List<string>();
297 foreach (var cell in row.CellsUsed())
298 {
299 rowData.Add(cell.GetString());
300 }
301 data.AddRow(rowData);
302 }
303 }
304 catch (Exception ex)
305 {
306 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to extract range [{range}]", null, Enums.GPALObjectType.None, ex);
307 }
308
309 return data;
310 }
311
312 internal static double GetColumnSum(XLWorkbook workbook, string column)
313 {
314 double sum = 0;
315
316 try
317 {
318 var col = workbook.Worksheet(1).Column(column);
319 foreach (var cell in col.CellsUsed())
320 {
321 if (double.TryParse(cell.GetString(), out double value))
322 {
323 sum += value;
324 }
325 }
326 }
327 catch (Exception ex)
328 {
329 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to calculate sum for column [{column}]", null, Enums.GPALObjectType.None, ex);
330 }
331
332 return sum;
333 }
334
335 internal static List<string> GetColumnValues(IXLWorksheet worksheet, string column)
336 {
337 var values = new List<string>();
338
339 try
340 {
341 var col = worksheet.Column(column);
342 foreach (var cell in col.CellsUsed())
343 {
344 values.Add(cell.GetString());
345 }
346 }
347 catch (Exception ex)
348 {
349 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to get values for column [{column}]", null, Enums.GPALObjectType.None, ex);
350 }
351
352 return values;
353 }
354
355 internal static List<string> GetRowValues(IXLWorksheet worksheet, int rowNumber)
356 {
357 var values = new List<string>();
358
359 try
360 {
361 var row = worksheet.Row(rowNumber);
362 foreach (var cell in row.CellsUsed())
363 {
364 values.Add(cell.GetString());
365 }
366 }
367 catch (Exception ex)
368 {
369 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to get values for row [{rowNumber}]", null, Enums.GPALObjectType.None, ex);
370 }
371
372 return values;
373 }
374
375 internal static void SearchAndReplaceByValue(IXLWorksheet worksheet, string range, string searchValue, string replaceValue)
376 {
377 try
378 {
379 var xlRange = worksheet.Range(range);
380 foreach (var cell in xlRange.CellsUsed())
381 {
382 if (cell.GetString() == searchValue)
383 {
384 cell.Value = replaceValue;
385 }
386 }
387 }
388 catch (Exception ex)
389 {
390 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to search and replace in range [{range}]", null, Enums.GPALObjectType.None, ex);
391 }
392 }
393
394 internal static int SearchHeaderIndexByValue(XLWorkbook workbook, string range, string headerValue)
395 {
396 try
397 {
398 var worksheet = workbook.Worksheet(1);
399 var xlRange = worksheet.Range(range);
400 foreach (var cell in xlRange.CellsUsed())
401 {
402 if (cell.GetString() == headerValue)
403 {
404 return cell.Address.ColumnNumber;
405 }
406 }
407 }
408 catch (Exception ex)
409 {
410 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to search header in range [{range}]", null, Enums.GPALObjectType.None, ex);
411 }
412
413 return -1;
414 }
415
416 internal static string SearchValue(XLWorkbook workbook, string range, string searchValue)
417 {
418 try
419 {
420 var worksheet = workbook.Worksheet(1);
421 var xlRange = worksheet.Range(range);
422 foreach (var cell in xlRange.CellsUsed())
423 {
424 if (cell.GetString() == searchValue)
425 {
426 return cell.Address.ToString();
427 }
428 }
429 }
430 catch (Exception ex)
431 {
432 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to search value in range [{range}]", null, Enums.GPALObjectType.None, ex);
433 }
434
435 return null;
436 }
437
438 internal static void SetCellValue(XLWorkbook workbook, string cellAddress, string value)
439 {
440 try
441 {
442 workbook.Worksheet(1).Cell(cellAddress).Value = value;
443 }
444 catch (Exception ex)
445 {
446 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Failed to set cell value [{cellAddress}]", null, Enums.GPALObjectType.None, ex);
447 }
448 }
449
459 public static void WriteXlsxFromTokenizedData(
460 string outputPath,
461 IGPALGrid<string> grid,
462 List<int> rowsPerSheet,
463 IGPALGrid<string> columnNamesPerSheet,
464 List<string> sheetNames,
465 bool writeHeaders = true)
466 {
467 if (grid == null) throw new ArgumentNullException(nameof(grid));
468 if (rowsPerSheet == null) throw new ArgumentNullException(nameof(rowsPerSheet));
469 if (columnNamesPerSheet == null) throw new ArgumentNullException(nameof(columnNamesPerSheet));
470
471 using var workbook = new XLWorkbook();
472
473 int currentRowInGrid = 0;
474
475 for (int sheetIdx = 0; sheetIdx < rowsPerSheet.Count; sheetIdx++)
476 {
477 int totalRowsThisSheet = rowsPerSheet[sheetIdx];
478 if (totalRowsThisSheet == 0) continue;
479
480 var columnNames = columnNamesPerSheet[sheetIdx];
481
482 string sheetName = sheetNames != null && sheetIdx < sheetNames.Count
483 ? sheetNames[sheetIdx]
484 : $"Sheet{sheetIdx + 1}";
485
486 var worksheet = workbook.Worksheets.Add(sheetName);
487
488 int destRow = 1;
489
490 // Header
491 if (writeHeaders)
492 {
493 for (int col = 0; col < columnNames.Count; col++)
494 worksheet.Cell(1, col + 1).Value = columnNames[col];
495
496 worksheet.Row(1).Style.Font.Bold = true;
497 destRow = 2;
498 }
499
500 int startOffset = writeHeaders ? 1 : 0;
501 int dataRowsToWrite = totalRowsThisSheet - startOffset;
502
503 // === THIS IS THE ONLY IMPORTANT CHANGE ===
504 for (int i = 0; i < dataRowsToWrite; i++)
505 {
506 var sourceRow = grid[currentRowInGrid + startOffset + i];
507
508 // Always write ALL columns defined by the header, even if sourceRow has fewer items
509 for (int col = 0; col < columnNames.Count; col++)
510 {
511 string value = col < sourceRow.Count ? sourceRow[col] ?? string.Empty : string.Empty;
512 worksheet.Cell(destRow, col + 1).Value = value;
513 }
514
515 destRow++;
516 }
517
518 // Keep empty columns visible
519 if (columnNames.Count > 0)
520 worksheet.Columns(1, columnNames.Count).AdjustToContents();
521
522 currentRowInGrid += totalRowsThisSheet;
523 }
524
525 workbook.SaveAs(outputPath);
526 }
527 }
528}