在C#中创建指定时区的DateTime

218

我正在尝试创建一个单元测试来测试在机器上因错误设置后又进行了更正导致时区发生变化的情况。

在测试中,我需要能够创建非本地时区的DateTime对象,以确保无论用户位于何处都可以成功运行测试。

从DateTime构造函数中可以看到,TimeZone可以设置为本地时区、UTC时区或不指定。

如何创建具有特定时区(例如PST)的DateTime?


相关问题 - https://dev59.com/U3E85IYBdhLWcg3w8IbK - Oded
您对DateTime构造函数的描述指定了DateTimeKind,而不是时区。DateTimeKind的实用性极为有限。 - Suncat2000
10个回答

273

Jon的回答提到了 TimeZone,但我建议使用TimeZoneInfo

个人来说,在可能的情况下(至少对于过去的时间而言;存储未来的UTC存在潜在问题),我喜欢将事物保留在UTC中,因此我建议使用以下结构:

public struct DateTimeWithZone
{
    private readonly DateTime utcDateTime;
    private readonly TimeZoneInfo timeZone;

    public DateTimeWithZone(DateTime dateTime, TimeZoneInfo timeZone)
    {
        var dateTimeUnspec = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
        utcDateTime = TimeZoneInfo.ConvertTimeToUtc(dateTimeUnspec, timeZone); 
        this.timeZone = timeZone;
    }

    public DateTime UniversalTime { get { return utcDateTime; } }

    public TimeZoneInfo TimeZone { get { return timeZone; } }

    public DateTime LocalTime
    { 
        get 
        { 
            return TimeZoneInfo.ConvertTime(utcDateTime, timeZone); 
        }
    }        
}

您可能希望将“TimeZone”更改为“TimeZoneInfo”,以使事情更清晰 - 我个人更喜欢简短的名称。


5
很抱歉,我不知道有任何与之等效的SQL Server结构。我建议将时区名称作为一列,将UTC值作为另一列。分别获取它们,然后您可以相当容易地创建实例。 - Jon Skeet
3
关于使用接受 DateTime 和 TimeZoneInfo 的构造函数的预期用途不确定,但考虑到您调用了 dateTime.ToUniversalTime() 方法,我猜测您认为它可能是在本地时间。如果是这种情况,我认为您应该确实使用传入的 TimeZoneInfo 将其转换为 UTC,因为他们告诉您它应该在那个时区。 - IDisposable
2
@ChrisMoschini:到那个时候,你只是在发明自己的ID方案 - 没有其他人在世界上使用的方案。谢谢,我会坚持采用行业标准的zoneinfo。(很难看出例如“Europe/London”是毫无意义的。) - Jon Skeet
2
@ChrisMoschini:不同的例子:CST。那是UTC-5还是UTC-6?IST呢?在你的数据库中,它是以色列、印度还是爱尔兰?(即使你现在知道偏移量,观察相同缩写的不同国家可能会在不同的时间改变。因此,仍然存在关于它表示哪个实际时区的歧义。时区!=偏移量。)回到你的情况:你声称使用缩写最好解决了你的问题。使用行业标准的时区ID会更糟吗? - Jon Skeet
6
我会继续推荐使用行业标准、明确的zoneinfo ID而非含糊的缩写。这并不是偏爱哪个库的问题——库的作者真的不是问题所在。如果有人希望使用另一个具有良好标识符选择的库,那也可以。然而,时区标识符的选择很重要,我认为让读者意识到缩写是含糊的是非常重要的,就像我用IST的例子所展示的那样。 - Jon Skeet
显示剩余18条评论

69

2
谢谢,这是一个很好的实现方式。在你获得了正确时区内的DateTimeOffset对象后,你可以使用.UtcDateTime属性来获取你创建的UTC时间。如果你将日期存储在UTC中,那么将它们转换为每个用户的本地时间就不是什么大问题了 :) - Redth
8
我认为这个处理夏令时的方式不正确,因为有些时区会遵守夏令时而另一些则不会。此外,“在当天”夏令时开始/结束时,那一天的部分时间会出现偏差。 - crokusek
41
DST是特定时区的规则,DateTimeOffset与任何时区都不相关。不要将UTC偏移值(例如-5)与时区混淆。它不是时区,而是偏移量。同一个偏移量通常被许多时区共享,因此这是一种引用时区的模糊方式。由于DateTimeOffset与偏移量关联而不是时区,因此它不可能应用DST规则。因此,在DateTimeOffset结构中(例如在其Hours和TimeOfDay属性中),每年的每一天早上3点都将保持为早上3点,没有例外。 - Triynko
1
你可能会感到困惑的是,如果你查看DateTimeOffset的LocalDateTime属性。该属性不是DateTimeOffset,而是DateTime实例,其类型为DateTimeKind.Local。该实例与时区相关联...无论本地系统时区是什么。该属性将反映夏令时。 - Triynko
5
DateTimeOffset 的真正问题在于它不包含足够的信息。它只包含一个偏移量,而不是时区。这个偏移量在多个时区中是不明确的。 - Triynko
显示剩余2条评论

