@@ -181,8 +181,10 @@ sealed interface TimeOffset {
181
181
182
182
data class HhMm (val hours : Int , val minutes : Int , val sign : Sign ) : TimeOffset {
183
183
init {
184
- require(hours in 0 .. 18 ) { " invalid hours: $hours (must be in the closed range 0..23)" }
185
- require(minutes in 0 .. 59 ) { " invalid minutes: $minutes (must be in the closed range 0..59)" }
184
+ require(hours in validHours) { " invalid hours: $hours (must be in the closed range 0..23)" }
185
+ require(minutes in validMinutes) {
186
+ " invalid minutes: $minutes (must be in the closed range 0..59)"
187
+ }
186
188
require(hours != 18 || minutes == 0 ) { " invalid minutes: $minutes (must be 0 when hours=18)" }
187
189
}
188
190
@@ -196,15 +198,40 @@ sealed interface TimeOffset {
196
198
append(" $minutes " .padStart(2 , ' 0' ))
197
199
}
198
200
201
+ fun toSeconds (): Int {
202
+ val absValue = hours + (minutes * 60 )
203
+ return when (sign) {
204
+ Sign .Positive -> absValue
205
+ Sign .Negative -> - absValue
206
+ }
207
+ }
208
+
199
209
override fun toString () =
200
210
" HhMm(hours=$hours , minutes=$minutes , sign=$sign , " +
201
211
" zoneOffset=$zoneOffset , rfc3339String=$rfc3339String )"
202
212
213
+ operator fun compareTo (other : HhMm ): Int = toSeconds() - other.toSeconds()
214
+
203
215
@Suppress(" unused" )
204
216
enum class Sign (val char : Char , val multiplier : Int ) {
205
217
Positive (' +' , 1 ),
206
218
Negative (' -' , - 1 ),
207
219
}
220
+
221
+ companion object {
222
+ val validHours = 0 .. 18
223
+ val validMinutes = 0 .. 59
224
+ val maxSeconds: Int = 18 * 60
225
+
226
+ fun forSeconds (seconds : Int , sign : Sign ): HhMm {
227
+ require(seconds in 0 .. maxSeconds) {
228
+ " invalid seconds: $seconds (must be between 0 and $maxSeconds , inclusive)"
229
+ }
230
+ val hours = seconds / 60
231
+ val minutes = seconds - (hours * 60 )
232
+ return HhMm (hours = hours, minutes = minutes, sign = sign)
233
+ }
234
+ }
208
235
}
209
236
}
210
237
@@ -219,7 +246,6 @@ object JavaTimeArbs {
219
246
val minuteArb = minute()
220
247
val secondArb = second()
221
248
val nanosecondArb = nanosecond().orNull(nullProbability = 0.15 )
222
- val timeOffsetArb = timeOffset()
223
249
224
250
return arbitrary(JavaTimeInstantEdgeCases .all) {
225
251
val year = yearArb.bind()
@@ -230,7 +256,55 @@ object JavaTimeArbs {
230
256
val minute = minuteArb.bind()
231
257
val second = secondArb.bind()
232
258
val nanosecond = nanosecondArb.bind()
233
- val timeOffset = timeOffsetArb.bind()
259
+
260
+ val instantUtc =
261
+ OffsetDateTime .of(
262
+ year,
263
+ month,
264
+ day,
265
+ hour,
266
+ minute,
267
+ second,
268
+ nanosecond?.nanoseconds ? : 0 ,
269
+ ZoneOffset .UTC ,
270
+ )
271
+ .toInstant()
272
+
273
+ // The valid range below was copied from:
274
+ // com.google.firebase.Timestamp.Timestamp.validateRange() 253_402_300_800
275
+ val validEpochSecondRange = - 62_135_596_800 .. 253_402_300_800
276
+
277
+ val numSecondsBelowMaxEpochSecond = validEpochSecondRange.last - instantUtc.epochSecond
278
+ require(numSecondsBelowMaxEpochSecond > 0 ) {
279
+ " internal error gh98nqedss: " +
280
+ " invalid numSecondsBelowMaxEpochSecond: $numSecondsBelowMaxEpochSecond "
281
+ }
282
+ val maxTimeZoneOffset =
283
+ if (numSecondsBelowMaxEpochSecond >= TimeOffset .HhMm .maxSeconds) {
284
+ null
285
+ } else {
286
+ TimeOffset .HhMm .forSeconds(
287
+ numSecondsBelowMaxEpochSecond.toInt(),
288
+ TimeOffset .HhMm .Sign .Negative
289
+ )
290
+ }
291
+
292
+ val numSecondsAboveMinEpochSecond = instantUtc.epochSecond - validEpochSecondRange.first
293
+ require(numSecondsAboveMinEpochSecond > 0 ) {
294
+ " internal error mje6a4mrbm: " +
295
+ " invalid numSecondsAboveMinEpochSecond: $numSecondsAboveMinEpochSecond "
296
+ }
297
+ val minTimeZoneOffset =
298
+ if (numSecondsAboveMinEpochSecond >= TimeOffset .HhMm .maxSeconds) {
299
+ null
300
+ } else {
301
+ TimeOffset .HhMm .forSeconds(
302
+ numSecondsAboveMinEpochSecond.toInt(),
303
+ TimeOffset .HhMm .Sign .Positive
304
+ )
305
+ }
306
+
307
+ val timeOffset = timeOffset(min = minTimeZoneOffset, max = maxTimeZoneOffset).bind()
234
308
235
309
val instant =
236
310
OffsetDateTime .of(
@@ -245,14 +319,24 @@ object JavaTimeArbs {
245
319
)
246
320
.toInstant()
247
321
248
- // The valid range below was copied from:
249
- // com.google.firebase.Timestamp.Timestamp.validateRange()
250
- require(instant.epochSecond in - 62_135_596_800 until 253_402_300_800 ) {
322
+ require(instant.epochSecond >= validEpochSecondRange.first) {
251
323
" internal error weppxzqj2y: " +
252
- " instant.epochSecond out of range: ${instant.epochSecond} (" +
253
- " year=$year , month=$month , day=$day , " +
324
+ " instant.epochSecond out of range by " +
325
+ " ${validEpochSecondRange.first - instant.epochSecond} : ${instant.epochSecond} (" +
326
+ " validEpochSecondRange.first=${validEpochSecondRange.first} , "
327
+ " year=$year , month=$month , day=$day , " +
254
328
" hour=$hour , minute=$minute , second=$second , " +
255
- " nanosecond=$nanosecond timeOffset=$timeOffset )"
329
+ " nanosecond=$nanosecond timeOffset=$timeOffset , " +
330
+ " minTimeZoneOffset=$minTimeZoneOffset , maxTimeZoneOffset=$maxTimeZoneOffset )"
331
+ }
332
+ require(instant.epochSecond <= validEpochSecondRange.last) {
333
+ " internal error yxga5xy9bm: " +
334
+ " instant.epochSecond out of range by " +
335
+ " ${instant.epochSecond - validEpochSecondRange.last} : ${instant.epochSecond} (" +
336
+ " validEpochSecondRange.last=${validEpochSecondRange.last} , " +
337
+ " year=$year , month=$month , day=$day , " +
338
+ " nanosecond=$nanosecond timeOffset=$timeOffset , " +
339
+ " minTimeZoneOffset=$minTimeZoneOffset , maxTimeZoneOffset=$maxTimeZoneOffset )"
256
340
}
257
341
258
342
val string = buildString {
@@ -282,7 +366,10 @@ object JavaTimeArbs {
282
366
}
283
367
}
284
368
285
- fun timeOffset (): Arb <TimeOffset > = Arb .choice(timeOffsetUtc(), timeOffsetHhMm())
369
+ fun timeOffset (
370
+ min : TimeOffset .HhMm ? ,
371
+ max : TimeOffset .HhMm ? ,
372
+ ): Arb <TimeOffset > = Arb .choice(timeOffsetUtc(), timeOffsetHhMm(min = min, max = max))
286
373
287
374
fun timeOffsetUtc (
288
375
case : Arb <TimeOffset .Utc .Case > = Arb .enum(),
@@ -292,20 +379,42 @@ object JavaTimeArbs {
292
379
sign : Arb <TimeOffset .HhMm .Sign > = Arb .enum(),
293
380
hour : Arb <Int > = Arb .positiveIntWithUniformNumDigitsProbability(0..18),
294
381
minute : Arb <Int > = minute(),
295
- ): Arb <TimeOffset .HhMm > =
296
- arbitrary(
382
+ min : TimeOffset .HhMm ? ,
383
+ max : TimeOffset .HhMm ? ,
384
+ ): Arb <TimeOffset .HhMm > {
385
+ require(min == = null || max == = null || min.toSeconds() < max.toSeconds()) {
386
+ " min must be strictly less than max, but got: " +
387
+ " min=$min (${min!! .toSeconds()} seconds), " +
388
+ " max=$max (${max!! .toSeconds()} seconds), " +
389
+ " a difference of ${min.toSeconds() - max.toSeconds()} seconds"
390
+ }
391
+
392
+ return arbitrary(
297
393
edgecases =
298
394
listOf (
299
- TimeOffset .HhMm (hours = 0 , minutes = 0 , sign = TimeOffset .HhMm .Sign .Positive ),
300
- TimeOffset .HhMm (hours = 0 , minutes = 0 , sign = TimeOffset .HhMm .Sign .Negative ),
301
- TimeOffset .HhMm (hours = 17 , minutes = 59 , sign = TimeOffset .HhMm .Sign .Positive ),
302
- TimeOffset .HhMm (hours = 17 , minutes = 59 , sign = TimeOffset .HhMm .Sign .Negative ),
303
- TimeOffset .HhMm (hours = 18 , minutes = 0 , sign = TimeOffset .HhMm .Sign .Positive ),
304
- TimeOffset .HhMm (hours = 18 , minutes = 0 , sign = TimeOffset .HhMm .Sign .Negative ),
305
- )
395
+ TimeOffset .HhMm (hours = 0 , minutes = 0 , sign = TimeOffset .HhMm .Sign .Positive ),
396
+ TimeOffset .HhMm (hours = 0 , minutes = 0 , sign = TimeOffset .HhMm .Sign .Negative ),
397
+ TimeOffset .HhMm (hours = 17 , minutes = 59 , sign = TimeOffset .HhMm .Sign .Positive ),
398
+ TimeOffset .HhMm (hours = 17 , minutes = 59 , sign = TimeOffset .HhMm .Sign .Negative ),
399
+ TimeOffset .HhMm (hours = 18 , minutes = 0 , sign = TimeOffset .HhMm .Sign .Positive ),
400
+ TimeOffset .HhMm (hours = 18 , minutes = 0 , sign = TimeOffset .HhMm .Sign .Negative ),
401
+ )
402
+ .filter { (min == = null || it >= min) || (max == = null || it <= max) }
306
403
) {
307
- TimeOffset .HhMm (hours = hour.bind(), minutes = minute.bind(), sign = sign.bind())
404
+ var count = 0
405
+ var hhmm: TimeOffset .HhMm
406
+ while (true ) {
407
+ count++
408
+ hhmm = TimeOffset .HhMm (hours = hour.bind(), minutes = minute.bind(), sign = sign.bind())
409
+ if ((min == = null || hhmm >= min) && (max == = null || hhmm <= max)) {
410
+ break
411
+ } else if (count > 1000 ) {
412
+ throw Exception (" internal error j878fp4gmr: exhausted attempts to generate HhMm" )
413
+ }
414
+ }
415
+ hhmm
308
416
}
417
+ }
309
418
310
419
fun year (): Arb <Int > = Arb .int(MIN_YEAR .. MAX_YEAR )
311
420
0 commit comments