diff --git a/Source/Bogus.Tests/DataSetTests/DateTest.cs b/Source/Bogus.Tests/DataSetTests/DateTest.cs index 0bffc883..7bebb005 100644 --- a/Source/Bogus.Tests/DataSetTests/DateTest.cs +++ b/Source/Bogus.Tests/DataSetTests/DateTest.cs @@ -460,6 +460,90 @@ public void will_adjust_end_time_to_avoid_dst_transition() } } + [FactWhenDaylightSavingsSupported] + public void works_when_range_is_exactly_daylight_savings_transition_window() + { + // Arrange + var faker = new Faker(); + + faker.Random = new Randomizer(localSeed: 5); + + var dstRules = TimeZoneInfo.Local.GetAdjustmentRules(); + + var now = DateTime.Now; + + var effectiveRule = dstRules.Single(rule => (rule.DateStart <= now) && (rule.DateEnd >= now)); + + var transitionStartTime = CalculateTransitionDateTime(now, effectiveRule.DaylightTransitionStart); + var transitionEndTime = transitionStartTime + effectiveRule.DaylightDelta; + + // Act & Assert + for (int i = 0; i < 10000; i++) + { + var sample = faker.Date.Between(transitionStartTime, transitionEndTime); + + sample.Should().BeOneOf(transitionStartTime, transitionEndTime); + } + } + + [FactWhenDaylightSavingsSupported] + public void works_when_range_start_is_exactly_daylight_savings_transition_window_start() + { + // Arrange + var faker = new Faker(); + + faker.Random = new Randomizer(localSeed: 5); + + var dstRules = TimeZoneInfo.Local.GetAdjustmentRules(); + + var now = DateTime.Now; + + var effectiveRule = dstRules.Single(rule => (rule.DateStart <= now) && (rule.DateEnd >= now)); + + var transitionStartTime = CalculateTransitionDateTime(now, effectiveRule.DaylightTransitionStart); + var transitionEndTime = transitionStartTime + effectiveRule.DaylightDelta; + + var windowStart = transitionStartTime; + var windowEnd = transitionEndTime.AddMinutes(-5); + + // Act & Assert + for (int i = 0; i < 10000; i++) + { + var sample = faker.Date.Between(windowStart, windowEnd); + + sample.Should().Be(windowStart); + } + } + + [FactWhenDaylightSavingsSupported] + public void works_when_range_end_is_exactly_daylight_savings_transition_window_end() + { + // Arrange + var faker = new Faker(); + + faker.Random = new Randomizer(localSeed: 5); + + var dstRules = TimeZoneInfo.Local.GetAdjustmentRules(); + + var now = DateTime.Now; + + var effectiveRule = dstRules.Single(rule => (rule.DateStart <= now) && (rule.DateEnd >= now)); + + var transitionStartTime = CalculateTransitionDateTime(now, effectiveRule.DaylightTransitionStart); + var transitionEndTime = transitionStartTime + effectiveRule.DaylightDelta; + + var windowStart = transitionStartTime.AddMinutes(5); + var windowEnd = transitionEndTime; + + // Act & Assert + for (int i = 0; i < 10000; i++) + { + var sample = faker.Date.Between(windowStart, windowEnd); + + sample.Should().Be(windowEnd); + } + } + private DateTime CalculateTransitionDateTime(DateTime now, TimeZoneInfo.TransitionTime transition) { // Based on code found at: https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo.transitiontime.isfixeddaterule diff --git a/Source/Bogus/DataSets/Date.cs b/Source/Bogus/DataSets/Date.cs index d5997b0b..955c3c35 100644 --- a/Source/Bogus/DataSets/Date.cs +++ b/Source/Bogus/DataSets/Date.cs @@ -144,8 +144,17 @@ public DateTime Between(DateTime start, DateTime end) var value = new DateTime(minTicks, DateTimeKind.Utc) + partTimeSpan; if (start.Kind != DateTimeKind.Utc) + { value = value.ToLocalTime(); + // Right around daylight savings time transition, there can be two different local DateTime values + // that are actually exactly the same DateTime. The ToLocalTime conversion might pick the wrong + // one in edge cases; it will pick the later one, and if the caller's window includes the earlier + // one, we should return that instead to follow the principle of least surprise. + if (value > end) + value = end; + } + return value; } @@ -178,12 +187,20 @@ private void ComputeRealRange(ref DateTime start, ref DateTime end) #if !NETSTANDARD1_3 var window = GetForwardDSTTransitionWindow(start); - if ((start > window.Start) && (start <= window.End)) - start = new DateTime(window.End.Ticks, start.Kind); + if ((start >= window.Start) && (start <= window.End)) + { + if ((start == window.Start) && (end >= window.Start) && (end <= window.End)) + end = start; + else + start = new DateTime(window.End.Ticks, start.Kind); + + if (start == end) + return; + } window = GetForwardDSTTransitionWindow(end); - if ((end >= window.Start) && (end < window.End)) + if ((end >= window.Start) && (end <= window.End)) end = new DateTime(window.Start.Ticks, end.Kind); if (start > end)