如何在 C# 项目中使用 gas days?

How to work with gas days in a C# project?

天然气日定义为 24 小时的时间范围,从欧洲标准时间的 5:00 UTC 开始,到第二天的 5:00 UTC 结束。在欧洲夏令时期间,它从 4:00 UTC 开始,到第二天的 4:00 UTC 结束(英文解释见 Wikipedia, or ACER)。

我需要在应用程序中使用 gas days 来执行以下操作:

  1. 获取任何给定 UTC 时间戳的当前加油日。示例:“2022-03-22 05:00:00”(UTC)应变为“2022-03-22 00:00:00”(加油日)。
  2. 添加 and/or 从特定加油日减去时间跨度(天、小时等)以获得新的时间戳,该时间戳也考虑了 DST。例如,这意味着如果我从时间戳“2022-03-29 04:00:00”(UTC)(等于加油日“2022-03-29”)中减去 7 天,我想得到时间戳“2022-03-22 05:00:00”(UTC)。

这对我来说好像“加油日”应该作为类似于时区的东西可用,然后我可以在我的应用程序中使用它,但尝试使用 DateTime 或 [=11 来做到这一点=] 让我完全不知道我应该做什么才能完成这项工作。

任何人都可以指出我必须做些什么才能让我进行上面解释的计算的正确方向吗?是否有一个图书馆可以使这更容易做到?例如,我已经研究过 NodaTime,但我在其文档中找不到任何能让我更轻松地解决此任务的内容。

这是一个快速解决方案,可将您的加油日逻辑实现到附加到 System.DateTime 类型的 C# 扩展方法中。如果您想使用该类型,也可以使用类似的逻辑将其附加到 DateTimeOffset

代码如下:

public static class SpecialDateExtensions
{
    public static DateTime GetGasDay(this DateTime current)
    {
        var datePart = current.Date;
        var gasDayThreshold = datePart.AddHours(5);
        return current > gasDayThreshold ? datePart : datePart.AddDays(-1);
    }
}

static void Main(string[] args)
{
    // usage 
    var now = DateTime.Now;
    var gasday = now.GetGasDay();
}

您可以创建一个符合 Gas Day 规则的自定义时区。

由于您提到的规则基于另一个时区,因此很容易引入其他时区的规则,并以此为基础:

static TimeZoneInfo CreateGasDayTimezone()
{
    // Use CET adjustment rules for daylight saving time
    var cet = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time");
    var cetAdjustmentRules = cet.GetAdjustmentRules();

    // Create a new timezone offset by 5 hours off UTC, using CET for DST
    var gasday = TimeZoneInfo.CreateCustomTimeZone(
        "Europe/Gas_Day",
        TimeSpan.FromHours(-5),
        "Gas Day", 
        "Gas Day (Standard)", 
        "Gas Day (Daylight)", 
        cetAdjustmentRules);

    return gasday;
}

使用新时区非常简单:

var date = new DateTime(2020, 1, 1);
var gasdayZone = CreateGasDayTimezone();
var dateAsGasDay = TimeZoneInfo.ConvertTimeFromUtc(date, gasdayZone);
Console.WriteLine(date + " is " + dateAsGasDay + " in gas day");

我将使用一个稍微更精确的定义:“Gas Days”(或更确切地说是“Gas Time”,因为这里有时间部分)基于德国当地时区,但偏移了 6 小时,因此 06:00 在德国是 00:00 在加油时间。

正如您在自己的实验中发现的那样,自定义时区方法并不那么容易实现,因为所有转换都是根据德国当地的日期和时间发生的,即使气体转移推动了价值进入不同的日期。 (使用 NodaTime 可能可行,但使用 TimeZoneInfo 则不行。)

考虑到这一点,您需要在德国时间进行所有转换和操作,然后将它们转移到 Gas Time。一些扩展方法在这里很有用。解释性注释内联代码。

public static class GasTimeExtensions
{
    private static readonly TimeZoneInfo GermanyTimeZone =
        TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
        // (use "W. Europe Standard Time" for .NET < 6 on Windows)

    private static readonly TimeSpan GasTimeOffset = TimeSpan.FromHours(6);

    /// <summary>
    /// Adjusts the provided <paramref name="dateTimeOffset"/> to Gas Time.
    /// </summary>
    /// <param name="dateTimeOffset">The value to adjust.</param>
    /// <returns>The adjusted value.</returns>
    public static DateTimeOffset AsGasTime(this DateTimeOffset dateTimeOffset)
    {
        // Convert to Germany's local time.
        var germanyTime = TimeZoneInfo.ConvertTime(dateTimeOffset, GermanyTimeZone);
        
        // Shift for Gas Time.
        return germanyTime.ToOffset(germanyTime.Offset - GasTimeOffset);
    }

    /// <summary>
    /// Adjusts the provided <paramref name="dateTime"/> to Gas Time.
    /// </summary>
    /// <param name="dateTime">The value to adjust.</param>
    /// <returns>The adjusted value.</returns>
    public static DateTime AsGasTime(this DateTime dateTime)
    {
        // Always go through a DateTimeOffset to ensure conversions and adjustments are applied.
        return dateTime.ToGasDateTimeOffset().DateTime;
    }

