首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >转化NodaTime ZoneIntervals

转化NodaTime ZoneIntervals
EN

Code Review用户
提问于 2020-02-06 15:18:47
回答 1查看 173关注 0票数 4

版本2可以找到这里

我工作的公司在世界各地都有客户。我使用一个时间序列数据库,其中包含每个客户站点的制造过程数据。我被要求提供过去两年的每日平均数。从第三方时间序列数据库中请求平均值是很容易的。困难在于,每个请求都需要针对每个站点的时区发出。

NodaTime的ZoneInterval为我提供了一些信息,但我需要将它转换为第三方数据库。对时间序列数据库的调用期望UTC的开始和结束时间,您可以要求以均匀间隔的TimeSpan间隔返回摘要--考虑这里的时间而不是“一天”。对于一年中的大多数日子来说,这是非常容易的,除了任何DST过渡天,其中的一天长度不是24小时。

下面是用于执行自定义转换的ZonedDateRange.cs类:

代码语言:javascript
复制
using System;
using System.Collections.Generic;
using System.Linq;
using NodaTime;
using NodaTime.TimeZones;

namespace NodaTime_Zoned_Ranges
{
    public class ZonedDateRange
    {
        public enum DayState { Standard, DST, SpringForward, FallBack }

        public DateTimeZone Zone { get; private set; }
        public DayState State { get; private set; }
        public LocalDate StartDay { get; private set; }
        public LocalDate EndDay { get; private set; }

        public ZonedDateTime ZoneStart => Zone.AtStartOfDay(StartDay);
        public ZonedDateTime ZoneEnd => Zone.AtStartOfDay(EndDay.PlusDays(1));

        public DateTime UtcStart => ZoneStart.ToDateTimeUtc();
        public DateTime UtcEnd => ZoneEnd.ToDateTimeUtc();

        public double HoursPerDay => IsTransitionDay ? (UtcEnd - UtcStart).TotalHours : 24;

        public int DaysInRange => IsTransitionDay ? 1 : (int)((ZoneStart - ZoneEnd).TotalDays);


        // -1 = Falling back DAY, +1 Spring Forward DAY, 0 means no transition occuring BUT the day still could be DST.
        public int Transition => (State == DayState.FallBack) ? Backward : (State == DayState.SpringForward) ? Forward : None;
        public bool IsTransitionDay => (Transition != None);

        public const int Backward = -1;
        public const int Forward = 1;
        public const int None = 0;

        // Private constructor forces using static factory.
        private ZonedDateRange() { }

        // A list should be fairly small.  Consider U.S. Central Time for an entire calendar year.  There will only be 5 items in the list.
        //      1) CST from Jan 1 to the day before Spring forward.
        //      2) Spring Forward transition day (one day is both start and end)
        //      3) CDT from day after Spring Forward and day before Fall Back.
        //      4) Fall Back transition day (again, only 1 day in range)
        //      5) CST after Fall Back day
        // The most important thing is that all days in a range will have the same length.
        // That way you can safely average in whatever that length is.

        public static IEnumerable GenerateRanges(DateTimeZone zone, Instant anchorInstant, int days)
        {
            if (zone == null)
            {
                throw new ArgumentNullException(nameof(zone));
            }

            var anchorDay = anchorInstant.InZone(zone).Date;

            // If days is negative, anchorInstant is the endDay and we go back in time to get the start day.
            // Otherwise, anchorDay is the anchorInstant and we go forward in time to get the end day.
            var inclusiveStartDay = (days < 0) ? anchorDay.PlusDays(days) : anchorDay;
            var inclusiveEndDay = (days < 0) ? anchorDay : anchorDay.PlusDays(days);

            return GenerateRanges(zone, inclusiveStartDay, inclusiveEndDay);
        }

