检查表示日期时间的字符串是否包含时区

Check if a string representing a datetime includes a timezone

提问人:Emil Bode 提问时间:5/1/2023 更新时间:5/2/2023 访问量:163

问:

我想将字符串解析为表示时间戳的对象,但是在向其添加正确的时区时遇到了一些麻烦。

具体来说,在解析时,我找不到一种方法来区分添加了时区偏移量的字符串和未添加时区偏移量的字符串。

用例

我正在阅读几个 xml 文件。在这些文件中,存在多个时间戳,可以是 UTC、我的本地时区或某个第三时区。
或者,它们可能没有任何时区信息作为字符串的一部分,在这种情况下,我们应该回退到 xml 中其他地方指定的默认时区。同样,该默认值可以是 UTC、我的本地时区或第三个区域。
最终,我想将所有这些时间戳转换为UTC。

因此,我可能有以下数据(在系统时区为 Europe/Amsterdam 的 PC 上,当前为 UTC+2):

File_one.xml

<data>
  <timestamp eventName="First">2023-5-1T12:01:00Z</timestamp>
  <timestamp eventName="Second">2023-5-1T12:02:00+02:00</timestamp>
  <timestamp eventName="Third">2023-5-1T12:03:00+04:00</timestamp>
  <timestamp eventName="Fourth">2023-5-1T12:04:00</timestamp>
</data>
<configuration>
  <timezone>Europe/Amsterdam</timezone>
</configuration>

还有file_two.xml

<data>
  <timestamp eventName="Fifth">2023-5-1T12:05:00Z</timestamp>
  <timestamp eventName="Sixth">2023-5-1T12:06:00+02:00</timestamp>
  <timestamp eventName="Seventh">2023-5-1T12:07:00+04:00</timestamp>
  <timestamp eventName="Eighth">2023-5-1T12:08:00</timestamp>
</data>
<configuration>
  <timezone>America/New_York</timezone>
</configuration>

在进行所有解析后,这应该会在 UTC 中产生以下时间戳:

First   2023-05-01T12:01:00
Second  2023-05-01T10:02:00
Third   2023-05-01T08:03:00
Fourth  2023-05-01T10:04:00
Fifth   2023-05-01T12:05:00
Sixth   2023-05-01T10:06:00
Seventh 2023-05-01T08:07:00
Eighth  2023-05-01T16:08:00

我的主要问题,这个问题是关于什么的,是与.EighthSixth

行不通的方法。

我尝试使用它们 和 的 /-方法。在解析没有时区的值时,两者似乎都假设我的本地时区。稍后为具有本地时区的值添加时区也行不通,因为这意味着弄乱了实际位于我本地时区的那些值,即 和。DateTimeDateTimeOffsetParseTryParseSecondSixth

我尝试的另一种方法是首先使用带或不带时区信息来解析这些值,但不幸的是,我的实际时间戳并不像这里的示例那样整洁,而且我不确定我可以期望的所有确切格式。我想要的只是区分偏移和根本没有偏移。TryParseExact

(我可能混淆了时区和时间偏移这两个词。在这种情况下,差异并不重要)

C# .NET 日期时间 偏移

评论

1赞 sommmen 5/1/2023
我喜欢你添加一个“不起作用”部分,但也许下次放入你尝试过的实际代码示例。
0赞 Jack A. 5/1/2023
我认为您可以使用正则表达式检测时区的缺失。然后,在使用 DateTimeOffset 进行分析之前,将默认时区追加到字符串。

答:

1赞 sommmen 5/1/2023 #1

我遇到了类似的情况,所以我将谈谈我在这种情况下做了什么,希望它能让你走上正确的道路。如果它完全关闭,请在评论中告诉我,我会删除它。

我假设你无法控制文件的生成方式,你不能只修复文档以使用包括时区在内的单一格式。

我有几个各种格式(excel/csv/tsv/json 等)的表格数据文件,它们都使用各种格式的日期时间。我编写了以下帮助程序方法,该方法检查从最具体到最不具体的各种情况的一堆格式。

public static bool TryParseExactDayMonthYear(ReadOnlySpan<char> s, out DateTime result) => TryParseExactDayMonthYear(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out result);

