namespace Speckle.Converters.CSiShared.Extensions; /// /// Csi Api returns a one-dimensional array of the table data. Any cDatabaseTable queries will require some processing. /// The TableData extension processes queries for GetTableForDisplayArray. /// /// /// TableData implemented as a record. Reasons for this include: /// /// Keeping data immutable (preventing accidental modifications). /// Better choice for large data sets (heap allocation). /// /// Notes: /// /// A cDatabaseTable query returns ALL objects of a type. This is an expensive operation. However, the typical use-case involves sending the entire Etabs/Sap model. /// High initial memory usage when creating dictionaries for all rows of data. /// Benefits of the dictionary evident during send operations when most/all objects are sent (and thus queried). /// Single upfront dictionary creation preferred over repeated on-demand creation /// Yes, Csi returns all data as strings. Even int, double etc. /// /// public record TableData { private readonly string[] _columnNames; // "fieldKeys" in api docs private readonly string[] _rawTableData; // indicating raw, one-dimensional array of table data (before processing) private readonly int _rowCount; // Number of rows private IReadOnlyDictionary>? _processedRows; // Cached data structure private readonly string _indexColumn; // column used to index/identify rows (typically, "UniqueName") public TableData(string[] columnNames, string[] rawTableData, int rowCount, string indexColumn) { _columnNames = columnNames; _rawTableData = rawTableData; _rowCount = rowCount; _indexColumn = indexColumn; } /// /// Gets table data as a dictionary mapping indexColumn (typically "UniqueName" to _processedRows). /// Each row is itself a dictionary mapping column names to their values. /// Computed once on first access and cached. /// /// /// Motivation: /// /// One-dimensional array => structured dictionary format /// Each row keyed by its "UniqueName" value /// Each row value is itself a dictionary of field keys to values /// /// public IReadOnlyDictionary> Rows { get { if (_processedRows != null) // Lazy loading - only build dictionary when first accessed { return _processedRows; } var columnsPerRow = _columnNames.Length; var indexColumnIndex = Array.IndexOf(_columnNames, _indexColumn); if (indexColumnIndex == -1) { throw new InvalidOperationException( $"Row data structured according specified '{_indexColumn}' field. This was not found in the database." ); } // Pre-size dictionary with known capacity var rows = new Dictionary>(_rowCount); // Create a field index lookup to avoid repeated Array.IndexOf calls var fieldIndexLookup = new Dictionary(columnsPerRow); for (int i = 0; i < _columnNames.Length; i++) { fieldIndexLookup[_columnNames[i]] = i; } // Process each row for (int rowStart = 0; rowStart < _rawTableData.Length; rowStart += columnsPerRow) { var keyValue = _rawTableData[rowStart + indexColumnIndex]; // Pre-size the row dictionary var row = new Dictionary(columnsPerRow, StringComparer.Ordinal); // Use index lookup instead of repeated string comparisons foreach (var kvp in fieldIndexLookup) { row[kvp.Key] = _rawTableData[rowStart + kvp.Value]; } rows[keyValue] = row; } _processedRows = rows; return _processedRows; } } /// /// Retrieves a string value from a specific row and column from the table data. /// /// The unique identifier for the row, matching the value in the index column (e.g., "UniqueName") /// The name of the column containing the desired value /// The string value found at the specified row and column intersection /// Thrown when either the row or column is not found in the table public string GetRowValue(string rowKey, string columnName) { if (TryGetValue(rowKey, columnName, out var value)) { return value; } throw new InvalidOperationException($"Failed to get value for row '{rowKey}', column '{columnName}'"); } private bool TryGetValue(string rowKey, string columnName, out string value) { if (Rows.TryGetValue(rowKey, out var row) && row.TryGetValue(columnName, out value!)) { return true; } value = string.Empty; return false; } }