331 lines
11 KiB
C#
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 ));
|
|
} |