56

这里的其他回答很有用,但它们没有涵盖如何访问太平洋时区 - 下面是方法:

public static DateTime GmtToPacific(DateTime dateTime)
{
    return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
        TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"));
}

奇怪的是,尽管“太平洋标准时间”通常意味着与“太平洋夏令时间”有所不同,但在这种情况下,它指的是太平洋时间。实际上,如果您使用 FindSystemTimeZoneById 获取它,其中一个可用的属性是一个布尔值,告诉您该时区当前是否处于夏令时。

您可以在我最终放弃组合处理DateTime的库中看到更广泛的示例,具体取决于用户的提问位置和不同的TimeZone:

https://github.com/b9chris/TimeZoneInfoLib.Net

这在Windows之外是行不通的(例如Linux上的Mono),因为时间列表来自Windows Registry: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\

在此之下,您将找到键(Registry Editor中的文件夹图标);这些键的名称就是您要传递给 FindSystemTimeZoneById 的参数。在Linux上,您必须使用一组独立的Linux标准时区定义,但我尚未充分探讨。


2
此外还有ConvertTimeBySystemTimeZoneId()函数,例如:TimeZoneInfo.ConvertTimeBySystemTimeZoneId(DateTime.UtcNow, "Central Standard Time")。 - Brent
在Windows的TimeZone Id List中也可以看到这个答案:https://dev59.com/c2Yq5IYBdhLWcg3w2EFi#24460750 - yu yang Jian

6

我稍微改变了Jon Skeet的答案,使用扩展方法适用于网络。它在Azure上也运行得很好。

public static class DateTimeWithZone
{

private static readonly TimeZoneInfo timeZone;

static DateTimeWithZone()
{
//I added web.config <add key="CurrentTimeZoneId" value="Central Europe Standard Time" />
//You can add value directly into function.
    timeZone = TimeZoneInfo.FindSystemTimeZoneById(ConfigurationManager.AppSettings["CurrentTimeZoneId"]);
}


public static DateTime LocalTime(this DateTime t)
{
     return TimeZoneInfo.ConvertTime(t, timeZone);   
}
}

使用静态时区并不是一个好主意(通常情况下,应避免使用静态数据 https://dev59.com/A2w05IYBdhLWcg3wyk3n)。该类应支持不止一个硬编码的时区。 - Michael Freidgeim

3
我喜欢Jon Skeet的答案,但还想补充一点。我不确定Jon是否希望将ctor始终传递到本地时区。但我想将其用于其他本地之外的情况。
我正在从数据库中读取值,我知道该数据库所在的时区。因此,在ctor中,我将传递数据库的时区。但是,我希望值以本地时间显示。Jon的LocalTime并未返回原始日期转换为本地时区日期。它返回转换为原始时区的日期(你传递给ctor的任何内容)。
我认为这些属性名称可以澄清问题...
public DateTime TimeInOriginalZone { get { return TimeZoneInfo.ConvertTime(utcDateTime, timeZone); } }
public DateTime TimeInLocalZone    { get { return TimeZoneInfo.ConvertTime(utcDateTime, TimeZoneInfo.Local); } }
public DateTime TimeInSpecificZone(TimeZoneInfo tz)
{
    return TimeZoneInfo.ConvertTime(utcDateTime, tz);
}

3
尝试使用 TimeZoneInfo.ConvertTime(dateTime, sourceTimeZone, destinationTimeZone) 进行转换。

问题是创建,而不是转换。这段代码只是转换。 - Alessandro Lendaro

2

你需要为此创建一个自定义对象。你的自定义对象将包含两个值:

  • 一个 DateTime 值
  • 一个 TimeZone 对象

不确定是否已经有 CLR 提供的数据类型具有该功能,但至少 TimeZone 组件已经可用。


FYI:TimeZone类早已被弃用。它太过受限,就像DateTimeKind一样受限。TimeZoneInfo是一个重大的改进,但未能确定何时应用 - 以及何时不应用 - 夏令时调整。 - Suncat2000

1

如果需要特定时区的带偏移量的日期/时间(既不是本地时间,也不是UTC时间),可以使用DateTimeOffset类:

  var time = TimeSpan.Parse("9:00");
  var est = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
  var nationalDateTime = new DateTimeOffset(DateTime.Today.Ticks + time.Ticks, est.BaseUtcOffset);

DateTimeOffset不指定时区。@Tryinko在他的评论中解释得很好。 - Suncat2000

1
使用 TimeZones 类可以轻松创建特定时区的日期。
TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfo.FindSystemTimeZoneById(TimeZones.Paris.Id));

