DateTime 转换为 Unix Epoch 添加 Phantom Hour

DateTime Conversion to Unix Epoch Adding Phantom Hour

我有以下转换方法来转换 Unix Epoch 时间戳

public static class DateTimeHelpers
{
    public static DateTime UnixEpoch()
    {
        return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    }

    public static DateTime FromMillisecondsSinceUnixEpoch(long milliseconds)
    {
        return UnixEpoch().AddMilliseconds(milliseconds).ToLocalTime();
    }

    public static long ToMillisecondsSinceUnixEpoch(DateTime dateTime)
    {
        return (long)(dateTime - UnixEpoch()).TotalMilliseconds;
    }
}

问题是(男孩,这看起来像是基本的东西),我设置了一个 DateTime 我想要然后尝试转换为 Unix-Time 但返回的毫秒时间戳是 +01:00 小时,我想要知道为什么吗?

我使用的代码是

DateTime startDate = new DateTime(2015, 10, 1, 0, 0, 0, 0, DateTimeKind.Utc);

long startMillis = DateTimeHelpers.ToMillisecondsSinceUnixEpoch(startDate);

这给出 startMillis = 1443657600000 即 "Thursday October 01, 2015 01:00:00 (am) in time zone Europe/London (BST)"。我想要从 ToMillisecondsSinceUnixEpoch 返回的时间戳,即“2015/10/01 00:00:00”,我在这里缺少什么?

感谢您的宝贵时间。


编辑。我想做一些 Java 代码的等价物。这产生了 right 时间戳。为什么我可以在 Java 而不是 C# 中执行此操作?不管怎样代码

private static long ukTimeStringToUtcMillis(String s) {
    SimpleDateFormat sdf = makeSimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
    try {
        return sdf.parse(s).getTime();
    } catch (ParseException e) {
        throw new RuntimeException(e);
    }
}

private static SimpleDateFormat makeSimpleDateFormat(String s) {
    SimpleDateFormat sdf = new SimpleDateFormat(s);
    sdf.setTimeZone(TimeZone.getTimeZone("Europe/London"));
    return sdf;
}

我是这样用的

long timestamp = ukTimeStringToUtcMillis("2015-10-01T00:00:00.000");

这给出 timestamp = 1443654000000 即 "Thursday October 01, 2015 00:00:00 (am) in time zone Europe/London (BST)"。我在 C# 中缺少什么?我试过了

var ukTimeZone = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
DateTime unixEpoch = TimeZoneInfo.ConvertTime(
    new DateTime(1970, 1, 1, 0, 0, 0), ukTimeZone, ukTimeZone);

long startMillis = (long)(startDate - unixEpoch).TotalMilliseconds;
long endMillis = (long)(endDate - unixEpoch).TotalMilliseconds;

这增加了一个小时!?

如果我跟着你做的,你的测试代码以 UTC 时间开始:

DateTime startDate = new DateTime(2015, 10, 1, 0, 0, 0, 0, DateTimeKind.Utc);

但是在 FromMillisecondsSinceUnixEpoch 你 return LocalTime。按原样使用代码,它不会进行往返:

Console.WriteLine(dt.ToUniversalTime());
Console.WriteLine("{0}  {1}", ToMillisecondsSinceUnixEpoch(dt), 
    FromMillisecondsSinceUnixEpoch(ToMillisecondsSinceUnixEpoch(dt)));

10/1/2015 5:00:00 AM
1443657600000 10/1/2015 12:00:00 AM

如果我改变FromMillisecondsSinceUnixEpoch:

public static DateTime FromMillisecondsSinceUnixEpoch(long milliseconds)
{
    return UnixEpoch().AddMilliseconds(milliseconds).ToUniversalTime();
}

现在它将进行往返:

10/1/2015 5:00:00 AM
1443675600000 10/1/2015 5:00:00 AM

请注意,每个 MilliSecondsSince 都是相同的。你不能只看那个,因为没有上下文。

