From 459d369db5462f39e4a0e17eb40c2641d1b501cb Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Mon, 15 Mar 2021 20:32:12 -0500 Subject: [PATCH] Added unit test DateTest.will_not_generate_values_that_do_not_exist_due_to_daylight_savings that detects the error when run in a time zone with DST transitions. Updated DataSets/Date.cs to funnel all DateTime generation into Between and BetweenOffset, and updated the implementations of Between and BetweenOffset to convert the range to UTC before calculating the random value, and then convert the resulting UTC value back to local time, taking advantage of the framework's automatic DST calculations. --- Source/Bogus.Tests/DataSetTests/DateTest.cs | 79 +++++++++++++++++++++ Source/Bogus/DataSets/Date.cs | 61 +++++++--------- 2 files changed, 104 insertions(+), 36 deletions(-) diff --git a/Source/Bogus.Tests/DataSetTests/DateTest.cs b/Source/Bogus.Tests/DataSetTests/DateTest.cs index dd27f15d..4b9ae774 100644 --- a/Source/Bogus.Tests/DataSetTests/DateTest.cs +++ b/Source/Bogus.Tests/DataSetTests/DateTest.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using Bogus.DataSets; using FluentAssertions; using Xunit; @@ -346,5 +347,83 @@ public void can_set_global_static_time_source() d.PastOffset().Offset.Should().Be(DateTimeOffset.Now.Offset); d.RecentOffset().Offset.Should().Be(DateTimeOffset.Now.Offset); } + + public class FactWhenDaylightSavingsSupported : FactAttribute + { + public FactWhenDaylightSavingsSupported() + { + if (!TimeZoneInfo.Local.SupportsDaylightSavingTime) + { + Skip = "Test is only meaningful when Daylight Savings is supported by the local timezone."; + } + } + } + + [FactWhenDaylightSavingsSupported] + public void will_not_generate_values_that_do_not_exist_due_to_daylight_savings() + { + // 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.ToUniversalTime().AddHours(1).ToLocalTime(); + + // Act + var value = faker.Date.Between(transitionStartTime.AddHours(-1), transitionEndTime.AddHours(+1)); + + // Assert + if ((value >= transitionStartTime) && (value < transitionStartTime.AddHours(1))) + value.Should().NotBeBefore(transitionEndTime); + } + + 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 + + if (transition.IsFixedDateRule) + { + return new DateTime( + now.Year, + transition.Month, + transition.Day, + transition.TimeOfDay.Hour, + transition.TimeOfDay.Minute, + transition.TimeOfDay.Second, + transition.TimeOfDay.Millisecond); + } + + var calendar = CultureInfo.CurrentCulture.Calendar; + + var startOfWeek = transition.Week * 7 - 6; + + var firstDayOfWeek = (int)calendar.GetDayOfWeek(new DateTime(now.Year, transition.Month, 1)); + var changeDayOfWeek = (int)transition.DayOfWeek; + + int transitionDay = + firstDayOfWeek <= changeDayOfWeek + ? startOfWeek + changeDayOfWeek - firstDayOfWeek + : startOfWeek + changeDayOfWeek - firstDayOfWeek + 7; + + if (transitionDay > calendar.GetDaysInMonth(now.Year, transition.Month)) + transitionDay -= 7; + + return new DateTime( + now.Year, + transition.Month, + transitionDay, + transition.TimeOfDay.Hour, + transition.TimeOfDay.Minute, + transition.TimeOfDay.Second, + transition.TimeOfDay.Millisecond); + } } } \ No newline at end of file diff --git a/Source/Bogus/DataSets/Date.cs b/Source/Bogus/DataSets/Date.cs index 8ce76e4e..3a29cbbe 100644 --- a/Source/Bogus/DataSets/Date.cs +++ b/Source/Bogus/DataSets/Date.cs @@ -44,11 +44,7 @@ public DateTime Past(int yearsToGoBack = 1, DateTime? refDate = null) var minDate = maxDate.AddYears(-yearsToGoBack); - var totalTimeSpanTicks = (maxDate - minDate).Ticks; - - var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks); - - return maxDate - partTimeSpan; + return Between(minDate, maxDate); } /// @@ -62,11 +58,7 @@ public DateTimeOffset PastOffset(int yearsToGoBack = 1, DateTimeOffset? refDate var minDate = maxDate.AddYears(-yearsToGoBack); - var totalTimeSpanTicks = (maxDate - minDate).Ticks; - - var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks); - - return maxDate - partTimeSpan; + return BetweenOffset(minDate, maxDate); } /// @@ -112,11 +104,7 @@ public DateTime Future(int yearsToGoForward = 1, DateTime? refDate = null) var maxDate = minDate.AddYears(yearsToGoForward); - var totalTimeSpanTicks = (maxDate - minDate).Ticks; - - var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks); - - return minDate + partTimeSpan; + return Between(minDate, maxDate); } /// @@ -130,11 +118,7 @@ public DateTimeOffset FutureOffset(int yearsToGoForward = 1, DateTimeOffset? ref var maxDate = minDate.AddYears(yearsToGoForward); - var totalTimeSpanTicks = (maxDate - minDate).Ticks; - - var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks); - - return minDate + partTimeSpan; + return BetweenOffset(minDate, maxDate); } /// @@ -144,14 +128,22 @@ public DateTimeOffset FutureOffset(int yearsToGoForward = 1, DateTimeOffset? ref /// End time public DateTime Between(DateTime start, DateTime end) { - var minTicks = Math.Min(start.Ticks, end.Ticks); - var maxTicks = Math.Max(start.Ticks, end.Ticks); + var startTicks = start.ToUniversalTime().Ticks; + var endTicks = end.ToUniversalTime().Ticks; + + var minTicks = Math.Min(startTicks, endTicks); + var maxTicks = Math.Max(startTicks, endTicks); var totalTimeSpanTicks = maxTicks - minTicks; var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks); - return new DateTime(minTicks, start.Kind) + partTimeSpan; + var value = new DateTime(minTicks, DateTimeKind.Utc) + partTimeSpan; + + if (start.Kind != DateTimeKind.Utc) + value = value.ToLocalTime(); + + return value; } /// @@ -161,14 +153,19 @@ public DateTime Between(DateTime start, DateTime end) /// End time public DateTimeOffset BetweenOffset(DateTimeOffset start, DateTimeOffset end) { - var minTicks = Math.Min(start.Ticks, end.Ticks); - var maxTicks = Math.Max(start.Ticks, end.Ticks); + var startTicks = start.ToUniversalTime().Ticks; + var endTicks = end.ToUniversalTime().Ticks; + + var minTicks = Math.Min(startTicks, endTicks); + var maxTicks = Math.Max(startTicks, endTicks); var totalTimeSpanTicks = maxTicks - minTicks; var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks); - return new DateTimeOffset(minTicks, start.Offset) + partTimeSpan; + var dateTime = new DateTime(minTicks, DateTimeKind.Unspecified) + partTimeSpan; + + return new DateTimeOffset(dateTime + start.Offset, start.Offset); } /// @@ -182,11 +179,7 @@ public DateTime Recent(int days = 1, DateTime? refDate = null) var minDate = days == 0 ? SystemClock().Date : maxDate.AddDays(-days); - var totalTimeSpanTicks = (maxDate - minDate).Ticks; - - var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks); - - return maxDate - partTimeSpan; + return Between(minDate, maxDate); } /// @@ -200,11 +193,7 @@ public DateTimeOffset RecentOffset(int days = 1, DateTimeOffset? refDate = null) var minDate = days == 0 ? SystemClock().Date : maxDate.AddDays(-days); - var totalTimeSpanTicks = (maxDate - minDate).Ticks; - - var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks); - - return maxDate - partTimeSpan; + return BetweenOffset(minDate, maxDate); } ///