        public static IEnumerable GenerateRanges(DateTimeZone zone, LocalDate inclusiveStartDay, LocalDate inclusiveEndDay)
        {
            if (zone == null)
            {
                throw new ArgumentNullException(nameof(zone));
            }

            // Small adjustment to add an extra day to the inclusive end day.
            // When working with LocalDate(s) that are inclusive, we generally start at the start of the start day
            // but want to end at the END of the end day, which is really the start of the next day following the
            // end day.
            var exclusiveEndDay = inclusiveEndDay.PlusDays(1);

            var startInstant = inclusiveStartDay.AtStartOfDayInZone(zone).ToInstant();
            var endInstant = exclusiveEndDay.AtStartOfDayInZone(zone).ToInstant();

            // Just in case the start or end day occurs on transition day, we pad each endpoint with a few days.
            // We will later prune away this padding.
            var pad = Duration.FromDays(5);
            var padStartInstant = startInstant.Minus(pad);
            var padEndInstant = endInstant.Plus(pad);

            var intervals = zone.GetZoneIntervals(padStartInstant, padEndInstant).ToList();

            // Take care of easy cases.
            // Check count returned instead of custom SupportsDaylightSavingsTime method.
            // E.g. Argentina supported DST in the past, but since 2010 has been on Standard time only.
            if (intervals.Count == 1)
            {
                yield return Create(zone, inclusiveStartDay, exclusiveEndDay, DayState.Standard);
                yield break;
            }

            for (var index = 0; index < intervals.Count; index++)
            {
                var interval = ClampInterval(intervals[index], padStartInstant, padEndInstant);

                // Chop off the Start and End dates, since those are transition days.
                // That is move Start ahead 1 day, and move End back 1 day.
                var currStartDate = interval.Start.InZone(zone).Date.PlusDays(1);
                var currEndDate = interval.End.InZone(zone).Date.PlusDays(-1);

                var endLength = zone.HoursInDay(interval.End);

                var endState = DayState.Standard;
                if (endLength > NodaConstants.HoursPerDay)
                {
                    endState = DayState.FallBack;
                }
                else if (endLength < NodaConstants.HoursPerDay)
                {
                    endState = DayState.SpringForward;
                }

                var startState = (endState == DayState.FallBack) ? DayState.DST : DayState.Standard;
                var range = Create(zone, currStartDate, currEndDate, startState);
                AdjustEndPoints(range, inclusiveStartDay, exclusiveEndDay);
                if (IsOkayToOutput(range))
                {
                    yield return range;
                }

                var endTransitionDate = interval.End.InZone(zone).Date;
                range = Create(zone, endTransitionDate, endTransitionDate, endState);
                AdjustEndPoints(range, endTransitionDate, endTransitionDate);
                if (IsOkayToOutput(range))
                {
                    yield return range;
                }
            }
        }

        private static void AdjustEndPoints(ZonedDateRange range, LocalDate startDay, LocalDate endDay)
        {
            if (range.StartDay < startDay)
            {
                range.StartDay = startDay;
            }
            if (range.EndDay > endDay)
            {
                range.EndDay = endDay;
            }
        }

        private static bool IsOkayToOutput(ZonedDateRange range) => (range.UtcEnd > range.UtcStart);

        private static ZoneInterval ClampInterval(ZoneInterval interval, Instant start, Instant end)
        {
            var outstart = start;
            var outend = end;

            if (interval.HasStart && outstart < interval.Start)
            {
                outstart = interval.Start;
            }

            if (interval.HasEnd && interval.End < outend)
            {
                outend = interval.End;
            }

            return new ZoneInterval(interval.Name, outstart, outend, interval.WallOffset, interval.Savings);
        }

        private static ZonedDateRange Create(DateTimeZone zone, LocalDate startDate, LocalDate endDate, DayState state)
        {
            var range = new ZonedDateRange
            {
                Zone = zone,
                StartDay = startDate,
                EndDay = endDate,
                State = state
            };
            return range;
        }

        // This alters the StartDate and UtcStartTime so you may want to perform this on a Clone().
        internal void AdjustStartDateForward(LocalDate adjustedStartDate)
        {
            if (adjustedStartDate < StartDay || adjustedStartDate > EndDay)
            {
                throw new Exception($"The {nameof(adjustedStartDate)} must be exclusively within the current StartDate and EndDate.");
            }
            AdjustDates(adjustedStartDate, EndDay);
        }

        // This alters the EndDate and UtcEndTime so you may want to perform this on a Clone().
        internal void AdjustEndDateBackward(LocalDate adjustedEndDate)
        {
            if (adjustedEndDate < StartDay || adjustedEndDate > EndDay)
            {
                throw new Exception($"The {nameof(adjustedEndDate)} must be exclusively within the current StartDate and EndDate.");
            }
            AdjustDates(StartDay, adjustedEndDate);
        }

        private void AdjustDates(LocalDate adjustedStart, LocalDate adjustedEnd)
        {
            StartDay = adjustedStart;
            EndDay = adjustedEnd;
        }

