1
1
using System ;
2
+ using System . Globalization ;
2
3
3
4
namespace Bogus . DataSets
4
5
{
@@ -128,6 +129,8 @@ public DateTimeOffset FutureOffset(int yearsToGoForward = 1, DateTimeOffset? ref
128
129
/// <param name="end">End time</param>
129
130
public DateTime Between ( DateTime start , DateTime end )
130
131
{
132
+ ComputeRealRange ( ref start , ref end ) ;
133
+
131
134
var startTicks = start . ToUniversalTime ( ) . Ticks ;
132
135
var endTicks = end . ToUniversalTime ( ) . Ticks ;
133
136
@@ -146,26 +149,155 @@ public DateTime Between(DateTime start, DateTime end)
146
149
return value ;
147
150
}
148
151
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
+
149
288
/// <summary>
150
289
/// Get a random <see cref="DateTimeOffset"/> between <paramref name="start"/> and <paramref name="end"/>.
151
290
/// </summary>
152
291
/// <param name="start">Start time - The returned <seealso cref="DateTimeOffset"/> offset value is used from this parameter</param>
153
292
/// <param name="end">End time</param>
154
293
public DateTimeOffset BetweenOffset ( DateTimeOffset start , DateTimeOffset end )
155
294
{
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 ) ;
165
297
166
- var dateTime = new DateTime ( minTicks , DateTimeKind . Unspecified ) + partTimeSpan ;
298
+ var sample = Between ( startTime , endTime ) ;
167
299
168
- return new DateTimeOffset ( dateTime + start . Offset , start . Offset ) ;
300
+ return new DateTimeOffset ( new DateTime ( sample . Ticks , DateTimeKind . Unspecified ) , start . Offset ) ;
169
301
}
170
302
171
303
/// <summary>
0 commit comments