7

How do I properly serialize a DateTime object (e.g. using a BinaryWriter), and preserve its complete state?

I was under the impression that a date time was only represented by an internal long integer, and that this integer was accessible as the Ticks property of the DateTime. However, looking at the implementation, the Ticks property actually returns a subset of the real internal data which is stored in an ulong called dateData

Ticks (which just gets InternalTicks) is implemented like so:

public long InternalTicks
{
    get { return (long) this.dateData & 4611686018427387903L; }
}

As far as I can see this means the dateData may contain information that is not revealed by the Ticks property.

Stranger yet, the BinaryFormatter Serialization of a DateTime does this in GetObjectData():

info.AddValue("ticks", this.InternalTicks);
info.AddValue("dateData", this.dateData);

That will output two longs in the stream, where one of them would be easily recovererd from the other!

How can I serialize my DateTime without risk of losing any of the internal state, (preferably of course in just 8 bytes, and without reflection). I'm thinking maybe it can be cast (unsafe), directly to an ulong?

Or am I worrying for no reason, will the Ticks property actually encode all the necessary state?

Anders Forsgren
  • 10,827
  • 4
  • 40
  • 77

2 Answers2

15

There are two pieces of information to worry about:

  • The Ticks
  • The DateTimeKind

Internally these are both encoded into a single long, the dateData like so:

this.dateData = (ulong) (ticks | (((long) kind) << 62));

So the Ticks property will not encode all the state. It will be missing the DateTimeKind information.

The dateData does encode all the data, so it is a curious thing that the serialiser stores both that and Ticks!

So what you could do is this:

ulong dataToSerialise = (ulong) (date.Ticks | ((long) date.Kind) << 62);

And when deserializing, you can do this:

long ticks = (long)(deserialisedData & 0x3FFFFFFFFFFFFFFF);
DateTimeKind kind = (DateTimeKind)(deserialisedData >> 62);
DateTime date = new DateTime(ticks, kind);

This does make use of knowledge about the internals of DateTime, and it could theoretically change in the future, which could break this kind of serialisation.


EDIT

There are some gotchas to do with local time adjustment.

So I'm going to suggest that instead of messing about with all of the above, you look at the DateTime.ToBinary() and DateTime.FromBinary() methods which will allow you to serialize as a long, subject to the caveats relating to the local time adjustment. These caveats are fully documented in the MSDN links above.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Thanks, the question though is: how to write the private field dateData to a BinaryWriter. That is: how do I unsafe cast a datetime to the ulong that is its internal state? – Anders Forsgren Apr 10 '13 at 07:53
  • Just replicate what the internal implementation does. I'll update with an example in a minute. – Matthew Watson Apr 10 '13 at 07:57
  • ah, you mean or-ing together the values. Clever. – Anders Forsgren Apr 10 '13 at 07:58
  • Doesn't this lose information about which `Local` kind was used? (i.e. information about DST being in effect for times which are during the DST on/off switch). – CodesInChaos Apr 10 '13 at 08:11
  • @CodesInChaos No it doesn't, because that's what DateTimeKind is for. If you use Reflector to inspect the implementation of DateTime, you'll see that there is only one ulong field used to store all the data. – Matthew Watson Apr 10 '13 at 08:16
  • @MatthewWatson There are `DateTime`s which equal `Kind` and `Ticks` which give different values when converted to `UTC`. That's because there are 4 kinds internally (including a second variant of `Local`) but only 3 of them are exposed to the public. It only affects the single hour per year, where the clock is turned back, but it loses information. – CodesInChaos Apr 10 '13 at 08:18
  • @CodesInChaos Nevertheless, you can look at the implementation and see for sure that there is *only* a single ulong field used to store all the information. And `DateTime` is a struct, so how can you have different kinds of them? I must confess, I'm not sure what you mean. I have the source code for `DateTime` in front of me and it definitely only has the single ulong field. – Matthew Watson Apr 10 '13 at 08:21
  • There are 2 bits used for the `Kind`, which allows 4 different kinds (`Unspecified`, `Utc`, `Local`, and `Local2`). When you check the `Kind` property, it returns `Local`, even if it's internally `Local2`. Since I don't have the source, I don't know how it's implemented. But if you construct two UTC times (differing by 1h) that map to the same local time due to the clock being turned back, they still survive a round-trip to `Local`. This proves that there must be a second `Local` kind that disambiguates these. Presumably stored as the 4th state of those two bits. – CodesInChaos Apr 10 '13 at 08:25
  • @CodesInChaos Aha, I think there's a single bit in dateData which may be set for that purpose, so let's sort that out. – Matthew Watson Apr 10 '13 at 08:26
  • So ToBinary() and or:ing together the kinds both have Caveats? Since the DateTime just *is* exactly 8 bytes of internal state, what I really want is to just write exactly those 8 bytes. – Anders Forsgren Apr 10 '13 at 08:49
  • 1
    @AndersForsgren Well if you did just serialize the internal 8 bytes blindly and then deserialize them in a *different* timezone (or indeed in a different DST time of year), you would get the *wrong* results, so it's better to use `ToBinary()` and `FromBinary()` which will give the correct results. – Matthew Watson Apr 10 '13 at 09:01
  • @MatthewWatson I guess there are 3 aspects of it: 1) roundtrip returns the same kind of date to the user 2) roundtrip writes the same bits on disk 3) UTC instant is preferred. I can have 2 out of 3. Using ToBinary() will give me 1+3. Forcing the kind to UTC when writing will give me 2+3. I think I actually prefer the latter. – Anders Forsgren Apr 10 '13 at 09:17
  • I want to add that `ToBinary()` takes about 3 times longer on local timestamps. So using UTC also has performance benefits. – Toxantron May 04 '16 at 06:15
5

i have done this with serialization for transmitting date in TCP Sockets

here is code you can serialize any object like this

public static byte[] DateToBytes(DateTime _Date)
{
    using (System.IO.MemoryStream MS = new System.IO.MemoryStream()) {
        BinaryFormatter BF = new BinaryFormatter();
        BF.Serialize(MS, _Date);
        return MS.GetBuffer();
    }
}


public static DateTime BytesToDate(byte[] _Data)
{
    using (System.IO.MemoryStream MS = new System.IO.MemoryStream(_Data)) {
        MS.Seek(0, SeekOrigin.Begin);
        BinaryFormatter BF = new BinaryFormatter();
        return (DateTime)BF.Deserialize(MS);
    }
}

EDIT

without binaryformatter

//uses 8 byte
DateTime tDate = DateAndTime.Now;
long dtVal = tDate.ToBinary();
//64bit binary

byte[] Bits = BitConverter.GetBytes(tDate.ToBinary());
//your byte output

//reverse
long nVal = BitConverter.ToInt64(Bits, 0);
//get 64bit binary
DateTime nDate = DateTime.FromBinary(nVal);
//convert it to date 
Jack Gajanan
  • 1,596
  • 14
  • 18
  • 1
    Should perhaps have been more clear: I don't want to use reflection, or binaryformatter, and the reason is that it needs to be fast and use only 8 bytes. – Anders Forsgren Apr 10 '13 at 08:12
  • Props for the use of BitConverter, new class to me and clean simple solution. :-) – Norman H Jan 21 '15 at 20:08