GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GPALGrid.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;
19using System.Collections.Generic;
20using System.Linq;
21using System.Text;
22using System.Threading.Tasks;
23using static GenerallyPositive.Enums;
24
25namespace GenerallyPositive
26{
54 internal class GPALGrid<T> : IGPALGrid<T>
55 {
56 private List<List<T>> data;
57 // rows arrive from whatever thread ran the workflow that made them, and GPAL.Workflow runs several
58 // of those at once, so adding a row is one step rather than a read of Count and a write after it
59 private readonly object rowLock = new object();
60
66 internal bool UseDedupe { get; private set; } = false;
67
73 public IGPALGrid<T> WithDedupeData(bool useDedupe = true)
74 {
75 UseDedupe = useDedupe;
76 return this;
77 }
78
82 internal GPALGrid()
83 {
84 data = new List<List<T>>();
85 }
86
109 public IAllowGridActions<T> AddRow(List<T> row)
110 {
111 if (row == null)
112 {
113 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Row is null in AddRow(List<T>). Skipping addition.", this, GPALObjectType.Other);
114 return this;
115 }
116
117 lock (rowLock)
118 data.Add(row);
119
120 return this;
121 }
135 internal void Add(List<T> row)
136 {
137 if (row == null)
138 {
139 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Row is null in AddRow(List<T>). Skipping addition.", this, GPALObjectType.Other);
140 }
141
142 data.Add(row);
143 }
144
145 public IAllowGridActions<T> Add(IGPALGrid<T> grid)
146 {
147 foreach (List<T> row in grid)
148 data.Add(row);
149
150 return this;
151 }
180 public IAllowGridActions<T> AddRow(object anonymousObject)
181 {
182 if (anonymousObject == null)
183 {
184 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Anonymous object is null in AddRow(object). Skipping addition.", this, GPALObjectType.Other);
185 return this;
186 }
187
188 // Convert anonymous object properties to a list of values
189 var values = anonymousObject.GetType()
190 .GetProperties()
191 .Select(prop => prop.GetValue(anonymousObject, null))
192 .ToList();
193
194 // Validate column count if the grid already has rows
195 lock (rowLock)
196 {
197 int expectedColumns = Columns;
198 if (data.Count > 0 && values.Count != expectedColumns)
199 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Anonymous object has [{values.Count}] values, but expected [{expectedColumns}]. Adding row as-is.", this, GPALObjectType.Other);
200 }
201
202 // Convert each value to type T
203 var row = new List<T>();
204 foreach (var value in values)
205 {
206 try
207 {
208 if (value == null)
209 {
210 row.Add(default(T));
211 continue;
212 }
213 row.Add((T)Convert.ChangeType(value, typeof(T)));
214 }
215 catch (Exception ex)
216 {
217 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Cannot convert value [{value}] to type [{typeof(T).Name}]. Using default value", this, GPALObjectType.Other, ex);
218 row.Add(default(T));
219 }
220 }
221
222 lock (rowLock)
223 data.Add(row);
224
225 return this;
226 }
227
255 public IAllowGridActions<T> AddRow(params T[] values)
256 {
257 if (values == null)
258 {
259 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Values array is null in AddRow(params T[]). Skipping addition.", this, GPALObjectType.Other);
260 return this;
261 }
262
263 // Validate column count if the grid already has rows, and add while still holding the gate
264 lock (rowLock)
265 {
266 int expectedColumns = Columns;
267 if (data.Count > 0 && values.Length != expectedColumns)
268 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Params array has [{values.Length}] values, but expected [{expectedColumns}]. Adding row as-is.", this, GPALObjectType.Other);
269
270 data.Add(new List<T>(values));
271 }
272 return this;
273 }
274
298 public IAllowGridActions<T> DeleteRow(int rowIdx)
299 {
300 if (rowIdx >= 0 && rowIdx < data.Count)
301 {
302 data.RemoveAt(rowIdx);
303 }
304 else
305 {
306 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid row index [{rowIdx}] in DeleteRow(int). Row count is [{data.Count}]. Skipping deletion.", this, GPALObjectType.Other);
307 }
308 return this;
309 }
310
334 public IAllowGridActions<T> DeleteRow(RowPosition rowPosition = RowPosition.Last)
335 {
336 int rowIdx;
337
338 if (data.Count == 0)
339 {
340 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Grid is empty in DeleteRow(RowPosition). Skipping deletion.", this, GPALObjectType.Other);
341 return this;
342 }
343
344 if (rowPosition == RowPosition.First)
345 {
346 rowIdx = 0;
347 }
348 else // position == RowPosition.Last
349 {
350 rowIdx = data.Count - 1;
351 }
352
353 data.RemoveAt(rowIdx);
354 return this;
355 }
356
383 public IAllowGridActions<T> AddColumn(List<T> column)
384 {
385 if (column == null)
386 {
387 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Column is null in AddColumn. Skipping addition.", this, GPALObjectType.Other);
388 return this;
389 }
390
391 for (int i = 0; i < column.Count; i++)
392 {
393 if (data.Count <= i)
394 {
395 data.Add(new List<T>());
396 }
397 data[i].Add(column[i]);
398 }
399 return this;
400 }
401
405 public int Rows => data.Count;
406
411 public int Columns
412 {
413 get
414 {
415 int count = 0;
416
417 foreach (var row in data)
418 count = Math.Max(count, row.Count);
419
420 return count;
421 }
422 }
423
448 public IAllowGridActions<T> AddCell(T value, int row, int column)
449 {
450 if (row >= 0 && row < Rows && column >= 0 && column < Columns)
451 {
452 data[row][column] = value;
453 }
454 else
455 {
456 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid cell coordinates ([{row}], [{column}]) in AddCell. Grid size is [{Rows}]x[{Columns}]. Skipping operation.", this, GPALObjectType.Other);
457 }
458 return this;
459 }
460
478 public T GetCell(int row, int column)
479 {
480 if (row >= 0 && row < Rows && column >= 0 && column < Columns)
481 {
482 return data[row][column];
483 }
484
485 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid cell coordinates ([{row}], [{column}]) in GetCell. Grid size is [{Rows}]x[{Columns}]. Returning default value.", this, GPALObjectType.Other);
486 return default(T);
487 }
488
503 public IAllowGridActions<T> Clear()
504 {
505 data = new List<List<T>>();
506 return this;
507 }
508
530 public List<T> this[int index]
531 {
532 get
533 {
534 List<T> retVal = index < data.Count && 0 <= index ? data[index] : new List<T>();
535
536 // reading past the end used to add empty rows until the index existed, so a read quietly changed
537 // Rows and Count(). An empty row says there is nothing there without making it true
538 if (index >= data.Count || 0 > index)
539 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
540 $"Row [{index}] was asked for and the grid holds [{data.Count}], so an empty row was answered with",
541 this, GPALObjectType.Other);
542
543 return retVal;
544 }
545 set
546 {
547 if (index >= data.Count)
548 {
549 for (int idx = index; idx >= data.Count; idx--)
550 data.Add(new List<T>());
551 }
552
553 if (value == null)
554 {
555 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Value is null in indexer set at index [{index}]. Setting to empty list.", this, GPALObjectType.Other);
556 data[index] = new List<T>();
557 return;
558 }
559
560 data[index] = value;
561 }
562 }
563
585 public T this[int row, int column]
586 {
587 get { return GetCell(row, column); }
588 set { AddCell(value, row, column); }
589 }
590
595 public IGPALGrid<T> ToGPALObject()
596 {
597 return this;
598 }
599
616 public bool Contains(T value)
617 {
618 if (value == null)
619 {
620 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Value is null in Contains. Returning false.", this, GPALObjectType.Other);
621 return false;
622 }
623
624 string valueString = value.ToString();
625 foreach (List<T> list in data)
626 foreach (T item in list)
627 if (item != null && item.ToString().Contains(valueString))
628 return true;
629 return false;
630 }
631
637 public int Count() => data.Count();
638
643 public IEnumerator<List<T>> GetEnumerator()
644 {
645 return data.GetEnumerator();
646 }
647
652 IEnumerator IEnumerable.GetEnumerator()
653 {
654 return GetEnumerator();
655 }
656
676 public object Clone()
677 {
678 GPALGrid<T> clone = new GPALGrid<T>();
679
680 foreach (List<T> row in data)
681 {
682 List<T> clonedRow = new List<T>();
683 foreach (T item in row)
684 {
685 if (item is ICloneable cloneable)
686 {
687 clonedRow.Add((T)cloneable.Clone());
688 }
689 else
690 {
691 clonedRow.Add(item);
692 }
693 }
694 clone.AddRow(clonedRow);
695 }
696
697 return clone;
698 }
699
706 public IAllowGridActions<T> InputFrom(GPALFile input)
707 {
708 FileHelper.TokenizeFile(input); // CAVEAT: NOTE: could tokenize multiple files? if wildcard? but we only grab top list
709
710 // the rows are copied out a cell at a time. Clone() answers with a GPALGrid, so casting its result
711 // to List<List<T>> threw InvalidCastException every time and this method could never run
712 IGPALGrid<string> tokens = ((IGPALFileInternal)input).TokenList[0];
713
714 data = new List<List<T>>();
715
716 foreach (List<string> row in tokens)
717 {
718 List<T> newRow = new List<T>();
719
720 foreach (string cell in row)
721 newRow.Add(null == cell ? default(T) : (T)Convert.ChangeType(cell, typeof(T)));
722
723 data.Add(newRow);
724 }
725
726 return this;
727 }
728
734 public IAllowGridActions<T> SaveTo(GPALFile output)
735 {
736 GPAL.Converter.WithInput(this).SaveTo(output);
737 return this;
738 }
739
747 public IAllowGridActions<T> InputFrom(IGPALDatabase input)
748 {
749 input.SaveTo(out IGPALGrid<string> readGrid);
750
751 data = new List<List<T>>();
752
753 if (null != readGrid)
754 foreach (List<string> row in readGrid)
755 {
756 List<T> newRow = new List<T>();
757
758 foreach (string cell in row)
759 newRow.Add(null == cell ? default(T) : (T)Convert.ChangeType(cell, typeof(T)));
760
761 data.Add(newRow);
762 }
763
764 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Read [{data.Count}] rows from the database into the grid.", input, GPALObjectType.Database);
765
766 return this;
767 }
768
777 public IAllowGridActions<T> SaveTo(IGPALDatabase output)
778 {
779 IGPALGrid<string> writeGrid = GPAL.GridForType<string>();
780
781 foreach (List<T> row in data)
782 {
783 List<string> newRow = new List<string>();
784
785 foreach (T cell in row)
786 newRow.Add(null == (object)cell ? null : cell.ToString());
787
788 writeGrid.AddRow(newRow);
789 }
790
791 output.Using(writeGrid).Execute(out int rowsWritten);
792
793 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Wrote [{rowsWritten}] rows from the grid to the database.", output, GPALObjectType.Database);
794
795 return this;
796 }
797
802 public List<T> ToList()
803 {
804 List<T> retVal = new List<T>();
805
806 foreach (List<T> row in data)
807 foreach (T col in row)
808 retVal.Add(col);
809
810 return retVal;
811 }
812 }
813}