版本2可以找到这里。
我工作的公司在世界各地都有客户。我使用一个时间序列数据库,其中包含每个客户站点的制造过程数据。我被要求提供过去两年的每日平均数。从第三方时间序列数据库中请求平均值是很容易的。困难在于,每个请求都需要针对每个站点的时区发出。
NodaTime的ZoneInterval为我提供了一些信息,但我需要将它转换为第三方数据库。对时间序列数据库的调用期望UTC的开始和结束时间,您可以要求以均匀间隔的TimeSpan间隔返回摘要--考虑这里的时间而不是“一天”。对于一年中的大多数日子来说,这是非常容易的,除了任何DST过渡天,其中的一天长度不是24小时。
下面是用于执行自定义转换的ZonedDateRange.cs类:
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类,用于一些方便的扩展:
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:
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窗口输出:
*** 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时间。
除了“请检查我的代码以提高可读性和性能”这个始终隐含的问题之外,我没有其他具体的问题。
发布于 2020-02-08 10:37:43
旁白: Noda Time报告的区域间隔在我看来有点坏;这可能是因为它们来自Windows时区数据库。我需要研究的是,在“小时开始前的第二天”,转换不会发生。
我还没来得及彻底研究这个问题,但我有几个小建议:
你用了很多“天”,我会用“约会”。我发现这一点不那么含糊,因为“一天”可以同时指一个句号和一个日期。我已经调整了下面的代码假设。
var inclusiveStartDate = (days < 0) ? anchorDate.PlusDays(days) : anchorDate;
var inclusiveEndDate = (days < 0) ? anchorDate : anchorDate.PlusDays(days);这将更简单一些,例如无条件地添加days,然后只取min/max:
var anchorPlusDays = anchorDate.PlusDays(days);
var inclusiveStartDate = LocalDate.Min(anchorDate, anchorPlusDays);
var inclusiveEndDate = LocalDate.Max(anchorDate, anchorPlusDays);对于使用NodaTime类型的代码和使用BCL类型的代码,我个人都会使用单独的扩展类。
我可能会尝试使您的ZonedDateRange完全不可变(消除了对Clone的需求),并使用WithStartDate、WithEndDate方法,然后使AdjustEndpoints如下所示:
private static ZonedDateRange AdjustEndPoints(
ZonedDateRange range, LocalDate startDate, LocalDate endDate) =>
range.WithStartDate(LocalDate.Max(range.StartDate, startDate))
.WithEndDate(LocalDate.Min(range.EndDate, endDate));(如果参数等于当前值,WithStartDate和WithEndDate方法可以返回"this“。)
https://codereview.stackexchange.com/questions/236787
复制相似问题