diff --git a/Source/Bogus.Tests/DataSetTests/DateTest.cs b/Source/Bogus.Tests/DataSetTests/DateTest.cs
index d2ecd90b..0bffc883 100644
--- a/Source/Bogus.Tests/DataSetTests/DateTest.cs
+++ b/Source/Bogus.Tests/DataSetTests/DateTest.cs
@@ -394,6 +394,72 @@ public void will_not_generate_values_that_do_not_exist_due_to_daylight_savings()
value.Should().NotBeBefore(transitionEndTime);
}
+ [FactWhenDaylightSavingsSupported]
+ public void will_adjust_start_time_to_avoid_dst_transition()
+ {
+ // 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 + TimeSpan.FromTicks((transitionEndTime - transitionStartTime).Ticks / 2);
+ var windowEnd = transitionEndTime.AddMinutes(30);
+
+ // Act & Assert
+ bool haveSampleThatIsNotWindowEnd = false;
+
+ for (int i = 0; i < 10000; i++)
+ {
+ var sample = faker.Date.Between(windowStart, windowEnd);
+
+ sample.Should().BeOnOrAfter(transitionEndTime);
+ sample.Should().BeOnOrBefore(windowEnd);
+
+ haveSampleThatIsNotWindowEnd = (sample < windowEnd);
+ }
+
+ haveSampleThatIsNotWindowEnd.Should().BeTrue(because: "the effective range should include values other than windowEnd");
+ }
+
+ [FactWhenDaylightSavingsSupported]
+ public void will_adjust_end_time_to_avoid_dst_transition()
+ {
+ // 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(-30);
+ var windowEnd = transitionStartTime + TimeSpan.FromTicks((transitionEndTime - transitionStartTime).Ticks / 2);
+
+ // Act & Assert
+ for (int i = 0; i < 10000; i++)
+ {
+ var sample = faker.Date.Between(windowStart, windowEnd);
+
+ sample.Should().BeOnOrAfter(windowStart);
+ sample.Should().BeOnOrBefore(transitionStartTime);
+ }
+ }
+
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 4862eda5..d5997b0b 100644
--- a/Source/Bogus/DataSets/Date.cs
+++ b/Source/Bogus/DataSets/Date.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
namespace Bogus.DataSets
{
@@ -128,6 +129,8 @@ public DateTimeOffset FutureOffset(int yearsToGoForward = 1, DateTimeOffset? ref
/// End time
public DateTime Between(DateTime start, DateTime end)
{
+ ComputeRealRange(ref start, ref end);
+
var startTicks = start.ToUniversalTime().Ticks;
var endTicks = end.ToUniversalTime().Ticks;
@@ -146,6 +149,142 @@ public DateTime Between(DateTime start, DateTime end)
return value;
}
+ ///
+ /// Takes a date/time range, as indicated by and ,
+ /// and ensures that the range indicators are in the correct order and both reference actual
+ /// values. This takes into account the fact that when Daylight Savings Time
+ /// comes into effect, there is a 1-hour interval in the local calendar which does not exist, and
+ /// values in this change are not meaningful.
+ ///
+ /// This function only worries about the start and end times. Impossible
+ /// values within the range are excluded automatically by means of the
+ /// function.
+ ///
+ /// This function does not check Daylight Savings Time transitions when running under .NET Standard 1.3,
+ /// as this API does not expose Daylight Savings Time information.
+ ///
+ /// A ref to be adjusted forward out of an impossible date/time range if necessary.
+ /// A ref to be adjusted backward out of an impossible date/time range if necessary.
+ private void ComputeRealRange(ref DateTime start, ref DateTime end)
+ {
+ if (start > end)
+ {
+ var tmp = start;
+
+ start = end;
+ end = tmp;
+ }
+
+#if !NETSTANDARD1_3
+ var window = GetForwardDSTTransitionWindow(start);
+
+ if ((start > window.Start) && (start <= window.End))
+ start = new DateTime(window.End.Ticks, start.Kind);
+
+ window = GetForwardDSTTransitionWindow(end);
+
+ if ((end >= window.Start) && (end < window.End))
+ end = new DateTime(window.Start.Ticks, end.Kind);
+
+ if (start > end)
+ throw new Exception("DateTime range does not contain any real DateTime values due to daylight savings transitions");
+#endif
+ }
+
+#if !NETSTANDARD1_3
+ struct DateTimeRange
+ {
+ public DateTime Start;
+ public DateTime End;
+ }
+
+ ///
+ /// Finds the window of time that doesn't exist in the local timezone due to Daylight Savings Time coming into
+ /// effect. In timezones that do not have Daylight Savings Time transitions, this function returns .
+ ///
+ ///
+ /// A reference value for determining the DST transition window accurately. Daylight Savings Time
+ /// rules can change over time, and the API exposes information about which Daylight Savings
+ /// Time rules are in effect for which date ranges.
+ ///
+ ///
+ /// A that indicates the start & end of the interval of date/time values that do not
+ /// exist in the local calendar in the interval indicated by the supplied , or
+ /// if no such range exists.
+ ///
+ private DateTimeRange GetForwardDSTTransitionWindow(DateTime dateTime)
+ {
+ // Based on code found at: https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo.transitiontime.isfixeddaterule
+ var rule = FindEffectiveTimeZoneAdjustmentRule(dateTime);
+
+ if (rule == null)
+ return default(DateTimeRange);
+
+ var transition = rule.DaylightTransitionStart;
+
+ DateTime startTime;
+
+ if (transition.IsFixedDateRule)
+ {
+ startTime = new DateTime(
+ dateTime.Year,
+ transition.Month,
+ transition.Day,
+ transition.TimeOfDay.Hour,
+ transition.TimeOfDay.Minute,
+ transition.TimeOfDay.Second,
+ transition.TimeOfDay.Millisecond);
+ }
+ else
+ {
+ var calendar = CultureInfo.CurrentCulture.Calendar;
+
+ var startOfWeek = transition.Week * 7 - 6;
+
+ var firstDayOfWeek = (int)calendar.GetDayOfWeek(new DateTime(dateTime.Year, transition.Month, 1));
+ var changeDayOfWeek = (int)transition.DayOfWeek;
+
+ int transitionDay =
+ firstDayOfWeek <= changeDayOfWeek
+ ? startOfWeek + changeDayOfWeek - firstDayOfWeek
+ : startOfWeek + changeDayOfWeek - firstDayOfWeek + 7;
+
+ if (transitionDay > calendar.GetDaysInMonth(dateTime.Year, transition.Month))
+ transitionDay -= 7;
+
+ startTime = new DateTime(
+ dateTime.Year,
+ transition.Month,
+ transitionDay,
+ transition.TimeOfDay.Hour,
+ transition.TimeOfDay.Minute,
+ transition.TimeOfDay.Second,
+ transition.TimeOfDay.Millisecond);
+ }
+
+ return
+ new DateTimeRange()
+ {
+ Start = startTime,
+ End = startTime + rule.DaylightDelta,
+ };
+ }
+
+ ///
+ /// Identifies the timezone adjustment rule in effect in the local timezone at the specified
+ /// . If no adjustment rule is in effect, returns .
+ ///
+ /// The value for which to find an adjustment rule.
+ private TimeZoneInfo.AdjustmentRule FindEffectiveTimeZoneAdjustmentRule(DateTime dateTime)
+ {
+ foreach (var rule in TimeZoneInfo.Local.GetAdjustmentRules())
+ if ((dateTime >= rule.DateStart) && (dateTime <= rule.DateEnd))
+ return rule;
+
+ return default;
+ }
+#endif
+
///
/// Get a random between and .
///
@@ -153,19 +292,12 @@ public DateTime Between(DateTime start, DateTime end)
/// End time
public DateTimeOffset BetweenOffset(DateTimeOffset start, DateTimeOffset end)
{
- 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);
+ var startTime = new DateTime(start.DateTime.Ticks, DateTimeKind.Utc);
+ var endTime = new DateTime(end.DateTime.Ticks, DateTimeKind.Utc);
- var dateTime = new DateTime(minTicks, DateTimeKind.Unspecified) + partTimeSpan;
+ var sample = Between(startTime, endTime);
- return new DateTimeOffset(dateTime + start.Offset, start.Offset);
+ return new DateTimeOffset(new DateTime(sample.Ticks, DateTimeKind.Unspecified), start.Offset);
}
///