internal static BclAdjustmentRule FromUnixAdjustmentRule(TimeZoneInfo zone, TimeZoneInfo.AdjustmentRule rule) { // On .NET Core on Unix, each "adjustment rule" is effectively just a zone interval. The transitions are only used // to give the time of day values to combine with rule.DateStart and rule.DateEnd. It's all a bit odd. // The *last* adjustment rule internally can work like a normal Windows standard/daylight rule, but currently that's // not exposed properly. var bclLocalStart = rule.DateStart + rule.DaylightTransitionStart.TimeOfDay.TimeOfDay; var bclLocalEnd = rule.DateEnd + rule.DaylightTransitionEnd.TimeOfDay.TimeOfDay; var bclUtcStart = DateTime.SpecifyKind(bclLocalStart == DateTime.MinValue ? DateTime.MinValue : bclLocalStart - zone.BaseUtcOffset, DateTimeKind.Utc); var bclWallOffset = zone.GetUtcOffset(bclUtcStart); var bclSavings = rule.DaylightDelta; var bclUtcEnd = DateTime.SpecifyKind(rule.DateEnd == MaxDate ? DateTime.MaxValue : bclLocalEnd - (zone.BaseUtcOffset + bclSavings), DateTimeKind.Utc); var isDst = zone.IsDaylightSavingTime(bclUtcStart); // The BCL rule can't express "It's DST with a changed standard time" so we sometimes end // up with DST but no savings. Assume this means a savings of 1 hour. That's not a valid // assumption in all cases, but it's probably better than alternatives, given limited information. if (isDst && bclSavings == TimeSpan.Zero) { bclSavings = TimeSpan.FromHours(1); } // Sometimes the rule says "This rule doesn't apply daylight savings" but still has a daylight // savings delta. Extremely bizarre: just override the savings to zero. if (!isDst && bclSavings != TimeSpan.Zero) { bclSavings = TimeSpan.Zero; } // Handle changes crossing the international date line, which are represented as savings of +/-23 // hours (but could conceivably be more). if (bclSavings.Hours < -14) { bclSavings += TimeSpan.FromDays(1); } else if (bclSavings.Hours > 14) { bclSavings -= TimeSpan.FromDays(1); } var bclStandard = bclWallOffset - bclSavings; // Now all the values are sensible - and in particular, now the daylight savings are in a range that can be represented by // Offset - we can converted everything to Noda Time types. var nodaStart = bclUtcStart == DateTime.MinValue ? Instant.BeforeMinValue : bclUtcStart.ToInstant(); // The representation returned to us (not the internal representation) has an end point one second before the transition. var nodaEnd = bclUtcEnd == DateTime.MaxValue ? Instant.AfterMaxValue : bclUtcEnd.ToInstant() + Duration.FromSeconds(1); var nodaWallOffset = bclWallOffset.ToOffset(); var nodaStandard = bclStandard.ToOffset(); var nodaSavings = bclSavings.ToOffset(); var partialMap = PartialZoneIntervalMap.ForZoneInterval(isDst ? zone.StandardName : zone.DaylightName, nodaStart, nodaEnd, nodaWallOffset, nodaSavings); return(new BclAdjustmentRule(nodaStart, nodaEnd, nodaStandard, nodaSavings, partialMap)); }
internal static BclAdjustmentRule FromWindowsAdjustmentRule(TimeZoneInfo zone, TimeZoneInfo.AdjustmentRule rule) { // With .NET 4.6, adjustment rules can have their own standard offsets, allowing // a much more reasonable set of time zone data. Unfortunately, this isn't directly // exposed, but we can detect it by just finding the UTC offset for an arbitrary // time within the rule - the start, in this case - and then take account of the // possibility of that being in daylight saving time. Fortunately, we only need // to do this during the setup. var ruleStandardOffset = zone.GetUtcOffset(rule.DateStart); if (zone.IsDaylightSavingTime(rule.DateStart)) { ruleStandardOffset -= rule.DaylightDelta; } var standardOffset = ruleStandardOffset.ToOffset(); // Although the rule may have its own standard offset, the start/end is still determined // using the zone's standard offset. var zoneStandardOffset = zone.BaseUtcOffset.ToOffset(); // Note: this extends back from DateTime.MinValue to start of time, even though the BCL can represent // as far back as 1AD. This is in the *spirit* of a rule which goes back that far. var start = rule.DateStart == DateTime.MinValue ? Instant.BeforeMinValue : rule.DateStart.ToLocalDateTime().WithOffset(zoneStandardOffset).ToInstant(); // The end instant (exclusive) is the end of the given date, so we need to add a day. var end = rule.DateEnd == MaxDate ? Instant.AfterMaxValue : rule.DateEnd.ToLocalDateTime().PlusDays(1).WithOffset(zoneStandardOffset).ToInstant(); var savings = rule.DaylightDelta.ToOffset(); PartialZoneIntervalMap partialMap; // Some rules have DST start/end of "January 1st", to indicate that they're just in standard time. This is important // for rules which have a standard offset which is different to the standard offset of the zone itself. if (IsStandardOffsetOnlyRule(rule)) { partialMap = PartialZoneIntervalMap.ForZoneInterval(zone.StandardName, start, end, standardOffset, Offset.Zero); } else { var daylightRecurrence = new ZoneRecurrence(zone.DaylightName, savings, ConvertTransition(rule.DaylightTransitionStart), int.MinValue, int.MaxValue); var standardRecurrence = new ZoneRecurrence(zone.StandardName, Offset.Zero, ConvertTransition(rule.DaylightTransitionEnd), int.MinValue, int.MaxValue); IZoneIntervalMap recurringMap = new StandardDaylightAlternatingMap(standardOffset, standardRecurrence, daylightRecurrence); // Fake 1 hour savings if the adjustment rule claims to be 0 savings. See DaylightFakingZoneIntervalMap documentation below for more details. if (savings == Offset.Zero) { recurringMap = new DaylightFakingZoneIntervalMap(recurringMap, zone.DaylightName); } partialMap = new PartialZoneIntervalMap(start, end, recurringMap); } return(new BclAdjustmentRule(start, end, standardOffset, savings, partialMap)); }
private static IZoneIntervalMap BuildMap(BclAdjustmentRule[] rules, Offset standardOffset, string standardName) { Preconditions.CheckNotNull(standardName, nameof(standardName)); // First work out a naive list of partial maps. These will give the right offset at every instant, but not necessarily // correct intervals - we may we need to stitch intervals together. List <PartialZoneIntervalMap> maps = new List <PartialZoneIntervalMap>(); // Handle the start of time until the start of the first rule, if necessary. if (rules[0].Start.IsValid) { maps.Add(PartialZoneIntervalMap.ForZoneInterval(standardName, Instant.BeforeMinValue, rules[0].Start, standardOffset, Offset.Zero)); } for (int i = 0; i < rules.Length - 1; i++) { var beforeRule = rules[i]; var afterRule = rules[i + 1]; maps.Add(beforeRule.PartialMap); // If there's a gap between this rule and the next one, fill it with a fixed interval. if (beforeRule.End < afterRule.Start) { maps.Add(PartialZoneIntervalMap.ForZoneInterval(standardName, beforeRule.End, afterRule.Start, standardOffset, Offset.Zero)); } } var lastRule = rules[rules.Length - 1]; maps.Add(lastRule.PartialMap); // Handle the end of the last rule until the end of time, if necessary. if (lastRule.End.IsValid) { maps.Add(PartialZoneIntervalMap.ForZoneInterval(standardName, lastRule.End, Instant.AfterMaxValue, standardOffset, Offset.Zero)); } return(PartialZoneIntervalMap.ConvertToFullMap(maps)); }
// Visible for tests internal BclAdjustmentRule(ZoneInterval zoneInterval) { StandardOffset = zoneInterval.StandardOffset; Savings = zoneInterval.Savings; PartialMap = PartialZoneIntervalMap.ForZoneInterval(zoneInterval); }