@@ -469,16 +469,15 @@ public string ToString(long rowsToShow)
469469 protected static void PopulateColumnSortIndicesWithHeap < T > ( SortedDictionary < T , List < ValueTuple < int , int > > > heapOfValueAndListOfTupleOfSortAndBufferIndex ,
470470 PrimitiveDataFrameColumn < long > columnSortIndices ,
471471 PrimitiveDataFrameColumn < long > columnNullIndices ,
472- bool ascending ,
473472 bool putNullValuesLast ,
474473 GetBufferSortIndex getBufferSortIndex ,
475474 GetValueAndBufferSortIndexAtBuffer < T > getValueAndBufferSortIndexAtBuffer ,
476475 GetBufferLengthAtIndex getBufferLengthAtIndex )
477476 {
478- long i = ascending ? columnNullIndices . Length : columnSortIndices . Length - 1 ;
479-
480- if ( putNullValuesLast )
481- i -= columnNullIndices . Length ;
477+ // Always write left-to-right. For descending order, callers pass a SortedDictionary
478+ // with a reversed comparer so that ElementAt(0) yields the largest value first.
479+ // This ensures stable sorting: equal elements preserve their original relative order.
480+ long i = putNullValuesLast ? 0 : columnNullIndices . Length ;
482481
483482 while ( heapOfValueAndListOfTupleOfSortAndBufferIndex . Count > 0 )
484483 {
@@ -499,7 +498,7 @@ protected static void PopulateColumnSortIndicesWithHeap<T>(SortedDictionary<T, L
499498 int bufferIndex = sortAndBufferIndex . bufferIndex ;
500499 long bufferSortIndex = getBufferSortIndex ( bufferIndex , sortIndex ) ;
501500
502- columnSortIndices [ ascending ? i ++ : i -- ] = bufferSortIndex ;
501+ columnSortIndices [ i ++ ] = bufferSortIndex ;
503502
504503 if ( sortIndex + 1 < getBufferLengthAtIndex ( bufferIndex ) )
505504 {
@@ -758,5 +757,163 @@ private static void Sort2<TKey>(
758757 sortIndices [ j ] = temp ;
759758 }
760759 }
760+
761+ /// <summary>
762+ /// Stable merge sort that reorders <paramref name="sortIndices"/> so that
763+ /// the values in <paramref name="span"/> are visited in sorted order.
764+ /// </summary>
765+ protected static void MergeSortIndices < TKey > (
766+ ReadOnlySpan < TKey > span ,
767+ int length ,
768+ Span < int > sortIndices ,
769+ IComparer < TKey > comparer )
770+ {
771+ if ( length <= 1 )
772+ return ;
773+
774+ int [ ] auxiliary = System . Buffers . ArrayPool < int > . Shared . Rent ( length ) ;
775+ try
776+ {
777+ MergeSortIndicesRecursive ( span , sortIndices , auxiliary , 0 , length - 1 , comparer ) ;
778+ }
779+ finally
780+ {
781+ System . Buffers . ArrayPool < int > . Shared . Return ( auxiliary ) ;
782+ }
783+ }
784+
785+ /// <summary>
786+ /// Stable merge sort overload that accepts an <see cref="IList{TKey}"/> instead of a span,
787+ /// avoiding the need to copy to an array first.
788+ /// </summary>
789+ protected static void MergeSortIndices < TKey > (
790+ IList < TKey > list ,
791+ int length ,
792+ Span < int > sortIndices ,
793+ IComparer < TKey > comparer )
794+ {
795+ if ( length <= 1 )
796+ return ;
797+
798+ int [ ] auxiliary = System . Buffers . ArrayPool < int > . Shared . Rent ( length ) ;
799+ try
800+ {
801+ MergeSortIndicesRecursive ( list , sortIndices , auxiliary , 0 , length - 1 , comparer ) ;
802+ }
803+ finally
804+ {
805+ System . Buffers . ArrayPool < int > . Shared . Return ( auxiliary ) ;
806+ }
807+ }
808+
809+ private static void MergeSortIndicesRecursive < TKey > (
810+ ReadOnlySpan < TKey > span ,
811+ Span < int > sortIndices ,
812+ int [ ] auxiliary ,
813+ int lo ,
814+ int hi ,
815+ IComparer < TKey > comparer )
816+ {
817+ if ( lo >= hi )
818+ return ;
819+
820+ int mid = lo + ( hi - lo ) / 2 ;
821+ MergeSortIndicesRecursive ( span , sortIndices , auxiliary , lo , mid , comparer ) ;
822+ MergeSortIndicesRecursive ( span , sortIndices , auxiliary , mid + 1 , hi , comparer ) ;
823+ Merge ( span , sortIndices , auxiliary , lo , mid , hi , comparer ) ;
824+ }
825+
826+ private static void MergeSortIndicesRecursive < TKey > (
827+ IList < TKey > list ,
828+ Span < int > sortIndices ,
829+ int [ ] auxiliary ,
830+ int lo ,
831+ int hi ,
832+ IComparer < TKey > comparer )
833+ {
834+ if ( lo >= hi )
835+ return ;
836+
837+ int mid = lo + ( hi - lo ) / 2 ;
838+ MergeSortIndicesRecursive ( list , sortIndices , auxiliary , lo , mid , comparer ) ;
839+ MergeSortIndicesRecursive ( list , sortIndices , auxiliary , mid + 1 , hi , comparer ) ;
840+ MergeList ( list , sortIndices , auxiliary , lo , mid , hi , comparer ) ;
841+ }
842+
843+ private static void Merge < TKey > (
844+ ReadOnlySpan < TKey > span ,
845+ Span < int > sortIndices ,
846+ int [ ] auxiliary ,
847+ int lo ,
848+ int mid ,
849+ int hi ,
850+ IComparer < TKey > comparer )
851+ {
852+ for ( int k = lo ; k <= hi ; k ++ )
853+ {
854+ auxiliary [ k ] = sortIndices [ k ] ;
855+ }
856+
857+ int i = lo ;
858+ int j = mid + 1 ;
859+
860+ for ( int k = lo ; k <= hi ; k ++ )
861+ {
862+ if ( i > mid )
863+ {
864+ sortIndices [ k ] = auxiliary [ j ++ ] ;
865+ }
866+ else if ( j > hi )
867+ {
868+ sortIndices [ k ] = auxiliary [ i ++ ] ;
869+ }
870+ else if ( comparer . Compare ( span [ auxiliary [ i ] ] , span [ auxiliary [ j ] ] ) <= 0 )
871+ {
872+ sortIndices [ k ] = auxiliary [ i ++ ] ;
873+ }
874+ else
875+ {
876+ sortIndices [ k ] = auxiliary [ j ++ ] ;
877+ }
878+ }
879+ }
880+
881+ private static void MergeList < TKey > (
882+ IList < TKey > list ,
883+ Span < int > sortIndices ,
884+ int [ ] auxiliary ,
885+ int lo ,
886+ int mid ,
887+ int hi ,
888+ IComparer < TKey > comparer )
889+ {
890+ for ( int k = lo ; k <= hi ; k ++ )
891+ {
892+ auxiliary [ k ] = sortIndices [ k ] ;
893+ }
894+
895+ int i = lo ;
896+ int j = mid + 1 ;
897+
898+ for ( int k = lo ; k <= hi ; k ++ )
899+ {
900+ if ( i > mid )
901+ {
902+ sortIndices [ k ] = auxiliary [ j ++ ] ;
903+ }
904+ else if ( j > hi )
905+ {
906+ sortIndices [ k ] = auxiliary [ i ++ ] ;
907+ }
908+ else if ( comparer . Compare ( list [ auxiliary [ i ] ] , list [ auxiliary [ j ] ] ) <= 0 )
909+ {
910+ sortIndices [ k ] = auxiliary [ i ++ ] ;
911+ }
912+ else
913+ {
914+ sortIndices [ k ] = auxiliary [ j ++ ] ;
915+ }
916+ }
917+ }
761918 }
762919}
0 commit comments