Convert a c# decimal to big-endian byte array

Posted on

Problem

Following the avro schema documentation for decimals I’ve created a method to turn a decimal into a byte array. The goals are:

  1. Should be represented by a non-scaled integer
  2. Should be big-endian

I’ve tested the code I wrote, but I’d like to know if the program still works in any unforeseen corner cases.

private static byte[] ToBigEndianByteArray(decimal value, int scale)
{
    var bigInteger = new BigInteger(value * (decimal)Math.Pow(10, scale));
    var bytes = bigInteger.ToByteArray();
    return BitConverter.IsLittleEndian ? bytes.Reverse().Select(ReverseEndianness).ToArray() : bytes;

    static byte ReverseEndianness(byte input)
    {
        const int bits = 8;
        return (byte)Enumerable.Range(0, bits - 1)
            .Aggregate(0, (accumulator, index) =>
                BitAtIndexIsSet(input, index)
                    ? SetBitAtIndex(accumulator, bits - 1 - index)
                    : accumulator);

        static int SetBitAtIndex(int value, int index) => value | 1 << index;
        static bool BitAtIndexIsSet(int value, int index) => (value & (1 << index)) != 0;
    }
}

Example usage: ToBigEndianByteArray(8.45m, 2) produces 0000001101001101 (845 in decimal).

Solution

It does not look correct to me, in two ways:

  1. Computing Math.Pow(10, scale). This is not exact when scale is negative or large. Since it’s fine for moderate positive scales and the amount of inexactness in other cases typically won’t be very high, it may be difficult to actually produce a wrong result, but I would not dare to rely on it – especially for a format that is supposed to be exact.
  2. Big-endian does not mean reversing the bits in every byte, which I think the ReverseEndianness does, but it’s a relatively confusing function, and I don’t mean the bitwise part of it but rather everything except that. I would have used something like this adapted for C#. Doesn’t really matter in the end, because the specification does not say to reverse the bits to begin with, so you can remove that whole function.

Furthermore I think the API is hard to use correctly, because it assumes that you already have a scale, which is not trivial to find. Passing the wrong scale can very easily result in information being lost.

Alternatively, you may be able to use the GetBits method on the decimal, extract its internal scale (and use it directly as the scale in avro format, which uses the same kind of scales), and convert the integer part that it has internally to a big-endian (but not bit-reversed) array of bytes. It seems to me that this would severely limit the possibility of both incorrect API usage, and incorrect implementation.

Leave a Reply

Your email address will not be published. Required fields are marked *