dbMango/Rms.Risk.Mango.Pivot.Core/ArrayBasedPivotData.cs
Alexander Shabarshov 2a7a24c9e7 Initial contribution
2025-11-03 14:43:26 +00:00

331 lines
11 KiB
C#

/*
* dbMango
*
* Copyright 2025 Deutsche Bank AG
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System.Diagnostics;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
using MongoDB.Bson.Serialization.Attributes;
namespace Rms.Risk.Mango.Pivot.Core;
[BsonSerializer( typeof(ArrayBasedPivotDataSerializer))]
[JsonConverter( typeof(ArrayBasedPivotDataJsonSerializer))]
public class ArrayBasedPivotData : IPivotedData
{
private readonly List<string> _headers;
private List<(string OrigHeader, string DisplayHeader)>? _headersMap;
private readonly List<object?[]> _realData = [];
private readonly Dictionary<int, Type> _columnTypesCache = [];
private readonly List<string> _displayHeaders = [];
private int[]? _columnMap; // displayPos -> physicalPos in _headers
public static readonly ArrayBasedPivotData NoData = new(["No data"]) { Id = "no_data" };
public IReadOnlyCollection<string> Headers
{
get
{
if ( _displayHeaders.Count > 0 && _displayHeaders.Count == _headers.Count)
return _displayHeaders;
if ( !(_columnMap?.Length > 0) )
return _headers;
_displayHeaders.Clear();
_displayHeaders.AddRange(
_columnMap
.Select((physicalPos, displayPos) => (physicalPos, displayPos))
.OrderBy(x => x.displayPos)
.Select( x => _headers[x.physicalPos])
);
// _displayHeaders.AddRange(
// _headers
// .Select((x, i) => (Name: x, Order: Array.IndexOf(_columnMap, i, 0)))
// .OrderBy(x => x.Order)
// .Select(x => x.Name)
// .ToList()
// );
return _displayHeaders;
}
}
public IReadOnlyCollection<(string OrigHeader, string DesplayHeader)> HeadersMap => _headersMap ?? [];
public void UpdateHeaders(Func<string, string> changeColumnName)
{
// make a copy or original headers
if ( _headersMap != null )
throw new ApplicationException("UpdateHeaders can only be called once");
_headersMap = [];
for (var i = 0; i < _headers.Count; i++ )
{
var newName = changeColumnName(_headers[i]);
_headersMap.Add((OrigHeader:_headers[i], DisplayHeader: newName));
_headers[i] = newName;
}
_displayHeaders.Clear();
}
public string Id { get; set; } = "";
public DateTime ExpireAt { get; set; }
public int Count => _realData.Count;
public object? Get(int displayCol, int row) => this[displayCol, row];
public object? this[int displayCol, int row]
{
get
{
if (row >= _realData.Count)
throw new ArgumentException($"Row={row} is greater than Count={_realData.Count}");
var physicalCol = _columnMap != null && displayCol < _columnMap.Length
? _columnMap[displayCol]
: displayCol;
return physicalCol >= _realData[row].Length
? null
: _realData[row][physicalCol];
}
set
{
if (row >= _realData.Count)
throw new ArgumentException($"Row={row} is greater than Count={_realData.Count}");
var physicalCol = _columnMap != null && displayCol < _columnMap.Length
? _columnMap[displayCol]
: displayCol;
if (physicalCol >= _realData[row].Length)
throw new ArgumentException($"Col={row} is greater than Count={_realData[row].Length} or Row={row}");
_realData[row][physicalCol] = value;
}
}
public ArrayBasedPivotData(IEnumerable<string> headers)
{
_headers = headers.ToList();
}
public ArrayBasedPivotData(IPivotedData other)
: this( other.Headers )
{
var len = other.Headers.Count;
var o = new object?[len];
for ( var row = 0; row < other.Count; row++)
{
for (var col = 0; col < len; col++)
{
o[col] = other.Get(col, row);
}
Add(o);
}
}
private ArrayBasedPivotData(
IEnumerable<string> headers,
int[]? columnMap
)
{
_headers = headers.ToList();
_columnMap = columnMap == null ? null : [.. columnMap]; // make a copy
}
public void Add( IEnumerable<object?> data )
{
var row = new object?[_headers.Count];
var i = 0;
foreach ( var o in data )
{
if ( i >= _headers.Count )
throw new ArgumentException($"Length of supplied data must be at least {_headers.Count}", nameof(data));
row[i++] = o;
}
_realData.Add( row );
}
public void AddHeader(string header)
{
_headers.Add(header);
_displayHeaders.Clear();
_columnMap = null;
}
public bool Contains( string header ) => _headers.Any( x => x == header );
public Type GetColumnType(int displayCol)
{
if ( _columnTypesCache.TryGetValue( displayCol, out var t ) )
return t;
t = DetectColumnType( displayCol );
_columnTypesCache[displayCol] = t;
return t;
}
private Type DetectColumnType(int displayCol)
{
var guessDouble = 0;
var guessLong = 0;
var guessInt = 0;
var guessDec = 0;
for ( var i = 0; i < Math.Min( 200, Count); i++ )
{
var o = this[displayCol, i];
if (o == null)
continue;
if ( o is double )
guessDouble += 1;
else if (o is int)
guessInt += 1;
else if (o is long)
guessLong += 1;
else if (o is decimal)
guessDec += 1;
}
if (guessDec > guessDouble && guessDec > guessLong && guessDec > guessInt)
return typeof(decimal);
if (guessDouble > guessLong && guessDouble > guessInt)
return typeof(double);
if (guessLong > guessInt)
return typeof(long);
if (guessInt > 0)
return typeof(int);
return typeof(string);
}
public void ReorderColumns( IReadOnlyCollection<string> columnsOrder )
{
_displayHeaders.Clear();
if ( !(columnsOrder?.Count > 0) )
{
_columnMap = [];
return;
}
var src = new List<string>( _headers );
var colMap = new List<int>(); // position = display col / value = real col number
foreach ( var regex in columnsOrder.Select( x => new Regex( x, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline ) ).ToArray() )
{
var alpha = new List<string>(); // for alpha sorting
foreach ( var name in src )
{
var m = regex.Match( name );
if ( !m.Success )
continue;
alpha.Add( name );
}
alpha.Sort();
foreach ( var name in alpha )
{
src.Remove( name );
colMap.Add( _headers.IndexOf( name ) );
}
}
foreach (var name in src) // something what is not fall under any regex
colMap.Add(_headers.IndexOf(name));
Debug.Assert( colMap.Count == _headers.Count );
_columnMap = colMap.ToArray(); // set column mapping
}
/// <summary>
/// Create filtered copy of data. Filter func must return true if you want row to remain in the output copy.
/// </summary>
/// <param name="filter"></param>
/// <returns></returns>
public IPivotedData Filter(Func<int, bool> filter)
{
var res = new ArrayBasedPivotData(_headers, _columnMap );
for( var i = 0; i < _realData.Count; i++ )
{
if (!filter(i))
continue;
res.Add(_realData[i]);
}
return res;
}
/// <summary>
/// Create filtered copy of data. Resulting columns present in the data in order of appearance in newColumns. If column is missing it's omitted.
/// </summary>
public IPivotedData FilterColumns(IReadOnlyCollection<string> newColumns, Func<int, bool>? filter = null)
{
var newColMap = new List<(string, int)>();
foreach (var colName in newColumns)
{
var index = _headers.IndexOf(colName);
if ( index < 0 )
continue;
newColMap.Add((colName, index));
}
var res = new ArrayBasedPivotData( newColMap.Select(x => x.Item1).ToList() );
for( var i = 0; i < _realData.Count; i++ )
{
if (filter != null && !filter(i))
continue;
var newData = new object?[newColMap.Count];
foreach (var (oldIndex, newIndex) in newColMap.Select((x,pos) => (OldIndex: x.Item2, NewIndex: pos)))
{
newData[newIndex] = _realData[i].Length <= oldIndex
? null
: _realData[i][oldIndex]
;
}
res.Add(newData);
}
return res;
}
private class RowComparer(IPivotedData pivot, Func<object?[], object?[], Dictionary<string, int>, int> comparer) : IComparer<object?[]>
{
private readonly Dictionary<string, int> _columnMap = pivot.Headers.Select((x, i) => (x, i)).ToDictionary(x => x.x, x => x.i);
private readonly Func<object?[], object?[], Dictionary<string, int>, int> _comparer = comparer;
public int Compare( object?[]? x, object?[]? y )
=> x == null ? -1 : y == null ? 1 : _comparer( x, y, _columnMap );
}
/// <summary>
/// Custom sort for real data. Be careful with views.
/// </summary>
/// <param name="comparer"></param>
public void Sort( Func<object?[], object?[], Dictionary<string, int>, int> comparer ) => _realData.Sort(new RowComparer( this, comparer ));
}