Search code examples
algorithmsortingmathdeterministic

Sorting by two integer fields, one desc and one asc, by creating a single value


I have a collection of the following object:

public class TestObject
{
    public string Id { get; set;}
    public int Value1 { get; set; }
    public int Value2 { get; set; }
}

As you can see, it contains two integer values. I'd like to sort the collection by Value1 descending and then Value2 ascending.

For clarity, in SQL:

SELECT   *
FROM     testobjects
ORDER BY Value1 DESC
ORDER BY Value2 ASC

Or if you prefer, in c# linq:

testObjects
    .OrderByDescending(o => o.Value1)
    .ThenBy(o => o.Value2);

But here's the catch... I need to do this by combining the two numbers into a single numeric value that can then be sorted. (Why? CosmosDB, but that's aside the point!)

Off the top of my head it occurs to me that if we take (Value1 x 1000) - Value2 and then ordered by descending, provided Value2 doesn't exceed our arbitrary 1000 then this will probably work.

Will it? Surely there's a better way...?


Solution

  • So here's how you can make it compare Value1 and Value2, both ascending.

    1. Put Value1 into the high 32 bits of a 64-bit integer.
    2. Flip the high bit (the sign bit) on Value2, and place that into the low 32 bits.

    For example:

    public static long getKey(int val1, int val2)
    {
        long key = val1;
        key <<= 32;
        uint flipped = (uint)val2 ^ 0x80000000;
        key |= flipped;
        return key;
    }
    

    That would sort Value1 and Value2 ascending. But you want Value1 to sort descending. So just flip all the bits in the first value. Change the first line to:

    long key = (uint)~val1;
    

    So how does this work? I'm going to explain using 4-bit signed integers, which have the range -8 .. 7. The high bit is the sign bit. So the representation from low to high is:

                                   Bit     Unsigned
        Binary  Inverted  Value  Flipped   Value
    -8  1000      0111     7      0000      0
    -7  1001      0110     6      0001      1
    -6  1010      0101     5      0010      2
    -5  1011      0100     4      0011      3
    -4  1100      0011     3      0100      4
    -3  1101      0010     2      0101      5
    -2  1110      0001     1      0110      6
    -1  1111      0000     0      0111      7
     0  0000      1111    -1      1000      8
     1  0001      1110    -2      1001      9
     2  0010      1101    -3      1010     10
     3  0011      1100    -4      1011     11
     4  0100      1011    -5      1100     12
     5  0101      1010    -6      1101     13
     6  0110      1001    -7      1110     14
     7  0111      1000    -8      1111     15
    

    The first column is the number. Next column shows the two's complement binary representation. The "Inverted" column shows the binary result of inverting the bits, and the next column shows the decimal value that represents. This is the value that we place in the high bits of the result. As you can see, the numbers have switched places, with the lowest (-8) becoming the highest. That will give us a descending sort of the values.

    The lower bits don't matter unless the high bits are equal. The "Bit Flipped" column shows the binary representation of the value after we flip the high bit. What this does is map the numbers -8 through 7 to the unsigned range 0 through 15, thus ensuring that the lower bits of the number are compared correctly.