Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix interval random() behavior and add randomOrNull() #64

Merged
merged 4 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions core/src/commonMain/kotlin/io/islandtime/ranges/DateRange.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package io.islandtime.ranges

import io.islandtime.*
import io.islandtime.MAX_DATE_STRING_LENGTH
import io.islandtime.appendDate
import io.islandtime.base.DateTimeField
import io.islandtime.internal.MONTHS_PER_YEAR
import io.islandtime.measures.*
import io.islandtime.monthsSinceYear0
import io.islandtime.parser.*
import io.islandtime.parser.expectingGroupCount
import io.islandtime.parser.throwParserFieldResolutionException
import io.islandtime.ranges.internal.throwUnboundedIntervalException
import kotlin.random.Random
import kotlin.random.nextLong
Expand Down Expand Up @@ -201,13 +196,28 @@ fun String.toDateRange(

/**
* Return a random date within the range using the default random number generator.
* @throws NoSuchElementException if the range is empty
* @throws UnsupportedOperationException if the range is unbounded
* @see DateRange.randomOrNull
*/
fun DateRange.random(): Date = random(Random)

/**
* Return a random date within the range using the default random number generator or `null` if the
* range is empty or unbounded.
* @see DateRange.random
*/
fun DateRange.randomOrNull(): Date? = randomOrNull(Random)

/**
* Return a random date within the range using the supplied random number generator.
* @throws NoSuchElementException if the range is empty
* @throws UnsupportedOperationException if the range is unbounded
* @see DateRange.randomOrNull
*/
fun DateRange.random(random: Random): Date {
if (isUnbounded()) throwUnboundedIntervalException()

try {
val longRange = first.unixEpochDay..last.unixEpochDay
return Date.fromUnixEpochDay(random.nextLong(longRange))
Expand All @@ -216,6 +226,20 @@ fun DateRange.random(random: Random): Date {
}
}

/**
* Return a random date within the range using the supplied random number generator or `null` if
* the range is empty or unbounded.
* @see DateRange.random
*/
fun DateRange.randomOrNull(random: Random): Date? {
return if (isEmpty() || isUnbounded()) {
null
} else {
val longRange = first.unixEpochDay..last.unixEpochDay
Date.fromUnixEpochDay(random.nextLong(longRange))
}
}

/**
* Get a range containing all of the days up to, but not including [to]
*/
Expand Down
64 changes: 49 additions & 15 deletions core/src/commonMain/kotlin/io/islandtime/ranges/DateTimeInterval.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.islandtime.ranges

import io.islandtime.*
import io.islandtime.MAX_DATE_TIME_STRING_LENGTH
import io.islandtime.base.DateTimeField
import io.islandtime.measures.*
import io.islandtime.measures.internal.minusWithOverflow
Expand Down Expand Up @@ -49,7 +48,8 @@ class DateTimeInterval(
/**
* Convert this interval to a string in ISO-8601 extended format.
*/
override fun toString() = buildIsoString(MAX_DATE_TIME_STRING_LENGTH, StringBuilder::appendDateTime)
override fun toString() =
buildIsoString(MAX_DATE_TIME_STRING_LENGTH, StringBuilder::appendDateTime)

/**
* Get the [Duration] between the start and end date-time, assuming they're in the same time zone. In general, it's
Expand Down Expand Up @@ -235,7 +235,8 @@ class DateTimeInterval(
* @throws DateTimeParseException if parsing fails
* @throws DateTimeException if the parsed time is invalid
*/
fun String.toDateTimeInterval() = toDateTimeInterval(DateTimeParsers.Iso.Extended.DATE_TIME_INTERVAL)
fun String.toDateTimeInterval() =
toDateTimeInterval(DateTimeParsers.Iso.Extended.DATE_TIME_INTERVAL)

/**
* Convert a string to a [DateTimeInterval] using a specific parser.
Expand All @@ -254,13 +255,17 @@ fun String.toDateTimeInterval(
val start = when {
results[0].isEmpty() -> null
results[0].fields[DateTimeField.IS_UNBOUNDED] == 1L -> DateTimeInterval.UNBOUNDED.start
else -> results[0].toDateTime() ?: throwParserFieldResolutionException<DateTimeInterval>(this)
else -> results[0].toDateTime() ?: throwParserFieldResolutionException<DateTimeInterval>(
this
)
}

val end = when {
results[1].isEmpty() -> null
results[1].fields[DateTimeField.IS_UNBOUNDED] == 1L -> DateTimeInterval.UNBOUNDED.endExclusive
else -> results[1].toDateTime() ?: throwParserFieldResolutionException<DateTimeInterval>(this)
else -> results[1].toDateTime() ?: throwParserFieldResolutionException<DateTimeInterval>(
this
)
}

return when {
Expand All @@ -272,22 +277,50 @@ fun String.toDateTimeInterval(

/**
* Return a random date-time within the interval using the default random number generator.
* @throws NoSuchElementException if the interval is empty
* @throws UnsupportedOperationException if the interval is unbounded
* @see DateTimeInterval.randomOrNull
*/
fun DateTimeInterval.random(): DateTime = random(Random)

/**
* Return a random date-time within the interval using the default random number generator or
* `null` if the interval is empty or unbounded.
* @see DateTimeInterval.random
*/
fun DateTimeInterval.randomOrNull(): DateTime? = randomOrNull(Random)

/**
* Return a random date-time within the interval using the supplied random number generator.
* @throws NoSuchElementException if the interval is empty
* @throws UnsupportedOperationException if the interval is unbounded
* @see DateTimeInterval.randomOrNull
*/
fun DateTimeInterval.random(random: Random): DateTime {
try {
return DateTime.fromUnixEpochSecond(
random.nextLong(start.unixEpochSecondAt(UtcOffset.ZERO), endExclusive.unixEpochSecondAt(UtcOffset.ZERO)),
random.nextInt(start.unixEpochNanoOfSecond, endExclusive.unixEpochNanoOfSecond),
UtcOffset.ZERO
)
} catch (e: IllegalArgumentException) {
throw NoSuchElementException(e.message)
}
return random(
random,
secondGetter = { it.unixEpochSecondAt(UtcOffset.ZERO) },
nanosecondGetter = { it.unixEpochNanoOfSecond },
creator = { second, nanosecond ->
DateTime.fromUnixEpochSecond(second, nanosecond, UtcOffset.ZERO)
}
)
}

/**
* Return a random date-time within the interval using the supplied random number generator or
* `null` if the interval is empty or unbounded.
* @see DateTimeInterval.random
*/
fun DateTimeInterval.randomOrNull(random: Random): DateTime? {
return randomOrNull(
random,
secondGetter = { it.unixEpochSecondAt(UtcOffset.ZERO) },
nanosecondGetter = { it.unixEpochNanoOfSecond },
creator = { second, nanosecond ->
DateTime.fromUnixEpochSecond(second, nanosecond, UtcOffset.ZERO)
}
)
}

/**
Expand Down Expand Up @@ -339,7 +372,8 @@ fun durationBetween(start: DateTime, endExclusive: DateTime): Duration {
val secondDiff = endExclusive.secondsSinceUnixEpochAt(UtcOffset.ZERO) -
start.secondsSinceUnixEpochAt(UtcOffset.ZERO)

val nanoDiff = endExclusive.nanoOfSecondsSinceUnixEpoch minusWithOverflow start.nanoOfSecondsSinceUnixEpoch
val nanoDiff =
endExclusive.nanoOfSecondsSinceUnixEpoch minusWithOverflow start.nanoOfSecondsSinceUnixEpoch
return durationOf(secondDiff, nanoDiff)
}

Expand Down
46 changes: 31 additions & 15 deletions core/src/commonMain/kotlin/io/islandtime/ranges/InstantInterval.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
package io.islandtime.ranges

import io.islandtime.*
import io.islandtime.MAX_INSTANT_STRING_LENGTH
import io.islandtime.appendInstant
import io.islandtime.base.DateTimeField
import io.islandtime.measures.*
import io.islandtime.endOfDayAt
import io.islandtime.startOfDayAt
import io.islandtime.measures.nanoseconds
import io.islandtime.parser.*
import io.islandtime.ranges.internal.MAX_INCLUSIVE_END_DATE_TIME
import io.islandtime.ranges.internal.buildIsoString
import io.islandtime.toInstant
import io.islandtime.ranges.internal.random
import io.islandtime.ranges.internal.randomOrNull
import kotlin.random.Random

/**
Expand All @@ -33,7 +29,8 @@ class InstantInterval(
/**
* Convert this interval to a string in ISO-8601 extended format.
*/
override fun toString() = buildIsoString(MAX_INSTANT_STRING_LENGTH, StringBuilder::appendInstant)
override fun toString() =
buildIsoString(MAX_INSTANT_STRING_LENGTH, StringBuilder::appendInstant)

companion object {
/**
Expand Down Expand Up @@ -117,20 +114,39 @@ fun String.toInstantInterval(

/**
* Return a random instant within the interval using the default random number generator.
* @throws NoSuchElementException if the interval is empty
* @throws UnsupportedOperationException if the interval is unbounded
* @see InstantInterval.randomOrNull
*/
fun InstantInterval.random(): Instant = random(Random)

/**
* Return a random instant within the interval using the default random number generator or `null`
* if the interval is empty or unbounded.
* @see InstantInterval.random
*/
fun InstantInterval.randomOrNull(): Instant? = randomOrNull(Random)

/**
* Return a random instant within the interval using the supplied random number generator.
* @throws NoSuchElementException if the interval is empty
* @throws UnsupportedOperationException if the interval is unbounded
* @see InstantInterval.randomOrNull
*/
fun InstantInterval.random(random: Random): Instant {
try {
return Instant.fromUnixEpochSecond(
random.nextLong(start.unixEpochSecond, endExclusive.unixEpochSecond),
random.nextInt(start.unixEpochNanoOfSecond, endExclusive.unixEpochNanoOfSecond)
)
} catch (e: IllegalArgumentException) {
throw NoSuchElementException(e.message)
return random(random) { second, nanosecond ->
Instant.fromUnixEpochSecond(second, nanosecond)
}
}

/**
* Return a random instant within the interval using the supplied random number generator or `null`
* if the interval is empty or unbounded.
* @see InstantInterval.random
*/
fun InstantInterval.randomOrNull(random: Random): Instant? {
return randomOrNull(random) { second, nanosecond ->
Instant.fromUnixEpochSecond(second, nanosecond)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import io.islandtime.*
import io.islandtime.base.DateTimeField
import io.islandtime.measures.*
import io.islandtime.parser.*
import io.islandtime.parser.expectingGroupCount
import io.islandtime.parser.throwParserFieldResolutionException
import io.islandtime.ranges.internal.MAX_INCLUSIVE_END_DATE_TIME
import io.islandtime.ranges.internal.buildIsoString
import io.islandtime.ranges.internal.throwUnboundedIntervalException
import io.islandtime.ranges.internal.*
import kotlin.random.Random

/**
Expand All @@ -28,7 +24,8 @@ class OffsetDateTimeInterval(
/**
* Convert this interval to a string in ISO-8601 extended format.
*/
override fun toString() = buildIsoString(MAX_OFFSET_DATE_TIME_STRING_LENGTH, StringBuilder::appendOffsetDateTime)
override fun toString() =
buildIsoString(MAX_OFFSET_DATE_TIME_STRING_LENGTH, StringBuilder::appendOffsetDateTime)

/**
* Convert the interval into a [Period] of the same length.
Expand Down Expand Up @@ -126,7 +123,8 @@ class OffsetDateTimeInterval(
* @throws DateTimeParseException if parsing fails
* @throws DateTimeException if the parsed time is invalid
*/
fun String.toOffsetDateTimeInterval() = toOffsetDateTimeInterval(DateTimeParsers.Iso.Extended.OFFSET_DATE_TIME_INTERVAL)
fun String.toOffsetDateTimeInterval() =
toOffsetDateTimeInterval(DateTimeParsers.Iso.Extended.OFFSET_DATE_TIME_INTERVAL)

/**
* Convert a string to an [OffsetDateTimeInterval] using a specific parser.
Expand All @@ -146,13 +144,15 @@ fun String.toOffsetDateTimeInterval(
val start = when {
results[0].isEmpty() -> null
results[0].fields[DateTimeField.IS_UNBOUNDED] == 1L -> OffsetDateTimeInterval.UNBOUNDED.start
else -> results[0].toOffsetDateTime() ?: throwParserFieldResolutionException<OffsetDateTimeInterval>(this)
else -> results[0].toOffsetDateTime()
?: throwParserFieldResolutionException<OffsetDateTimeInterval>(this)
}

val end = when {
results[1].isEmpty() -> null
results[1].fields[DateTimeField.IS_UNBOUNDED] == 1L -> OffsetDateTimeInterval.UNBOUNDED.endExclusive
else -> results[1].toOffsetDateTime() ?: throwParserFieldResolutionException<OffsetDateTimeInterval>(this)
else -> results[1].toOffsetDateTime()
?: throwParserFieldResolutionException<OffsetDateTimeInterval>(this)
}

return when {
Expand All @@ -163,22 +163,42 @@ fun String.toOffsetDateTimeInterval(
}

/**
* Return a random date-time within the range using the default random number generator.
* Return a random date-time within the interval using the default random number generator. The
* offset of the start date-time will be used.
* @throws NoSuchElementException if the interval is empty
* @throws UnsupportedOperationException if the interval is unbounded
* @see OffsetDateTimeInterval.randomOrNull
*/
fun OffsetDateTimeInterval.random(): OffsetDateTime = random(Random)

/**
* Return a random date-time within the range using the supplied random number generator.
* Return a random date-time within the interval using the default random number generator or
* `null` if the interval is empty or unbounded. The offset of the start date-time will be used.
* @see OffsetDateTimeInterval.random
*/
fun OffsetDateTimeInterval.randomOrNull(): OffsetDateTime? = randomOrNull(Random)

/**
* Return a random date-time within the interval using the supplied random number generator. The
* offset of the start date-time will be used.
* @throws NoSuchElementException if the interval is empty
* @throws UnsupportedOperationException if the interval is unbounded
* @see OffsetDateTimeInterval.randomOrNull
*/
fun OffsetDateTimeInterval.random(random: Random): OffsetDateTime {
try {
return OffsetDateTime.fromUnixEpochSecond(
random.nextLong(start.unixEpochSecond, endExclusive.unixEpochSecond),
random.nextInt(start.unixEpochNanoOfSecond, endExclusive.unixEpochNanoOfSecond),
start.offset
)
} catch (e: IllegalArgumentException) {
throw NoSuchElementException(e.message)
return random(random) { second, nanosecond ->
OffsetDateTime.fromUnixEpochSecond(second, nanosecond, start.offset)
}
}

/**
* Return a random date-time within the interval using the supplied random number generator or
* `null` if the interval is empty or unbounded. The offset of the start date-time will be used.
* @see OffsetDateTimeInterval.random
*/
fun OffsetDateTimeInterval.randomOrNull(random: Random): OffsetDateTime? {
return randomOrNull(random) { second, nanosecond ->
OffsetDateTime.fromUnixEpochSecond(second, nanosecond, start.offset)
}
}

Expand Down
Loading