Skip to content

Commit

Permalink
Added unit test DateTest.will_not_generate_values_that_do_not_exist_d…
Browse files Browse the repository at this point in the history
…ue_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.
  • Loading branch information
logiclrd committed Mar 16, 2021
1 parent b9049ab commit b3f99bf
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 36 deletions.
82 changes: 82 additions & 0 deletions Source/Bogus.Tests/DataSetTests/DateTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Linq;
using Bogus.DataSets;
using FluentAssertions;
using Xunit;
Expand Down Expand Up @@ -352,5 +353,86 @@ public void can_get_timezone_string()
{
date.TimeZoneString().Should().Be("Asia/Yerevan");
}

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);

// When converting back, .NET picks the end of the transition window instead of the start.
var transitionEndTime = transitionStartTime.ToUniversalTime().ToLocalTime();

// Act
var value = faker.Date.Between(transitionStartTime.AddHours(-1), transitionEndTime.AddHours(+2));

// Assert
transitionEndTime.Should().NotBe(transitionStartTime);

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);
}
}
}
61 changes: 25 additions & 36 deletions Source/Bogus/DataSets/Date.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
Expand All @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand All @@ -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);
}

/// <summary>
Expand All @@ -144,14 +128,22 @@ public DateTimeOffset FutureOffset(int yearsToGoForward = 1, DateTimeOffset? ref
/// <param name="end">End time</param>
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;
}

/// <summary>
Expand All @@ -161,14 +153,19 @@ public DateTime Between(DateTime start, DateTime end)
/// <param name="end">End time</param>
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);
}

/// <summary>
Expand All @@ -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);
}

/// <summary>
Expand All @@ -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);
}

/// <summary>
Expand Down

0 comments on commit b3f99bf

Please sign in to comment.