/// <summary>
/// CultureInfo.InvariantCulture uses en-US notation, meaning day/month are twisted compared to the EU.
/// This tries various formats to get at least a D-M-Y datetime.
/// The datetime is set to UTC by default
/// </summary>
/// <param name="s"></param>
/// <param name="provider"></param>
/// <param name="style"></param>
/// <param name="result"></param>
/// <returns></returns>
public static bool TryParseExactDayMonthYear(ReadOnlySpan<char> s, IFormatProvider? provider, DateTimeStyles style, out DateTime result)
{
    if (DateTime.TryParseExact(s, "dd-MM-yyyy HH:mm", provider, style, out result)
        || DateTime.TryParseExact(s, "d-M-yyyy HH:mm", provider, style, out result)
        || DateTime.TryParseExact(s, "dd-MM-yyyy HH:mm:ss", provider, style, out result)
        || DateTime.TryParseExact(s, "d-M-yyyy HH:mm:ss", provider, style, out result)
        || DateTime.TryParseExact(s, "d-M-yyyy", provider, style, out result)
        || DateTime.TryParseExact(s, "dd-MM-yyyy", provider, style, out result)
        || DateTime.TryParseExact(s, "d-M-yy", provider, style, out result)
        || DateTime.TryParseExact(s, "dd-MM-yy", provider, style, out result)

        // Germany (DerKurrier)
        || DateTime.TryParseExact(s, "dd.MM.yyyy HH:mm", provider, style, out result)
        || DateTime.TryParseExact(s, "d.M.yyyy HH:mm", provider, style, out result)
        || DateTime.TryParseExact(s, "dd.MM.yyyy HH:mm:ss", provider, style, out result)
        || DateTime.TryParseExact(s, "d.M.yyyy HH:mm:ss", provider, style, out result)
        || DateTime.TryParseExact(s, "d.M.yyyy", provider, style, out result)
        || DateTime.TryParseExact(s, "dd.MM.yyyy", provider, style, out result)
        || DateTime.TryParseExact(s, "d.M.yy", provider, style, out result)
        || DateTime.TryParseExact(s, "dd.MM.yy", provider, style, out result)
        
        // Last resort
        || DateTime.TryParse(s, provider, style, out result)

        // For excel files created on a dutch machine...
        || DateTime.TryParse(s, CultureInfo.GetCultureInfo("nl-NL"), style, out result)
       )
    {
        result = DateTime.SpecifyKind(result, DateTimeKind.Utc);
        return true;
    }

    return false;
}

这里的速度对我来说并不重要。 我可以想象你可以采用同样的方法,传递不同的格式、时区和文化,以便你的案例得到覆盖。(我知道这并不漂亮)。

我考虑探索的另一个选择是使用库 NodaTime,它是为处理这些类型的情况而创建的

例如,请参阅此文档页面:

https://nodatime.org/2.2.x/userguide/type-choices