    /// <summary>
    /// Adjusts the provided <paramref name="dateTime"/> to Gas Time,
    /// and returns the result as a <see cref="DateTimeOffset"/>.
    /// </summary>
    /// <param name="dateTime">The value to adjust.</param>
    /// <returns>The adjusted value as a <see cref="DateTimeOffset"/>.</returns>
    public static DateTimeOffset ToGasDateTimeOffset(this DateTime dateTime)
    {
        if (dateTime.Kind != DateTimeKind.Unspecified)
        {
            // UTC and Local kinds will get their correct offset in the DTO constructor.
            return new DateTimeOffset(dateTime).AsGasTime();
        }
        
        // Treat the incoming value as already in gas time - we just need the offset applied.
        // However, we also need to account for values that might be during DST transitions.
        var germanyDateTime = dateTime + GasTimeOffset;
        if (GermanyTimeZone.IsInvalidTime(germanyDateTime))
        {
            // In the DST spring-forward gap, advance the clock forward.
            // This should only happen if the data was bad to begin with.
            germanyDateTime = germanyDateTime.AddHours(1);
        }

        // In the DST fall-back overlap, choose the offset of the *first* occurence,
        // which is the same as the offset before the transition.
        // Otherwise, we're not in a transition, just get the offset.
        var germanyOffset = GermanyTimeZone.GetUtcOffset(
            GermanyTimeZone.IsAmbiguousTime(germanyDateTime)
                ? germanyDateTime.AddHours(-1)
                : germanyDateTime);

        // Construct the Germany DTO, shift to gas time, and return.
        var germanyDateTimeOffset = new DateTimeOffset(germanyDateTime, germanyOffset);
        return germanyDateTimeOffset.ToOffset(germanyOffset - GasTimeOffset);
    }

    /// <summary>
    /// Add a number of calendar days to the provided <paramref name="dateTimeOffset"/>, with respect to Gas Time.
    /// </summary>
    /// <remarks>
    /// A day in Gas Time is not necessarily 24 hours, because some days may contain a German DST transition.
    /// </remarks>
    /// <param name="dateTimeOffset">The value to add to.</param>
    /// <param name="daysToAdd">The number of calendar days to add.</param>
    /// <returns>The result of the operation, as a <see cref="DateTimeOffset"/> in Gas Time.</returns>
    public static DateTimeOffset AddGasDays(this DateTimeOffset dateTimeOffset, int daysToAdd)
    {
        // Add calendar days (with respect to gas time) - not necessarily 24 hours.
        return dateTimeOffset.AsGasTime().DateTime.AddDays(daysToAdd).ToGasDateTimeOffset();
    }
    
    /// <summary>
    /// Add a number of calendar days to the provided <paramref name="dateTime"/>, with respect to Gas Time.
    /// </summary>
    /// <remarks>
    /// A day in Gas Time is not necessarily 24 hours, because some days may contain a German DST transition.
    /// </remarks>
    /// <param name="dateTime">The value to add to.</param>
    /// <param name="daysToAdd">The number of calendar days to add.</param>
    /// <returns>The result of the operation, as a <see cref="DateTime"/> in Gas Time.</returns>
    public static DateTime AddGasDays(this DateTime dateTime, int daysToAdd)
    {
        // Add calendar days (with respect to gas time) - not necessarily 24 hours.
        return dateTime.AsGasTime().AddDays(daysToAdd).AsGasTime();
    }
}

一些用法示例:

  • 转换特定时间戳

    var test = DateTimeOffset.Parse("2022-03-22T05:00:00Z").AsGasTime();
    Console.WriteLine($"{test:yyyy-MM-ddTHH:mm:sszzz} in Gas Time is 
    {test.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} UTC.");
    

    输出:

    2022-03-22T00:00:00-05:00 in Gas Time is 2022-03-22T05:00:00Z UTC.
    
  • 获取当前gas时间戳

    var now = DateTimeOffset.UtcNow.AsGasTime();
    Console.WriteLine($"It is now {now:yyyy-MM-ddTHH:mm:sszzz} in Gas Time ({now.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} UTC).");
    

    输出:

    It is now 2022-05-10T14:31:56-04:00 in Gas Time (2022-05-10T18:31:56Z UTC).
    
  • 添加绝对时间(小时、分钟、秒等)

    var start = DateTimeOffset.Parse("2022-03-26T05:00:00Z").AsGasTime();
    var end = start.AddHours(24 * 7).AsGasTime(); // add and correct any offset change
    Console.WriteLine($"Starting at {start:yyyy-MM-ddTHH:mm:sszzz} Gas Time ({start.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} UTC),");
    Console.WriteLine($"7 x 24hr intervals later is {end:yyyy-MM-ddTHH:mm:sszzz} Gas Time ({end.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} UTC).");
    

    输出:

    Starting at 2022-03-26T00:00:00-05:00 Gas Time (2022-03-26T05:00:00Z UTC),
    7 x 24hr intervals later is 2022-04-02T01:00:00-04:00 Gas Time (2022-04-02T05:00:00Z UTC).
    
  • 添加或减去日历日(由于夏令时转换,并非严格的 24 小时制)

    var start = DateTimeOffset.Parse("2022-03-26T05:00:00Z").AsGasTime();
    var end = start.AddGasDays(7); // add and correct any offset change
    Console.WriteLine($"Starting at {start:yyyy-MM-ddTHH:mm:sszzz} Gas Time ({start.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} UTC),");
    Console.WriteLine($"7 calendar days later is {end:yyyy-MM-ddTHH:mm:sszzz} Gas Time ({end.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} UTC).");
    

    输出:

    Starting at 2022-03-26T00:00:00-05:00 Gas Time (2022-03-26T05:00:00Z UTC),
    7 calendar days later is 2022-04-02T00:00:00-04:00 Gas Time (2022-04-02T04:00:00Z UTC).
    

请注意,在最后两个示例中,我选择了与您不同的日期,以演示跨 DST 转换。