7

I need to be able to convert from a Delphi Real48 to C# double.

I've got the bytes I need to convert but am looking for an elegant solution. to the problem.

Anybody out there had to do this before?

I'm needing to do the conversion in C#

Thanks in advance

mat-mcloughlin
  • 6,492
  • 12
  • 45
  • 62

5 Answers5

8

I've done some hunting around and I found some C++ code to do the job, converted it and it seems to be giving the right answer... damned if I understand it all though :S

    private static double Real48ToDouble(byte[] real48)
    {

        if (real48[0] == 0)
            return 0.0; // Null exponent = 0

        double exponent = real48[0] - 129.0;
        double mantissa = 0.0;

        for (int i = 1; i < 5; i++) // loop through bytes 1-4
        {
            mantissa += real48[i];
            mantissa *= 0.00390625; // mantissa /= 256
        }


        mantissa += (real48[5] & 0x7F);
        mantissa *= 0.0078125; // mantissa /= 128
        mantissa += 1.0;

        if ((real48[5] & 0x80) == 0x80) // Sign bit check
            mantissa = -mantissa;

        return mantissa * Math.Pow(2.0, exponent);
    }

If somebody can explain it that would be great :D

heinrich5991
  • 2,694
  • 1
  • 19
  • 26
mat-mcloughlin
  • 6,492
  • 12
  • 45
  • 62
  • Bytes 1 through 5 represent the fraction part of a number in scientific notation: `1.x * 2^e`. The mantissa is `1.x`. The *for* loop and the two following lines generate *x*. Suppose byte 1 is 0xa5. In binary, that's 10100101. Add that to `mantissa` to get `mantissa == 0xa5`. Then *shift* those bytes down into the fractional part to get the binary value 0.10100101. Shifting 8 is dividing by 256. Repeat for bytes 2 through 4. Byte 5 is special since we only want 7 bits — the eighth bit is the sign bit — so divide by 128 instead. Finally add 1 since that part is *implicit* (not stored anywhere). – Rob Kennedy Mar 24 '10 at 19:45
  • Byte 0 is the exponent. It's an unsigned number, but it's biased high by 129, so the first thing to do is correct for that bias. As mentioned in the previous comment, the number is in the form `1.x * 2^e`, where `1.x` is stored in `mantissa` and `e` is stored in `exponent`. The final line of code simply calculates that value as a double. – Rob Kennedy Mar 24 '10 at 19:49
  • 2
    Note to future readers: I'm fairly sure this code has errors. For one thing, it ignores the byte value at real48[4]. Caution advised. – Kevin A. Naudé Feb 05 '11 at 18:39
  • @KevinA.Naudé I'm pretty sure the error is fixed by including the byte value at real48[4]. (I added it to the answer) – heinrich5991 May 19 '13 at 12:26
  • @heinrich5991 Did you test the change? – Lasse V. Karlsen May 19 '13 at 12:38
  • @LasseV.Karlsen yes, with two reservations: 1) I didn't run this as C#-code, I wrote equivalent python code and 2) I didn't have a large test set, tested about 10 real48s. – heinrich5991 May 19 '13 at 12:59
  • @heinrich5991 The change looks good. I have no legacy data for testing. Here are things that need checking: 1) was big-endian byte order used for this format? 2) At least one online source suggests that both [the mantissa and exponent bits must be zero](http://home.online.no/~pjacklam/matlab/software/util/numutil/real48_as_uint8_to_double.m) to force a zero literal, while several sources imply that a zero in the exponent is sufficient. I no longer have any original TP documentation handy. – Kevin A. Naudé May 20 '13 at 09:58
  • @heinrich5991 Follow up. According to [Embarcadero](http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/devcommon/internaldataformats_xml.html), current makers of Delphi, the above code is now correct. Their docs also indicate that byte-order is little endian, and that the mantissa field can be ignored if the exponent field is zero. Of course, there may be other interpretations [in the wild](http://home.online.no/~pjacklam/matlab/software/util/numutil/real48_as_uint8_to_double.m). – Kevin A. Naudé May 21 '13 at 09:33
3
static double GetDoubleFromBytes(byte[] bytes)
{
    var real48 = new long[6];
    real48[0] = bytes[0];
    real48[1] = bytes[1];
    real48[2] = bytes[2];
    real48[3] = bytes[3];
    real48[4] = bytes[4];
    real48[5] = bytes[5];

    long sign = (real48[0] & 0x80) >> 7;

    long significand = 
        ((real48[0] % 0x80) << 32) + 
         (real48[1] << 24) + 
         (real48[2] << 16) + 
         (real48[3] << 8) + 
         (real48[4]);

    long exponent = bytes[5];

    if (exponent == 0)
    {
        return 0.0;
    }

    exponent += 894;
    long bits = (sign << 63) + (exponent << 52) + (significand << 13);
    return BitConverter.Int64BitsToDouble(bits);
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • From Delphi Basics: "Real48:Obsolete - The floating point type with the highest capacity and precision." In modern versions of Delphi that's an Extended (10 Bytes) – James Mar 24 '10 at 11:00
  • @Darin, I'm afraid this doesn't seem to be giving the correct answer – mat-mcloughlin Mar 24 '10 at 11:10
  • Thought I'd check SizeOf(Real48) does indeed yield 6. – James Mar 24 '10 at 11:28
  • 1
    When you assign `bits`, you've re-created the *bit pattern* of a double, but when you pass that to `Convert.ToDouble`, it treats it as an ordinary integer and converts that *integer value* to a double, the same as you'd get from an ordinary long-to-double assignment statement. What you probably want is `BitConverter.ToDouble` instead. – Rob Kennedy Mar 24 '10 at 19:32
2

Appreciate this is an old post, but also the following may be useful for those looking to do this in T-SQL (which I was).

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[ifn_HexReal48ToFloat]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
    drop function [dbo].[ifn_HexReal48ToFloat]
go

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

create function [dbo].[ifn_HexReal48ToFloat]
(
    @strRawHexBinary    char(12),       -- NOTE. Do not include the leading 0x
@bitReverseBytes    bit 
)
RETURNS FLOAT
AS
BEGIN

-- Reverse bytes if required
-- e.g. 3FF4 0000 0000 is stored as
--      0000 0000 F43F
declare @strNewValue    varchar(12)
if @bitReverseBytes = 1
begin   
    set @strNewValue='' 
    declare @intCounter int
    set @intCounter = 6

    while @intCounter>=0
    begin
        set @strNewValue = @strNewValue + substring(@strRawHexBinary, (@intCounter * 2) + 1,2) 
        set @intCounter = @intCounter - 1
    end 
end

-- Convert the raw string into a binary
declare @binBinaryFloat binary(6)
set @binBinaryFloat = convert(binary(6),'0x' + isnull(@strNewValue, @strRawHexBinary),1)

-- Based on original hex to float conversion at http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=81849
-- and storage format documented at 
-- http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/devcommon/internaldataformats_xml.html
-- Where, counting from the left
-- Sign         = bit 1
-- Exponent     = bits 41 - 48      with a bias of 129
-- Fraction     = bits 2 - 40


return

    SIGN
    (
        CAST(@binBinaryFloat AS BIGINT)
    )
    * 
    -- Fraction part. 39 bits. From left 2 - 40. 
    (
        1.0 + 
        (CAST(@binBinaryFloat AS BIGINT) & 0x7FFFFFFFFF00) * POWER(CAST(2 AS FLOAT), -47)
)
* 
    -- Exponent part. 8 bits. From left bits 41 -48
    POWER
    (
        CAST(2 AS FLOAT), 
        (
            CAST(@binBinaryFloat AS BIGINT) & 0xff
            - 129 
        ) 
    )

end

Confirmation

0.125 is 0x 0000 0000 007E (or 0x 7E00 0000 0000 reversed)

select dbo.ifn_HexReal48ToFloat('00000000007E', 0)
select dbo.ifn_HexReal48ToFloat('7E0000000000', 1) 

The input is a char12 as I had to extract the binary from the middle of 2 other larger binary fields and shunt them together so had it already as char12. Easy enough to change to be binary(6) input if don't need to do any manipulation beforehand.

As an aside, in the scenario I'm implementing into, the T-SQL variant is outperformed by C# CLR code so the C# code above may be better. Whilst not everywhere allows CLR code into SQL Server if you can then maybe you should. For more background an article at http://www.simple-talk.com/sql/t-sql-programming/clr-performance-testing/ does some in depth measurement which shows some dramatic differences between T-SQL and CLR.

Simon Molloy
  • 1,774
  • 1
  • 11
  • 13
1

I have been testing this and have found an error (as others have noticed) with negative values. Here is my tested version of the code. I tested this with 120,530 different random values ranging from 11,400,000.00 to -2,000,000.00

 //This seems to be the layout of the Real48 bits where
        //E = Exponent
        //S = Sign bit
        //F = Fraction

        //EEEEEEEE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF SFFFFFFF
        //12345678 12345678 12345678 12345678 12345678 12345678


        Double exponentbase = 129d;  // The exponent is offest by 129
        Double exponent = real48[0] - exponentbase; // deduct the offest. 

        // Calculate the mantissa 
        Double mantissa = 0.0;
        Double value = 1.0;

        // For Each Byte. 
        for (int iByte = 5; iByte >= 1; iByte--)
        {
            int startbit = 7;
            if (iByte == 5)
            { startbit = 6; } //skip the sign bit. 

            //For Each Bit 
            for (int iBit = startbit; iBit >= 0; iBit--)
            {
                value = value / 2;// Each bit is worth half the next bit but we're going backwards. 
                if (((real48[iByte] >> iBit) & 1) == 1) //if this bit is set. 
                {
                    mantissa += value; // add the value. 
                }

            }
        }

        if (mantissa == 1.0 && real48[0] == 0) // Test for null value 
            return 0.0;

        double result;

        result = (1 + mantissa) * Math.Pow(2.0, exponent);

        if ((real48[5] & 0x80) == 0x80) // Sign bit check 
            result = -result;

        return result;
Ben Buck
  • 11
  • 1
0

I've changed the code you've posted into a more readable format so you can see how it works:

        Double exponentbase = 129d;
        Double exponent = real48[0] - exponentbase; // The exponent is offest so deduct the base.

        // Now Calculate the mantissa
        Double mantissa = 0.0;
        Double value = 1.0;
        // For Each Byte.
        for (int i = 5; i >= 1; i--)
        {
            int startbit = 7;
            if (i == 5)
            { startbit = 6; } //skip the sign bit.

            //For Each Bit
            for (int j = startbit; j >= 0; j--)
            {
                value = value / 2;// Each bit is worth half the next bit but we're going backwards.
                if (((real48[i] >> j) & 1) == 1) //if this bit is set.
                {
                    mantissa += value; // add the value.
                }

            }
        }

        if (mantissa == 1.0 && real48[0] == 0) // Test for null value
            return 0.0;

        if ((real48[5] & 0x80) == 1) // Sign bit check
            mantissa = -mantissa;

        return (1 + mantissa) * Math.Pow(2.0, exponent);
James
  • 9,774
  • 5
  • 34
  • 58