        public ZonedDateRange Clone()
        {
            var clone = new ZonedDateRange();
            clone.Zone = Zone;
            clone.State = State;
            clone.StartDay = StartDay;
            clone.EndDay = EndDay;
            return clone;
        }

    }
} 

下面是Extensions.cs类,用于一些方便的扩展:

代码语言:javascript
复制
using System;
using NodaTime;

namespace NodaTime_Zoned_Ranges
{
    public static class Extensions
    {
        // For DST Transition days, hours will be less than or greater than 24.
        public static double HoursInDay(this DateTimeZone zone, Instant instant)
        {
            if (zone == null)
            {
                return NodaConstants.HoursPerDay;
            }

            var day = instant.InZone(zone).LocalDateTime.Date;

            var bod = zone.AtStartOfDay(day);
            var eod = zone.AtStartOfDay(day.PlusDays(1));

            return (eod.ToInstant() - bod.ToInstant()).TotalHours;
        }

        /// 
        /// Preferred format of ISO 8601 time string.  
        /// Unlike Round Trip format specifier of "o", this format will suppress decimal seconds
        /// if the input time does not have subseconds.
        /// 
        public const string DateTimeExtendedIsoFormat = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK";


        /// 
        /// Returns an ISO-8601 compliant time string.  
        /// If the input Kind is Local and TimeZoneInfo.Local is not "UTC", then the output string will contain a time zone offset. 
        /// Unlike ToString("o"), if the input time does not contain subseconds, the output string will omit subseconds.
        /// 
        /// DateTime
        /// String
        public static string ToIsoString(this DateTime time)
        {
            // TimeZoneInfo MUST use Equals method and not == operator.
            // Equals compares values where == compares object references.
            if (time.Kind == DateTimeKind.Local && TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
            {
                // Don't use time zone offset if Local time is UTC
                time = DateTime.SpecifyKind(time, DateTimeKind.Utc);
            }
            return time.ToString(DateTimeExtendedIsoFormat);
        }
    }
}

最后,下面是用于快速和肮脏测试的Program.cs

代码语言:javascript
复制
using System;
using NodaTime;

namespace NodaTime_Zoned_Ranges
{
    class Program
    {
        static void Main(string[] args)
        {
            var zoneIds = new string[] { "Central Brazilian Standard Time", "Singapore Standard Time" };

            var startDay = new LocalDate(2018, 1, 1);
            var endDay = new LocalDate(2019, 12, 31);

            foreach (var zoneId in zoneIds)
            {
                var zone = DateTimeZoneProviders.Bcl.GetZoneOrNull(zoneId);
                ZoneTest(zone, startDay, endDay);
            }

            Console.WriteLine("\n\nPress ENTER key");
            Console.ReadLine();
        }

