@@ -24,14 +24,14 @@ import com.google.firebase.dataconnect.testutil.property.arbitrary.JavaTimeEdgeC
2424import  com.google.firebase.dataconnect.testutil.property.arbitrary.JavaTimeEdgeCases.MIN_NANO 
2525import  com.google.firebase.dataconnect.testutil.property.arbitrary.JavaTimeEdgeCases.MIN_YEAR 
2626import  com.google.firebase.dataconnect.testutil.toTimestamp 
27+ import  io.kotest.common.mapError 
2728import  io.kotest.property.Arb 
2829import  io.kotest.property.arbitrary.arbitrary 
2930import  io.kotest.property.arbitrary.choice 
3031import  io.kotest.property.arbitrary.enum 
3132import  io.kotest.property.arbitrary.int 
3233import  io.kotest.property.arbitrary.of 
3334import  io.kotest.property.arbitrary.orNull 
34- import  kotlin.random.nextInt 
3535import  org.threeten.bp.Instant 
3636import  org.threeten.bp.OffsetDateTime 
3737import  org.threeten.bp.ZoneOffset 
@@ -153,7 +153,11 @@ private fun Instant.toFdcFieldRegex(): Regex {
153153  return  Regex (pattern)
154154}
155155
156- data class  Nanoseconds (val  nanoseconds :  Int , val  string :  String )
156+ data class  Nanoseconds (
157+   val  nanoseconds :  Int ,
158+   val  string :  String ,
159+   val  digitCounts :  JavaTimeArbs .NanosecondComponents 
160+ )
157161
158162sealed  interface  TimeOffset  {
159163
@@ -177,8 +181,12 @@ sealed interface TimeOffset {
177181
178182  data class  HhMm (val  hours :  Int , val  minutes :  Int , val  sign :  Sign ) : TimeOffset {
179183    init  {
180-       require(hours in  0 .. 18 ) { " invalid hours: $hours  (must be in the closed range 0..23)"   }
181-       require(minutes in  0 .. 59 ) { " invalid minutes: $minutes  (must be in the closed range 0..59)"   }
184+       require(hours in  validHours) {
185+         " invalid hours: $hours  (must be in the closed range $validHours )" 
186+       }
187+       require(minutes in  validMinutes) {
188+         " invalid minutes: $minutes  (must be in the closed range $validMinutes )" 
189+       }
182190      require(hours !=  18  ||  minutes ==  0 ) { " invalid minutes: $minutes  (must be 0 when hours=18)"   }
183191    }
184192
@@ -192,15 +200,44 @@ sealed interface TimeOffset {
192200      append(" $minutes "  .padStart(2 , ' 0'  ))
193201    }
194202
203+     fun  toSeconds (): Int  {
204+       val  absValue =  (hours *  SECONDS_PER_HOUR ) +  (minutes *  SECONDS_PER_MINUTE )
205+       return  when  (sign) {
206+         Sign .Positive  ->  absValue
207+         Sign .Negative  ->  - absValue
208+       }
209+     }
210+ 
195211    override  fun  toString () = 
196212      " HhMm(hours=$hours , minutes=$minutes , sign=$sign , "   + 
197213        " zoneOffset=$zoneOffset , rfc3339String=$rfc3339String )" 
198214
215+     operator  fun  compareTo (other :  HhMm ): Int  =  toSeconds() -  other.toSeconds()
216+ 
199217    @Suppress(" unused"  )
200218    enum  class  Sign (val  char :  Char , val  multiplier :  Int ) {
201219      Positive (' +'  , 1 ),
202220      Negative (' -'  , - 1 ),
203221    }
222+ 
223+     companion  object  {
224+       private  const  val  SECONDS_PER_MINUTE :  Int  =  60 
225+       private  const  val  SECONDS_PER_HOUR :  Int  =  60  *  SECONDS_PER_MINUTE 
226+ 
227+       val  validHours =  0 .. 18 
228+       val  validMinutes =  0 .. 59 
229+ 
230+       val  maxSeconds:  Int  =  18  *  SECONDS_PER_HOUR 
231+ 
232+       fun  forSeconds (seconds :  Int , sign :  Sign ): HhMm  {
233+         require(seconds in  0 .. maxSeconds) {
234+           " invalid seconds: $seconds  (must be between 0 and $maxSeconds , inclusive)" 
235+         }
236+         val  hours =  seconds /  SECONDS_PER_HOUR 
237+         val  minutes =  (seconds -  (hours *  SECONDS_PER_HOUR )) /  SECONDS_PER_MINUTE 
238+         return  HhMm (hours =  hours, minutes =  minutes, sign =  sign)
239+       }
240+     }
204241  }
205242}
206243
@@ -215,7 +252,6 @@ object JavaTimeArbs {
215252    val  minuteArb =  minute()
216253    val  secondArb =  second()
217254    val  nanosecondArb =  nanosecond().orNull(nullProbability =  0.15 )
218-     val  timeOffsetArb =  timeOffset()
219255
220256    return  arbitrary(JavaTimeInstantEdgeCases .all) {
221257      val  year =  yearArb.bind()
@@ -226,7 +262,55 @@ object JavaTimeArbs {
226262      val  minute =  minuteArb.bind()
227263      val  second =  secondArb.bind()
228264      val  nanosecond =  nanosecondArb.bind()
229-       val  timeOffset =  timeOffsetArb.bind()
265+ 
266+       val  instantUtc = 
267+         OffsetDateTime .of(
268+             year,
269+             month,
270+             day,
271+             hour,
272+             minute,
273+             second,
274+             nanosecond?.nanoseconds ? :  0 ,
275+             ZoneOffset .UTC ,
276+           )
277+           .toInstant()
278+ 
279+       //  The valid range below was copied from:
280+       //  com.google.firebase.Timestamp.Timestamp.validateRange() 253_402_300_800
281+       val  validEpochSecondRange =  - 62_135_596_800 .. 253_402_300_800 
282+ 
283+       val  numSecondsBelowMaxEpochSecond =  validEpochSecondRange.last -  instantUtc.epochSecond
284+       require(numSecondsBelowMaxEpochSecond >  0 ) {
285+         " internal error gh98nqedss: "   + 
286+           " invalid numSecondsBelowMaxEpochSecond: $numSecondsBelowMaxEpochSecond " 
287+       }
288+       val  minTimeZoneOffset = 
289+         if  (numSecondsBelowMaxEpochSecond >=  TimeOffset .HhMm .maxSeconds) {
290+           null 
291+         } else  {
292+           TimeOffset .HhMm .forSeconds(
293+             numSecondsBelowMaxEpochSecond.toInt(),
294+             TimeOffset .HhMm .Sign .Negative 
295+           )
296+         }
297+ 
298+       val  numSecondsAboveMinEpochSecond =  instantUtc.epochSecond -  validEpochSecondRange.first
299+       require(numSecondsAboveMinEpochSecond >  0 ) {
300+         " internal error mje6a4mrbm: "   + 
301+           " invalid numSecondsAboveMinEpochSecond: $numSecondsAboveMinEpochSecond " 
302+       }
303+       val  maxTimeZoneOffset = 
304+         if  (numSecondsAboveMinEpochSecond >=  TimeOffset .HhMm .maxSeconds) {
305+           null 
306+         } else  {
307+           TimeOffset .HhMm .forSeconds(
308+             numSecondsAboveMinEpochSecond.toInt(),
309+             TimeOffset .HhMm .Sign .Positive 
310+           )
311+         }
312+ 
313+       val  timeOffset =  timeOffset(min =  minTimeZoneOffset, max =  maxTimeZoneOffset).bind()
230314
231315      val  instant = 
232316        OffsetDateTime .of(
@@ -241,6 +325,27 @@ object JavaTimeArbs {
241325          )
242326          .toInstant()
243327
328+       require(instant.epochSecond >=  validEpochSecondRange.first) {
329+         " internal error weppxzqj2y: "   + 
330+           " instant.epochSecond out of range by "   + 
331+           " ${validEpochSecondRange.first -  instant.epochSecond} : ${instant.epochSecond}  ("   + 
332+           " validEpochSecondRange.first=${validEpochSecondRange.first} , "   + 
333+           " year=$year , month=$month , day=$day , "   + 
334+           " hour=$hour , minute=$minute , second=$second , "   + 
335+           " nanosecond=$nanosecond  timeOffset=$timeOffset , "   + 
336+           " minTimeZoneOffset=$minTimeZoneOffset , maxTimeZoneOffset=$maxTimeZoneOffset )" 
337+       }
338+       require(instant.epochSecond <=  validEpochSecondRange.last) {
339+         " internal error yxga5xy9bm: "   + 
340+           " instant.epochSecond out of range by "   + 
341+           " ${instant.epochSecond -  validEpochSecondRange.last} : ${instant.epochSecond}  ("   + 
342+           " validEpochSecondRange.last=${validEpochSecondRange.last} , "   + 
343+           " year=$year , month=$month , day=$day , "   + 
344+           " hour=$hour , minute=$minute , second=$second , "   + 
345+           " nanosecond=$nanosecond  timeOffset=$timeOffset , "   + 
346+           " minTimeZoneOffset=$minTimeZoneOffset , maxTimeZoneOffset=$maxTimeZoneOffset )" 
347+       }
348+ 
244349      val  string =  buildString {
245350        append(year)
246351        append(' -'  )
@@ -268,7 +373,10 @@ object JavaTimeArbs {
268373    }
269374  }
270375
271-   fun  timeOffset (): Arb <TimeOffset > =  Arb .choice(timeOffsetUtc(), timeOffsetHhMm())
376+   fun  timeOffset (
377+     min :  TimeOffset .HhMm ? ,
378+     max :  TimeOffset .HhMm ? ,
379+   ): Arb <TimeOffset > =  Arb .choice(timeOffsetUtc(), timeOffsetHhMm(min =  min, max =  max))
272380
273381  fun  timeOffsetUtc (
274382    case :  Arb <TimeOffset .Utc .Case > = Arb .enum(),
@@ -278,20 +386,45 @@ object JavaTimeArbs {
278386    sign :  Arb <TimeOffset .HhMm .Sign > = Arb .enum(),
279387    hour :  Arb <Int > = Arb .positiveIntWithUniformNumDigitsProbability(0..18),
280388    minute :  Arb <Int > = minute(),
281-   ): Arb <TimeOffset .HhMm > = 
282-     arbitrary(
389+     min :  TimeOffset .HhMm ? ,
390+     max :  TimeOffset .HhMm ? ,
391+   ): Arb <TimeOffset .HhMm > {
392+     require(min == =  null  ||  max == =  null  ||  min.toSeconds() <  max.toSeconds()) {
393+       " min must be strictly less than max, but got: "   + 
394+         " min=$min  (${min!! .toSeconds()}  seconds), "   + 
395+         " max=$max  (${max!! .toSeconds()}  seconds), "   + 
396+         " a difference of ${min.toSeconds() -  max.toSeconds()}  seconds" 
397+     }
398+ 
399+     fun  isBetweenMinAndMax (other :  TimeOffset .HhMm ): Boolean  = 
400+       (min == =  null  ||  other >=  min) &&  (max == =  null  ||  other <=  max)
401+ 
402+     return  arbitrary(
283403      edgecases = 
284404        listOf (
285-           TimeOffset .HhMm (hours =  0 , minutes =  0 , sign =  TimeOffset .HhMm .Sign .Positive ),
286-           TimeOffset .HhMm (hours =  0 , minutes =  0 , sign =  TimeOffset .HhMm .Sign .Negative ),
287-           TimeOffset .HhMm (hours =  17 , minutes =  59 , sign =  TimeOffset .HhMm .Sign .Positive ),
288-           TimeOffset .HhMm (hours =  17 , minutes =  59 , sign =  TimeOffset .HhMm .Sign .Negative ),
289-           TimeOffset .HhMm (hours =  18 , minutes =  0 , sign =  TimeOffset .HhMm .Sign .Positive ),
290-           TimeOffset .HhMm (hours =  18 , minutes =  0 , sign =  TimeOffset .HhMm .Sign .Negative ),
291-         )
405+             TimeOffset .HhMm (hours =  0 , minutes =  0 , sign =  TimeOffset .HhMm .Sign .Positive ),
406+             TimeOffset .HhMm (hours =  0 , minutes =  0 , sign =  TimeOffset .HhMm .Sign .Negative ),
407+             TimeOffset .HhMm (hours =  17 , minutes =  59 , sign =  TimeOffset .HhMm .Sign .Positive ),
408+             TimeOffset .HhMm (hours =  17 , minutes =  59 , sign =  TimeOffset .HhMm .Sign .Negative ),
409+             TimeOffset .HhMm (hours =  18 , minutes =  0 , sign =  TimeOffset .HhMm .Sign .Positive ),
410+             TimeOffset .HhMm (hours =  18 , minutes =  0 , sign =  TimeOffset .HhMm .Sign .Negative ),
411+           )
412+           .filter(::isBetweenMinAndMax)
292413    ) {
293-       TimeOffset .HhMm (hours =  hour.bind(), minutes =  minute.bind(), sign =  sign.bind())
414+       var  count =  0 
415+       var  hhmm:  TimeOffset .HhMm 
416+       while  (true ) {
417+         count++ 
418+         hhmm =  TimeOffset .HhMm (hours =  hour.bind(), minutes =  minute.bind(), sign =  sign.bind())
419+         if  (isBetweenMinAndMax(hhmm)) {
420+           break 
421+         } else  if  (count >  1000 ) {
422+           throw  Exception (" internal error j878fp4gmr: exhausted attempts to generate HhMm"  )
423+         }
424+       }
425+       hhmm
294426    }
427+   }
295428
296429  fun  year (): Arb <Int > =  Arb .int(MIN_YEAR .. MAX_YEAR )
297430
@@ -316,8 +449,12 @@ object JavaTimeArbs {
316449        repeat(digitCounts.leadingZeroes) { append(' 0'  ) }
317450        if  (digitCounts.proper >  0 ) {
318451          append(nonZeroDigits.bind())
319-           repeat(digitCounts.proper -  2 ) { append(digits.bind()) }
320-           append(nonZeroDigits.bind())
452+           if  (digitCounts.proper >  1 ) {
453+             if  (digitCounts.proper >  2 ) {
454+               repeat(digitCounts.proper -  2 ) { append(digits.bind()) }
455+             }
456+             append(nonZeroDigits.bind())
457+           }
321458        }
322459        repeat(digitCounts.trailingZeroes) { append(' 0'  ) }
323460      }
@@ -327,18 +464,29 @@ object JavaTimeArbs {
327464        if  (nanosecondsStringTrimmed.isEmpty()) {
328465          0 
329466        } else  {
330-           nanosecondsStringTrimmed.toInt()
467+           val  toIntResult =  nanosecondsStringTrimmed.runCatching { toInt() }
468+           toIntResult.mapError { exception -> 
469+             Exception (
470+               " internal error qbdgapmye2: "   + 
471+                 " failed to parse nanosecondsStringTrimmed as an int: "   + 
472+                 " \" $nanosecondsStringTrimmed \"  (digitCounts=$digitCounts )"  ,
473+               exception
474+             )
475+           }
476+           toIntResult.getOrThrow()
331477        }
332478
333-       Nanoseconds (nanosecondsInt, nanosecondsString)
479+       check(nanosecondsInt in  0 .. 999_999_999 ) {
480+         " internal error c7j2myw6bd: "   + 
481+           " nanosecondsStringTrimmed parsed to a value outside the valid range: "   + 
482+           " $nanosecondsInt  (digitCounts=$digitCounts )" 
483+       }
484+ 
485+       Nanoseconds (nanosecondsInt, nanosecondsString, digitCounts)
334486    }
335487  }
336488
337-   private  data class  NanosecondComponents (
338-     val  leadingZeroes :  Int ,
339-     val  proper :  Int ,
340-     val  trailingZeroes :  Int 
341-   )
489+   data class  NanosecondComponents (val  leadingZeroes :  Int , val  proper :  Int , val  trailingZeroes :  Int )
342490
343491  private  fun  nanosecondComponents (): Arb <NanosecondComponents > = 
344492    arbitrary(
0 commit comments