5

I have already found several answers related to converting a std::time_t value to System::DateTime and back. However, almost all answers seem to neglect that the type of std::time_t is actually undefined in the standard. Most solutions just cast std::time_t to whatever needed or apply arithmetic operations to a std::time_t object which is possible since it's an arithmetic type, but there is no specification about the result of such an operation. I know that most compilers define time_t as an int of some size but the fact alone that it has changed from int32 to int64 in many implementations recently shows that changes are indeed possible.

So I've come up with this solution which should work with any type of std::time_t. It works from what I have seen. But I was wondering - Are there any possible pitfalls I might be unaware of?

template <>
inline System::DateTime marshal_as(const std::time_t &from_object)
{
    // Returns DateTime in Local time format from time_t (assumed to be UTC)
    const auto unix_epoch = makeUtcTime(1970, 1, 1, 0, 0, 0);
    const auto unix_epoch_dt = System::DateTime(1970, 1, 1, 0, 0, 0, System::DateTimeKind::Utc);
    const auto secondsSinceEpoch = std::difftime(from_object, unix_epoch);
    return const_cast<System::DateTime&>(unix_epoch_dt).AddSeconds(secondsSinceEpoch).ToLocalTime();
} // end of System::DateTime marshal_as(const std::time_t &from_object)

template <>
inline std::time_t marshal_as(const System::DateTime &from_object)
{
    // Returns time_t in UTC format from DateTime
    auto from_dt = const_cast<System::DateTime&>(from_object).ToUniversalTime();
    return makeUtcTime(from_dt.Year, from_dt.Month, from_dt.Day, from_dt.Hour, from_dt.Minute, from_dt.Second);
} // end of std::time_t marshal_as(const System::DateTime &from_object)

3 assumptions were made:

  • Resulting std::time_t should be in UTC since it doesn't contain any info on localization
  • Resulting System::DateTime should be local time since System::DateTime::Now returns a localized DateTime
  • makeUtcTime is a helper function creating a std::tm from the values supplied and creates a UTC std::time_t out of it. This is currently implemented using _mkgmtime because our interop code can safely rely on the existence of Microsoft extensions. However, a UTC version of mktime is readily available in other compilers as well (standard mktime expects local time).

2 less important things to consider:

  • The const_cast is necessary because the marshal_as-template expects a const T& as parameter and I can't access the properties of a const .NET value-type object. However there might be a better solution.
  • Should the unix_epoch... stuff be static const?

(I wasn't sure if this should be posted on "Programmers Exchange" since it's more of a discussion but since it's a very specific C++ question I thought SO might be the better place to ask)

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Excelcius
  • 1,680
  • 1
  • 14
  • 31
  • There are two possible time_t definitions, depending on whether _USE_32BIT_TIME_T is defined. If defined, it will work for as long as 32-bit time_t works. System::DateTime is 64 bit. – cup Jan 21 '14 at 20:32
  • That's true, thanks for the info. But since the resulting time_t in the second marshal_as is constructed using the calendar-values (Year, Month, ...), the worst thing that can happen is that the time_t is returned as (time_t)(-1), which basically means the conversion failed because the chosen time_t implementation can't represent the DateTime. But there you go, reason #1 why simply converting TotalSeconds to time_t might fail miserably. – Excelcius Jan 21 '14 at 20:55
  • See here: http://stackoverflow.com/questions/9864339/boost-parse-date-time-string-and-yield-net-compatible-ticks-value – Ben Feb 01 '14 at 10:00

2 Answers2

16

It just isn't very productive to insist on a "standard conformant" way to make this conversion. The only place where an std::time_t and a System::DateTime are ever going to meet is covered by the Ecma-372 standard. Of which there is right now, and surely will ever be, only one implementation. The Mono project could be assumed to be the most likely source of another one, but right now they appear entirely uninterested in providing a mixed-mode implementation, the only reason you'd ever consider using C++/CLI.

std::time_t is steadily marching towards the Y2K38 disaster. With Microsoft pre-emptively having done something about it, and really having to do so because they went for LLP64, but everybody else counting on their LP64 data model keeping them out of trouble. In other words, no remaining 32-bit processors still running in 2038. This could well be a self-fulfilling prophesy.

Regardless, the conversion is going to have to work with elapsed seconds since 1/1/1970. And that can be a 32-bit or a 64-bit integral value, depending on implementation. The only warranty I can give is that this code is at least good until 2038:

#include <ctime>

using namespace System;

public ref class Conversions {
public:
    static DateTime time_t2DateTime(std::time_t date) {
        double sec = static_cast<double>(date);
        return DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind::Utc).AddSeconds(sec);
    }
    static std::time_t DateTime2time_t(DateTime date) {
        TimeSpan diff = date.ToUniversalTime() - DateTime(1970, 1, 1);
        return static_cast<std::time_t>(diff.TotalSeconds);
    }
};
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thanks, I was hoping for a more direct review of my code, but you're absolutely right, in this context I might as well assume `std::time_t` to be what Microsoft wanted it to be and keep the code to a minimum. Thanks for the elaborate answer. – Excelcius Feb 08 '14 at 08:58
  • 4
    time_t is in seconds, but you are adding the value as milliseconds. I think you should use `AddSeconds` and `TotalSeconds`. – Yongwei Wu Dec 04 '14 at 05:17
  • Great answer but it should be .AddSeconds instead of .AddMilliSeconds. Also for local time use DateTime::Local instead of DateTime::Utc. – Vijay Bansal Feb 27 '18 at 16:00
  • Fixed for seconds, but I think I need to stick with UTC. – Hans Passant Feb 27 '18 at 16:49
0

Here's the solution my team arrived at:

DateTime represents the number of mixed-fractional days since midnight December 30, 1899, expressed as a double. I believe that this epoch date was used to account for the fact that 1900 was not a leap year, and it allows for an extra two days (Why two and not one? - It is not clear to me why Dec 31, 1899 was not chosen as their epoch.)

So a DateTime of 2.50 would be equivalent to January 1, 1900 12:00:00 , (i.e. the fraction represents 1/2 the day - 12PM).

We calculated that Jan 1, 1970 - the Unix Epoch - is 25569 days after the DateTime Epoch.

So the equivalent formula would be:

#include <time.h>
System::DateTime toDateTime(time_t &t)
{
    return 25569.0 + t / 86400.0; // ensure you use floating point math or you will truncate the fractional portion
}
  • The Microsoft documents says this about the one-parameter DateTime constructor: A date and time expressed in the number of 100-nanosecond intervals that have elapsed since January 1, 0001 at 00:00:00.000 in the Gregorian calendar. What information are you referring to? – Yongwei Wu Dec 04 '14 at 02:36
  • `Why two and not one? - It is not clear to me why Dec 31, 1899 was not chosen as their epoch.)` I think Dec. 30 might be a mistake/typo because I cannot find any information about it actually being Dec. 30 rather than Dec. 31. In fact, I can find at least [one reference](https://msdn.microsoft.com/en-us/library/w4ddyt9h.aspx) to Microsoft using Dec. 31, 1899 as an epoch, but none for Dec. 30. – Synetech Dec 22 '15 at 03:30
  • Could it be related to this: http://www.cpearson.com/excel/datetime.htm – elegant dice Jun 20 '16 at 14:40
  • Yes! That would explain the 1 day discrepancy. Great find! So you can correct my statement. It really does start on January 0, 1900, (i.e. Dec 31, 1899) But Feb 29, 1900 mistakenly exists! Talk about folklore... – Elli Barasch Jun 21 '16 at 20:56