Skip to content

Commit 3489f09

Browse files
committed
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.
1 parent b9049ab commit 3489f09

File tree

2 files changed

+104
-36
lines changed

2 files changed

+104
-36
lines changed

Source/Bogus.Tests/DataSetTests/DateTest.cs

+79
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Globalization;
3+
using System.Linq;
34
using Bogus.DataSets;
45
using FluentAssertions;
56
using Xunit;
@@ -352,5 +353,83 @@ public void can_get_timezone_string()
352353
{
353354
date.TimeZoneString().Should().Be("Asia/Yerevan");
354355
}
356+
357+
public class FactWhenDaylightSavingsSupported : FactAttribute
358+
{
359+
public FactWhenDaylightSavingsSupported()
360+
{
361+
if (!TimeZoneInfo.Local.SupportsDaylightSavingTime)
362+
{
363+
Skip = "Test is only meaningful when Daylight Savings is supported by the local timezone.";
364+
}
365+
}
366+
}
367+
368+
[FactWhenDaylightSavingsSupported]
369+
public void will_not_generate_values_that_do_not_exist_due_to_daylight_savings()
370+
{
371+
// Arrange
372+
var faker = new Faker();
373+
374+
faker.Random = new Randomizer(localSeed: 5);
375+
376+
var dstRules = TimeZoneInfo.Local.GetAdjustmentRules();
377+
378+
var now = DateTime.Now;
379+
380+
var effectiveRule = dstRules.Single(rule => (rule.DateStart <= now) && (rule.DateEnd >= now));
381+
382+
var transitionStartTime = CalculateTransitionDateTime(now, effectiveRule.DaylightTransitionStart);
383+
384+
var transitionEndTime = transitionStartTime.ToUniversalTime().AddHours(1).ToLocalTime();
385+
386+
// Act
387+
var value = faker.Date.Between(transitionStartTime.AddHours(-1), transitionEndTime.AddHours(+1));
388+
389+
// Assert
390+
if ((value >= transitionStartTime) && (value < transitionStartTime.AddHours(1)))
391+
value.Should().NotBeBefore(transitionEndTime);
392+
}
393+
394+
private DateTime CalculateTransitionDateTime(DateTime now, TimeZoneInfo.TransitionTime transition)
395+
{
396+
// Based on code found at: https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo.transitiontime.isfixeddaterule
397+
398+
if (transition.IsFixedDateRule)
399+
{
400+
return new DateTime(
401+
now.Year,
402+
transition.Month,
403+
transition.Day,
404+
transition.TimeOfDay.Hour,
405+
transition.TimeOfDay.Minute,
406+
transition.TimeOfDay.Second,
407+
transition.TimeOfDay.Millisecond);
408+
}
409+
410+
var calendar = CultureInfo.CurrentCulture.Calendar;
411+
412+
var startOfWeek = transition.Week * 7 - 6;
413+
414+
var firstDayOfWeek = (int)calendar.GetDayOfWeek(new DateTime(now.Year, transition.Month, 1));
415+
var changeDayOfWeek = (int)transition.DayOfWeek;
416+
417+
int transitionDay =
418+
firstDayOfWeek <= changeDayOfWeek
419+
? startOfWeek + changeDayOfWeek - firstDayOfWeek
420+
: startOfWeek + changeDayOfWeek - firstDayOfWeek + 7;
421+
422+
if (transitionDay > calendar.GetDaysInMonth(now.Year, transition.Month))
423+
transitionDay -= 7;
424+
425+
return new DateTime(
426+
now.Year,
427+
transition.Month,
428+
transitionDay,
429+
transition.TimeOfDay.Hour,
430+
transition.TimeOfDay.Minute,
431+
transition.TimeOfDay.Second,
432+
transition.TimeOfDay.Millisecond);
433+
}
355434
}
356435
}

Source/Bogus/DataSets/Date.cs

+25-36
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,7 @@ public DateTime Past(int yearsToGoBack = 1, DateTime? refDate = null)
4444

4545
var minDate = maxDate.AddYears(-yearsToGoBack);
4646

47-
var totalTimeSpanTicks = (maxDate - minDate).Ticks;
48-
49-
var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks);
50-
51-
return maxDate - partTimeSpan;
47+
return Between(minDate, maxDate);
5248
}
5349

5450
/// <summary>
@@ -62,11 +58,7 @@ public DateTimeOffset PastOffset(int yearsToGoBack = 1, DateTimeOffset? refDate
6258

6359
var minDate = maxDate.AddYears(-yearsToGoBack);
6460

65-
var totalTimeSpanTicks = (maxDate - minDate).Ticks;
66-
67-
var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks);
68-
69-
return maxDate - partTimeSpan;
61+
return BetweenOffset(minDate, maxDate);
7062
}
7163

