Converting between NTP and C# DateTime
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;
Suggestion To Cameron's Solution: use
ntpEpoch = (new DateTime(1900, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).Ticks;
to make sure that you dont calculate from your local time
Same as others but without division
return (ulong)(elapsedTime.Ticks * 1e-7 * 4294967296ul)
Or
return (ulong)(((long)(elapsedTime.Ticks * 0.0000001) << 32) + (elapsedTime.TotalMilliseconds % 1000 * 4294967296 * 0.001));
//TicksPerPicosecond = 0.0000001m
//4294967296 = uint.MaxValue + 1
//0.001 == PicosecondsPerNanosecond
The full method would then be:
public static System.DateTime UtcEpoch2036 = new System.DateTime(2036, 2, 7, 6, 28, 16, System.DateTimeKind.Utc);
public static System.DateTime UtcEpoch1900 = new System.DateTime(1900, 1, 1, 0, 0, 0, System.DateTimeKind.Utc);
public static ulong DateTimeToNptTimestamp(ref System.DateTime value/*, bool randomize = false*/)
{
System.DateTime baseDate = value >= UtcEpoch2036 ? UtcEpoch2036 : UtcEpoch1900;
System.TimeSpan elapsedTime = value > baseDate ? value.ToUniversalTime() - baseDate.ToUniversalTime() : baseDate.ToUniversalTime() - value.ToUniversalTime();
//Media.Common.Extensions.TimeSpan.TimeSpanExtensions.MicrosecondsPerMillisecond = 1000
//TicksPerPicosecond = 0.0000001m = 1e-7
//4294967296 = uint.MaxValue + 1
//0.001 == PicosecondsPerNanosecond = 1e-3
//429496.7296 Picoseconds = 4.294967296e-7 Seconds
//4.294967296e-7 * 1000 Milliseconds per second = 0.0004294967296 * 1e+9 (PicosecondsPerMilisecond) = 429.4967296
//0.4294967296 nanoseconds * 100 nanoseconds = 1 tick = 42.94967296 * 10000 ticks per millisecond = 429496.7296 / 1000 = 429.49672960000004
unchecked
{
//return (ulong)((long)(elapsedTime.Ticks * 0.0000001m) << 32 | (long)((decimal)elapsedTime.TotalMilliseconds % 1000 * 4294967296m * 0.001m));
//return (ulong)(((long)(elapsedTime.Ticks * 0.0000001m) << 32) + (elapsedTime.TotalMilliseconds % 1000 * 4294967296ul * 0.001));
//return (ulong)(elapsedTime.Ticks * 1e-7 * 4294967296ul); //ie-7 * 4294967296ul = 429.4967296 has random diff which complies better? (In order to minimize bias and help make timestamps unpredictable to an intruder, the non - significant bits should be set to an unbiased random bit string.)
//return (ulong)(elapsedTime.Ticks * 429.4967296m);//decimal precision is better but we still lose precision because of the magnitude? 0.001 msec dif ((ulong)(elapsedTime.Ticks * 429.4967296000000000429m))
//429.49672960000004m has reliable 003 msec diff
//Has 0 diff but causes fraction to be different from examples...
//return (ulong)((elapsedTime.Ticks + 1) * 429.4967296m);
//Also adding + 429ul;
return (ulong)(elapsedTime.Ticks * 429.496729600000000000429m);
//var ticks = (ulong)(elapsedTime.Ticks * 429.496729600000000000429m); //Has 0 diff on .137 measures otherwise 0.001 msec or 1 tick, keeps the examples the same.
//if(randomize) ticks ^= (ulong)(Utility.Random.Next() & byte.MaxValue);
//return ticks;
}
Where as the reverse would be:
public static System.DateTime NptTimestampToDateTime(ref uint seconds, ref uint fractions, System.DateTime? epoch = null)
{
//Convert to ticks
//ulong ticks = (ulong)((seconds * System.TimeSpan.TicksPerSecond) + ((fractions * System.TimeSpan.TicksPerSecond) / 0x100000000L)); //uint.MaxValue + 1
unchecked
{
//Convert to ticks,
//'UtcEpoch1900.AddTicks(seconds * System.TimeSpan.TicksPerSecond + ((long)(fractions * 1e+12))).Millisecond' threw an exception of type 'System.ArgumentOutOfRangeException'
//0.01 millisecond = 1e+7 picseconds = 10000 nanoseconds
//10000 nanoseconds = 10 micros = 10000000 pioseconds
//0.001 Centisecond = 10 Microsecond
//1 Tick = 0.1 Microsecond
//0.1 * 100 Nanos Per Tick = 100
//TenMicrosecondsPerPicosecond = 10000000 = TimeSpan.TicksPerSecond = 10000000
//System.TimeSpan.TicksPerSecond is fine here also...
long ticks = seconds * System.TimeSpan.TicksPerSecond + ((long)(fractions * Media.Common.Extensions.TimeSpan.TimeSpanExtensions.TenMicrosecondsPerPicosecond) >> Common.Binary.BitsPerInteger);
//Return the result of adding the ticks to the epoch
//If the epoch was given then use that value otherwise determine the epoch based on the highest bit.
return epoch.HasValue ? epoch.Value.AddTicks(ticks) :
(seconds & 0x80000000L) == 0 ?
UtcEpoch2036.AddTicks(ticks) :
UtcEpoch1900.AddTicks(ticks);
}
}