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;
}
}