3
抱歉,此处没有提供Asp .NET Core 2.2版本的相关内容。Visual Studio 2017建议我安装Outlook Nuget软件包。请理解。 - Machado
TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")) => TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfo.FindSystemTimeZoneById("太平洋标准时间")) - AZ_
2
你从哪里获取 TimeZones.Paris.Id 的值? - InteXX

1

我在Jon Skeet的答案基础上添加了一些额外的内容,使其更接近于DateTime。大部分情况下,这将简化比较、相等性和转换。我发现DateTimeZoned.Now("")函数特别有用。

需要注意的一点是,这个结构体是在.NET 6中编写的。因此,如果您使用的是旧版本,可能需要替换一些新语言特性的用法。

此外,运算符和接口的实现受到了GitHub上的DateTime.cs .NET参考的启发。

/// <summary>
/// This value type represents a date and time with a specific time zone applied. If no time zone is provided, the local system time zone will be used.
/// </summary>
public readonly struct DateTimeZoned : IComparable, IComparable<DateTimeZoned>, IEquatable<DateTimeZoned>
{
    /// <summary>
    /// Creates a new zoned <see cref="DateTime"/> with the system time zone.
    /// </summary>
    /// <param name="dateTime">The local <see cref="DateTime"/> to apply a time zone to.</param>
    public DateTimeZoned(DateTime dateTime)
    {
        var local = DateTime.SpecifyKind(dateTime, DateTimeKind.Local);

        UniversalTime = TimeZoneInfo.ConvertTimeToUtc(local, TimeZoneInfo.Local);
        TimeZone = TimeZoneInfo.Local;
    }

    /// <summary>
    /// Creates a new zoned <see cref="DateTime"/> with the specified time zone.
    /// </summary>
    /// <param name="dateTime">The <see cref="DateTime"/> to apply a time zone to.</param>
    /// <param name="timeZone">The time zone to apply.</param>
    /// <remarks>
    /// Assumes the provided <see cref="DateTime"/> is from the specified time zone.
    /// </remarks>
    public DateTimeZoned(DateTime dateTime, TimeZoneInfo timeZone)
    {
        var unspecified = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);

        UniversalTime = TimeZoneInfo.ConvertTimeToUtc(unspecified, timeZone);
        TimeZone = timeZone;
    }

    /// <summary>
    /// Creates a new zoned <see cref="DateTime"/> with the specified time zone.
    /// </summary>
    /// <param name="dateTime">The <see cref="DateTime"/> to apply a time zone to.</param>
    /// <param name="timeZone">The time zone to apply.</param>
    /// <remarks>
    /// Assumes the provided <see cref="DateTime"/> is from the specified time zone.
    /// </remarks>
    public DateTimeZoned(DateTime dateTime, string timeZone)
    {
        var unspecified = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
        var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);

        UniversalTime = TimeZoneInfo.ConvertTimeToUtc(unspecified, timeZoneInfo);
        TimeZone = timeZoneInfo;
    }

    /// <summary>
    /// The UTC <see cref="DateTime"/> for the stored value.
    /// </summary>
    public DateTime UniversalTime { get; init; }

    /// <summary>
    /// The selected time zone.
    /// </summary>
    public TimeZoneInfo TimeZone { get; init; }

    /// <summary>
    /// The localized <see cref="DateTime"/> for the stored value.
    /// </summary>
    public DateTime LocalTime => TimeZoneInfo.ConvertTime(UniversalTime, TimeZone);

    /// <summary>
    /// Specifies whether UTC and localized values are the same.
    /// </summary>
    public bool IsUtc => UniversalTime == LocalTime;

    /// <summary>
    /// Returns a new <see cref="DateTimeZoned"/> with the current <see cref="LocalTime"/> converted to the target time zone.
    /// </summary>
    /// <param name="timeZone">The time zone to convert to.</param>
    public DateTimeZoned ConvertTo(string timeZone)
    {
        var converted = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(LocalTime, TimeZone.Id, timeZone);
        return new DateTimeZoned(converted, timeZone);
    }

    /// <summary>
    /// Returns a new <see cref="DateTimeZoned"/> with the current <see cref="LocalTime"/> converted to the target time zone.
    /// </summary>
    /// <param name="timeZone">The time zone to convert to.</param>
    public DateTimeZoned ConvertTo(TimeZoneInfo timeZone)
    {
        var converted = TimeZoneInfo.ConvertTime(LocalTime, TimeZone, timeZone);
        return new DateTimeZoned(converted, timeZone.Id);
    }

    /// <summary>
    /// Returns the value as a string in the round-trip date/time pattern.
    /// </summary>
    /// <remarks>
    /// This applies the .ToString("o") option on <see cref="LocalTime"/>.
    /// </remarks>
    public string ToLocalString()
    {
        var local = new DateTimeOffset(LocalTime, TimeZone.BaseUtcOffset);
        return local.ToString("o");
    }

    /// <summary>
    /// Returns the value as a string in the universal sortable date/time pattern.
    /// </summary>
    /// <remarks>
    /// This is applies the .ToString("u") option on <see cref="UniversalTime"/>.
    /// </remarks>
    public string ToUniversalString()
    {
        return UniversalTime.ToString("u");
    }

    /// <summary>
    /// Returns a <see cref="DateTime"/> representing the current date and time adjusted to the system time zone.
    /// </summary>
    /// <remarks>
    /// This is functionally equivalent to <see cref="DateTime.Now"/> and has been added for completeness.
    /// </remarks>
    public static DateTime Now() => TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Local);

    /// <summary>
    /// Returns a <see cref="DateTime"/> representing the current date and time adjusted to the specified time zone.
    /// </summary>
    /// <param name="timeZone">The time zone to apply.</param>
    public static DateTime Now(TimeZoneInfo timeZone) => TimeZoneInfo.ConvertTime(DateTime.UtcNow, timeZone);

    /// <summary>
    /// Returns a <see cref="DateTime"/> representing the current date and time adjusted to the specified time zone.
    /// </summary>
    /// <param name="timeZone">The time zone to apply.</param>
    public static DateTime Now(string timeZone)
    {
        var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
        return TimeZoneInfo.ConvertTime(DateTime.UtcNow, timeZoneInfo);
    }

    /// <inheritdoc/>
    public override bool Equals(object? value)
    {
        return value is DateTimeZoned d2 && this == d2;
    }

    /// <inheritdoc/>
    public bool Equals(DateTimeZoned value)
    {
        return this == value;
    }

    /// <summary>
    /// Compares two <see cref="DateTimeZoned"/> values for equality.
    /// </summary>
    /// <param name="d1">The first value to compare.</param>
    /// <param name="d2">The second value to compare.</param>
    /// <returns>
    /// Returns <see langword="true"/> if the two <see cref="DateTimeZoned"/> values are equal, or <see langword="false"/> if they are not equal.
    /// </returns>
    public static bool Equals(DateTimeZoned d1, DateTimeZoned d2)
    {
        return d1 == d2;
    }

    /// <summary>
    /// Compares two <see cref="DateTimeZoned"/> values, returning an integer that indicates their relationship.
    /// </summary>
    /// <param name="d1">The first value to compare.</param>
    /// <param name="d2">The second value to compare.</param>
    /// <returns>
    /// Returns 1 if the first value is greater than the second, -1 if the second value is greater than the first, or 0 if the two values are equal.
    /// </returns>
    public static int Compare(DateTimeZoned d1, DateTimeZoned d2)
    {
        var ticks1 = d1.UniversalTime.Ticks;
        var ticks2 = d2.UniversalTime.Ticks;

        if (ticks1 > ticks2) 
            return 1;
        else if (ticks1 < ticks2) 
            return -1;
        else
            return 0;
    }

    /// <inheritdoc/>
    public int CompareTo(object? value)
    {
        if (value == null) 
            return 1;

        if (value is not DateTimeZoned)
            throw new ArgumentException(null, nameof(value));

        return Compare(this, (DateTimeZoned)value);
    }

    /// <inheritdoc/>
    public int CompareTo(DateTimeZoned value)
    {
        return Compare(this, value);
    }

    /// <inheritdoc/>
    public override int GetHashCode()
    {
        var ticks = UniversalTime.Ticks;
        return unchecked((int)ticks) ^ (int)(ticks >> 32);
    }

    public static TimeSpan operator -(DateTimeZoned d1, DateTimeZoned d2) => new(d1.UniversalTime.Ticks - d2.UniversalTime.Ticks);

    public static bool operator ==(DateTimeZoned d1, DateTimeZoned d2) => d1.UniversalTime.Ticks == d2.UniversalTime.Ticks;

    public static bool operator !=(DateTimeZoned d1, DateTimeZoned d2) => d1.UniversalTime.Ticks != d2.UniversalTime.Ticks;

    public static bool operator <(DateTimeZoned d1, DateTimeZoned d2) => d1.UniversalTime.Ticks < d2.UniversalTime.Ticks;

    public static bool operator <=(DateTimeZoned d1, DateTimeZoned d2) => d1.UniversalTime.Ticks <= d2.UniversalTime.Ticks;

    public static bool operator >(DateTimeZoned d1, DateTimeZoned d2) => d1.UniversalTime.Ticks > d2.UniversalTime.Ticks;

    public static bool operator >=(DateTimeZoned d1, DateTimeZoned d2) => d1.UniversalTime.Ticks >= d2.UniversalTime.Ticks;
}


非常好。谢谢! - Ben

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接