Nodatime 还包含一个时区数据库,该数据库可以处理诸如“Europe/Amsterdam”之类的值(.nets TimeZoneInfo 不使用该值。例如,请参阅将 php datetime 类型转换为 .net DateTime 的代码片段;

private static DateTime ParsePhpDateTimeString(string dateString, string timezone, long timezoneType)
{
    var localDateTimePattern = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss.ffffff");
    var localDateTime = localDateTimePattern.Parse(dateString).GetValueOrThrow();

    // See: https://stackoverflow.com/a/17711005/4122889
    //  Type 1; A UTC offset, such as in new DateTime("17 July 2013 -0300");
    //  Type 2; A timezone abbreviation, such as in new DateTime("17 July 2013 GMT");
    //  Type 3: A timezone identifier, such as in new DateTime("17 July 2013", new DateTimeZone("Europe/London"));

    switch (timezoneType)
    {
        case 1:
            var offSetPattern = OffsetPattern.CreateWithInvariantCulture("+HH:mm");
            var offset = offSetPattern.Parse(timezone).Value;
            var zonedDateTimeFromOffset = localDateTime.InZoneStrictly(DateTimeZone.ForOffset(offset));
            return zonedDateTimeFromOffset.ToDateTimeUtc();
        case 2:
            throw new NotSupportedException("Not (Yet) support converting from timeZonetype 2 - but doable to add in!");
        case 3:
            var dateTimeZone = DateTimeZoneProviders.Tzdb[timezone];
            var zonedDateTime = dateTimeZone.AtStrictly(localDateTime);
            var dateTimeUtc = zonedDateTime.ToDateTimeUtc();
            return dateTimeUtc;
        default:
            throw new ArgumentOutOfRangeException(nameof(timezoneType));
    }
}

评论

0赞 Emil Bode 5/1/2023
谢谢,看起来是我可以用作起点的方法。我要研究一下Nodatime库,它看起来很有前途。
0赞 Salman A 5/2/2023 #2

输入日期仅包含两种类型:

  • 明确(等)2023-5-1T12:01:00Z2023-5-1T12:02:00+02:00
  • 模棱两可 (,2023-5-1T12:04:002023-5-1T12:08:00)
    • 幸运的是,您知道 IANA 时区名称

对于明确的日期,您可以使用 .结果包含将日期时间转换为 UTC 所需的信息。DateTimeOffset.ParseExact

对于不明确的日期,您需要将 IANA 时区名称(欧洲/阿姆斯特丹和美国/New_York)转换为 Windows 可以理解的名称(西欧标准时间和东部标准时间)。这可以通过功能来完成,但它不能开箱即用。TimeZoneInfo.TryConvertIanaIdToWindowsId

如果让它正常工作,则可以使用它来获取该时区的 UTC 偏移量,然后生成一个 DateTimeOffset 对象:TimeZoneInfo.GetUtcOffset

using System.Text.RegularExpressions;

List<(string dateString, string timezoneName)> tests = new()
{
    ("2023-5-1T12:01:00Z", "Europe/Amsterdam"),
    ("2023-5-1T12:02:00+02:00", "Europe/Amsterdam"),
    ("2023-5-1T12:03:00+04:00", "Europe/Amsterdam"),
    ("2023-5-1T12:04:00", "Europe/Amsterdam"),
    ("2023-5-1T12:05:00Z", "America/New_York"),
    ("2023-5-1T12:06:00+02:00", "America/New_York"),
    ("2023-5-1T12:07:00+04:00", "America/New_York"),
    ("2023-5-1T12:08:00", "America/New_York")
};
foreach (var test in tests)
{
    string dateString = test.dateString;
    string timezoneName = test.timezoneName;
    Regex tzTest = new Regex("(?:Z|[+-]\\d\\d:\\d\\d)$", RegexOptions.Compiled);
    DateTimeOffset result;
    if (tzTest.IsMatch(dateString))
    {
        result = DateTimeOffset.ParseExact(dateString, "yyyy-M-d\\THH:mm:ssK", null);
    }
    else
    {
        TimeZoneInfo.TryConvertIanaIdToWindowsId(timezoneName, out string? timezoneName_w);
        if (timezoneName_w == null)
        {
            throw new ArgumentException($"Timezone name conversion failed for {timezoneName}");
        }
        DateTime datetime = DateTime.ParseExact(dateString, "yyyy-M-d\\THH:mm:ss", null);
        TimeSpan offset = TimeZoneInfo.FindSystemTimeZoneById(timezoneName_w).GetUtcOffset(datetime);
        result = new DateTimeOffset(datetime, offset);
    }
    Console.WriteLine("{0} {1} => {2}", dateString.PadRight(24), timezoneName.PadRight(20), result.ToUniversalTime().ToString("o"));
}

结果:

2023-5-1T12:01:00Z       Europe/Amsterdam     => 2023-05-01T12:01:00.0000000+00:00
2023-5-1T12:02:00+02:00  Europe/Amsterdam     => 2023-05-01T10:02:00.0000000+00:00
2023-5-1T12:03:00+04:00  Europe/Amsterdam     => 2023-05-01T08:03:00.0000000+00:00
2023-5-1T12:04:00        Europe/Amsterdam     => 2023-05-01T10:04:00.0000000+00:00
2023-5-1T12:05:00Z       America/New_York     => 2023-05-01T12:05:00.0000000+00:00
2023-5-1T12:06:00+02:00  America/New_York     => 2023-05-01T10:06:00.0000000+00:00
2023-5-1T12:07:00+04:00  America/New_York     => 2023-05-01T08:07:00.0000000+00:00
2023-5-1T12:08:00        America/New_York     => 2023-05-01T16:08:00.0000000+00:00