/* * 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. */ namespace Rms.Risk.Mango.Pivot.Core.Models; public class TransposedPivotData : NotifyPropertyChangedBase, IPivotedData { private readonly IPivotedData _data; private IPivotedData? _transposedData; public TransposedPivotData( IPivotedData data, string columnHeaderColumn, string [] rowHeaderColumns, string dataColumn, CancellationToken token ) { _data = data; _columnHeaderColumn = columnHeaderColumn; _rowHeaderColumns = rowHeaderColumns; _dataColumn = dataColumn; _transposedData = CreateTransposedTable( token ); Id = $"{data.Id}-Transposed"; } public string Id { get; set; } private string [] _rowHeaderColumns; public string [] RowHeaderColumn { get => _rowHeaderColumns; set { if (_rowHeaderColumns == value) return; _rowHeaderColumns = value; Refresh(); OnPropertyChanged(() => RowHeaderColumn); } } private string _columnHeaderColumn; public string ColumnHeaderColumn { get => _columnHeaderColumn; set { if (_columnHeaderColumn == value) return; _columnHeaderColumn = value; Refresh(); OnPropertyChanged(() => ColumnHeaderColumn); } } private string _dataColumn; public string DataColumn { get => _dataColumn; set { if (_dataColumn == value) return; _dataColumn = value; Refresh(); OnPropertyChanged(() => DataColumn); } } #region IPivotedData public IReadOnlyCollection Headers => _transposedData?.Headers ?? _data.Headers; public int Count => _transposedData?.Count ?? _data.Count; public Dictionary GetColumnPositions() { var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); // this is safer than calling ToDictionary as it handles duplicate headers foreach ( var (key, pos) in Headers.Select((x, i) => (Key: x, Value: i)) ) { dict.TryAdd(key, pos); } return dict; } public object? Get( int col, int row ) { if ( _transposedData != null ) { if ( col >= _transposedData.Headers.Count || row >= _transposedData.Count ) return null; return _transposedData.Get( col, row ); } if (col >= _data.Headers.Count || row >= _data.Count) return null; return _data.Get( col, row ); } public Type GetColumnType( int col ) => _transposedData != null ? _transposedData.GetColumnType( col ) : _data.GetColumnType( col ); public IPivotedData Filter(Func filter) { var data = _data.Filter(filter); return new TransposedPivotData(data, _columnHeaderColumn, _rowHeaderColumns, _dataColumn, CancellationToken.None); } #endregion private CancellationTokenSource _cancellationTokenSource = new(); private async void Refresh() { try { await _cancellationTokenSource.CancelAsync(); _cancellationTokenSource = new(); var token = _cancellationTokenSource.Token; var data = await Task.Run( () => CreateTransposedTable(token), token ); if ( data == null ) return; _transposedData = data; OnPropertyChanged( () => Headers ); OnPropertyChanged( () => Count ); } catch ( Exception ) { // } } private ArrayBasedPivotData? CreateTransposedTable( CancellationToken token ) { if ( token.IsCancellationRequested ) return null; if ( _data == null || _data.Headers.Count == 0 || _data.Count == 0 ) return null; var rhc = RowHeaderColumn; var chc = ColumnHeaderColumn; var dc = DataColumn; if ( rhc == null || rhc.Length == 0 || string.IsNullOrWhiteSpace( chc ) || string.IsNullOrWhiteSpace( dc ) ) return null; if ( rhc.Any( x => !_data.Headers.Contains( x ) ) || !_data.Headers.Contains( chc ) || !_data.Headers.Contains( dc ) ) return null; var headersDict = _data.Headers.Select((s, i) => (s, i)).ToDictionary(x => x.s, x => x.i); var columns = new Dictionary(StringComparer.OrdinalIgnoreCase); var colPos = headersDict[chc]; var count = rhc.Length; for ( var row = 0; row < _data.Count; row++ ) { var v = ConvertToString( _data.Get( colPos, row ) ); if ( columns.ContainsKey( v ) ) continue; columns[v] = count++; } if ( token.IsCancellationRequested ) return null; var rows = new Dictionary(StringComparer.OrdinalIgnoreCase); var rowPos = rhc.Select( x => headersDict[x]).ToArray(); count = 0; for ( var row = 0; row < _data.Count; row++ ) { var r = row; var v = string.Join("|", rowPos.Select( x => ConvertToString( _data.Get( x, r ) ))); if ( rows.ContainsKey( v ) ) continue; rows[v] = count++; } if ( token.IsCancellationRequested ) return null; var d = new ArrayBasedPivotData( rhc .Concat( columns .OrderBy( x => x.Value ) .Select( x => x.Key ) ) ); // preallocate space for ( var row = 0; row < rows.Count; row++ ) { d.Add( new object[columns.Count+rhc.Length] ); var k = rows.First( x => x.Value == row ).Key.Split( '|' ); for ( var col = 0; col < k.Length; col++ ) d[col, row] = k[col]; } if ( token.IsCancellationRequested ) return null; var dataPos = headersDict[dc]; for ( var row = 0; row < _data.Count; row++ ) { if ( row % 2048 == 0) if ( token.IsCancellationRequested ) return null; var destCol = columns[ConvertToString(_data.Get( colPos, row ))]; var r = row; var v = string.Join("|", rowPos.Select( x => ConvertToString( _data.Get( x, r ) ))); var destRow = rows[v]; var dest = d[destCol, destRow]; var src = _data.Get( dataPos, row ); if ( src == null ) continue; if (dest == null) d[destCol, destRow] = src; else { switch ( dest ) { case double dbl: dest = dbl + Convert.ToDouble( src ); break; case int i: dest = i + Convert.ToInt32( src ); break; case long l: dest = l + Convert.ToInt64( src ); break; case decimal dec: dest = dec + Convert.ToInt64( src ); break; default: dest = src; break; } d[destCol, destRow] = dest; } } return token.IsCancellationRequested ? null : d; } private static string ConvertToString( object? val ) { var v = val is DateTime time ? time.ToString( "yyyy-MM-dd" ) : val?.ToString() ?? ""; return v; } }