        private static void ZoneTest(DateTimeZone zone, LocalDate startDay, LocalDate endDay)
        {
            Console.WriteLine($"\n\n*** TEST FOR ZONE: {zone.Id} , Start:{startDay} , End:{endDay}\n");

            var startInstant = startDay.AtStartOfDayInZone(zone).ToInstant();
            var endInstant = endDay.PlusDays(1).AtStartOfDayInZone(zone).ToInstant();

            Console.WriteLine("NodaTime DateTimeZone.GetZoneIntervals");
            var intervals = zone.GetZoneIntervals(startInstant, endInstant);

            var i = 0;
            foreach (var interval in intervals)
            {
                Console.WriteLine($"  [{i++}]: {interval}");
            }

            Console.WriteLine("\nCustom ZonedDateRange");
            i = 0;
            var ranges = ZonedDateRange.GenerateRanges(zone, startDay, endDay);
            foreach (var range in ranges)
            {
                Console.WriteLine($"  [{i++}]: {range.State,13}: [{range.UtcStart.ToIsoString()}, {range.UtcEnd.ToIsoString()}] HoursPerDay: {range.HoursPerDay}");
            }
        }
    }
}

下面是示例Console窗口输出

代码语言:javascript
复制
*** TEST FOR ZONE: Central Brazilian Standard Time , Start:Monday, January 1, 2018 , End:Tuesday, December 31, 2019

NodaTime DateTimeZone.GetZoneIntervals
  [0]: Central Brazilian Daylight Time: [2017-10-15T03:59:59Z, 2018-02-18T02:59:59Z) -03 (+01)
  [1]: Central Brazilian Standard Time: [2018-02-18T02:59:59Z, 2018-11-04T03:59:59Z) -04 (+00)
  [2]: Central Brazilian Daylight Time: [2018-11-04T03:59:59Z, 2019-02-17T03:00:00Z) -03 (+01)
  [3]: Central Brazilian Standard Time: [2019-02-17T03:00:00Z, EndOfTime) -04 (+00)

Custom ZonedDateRange
  [0]:           DST: [2018-01-01T03:00:00Z, 2018-02-17T03:00:00Z] HoursPerDay: 24
  [1]:      FallBack: [2018-02-17T03:00:00Z, 2018-02-18T04:00:00Z] HoursPerDay: 25
  [2]:      Standard: [2018-02-18T04:00:00Z, 2018-11-04T03:59:59.999Z] HoursPerDay: 24
  [3]: SpringForward: [2018-11-04T03:59:59.999Z, 2018-11-05T03:00:00Z] HoursPerDay: 23.0000002777778
  [4]:           DST: [2018-11-05T03:00:00Z, 2019-02-16T03:00:00Z] HoursPerDay: 24
  [5]:      FallBack: [2019-02-16T03:00:00Z, 2019-02-17T04:00:00Z] HoursPerDay: 25
  [6]:      Standard: [2019-02-17T04:00:00Z, 2020-01-02T04:00:00Z] HoursPerDay: 24
  [7]:      Standard: [2020-01-06T04:00:00Z, 2020-01-07T04:00:00Z] HoursPerDay: 24


*** TEST FOR ZONE: Singapore Standard Time , Start:Monday, January 1, 2018 , End:Tuesday, December 31, 2019

NodaTime DateTimeZone.GetZoneIntervals
  [0]: Malay Peninsula Standard Time: [StartOfTime, EndOfTime) +08 (+00)

Custom ZonedDateRange
  [0]:      Standard: [2017-12-31T16:00:00Z, 2020-01-01T16:00:00Z] HoursPerDay: 24


Press ENTER key

根据输出,我希望您能够理解为什么我需要执行转换。对于巴西,我可以对我的第三方数据库进行8个具体的摘要调用,每个调用都有不同的UTC开始和结束时间,以及一天的长度。对于新加坡,你可以看到我可以从没有开始或结束时间的间隔中得到非常具体的UTC时间。

除了“请检查我的代码以提高可读性和性能”这个始终隐含的问题之外,我没有其他具体的问题。

EN

回答 1

Code Review用户

回答已采纳

发布于 2020-02-08 10:37:43

旁白: Noda Time报告的区域间隔在我看来有点坏;这可能是因为它们来自Windows时区数据库。我需要研究的是,在“小时开始前的第二天”,转换不会发生。

我还没来得及彻底研究这个问题,但我有几个小建议:

命名

你用了很多“天”,我会用“约会”。我发现这一点不那么含糊,因为“一天”可以同时指一个句号和一个日期。我已经调整了下面的代码假设。

GenerateRanges

代码语言:javascript
复制
var inclusiveStartDate = (days < 0) ? anchorDate.PlusDays(days) : anchorDate;
var inclusiveEndDate = (days < 0) ? anchorDate : anchorDate.PlusDays(days);

这将更简单一些,例如无条件地添加days,然后只取min/max:

代码语言:javascript
复制
var anchorPlusDays = anchorDate.PlusDays(days);
var inclusiveStartDate = LocalDate.Min(anchorDate, anchorPlusDays);
var inclusiveEndDate = LocalDate.Max(anchorDate, anchorPlusDays);

扩展

对于使用NodaTime类型的代码和使用BCL类型的代码,我个人都会使用单独的扩展类。

AdjustEndpoints

我可能会尝试使您的ZonedDateRange完全不可变(消除了对Clone的需求),并使用WithStartDateWithEndDate方法,然后使AdjustEndpoints如下所示:

代码语言:javascript
复制
private static ZonedDateRange AdjustEndPoints(
    ZonedDateRange range, LocalDate startDate, LocalDate endDate) =>
    range.WithStartDate(LocalDate.Max(range.StartDate, startDate))
         .WithEndDate(LocalDate.Min(range.EndDate, endDate));

(如果参数等于当前值,WithStartDateWithEndDate方法可以返回"this“。)

票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/236787

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档