Search code examples
c#ntp

Converting between NTP and C# DateTime


I use the following code to convert between NTP and a C# DateTime. I think the forward corversion is correct, but backwards is wrong.

See the following code to convert 8 bytes into a DatTime:

Convert NTP to DateTime

public static ulong GetMilliSeconds(byte[] ntpTime)
{
    ulong intpart = 0, fractpart = 0;

    for (var i = 0; i <= 3; i++)
        intpart = 256 * intpart + ntpTime[i];
    for (var i = 4; i <= 7; i++)
        fractpart = 256 * fractpart + ntpTime[i];

    var milliseconds = intpart * 1000 + ((fractpart * 1000) / 0x100000000L);

    Debug.WriteLine("intpart:      " + intpart);
    Debug.WriteLine("fractpart:    " + fractpart);
    Debug.WriteLine("milliseconds: " + milliseconds);
    return milliseconds;
}

public static DateTime ConvertToDateTime(byte[] ntpTime)
{
    var span = TimeSpan.FromMilliseconds(GetMilliSeconds(ntpTime));
    var time = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    time += span;
    return time;
}

Convert from DateTime to NTP

public static byte[] ConvertToNtp(ulong milliseconds)
{
    ulong intpart = 0, fractpart = 0;
    var ntpData = new byte[8];

    intpart = milliseconds / 1000;
    fractpart = ((milliseconds % 1000) * 0x100000000L) / 1000;

    Debug.WriteLine("intpart:      " + intpart);
    Debug.WriteLine("fractpart:    " + fractpart);
    Debug.WriteLine("milliseconds: " + milliseconds);

    var temp = intpart;
    for (var i = 3; i >= 0; i--)
    {
        ntpData[i] = (byte)(temp % 256);
        temp = temp / 256;
    }

    temp = fractpart;
    for (var i = 7; i >= 4; i--)
    {
        ntpData[i] = (byte)(temp % 256);
        temp = temp / 256;
    }
    return ntpData;
}

The following input produces the output:

bytes = { 131, 170, 126, 128,
           46, 197, 205, 234 }

var ms = GetMilliSeconds(bytes );
var ntp = ConvertToNtp(ms)

//GetMilliSeconds output
milliseconds: 2208988800182
intpart:      2208988800
fractpart:    784715242

//ConvertToNtp output
milliseconds: 2208988800182
intpart:      2208988800
fractpart:    781684047

Notice that the conversion from milliseconds to fractional part is wrong. Why?

Update:

As Jonathan S. points out - it's loss of fraction. So instead of converting back and forth, I want to manipulate with the NTP timestamp directly. More specific, add milliseconds to it. I would assume the following function would do just that, but I'm having a hard time validating it. I am very unsure about the fraction-part.

public static void AddMilliSeconds(ref byte[] ntpTime, ulong millis)
{
    ulong intpart = 0, fractpart = 0;

    for (var i = 0; i < 4; i++)
        intpart = 256 * intpart + ntpTime[i];
    for (var i = 4; i <= 7; i++)
        fractpart = 256 * fractpart + ntpTime[i];

    intpart += millis / 1000;
    fractpart += millis % 1000;

    var newIntpart = BitConverter.GetBytes(SwapEndianness(intpart));
    var newFractpart = BitConverter.GetBytes(SwapEndianness(fractpart));

    for (var i = 0; i < 8; i++)
    {
        if (i < 4)
            ntpTime[i] = newIntpart[i];
        if (i >= 4)
            ntpTime[i] = newFractpart[i - 4];
    }
}

Solution

  • What you're running into here is loss of precision in the conversion from NTP timestamp to milliseconds. When you convert from NTP to milliseconds, you're dropping part of the fraction. When you then take that value and try to convert back, you get a value that's slightly different. You can see this more clearly if you change your ulong values to decimal values, as in this test:

    public static decimal GetMilliSeconds(byte[] ntpTime)
    {
        decimal intpart = 0, fractpart = 0;
    
        for (var i = 0; i <= 3; i++)
            intpart = 256 * intpart + ntpTime[i];
        for (var i = 4; i <= 7; i++)
            fractpart = 256 * fractpart + ntpTime[i];
    
        var milliseconds = intpart * 1000 + ((fractpart * 1000) / 0x100000000L);
    
        Console.WriteLine("milliseconds: " + milliseconds);
        Console.WriteLine("intpart:      " + intpart);
        Console.WriteLine("fractpart:    " + fractpart);
        return milliseconds;
    }
    
    public static byte[] ConvertToNtp(decimal milliseconds)
    {
        decimal intpart = 0, fractpart = 0;
        var ntpData = new byte[8];
    
        intpart = milliseconds / 1000;
        fractpart = ((milliseconds % 1000) * 0x100000000L) / 1000m;
    
        Console.WriteLine("milliseconds: " + milliseconds);
        Console.WriteLine("intpart:      " + intpart);
        Console.WriteLine("fractpart:    " + fractpart);
    
        var temp = intpart;
        for (var i = 3; i >= 0; i--)
        {
            ntpData[i] = (byte)(temp % 256);
            temp = temp / 256;
        }
    
        temp = fractpart;
        for (var i = 7; i >= 4; i--)
        {
            ntpData[i] = (byte)(temp % 256);
            temp = temp / 256;
        }
        return ntpData;
    }
    
    public static void Main(string[] args)
    {
        byte[] bytes = { 131, 170, 126, 128,
               46, 197, 205, 234 };
    
        var ms = GetMilliSeconds(bytes);
        Console.WriteLine();
        var ntp = ConvertToNtp(ms);
    }
    

    This yields the following result:

    milliseconds: 2208988800182.7057548798620701
    intpart:      2208988800
    fractpart:    784715242
    
    milliseconds: 2208988800182.7057548798620701
    intpart:      2208988800.1827057548798620701
    fractpart:    784715242.0000000000703594496
    

    It's the ~0.7 milliseconds that are screwing things up here.

    Since the NTP timestamp includes a 32-bit fractional second ("a theoretical resolution of 2^-32 seconds or 233 picoseconds"), a conversion to integer milliseconds will result in a loss of precision.

    Response to Update:

    Adding milliseconds to the NTP timestamp wouldn't be quite as simple as adding the integer parts and the fraction parts. Think of adding the decimals 1.75 and 2.75. 0.75 + 0.75 = 1.5, and you'd need to carry the one over to the integer part. Also, the fraction part in the NTP timestamp is not base-10, so you can't just add the milliseconds. Some conversion is necessary, using a proportion like ms / 1000 = ntpfrac / 0x100000000.

    This is entirely untested, but I'd think you'd want to replace your intpart += and fracpart += lines in AddMilliSeconds to be more like this:

    intpart += millis / 1000;
    
    ulong fractsum = fractpart + (millis % 1000) / 1000 * 0x100000000L);
    
    intpart += fractsum / 0x100000000L;
    fractpart = fractsum % 0x100000000L;