11using System ;
2+ using System . Globalization ;
23
34namespace Bogus . DataSets
45{
@@ -128,6 +129,8 @@ public DateTimeOffset FutureOffset(int yearsToGoForward = 1, DateTimeOffset? ref
128129 /// <param name="end">End time</param>
129130 public DateTime Between ( DateTime start , DateTime end )
130131 {
132+ ComputeRealRange ( ref start , ref end ) ;
133+
131134 var startTicks = start . ToUniversalTime ( ) . Ticks ;
132135 var endTicks = end . ToUniversalTime ( ) . Ticks ;
133136
@@ -146,26 +149,155 @@ public DateTime Between(DateTime start, DateTime end)
146149 return value ;
147150 }
148151
152+ /// <summary>
153+ /// Takes a date/time range, as indicated by <paramref name="start"/> and <paramref name="end"/>,
154+ /// and ensures that the range indicators are in the correct order and both reference actual
155+ /// <see cref="DateTime"/> values. This takes into account the fact that when Daylight Savings Time
156+ /// comes into effect, there is a 1-hour interval in the local calendar which does not exist, and
157+ /// <see cref="DateTime"/> values in this change are not meaningful.
158+ ///
159+ /// This function only worries about the start and end times. Impossible <see cref="DateTime"/>
160+ /// values within the range are excluded automatically by means of the <see cref="DateTime.ToLocalTime"/>
161+ /// function.
162+ ///
163+ /// This function does not check Daylight Savings Time transitions when running under .NET Standard 1.3,
164+ /// as this API does not expose Daylight Savings Time information.
165+ /// </summary>
166+ /// <param name="start">A ref <see cref="DateTime"/> to be adjusted forward out of an impossible date/time range if necessary.</param>
167+ /// <param name="end">A ref <see cref="DateTime"/> to be adjusted backward out of an impossible date/time range if necessary.</param>
168+ private void ComputeRealRange ( ref DateTime start , ref DateTime end )
169+ {
170+ if ( start > end )
171+ {
172+ var tmp = start ;
173+
174+ start = end ;
175+ end = tmp ;
176+ }
177+
178+ #if ! NETSTANDARD1_3
179+ var window = GetForwardDSTTransitionWindow ( start ) ;
180+
181+ if ( ( start > window . Start ) && ( start <= window . End ) )
182+ start = new DateTime ( window . End . Ticks , start . Kind ) ;
183+
184+ window = GetForwardDSTTransitionWindow ( end ) ;
185+
186+ if ( ( end >= window . Start ) && ( end < window . End ) )
187+ end = new DateTime ( window . Start . Ticks , end . Kind ) ;
188+
189+ if ( start > end )
190+ throw new Exception ( "DateTime range does not contain any real DateTime values due to daylight savings transitions" ) ;
191+ #endif
192+ }
193+
194+ #if ! NETSTANDARD1_3
195+ struct DateTimeRange
196+ {
197+ public DateTime Start ;
198+ public DateTime End ;
199+ }
200+
201+ /// <summary>
202+ /// Finds the window of time that doesn't exist in the local timezone due to Daylight Savings Time coming into
203+ /// effect. In timezones that do not have Daylight Savings Time transitions, this function returns <see cref="null"/>.
204+ /// </summary>
205+ /// <param name="dateTime">
206+ /// A reference <see cref="DateTime"/> value for determining the DST transition window accurately. Daylight Savings Time
207+ /// rules can change over time, and the <see cref="TimeZoneInfo"/> API exposes information about which Daylight Savings
208+ /// Time rules are in effect for which date ranges.
209+ /// </param>
210+ /// <returns>
211+ /// A <see cref="DateTimeRange"/> that indicates the start & end of the interval of date/time values that do not
212+ /// exist in the local calendar in the interval indicated by the supplied <paramref name="dateTime"/>, or <see cref="null"/>
213+ /// if no such range exists.
214+ /// </returns>
215+ private DateTimeRange GetForwardDSTTransitionWindow ( DateTime dateTime )
216+ {
217+ // Based on code found at: https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo.transitiontime.isfixeddaterule
218+ var rule = FindEffectiveTimeZoneAdjustmentRule ( dateTime ) ;
219+
220+ if ( rule == null )
221+ return default ( DateTimeRange ) ;
222+
223+ var transition = rule . DaylightTransitionStart ;
224+
225+ DateTime startTime ;
226+
227+ if ( transition . IsFixedDateRule )
228+ {
229+ startTime = new DateTime (
230+ dateTime . Year ,
231+ transition . Month ,
232+ transition . Day ,
233+ transition . TimeOfDay . Hour ,
234+ transition . TimeOfDay . Minute ,
235+ transition . TimeOfDay . Second ,
236+ transition . TimeOfDay . Millisecond ) ;
237+ }
238+ else
239+ {
240+ var calendar = CultureInfo . CurrentCulture . Calendar ;
241+
242+ var startOfWeek = transition . Week * 7 - 6 ;
243+
244+ var firstDayOfWeek = ( int ) calendar . GetDayOfWeek ( new DateTime ( dateTime . Year , transition . Month , 1 ) ) ;
245+ var changeDayOfWeek = ( int ) transition . DayOfWeek ;
246+
247+ int transitionDay =
248+ firstDayOfWeek <= changeDayOfWeek
249+ ? startOfWeek + changeDayOfWeek - firstDayOfWeek
250+ : startOfWeek + changeDayOfWeek - firstDayOfWeek + 7 ;
251+
252+ if ( transitionDay > calendar . GetDaysInMonth ( dateTime . Year , transition . Month ) )
253+ transitionDay -= 7 ;
254+
255+ startTime = new DateTime (
256+ dateTime . Year ,
257+ transition . Month ,
258+ transitionDay ,
259+ transition . TimeOfDay . Hour ,
260+ transition . TimeOfDay . Minute ,
261+ transition . TimeOfDay . Second ,
262+ transition . TimeOfDay . Millisecond ) ;
263+ }
264+
265+ return
266+ new DateTimeRange ( )
267+ {
268+ Start = startTime ,
269+ End = startTime + rule . DaylightDelta ,
270+ } ;
271+ }
272+
273+ /// <summary>
274+ /// Identifies the timezone adjustment rule in effect in the local timezone at the specified
275+ /// <paramref name="dateTime"/>. If no adjustment rule is in effect, returns <see cref="null"/>.
276+ /// </summary>
277+ /// <param name="dateTime">The <see cref="DateTime"/> value for which to find an adjustment rule.</param>
278+ private TimeZoneInfo . AdjustmentRule FindEffectiveTimeZoneAdjustmentRule ( DateTime dateTime )
279+ {
280+ foreach ( var rule in TimeZoneInfo . Local . GetAdjustmentRules ( ) )
281+ if ( ( dateTime >= rule . DateStart ) && ( dateTime <= rule . DateEnd ) )
282+ return rule ;
283+
284+ return default ;
285+ }
286+ #endif
287+
149288 /// <summary>
150289 /// Get a random <see cref="DateTimeOffset"/> between <paramref name="start"/> and <paramref name="end"/>.
151290 /// </summary>
152291 /// <param name="start">Start time - The returned <seealso cref="DateTimeOffset"/> offset value is used from this parameter</param>
153292 /// <param name="end">End time</param>
154293 public DateTimeOffset BetweenOffset ( DateTimeOffset start , DateTimeOffset end )
155294 {
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 ) ;
161-
162- var totalTimeSpanTicks = maxTicks - minTicks ;
163-
164- var partTimeSpan = RandomTimeSpanFromTicks ( totalTimeSpanTicks ) ;
295+ var startTime = new DateTime ( start . DateTime . Ticks , DateTimeKind . Utc ) ;
296+ var endTime = new DateTime ( end . DateTime . Ticks , DateTimeKind . Utc ) ;
165297
166- var dateTime = new DateTime ( minTicks , DateTimeKind . Unspecified ) + partTimeSpan ;
298+ var sample = Between ( startTime , endTime ) ;
167299
168- return new DateTimeOffset ( dateTime + start . Offset , start . Offset ) ;
300+ return new DateTimeOffset ( new DateTime ( sample . Ticks , DateTimeKind . Unspecified ) + start . Offset , start . Offset ) ;
169301 }
170302
171303 /// <summary>
0 commit comments