我在参考源中找不到它,但 DateTime 肯定足够聪明,可以在 TZ 不同时进行调整,然后再进行减法。 (否则很难解释 1443675600000 如何代表同一日期的 3 个时间跨度(2 个给我,1 个给你)。

几件事:

  • 如果可能,您应该使用 DateTimeOffset 而不是 DateTime 来进行此类操作。 DateTimeOffset 总是一个特定的时刻,而 DateTime 可能 是,也可能不是,这取决于 Kind 以及您坚持的程度各种方法如何解释 Kind 的微妙之处。

  • 如果您使用 DateTimeOffset,并且您的目标是 .NET 4.6 或更高版本(或 .NET Core),那么您可以使用内置的 DateTimeOffset.FromUnixTimeMilliseconds and ToUnixTimeMilliseconds 方法,而不是创建自己的。

  • 您可以考虑使用 Noda Time 开源库,因为它为大多数使用日期和时间的应用程序增加了重要价值。

    • 例如,如果您想使用 tzdb 时区,例如您提到的 "Europe/London" 时区,那么您可以使用 DateTimeZoneProviders.Tzdb["Europe/London"].

现在我的其余回答假设您没有采纳上述任何建议,并且与您在问题中提供的代码有关。

  • 您已将 UnixEpoch 实现为静态方法。由于它的值永远不会改变,它可能应该作为一个静态 属性 来实现,并带有一个私有的只读支持字段。它也可以实现为 public 静态只读字段,尽管大多数人更喜欢通过属性公开这些字段。 (这些只是编码准则,但不会引入任何错误。)

  • 在您的 FromMillisecondsSinceUnixEpoch 方法中,您正在调用 .ToLocalTime()。那应该被省略。您也不需要调用 .ToUniversalTime()。只是 return 添加毫秒的结果。 Kind 将是 Utc。如果您需要使用本地时间,请稍后进行转换 - 不要在此函数内。

  • 识别 ID "GMT Standard Time" 适用于伦敦,而不是 UTC。伦敦是 GMT (UTC+00:00) 或 BST (UTC+01:00),具体取决于相关日期和时间。

  • 识别DateTime.ToLocalTimeDateTime.ToUniversalTime在代码为运行的机器上在UTC和当前本地时区之间转换。那可能是伦敦,也可能是其他地方,具体取决于您的用例。如果您 运行 在 服务器 上,例如在 ASP.Net 网络应用程序中,那么依赖系统本地时区并不是一个好习惯。

  • 在您使用 TimeZoneInfo.ConvertTime 显示的代码中,由于您没有将 DateTimeKind.Utc 分配给输入,因此该值将具有 DateTimeKind.UnspecifiedConvertTime 会将其解释为已经属于源时区。由于您指定了相同的目的地时区,因此在大多数情况下这将是一个空操作。

  • 在同一个函数中,由于前面指定的原因,根据伦敦时间定义 unixEpoch 是无效的。另请注意,在 1970 年 1 月 1 日,伦敦不在 GMT,但实际上在 BST(当时称为 "British Standard Time",而不是 "British Summer time")。 TZDB 知道这一点,但它对于 Windows 时区和 TimeZoneInfo 来说太落后了。 "GMT Standard Time" Windows 区域仅反映了 BST/GMT 周围的当前规则,而不是当时有效的规则。

就转换您提供的 Java 代码而言,该函数以毫秒精度读取 ISO 8601 格式的字符串,在伦敦时区对其进行解释,将其转换为 UTC,并给出时间自 Unix 纪元以来的毫秒数。有几种方法可以做到这一点:

  • .Net 3.5+

    public static long UkTimeStringToUtcMillis(string s)
    {
        string format = "yyyy-MM-dd'T'HH:mm:ss.FFF";
        DateTime dt = DateTime.ParseExact(s, format, CultureInfo.InvariantCulture);
        TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
        DateTime utc = TimeZoneInfo.ConvertTimeToUtc(dt, tz);
        DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        return (long) (utc - epoch).TotalMilliseconds;
    }
    
  • .Net 4.6+ / .Net CoreCLR

    public static long UkTimeStringToUtcMillis(string s)
    {
        string format = "yyyy-MM-dd'T'HH:mm:ss.FFF";
        DateTime dt = DateTime.ParseExact(s, format, CultureInfo.InvariantCulture);
        TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
        TimeSpan offset = tz.GetUtcOffset(dt);
        DateTimeOffset dto = new DateTimeOffset(dt, offset);
        return dto.ToUnixTimeMilliseconds();
    }
    
  • 野田时间

    public static long UkTimeStringToUtcMillis(string s)
    {
        LocalDateTimePattern pattern = LocalDateTimePattern.ExtendedIsoPattern;
        LocalDateTime dt = pattern.Parse(s).Value;
        DateTimeZone tz = DateTimeZoneProviders.Tzdb["Europe/London"];
        Instant i = dt.InZoneLeniently(tz).ToInstant();
        return i.Ticks / NodaConstants.TicksPerMillisecond;
    }