commonMain.IntervalUnion.kt Maven / Gradle / Ivy
The newest version!
package io.github.whathecode.kotlinx.interval
/**
* Represents a set of all [T] values contained in a collection of disjoint, non-adjacent, [Interval]s.
* I.e., the intervals don't overlap, and there always lie some values of [T] in between any two intervals.
* If the collection contains no intervals, it represents an empty set.
*/
sealed interface IntervalUnion, TSize : Comparable> : Iterable>
{
/**
* Determines whether this is an empty set, i.e., no value of [T] is contained within.
*/
fun isEmpty(): Boolean = none()
/**
* Gets the upper and lower bound of this set, and whether they are included, as a canonical interval.
* Or, `null` if the set is empty.
*
* Unless this set is an [Interval], not all values lying within the upper and lower bound are part of this set.
*/
fun getBounds(): Interval?
/**
* Checks whether [value] lies within this set.
*/
operator fun contains( value: T ): Boolean
/**
* Return an [IntervalUnion] representing all [T] values in this set,
* excluding all [T] values in the specified interval [toSubtract].
*/
operator fun minus( toSubtract: Interval ): IntervalUnion
/**
* Return an [IntervalUnion] representing all [T] values in this set,
* and including all [T] in the specified interval [toAdd].
*/
operator fun plus( toAdd: Interval ): IntervalUnion
/**
* Determines whether [interval] has at least one value in common with this set.
*/
fun intersects( interval: Interval ): Boolean
/**
* Determines whether this [IntervalUnion] represents the same set of values as the [other] union.
*/
fun setEquals( other: IntervalUnion ): Boolean
}
/**
* Create an [IntervalUnion] which represents a set which contains no values.
*/
@Suppress( "UNCHECKED_CAST" )
internal inline fun , TSize : Comparable> emptyIntervalUnion() =
EmptyIntervalUnion as IntervalUnion
private data object EmptyIntervalUnion : IntervalUnion
{
override fun getBounds(): Interval? = null
override fun contains( value: Nothing ): Boolean = false
override fun minus( toSubtract: Interval ): IntervalUnion = this
override fun plus( toAdd: Interval ): IntervalUnion = toAdd
override fun intersects( interval: Interval ): Boolean = false
override fun setEquals( other: IntervalUnion ): Boolean = other == this
override fun iterator(): Iterator> = emptyList>().iterator()
}
/**
* Create an [IntervalUnion] which holds two disjoint, non-adjacent, and non-empty [IntervalUnion]'s.
*
* @throws IllegalArgumentException when [union1] or [union2] is empty,
* or the two pairs are not disjoint and non-adjacent.
*/
internal fun , TSize : Comparable> intervalUnionPair(
union1: IntervalUnion,
union2: IntervalUnion
): IntervalUnion
{
val compare = IntervalUnionComparison.of( union1, union2 )
return IntervalUnionPair( compare )
}
/**
* Combines the previously compared [IntervalUnion]'s into a single new [IntervalUnion] containing both
* (both are returned when iterated), provided that they are disjoint and non-adjacent.
*
* @throws IllegalArgumentException when [IntervalUnionComparison.isSplitPair] is false.
*/
internal fun , TSize : Comparable, TUnion : IntervalUnion>
IntervalUnionComparison.asSplitPair(): IntervalUnion = IntervalUnionPair( this )
private class IntervalUnionPair, TSize : Comparable, TUnion : IntervalUnion>(
unionPair: IntervalUnionComparison
) : IntervalUnion
{
init { require( unionPair.isSplitPair ) { "The pair of unions passed are not disjoint and non-adjacent." } }
private val lower: TUnion = unionPair.lower
private val upper: TUnion = unionPair.upper
private val bounds: Interval = Interval(
unionPair.lowerBounds.start, unionPair.lowerBounds.isStartIncluded,
unionPair.upperBounds.end, unionPair.upperBounds.isEndIncluded,
unionPair.lowerBounds.operations )
override fun iterator(): Iterator> =
( lower.asSequence() + upper.asSequence() ).iterator()
override fun getBounds(): Interval = bounds
override fun contains( value: T ): Boolean
{
if ( lower is Interval<*, *> && value in lower ) return true
if ( upper is Interval<*, *> && value in upper ) return true
// Use bounds check to preempt unnecessary recursion for interval unions.
if ( lower.getBounds()?.contains( value ) == true ) return value in lower
if ( upper.getBounds()?.contains( value ) == true ) return value in upper
return false
}
override fun minus( toSubtract: Interval ): IntervalUnion
{
// When `toSubtract` lies outside this union's bounds, no intervals are affected.
// When it fully encompasses this union, nothing is left after subtraction.
val pairCompare = IntervalUnionComparison.of( this, toSubtract )
if ( pairCompare.isSplitPair ) return this
if ( pairCompare.isFullyEncompassed && pairCompare.lower == toSubtract ) return emptyIntervalUnion()
// Ignore `lower` or `upper if it would become empty after subtraction; recurse on the side with a remainder.
val lowerPairCompare = IntervalUnionComparison.of( lower, toSubtract )
if ( lowerPairCompare.isFullyEncompassed ) return upper - toSubtract
val upperPairCompare = IntervalUnionComparison.of( upper, toSubtract )
if ( upperPairCompare.isFullyEncompassed ) return lower - toSubtract
// Both `lower` and `upper` have a remainder, so recursively subtract from both sides.
return intervalUnionPair( lower - toSubtract, upper - toSubtract )
}
override fun plus( toAdd: Interval ): IntervalUnion
{
// When `toAdd` lies outside this union's bounds, prepend or append it.
// When it fully encompasses this union, `toAdd` replaces it.
val pairCompare = IntervalUnionComparison.of( this, toAdd )
if ( pairCompare.isSplitPair ) return pairCompare.asSplitPair()
if ( pairCompare.isFullyEncompassed && pairCompare.lower == toAdd ) return toAdd
// When `toAdd` only impacts `lower` or `upper`, recursively add the new interval to the impacted union.
// Or, if neither are impacted, the new interval must lie in between `lower` and `upper`.
val lowerPairCompare = IntervalUnionComparison.of( lower, toAdd )
val upperPairCompare = IntervalUnionComparison.of( upper, toAdd )
if ( lowerPairCompare.isSplitPair )
{
return if ( upperPairCompare.isSplitPair ) intervalUnionPair( lowerPairCompare.asSplitPair(), upper )
else intervalUnionPair( lower, upper + toAdd )
}
else if ( upperPairCompare.isSplitPair ) return intervalUnionPair( lower + toAdd, upper )
// When both the `lower` and `upper` union are impacted, but either is fully encompassed by `toAdd`,
// recursively add `toAdd` to the only partially impacted union.
if ( lowerPairCompare.isFullyEncompassed ) return upper + toAdd
if ( upperPairCompare.isFullyEncompassed ) return lower + toAdd
// With all other options excluded, both `lower` and `upper` are partially impacted.
// TODO: This doesn't make use of known bounds of nested unions in `upper`, only for `lower` through recursion.
// This can likely be optimized.
return upper.fold( lower + toAdd ) { result, upperInterval -> result + upperInterval }
}
override fun intersects( interval: Interval ): Boolean
{
if ( lower is Interval<*, *> && lower.intersects( interval ) ) return true
if ( upper is Interval<*, *> && upper.intersects( interval ) ) return true
// Use bounds check to preempt unnecessary recursion for interval unions.
if ( lower.getBounds()?.intersects( interval ) == true ) return lower.intersects( interval )
if ( upper.getBounds()?.intersects( interval ) == true ) return upper.intersects( interval )
return false
}
override fun setEquals( other: IntervalUnion ): Boolean
{
val iterateThis = map { it.canonicalize() }.iterator()
val iterateOther = other.map { it.canonicalize() }.iterator()
while ( iterateThis.hasNext() )
{
if ( iterateOther.hasNext() && iterateThis.next() != iterateOther.next() ) return false
}
return true
}
override fun toString(): String = joinToString( prefix = "[", postfix = "]" )
}