7264
/// <summary>
@@ -112,11 +104,7 @@ public DateTime Future(int yearsToGoForward = 1, DateTime? refDate = null)
112104

113105
var maxDate = minDate.AddYears(yearsToGoForward);
114106

115-
var totalTimeSpanTicks = (maxDate - minDate).Ticks;
116-
117-
var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks);
118-
119-
return minDate + partTimeSpan;
107+
return Between(minDate, maxDate);
120108
}
121109

122110
/// <summary>
@@ -130,11 +118,7 @@ public DateTimeOffset FutureOffset(int yearsToGoForward = 1, DateTimeOffset? ref
130118

131119
var maxDate = minDate.AddYears(yearsToGoForward);
132120

133-
var totalTimeSpanTicks = (maxDate - minDate).Ticks;
134-
135-
var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks);
136-
137-
return minDate + partTimeSpan;
121+
return BetweenOffset(minDate, maxDate);
138122
}
139123

140124
/// <summary>
@@ -144,14 +128,22 @@ public DateTimeOffset FutureOffset(int yearsToGoForward = 1, DateTimeOffset? ref
144128
/// <param name="end">End time</param>
145129
public DateTime Between(DateTime start, DateTime end)
146130
{
147-
var minTicks = Math.Min(start.Ticks, end.Ticks);
148-
var maxTicks = Math.Max(start.Ticks, end.Ticks);
131+
var startTicks = start.ToUniversalTime().Ticks;
132+
var endTicks = end.ToUniversalTime().Ticks;
133+
134+
var minTicks = Math.Min(startTicks, endTicks);
135+
var maxTicks = Math.Max(startTicks, endTicks);
149136

150137
var totalTimeSpanTicks = maxTicks - minTicks;
151138

152139
var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks);
153140

154-
return new DateTime(minTicks, start.Kind) + partTimeSpan;
141+
var value = new DateTime(minTicks, DateTimeKind.Utc) + partTimeSpan;
142+
143+
if (start.Kind != DateTimeKind.Utc)
144+
value = value.ToLocalTime();
145+
146+
return value;
155147
}
156148

157149
/// <summary>
@@ -161,14 +153,19 @@ public DateTime Between(DateTime start, DateTime end)
161153
/// <param name="end">End time</param>
162154
public DateTimeOffset BetweenOffset(DateTimeOffset start, DateTimeOffset end)
163155
{
164-
var minTicks = Math.Min(start.Ticks, end.Ticks);
165-
var maxTicks = Math.Max(start.Ticks, end.Ticks);
156+
var startTicks = start.ToUniversalTime().Ticks;
157+
var endTicks = end.ToUniversalTime().Ticks;
158+
159+
var minTicks = Math.Min(startTicks, endTicks);
160+
var maxTicks = Math.Max(startTicks, endTicks);
166161

167162
var totalTimeSpanTicks = maxTicks - minTicks;
168163

169164
var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks);
170165

171-
return new DateTimeOffset(minTicks, start.Offset) + partTimeSpan;
166+
var dateTime = new DateTime(minTicks, DateTimeKind.Unspecified) + partTimeSpan;
167+
168+
return new DateTimeOffset(dateTime + start.Offset, start.Offset);
172169
}
173170

174171
/// <summary>
@@ -182,11 +179,7 @@ public DateTime Recent(int days = 1, DateTime? refDate = null)
182179

183180
var minDate = days == 0 ? SystemClock().Date : maxDate.AddDays(-days);
184181

185-
var totalTimeSpanTicks = (maxDate - minDate).Ticks;
186-
187-
var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks);
188-
189-
return maxDate - partTimeSpan;
182+
return Between(minDate, maxDate);
190183
}
191184

192185
/// <summary>
@@ -200,11 +193,7 @@ public DateTimeOffset RecentOffset(int days = 1, DateTimeOffset? refDate = null)
200193

201194
var minDate = days == 0 ? SystemClock().Date : maxDate.AddDays(-days);
202195

203-
var totalTimeSpanTicks = (maxDate - minDate).Ticks;
204-
205-
var partTimeSpan = RandomTimeSpanFromTicks(totalTimeSpanTicks);
206-
207-
return maxDate - partTimeSpan;
196+
return BetweenOffset(minDate, maxDate);
208197
}
209198

210199
/// <summary>

0 commit comments

Comments
 (0)