Skip to content

Commit

Permalink
Shift ConstMultiSet code to impl classes (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
lbressler13 authored Mar 17, 2024
1 parent cf7e15e commit 0f796e6
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 352 deletions.
Original file line number Diff line number Diff line change
@@ -1,118 +1,13 @@
package xyz.lbres.kotlinutils.set.multiset.const

import xyz.lbres.kotlinutils.general.simpleIf
import xyz.lbres.kotlinutils.general.tryOrDefault
import xyz.lbres.kotlinutils.internal.constants.Suppressions
import xyz.lbres.kotlinutils.set.multiset.MultiSet
import xyz.lbres.kotlinutils.set.multiset.impl.MultiSetImpl
import xyz.lbres.kotlinutils.set.multiset.utils.countsToList
import xyz.lbres.kotlinutils.set.multiset.utils.countsToString
import xyz.lbres.kotlinutils.set.multiset.utils.createCountsMap
import kotlin.math.min

/**
* [MultiSet] implementation where values of elements are assumed to be constant.
* Behavior is not defined if values of elements are changed (i.e. elements are added to a mutable list).
*/
sealed class ConstMultiSet<E> constructor(private val initialElements: Collection<E>, private var initialCounts: Map<E, Int>? = null) : MultiSet<E> {
/**
* Number of elements in set.
*/
// TODO private
protected var _size: Int = initialElements.size
override val size: Int
get() = _size

/**
* Map where each key is an element in the set, and each value is the number of occurrences of the element in the set.
* Counts are guaranteed to be greater than 0.
*/
protected open val counts: Map<E, Int> = initializeCounts()

/**
* All distinct values contained in the set.
*/
override val distinctValues: Set<E> = (initialCounts ?: initializeCounts()).keys

/**
* String representation of the set
*/
protected open val string: String = countsToString(initialCounts ?: initializeCounts())

/**
* All elements in the set
*/
protected open val elements: Collection<E> = initialElements

/**
* Get the number of occurrences of a given element.
*
* @param element [E]
* @return [Int]: the number of occurrences of [element]. 0 if the element does not exist.
*/
override fun getCountOf(element: E): Int = counts.getOrDefault(element, 0)

/**
* Determine if an element is contained in the current set.
*
* @param element [E]
* @return [Boolean]: `true` if [element] is in the set, `false` otherwise
*/
override fun contains(element: E): Boolean = counts.contains(element)

/**
* Determine if all elements in a collection are contained in the current set.
* If the collection contains multiple occurrences of the same value, the function will check if this set contains at least as many occurrences as the collection.
*
* @param elements [Collection]<E>
* @return [Boolean]: `true` if the current set contains at least as many occurrences of each value as the collection, `false` otherwise
*/
override fun containsAll(elements: Collection<E>): Boolean {
val otherCounts = createCountsMap(elements)

return otherCounts.all { (element, otherCount) ->
otherCount <= getCountOf(element)
}
}

/**
* Create a new MultiSet with all values from both sets.
* If there are multiple occurrences of a value, the number of occurrences in the other set will be added to the number in this set.
* The returned set is **not** a ConstMultiSet.
*
* @param other [MultiSet]<E>: values to add to this set
* @return [MultiSet]<E>: MultiSet containing all values from both sets
*/
override operator fun plus(other: MultiSet<E>): MultiSet<E> {
val newCounts = combineCounts(other, Int::plus, true)
return MultiSetImpl(countsToList(newCounts))
}

/**
* Create a new MultiSet with values that are in this set but not the other set.
* If there are multiple occurrences of a value, the number of occurrences in the other set will be subtracted from the number in this set.
* The returned set is **not** a ConstMultiSet.
*
* @param other [MultiSet]<E>: values to subtract from this set
* @return [MultiSet]<E>: MultiSet containing the items in this set but not the other
*/
override operator fun minus(other: MultiSet<E>): MultiSet<E> {
val newCounts = combineCounts(other, Int::minus, false)
return MultiSetImpl(countsToList(newCounts))
}

/**
* Create a new MultiSet with values that are shared between the sets.
* If there are multiple occurrences of a value, the smaller number of occurrences will be used.
* The returned set is **not** a ConstMultiSet.
*
* @param other [MultiSet]<E>: MultiSet to intersect with current
* @return [MultiSet]<E>: MultiSet containing only values that are in both sets
*/
override infix fun intersect(other: MultiSet<E>): MultiSet<E> {
val newCounts = combineCounts(other, { val1, val2 -> min(val1, val2) }, false)
return MultiSetImpl(countsToList(newCounts))
}

@Suppress(Suppressions.FUNCTION_NAME)
infix fun `+c`(other: ConstMultiSet<E>): ConstMultiSet<E> = plusC(other)
Expand All @@ -125,112 +20,21 @@ sealed class ConstMultiSet<E> constructor(private val initialElements: Collectio
* @param other [ConstMultiSet]<E>: values to add to this set
* @return [ConstMultiSet]<E>: ConstMultiSet containing all values from both sets
*/
fun plusC(other: ConstMultiSet<E>): ConstMultiSet<E> {
val newCounts = combineCounts(other, Int::plus, true)
return ConstMultiSetImpl(countsToList(newCounts), newCounts)
}
abstract fun plusC(other: ConstMultiSet<E>): ConstMultiSet<E>

/**
* Alternate version of [minus], which returns a ConstMultiSet
*
* @param other [ConstMultiSet]<E>: values to subtract from this set
* @return [ConstMultiSet]<E>: ConstMultiSet containing the items in this set but not the other
*/
fun minusC(other: ConstMultiSet<E>): ConstMultiSet<E> {
val newCounts = combineCounts(other, Int::minus, false)
return ConstMultiSetImpl(countsToList(newCounts), newCounts)
}
abstract fun minusC(other: ConstMultiSet<E>): ConstMultiSet<E>

/**
* Alternate version of [intersect], which returns a ConstMultiSet
*
* @param other [ConstMultiSet]<E>: ConstMultiSet to intersect with current
* @return [ConstMultiSet]<E>: ConstMultiSet containing only values that are in both sets
*/
infix fun intersectC(other: ConstMultiSet<E>): ConstMultiSet<E> {
val newCounts = combineCounts(other, { val1, val2 -> min(val1, val2) }, false)
return ConstMultiSetImpl(countsToList(newCounts), newCounts)
}

/**
* Combine counts with another MultiSet, using the given operation
*
* @param other [MultiSet]<E>: MultiSet to combine with
* @param operation (Int, Int) -> Int: combination function
* @param useAllValues [Boolean]: if all values from both sets should be used to generate the new set. If `false`, only the values from this set will be used.
* @return [Map]<E, Int>: new counts map where each element has the number of occurrences specified by the operation
*/
private fun combineCounts(other: MultiSet<E>, operation: (count: Int, otherCount: Int) -> Int, useAllValues: Boolean): Map<E, Int> {
val values = simpleIf(useAllValues, { distinctValues + other.distinctValues }, { distinctValues })
return values.associateWith {
operation(getCountOf(it), other.getCountOf(it))
}.filter { it.value > 0 }
}

/**
* If the current set contains 0 elements.
*
* @return [Boolean]: `true` if the set contains 0 elements, `false` otherwise
*/
override fun isEmpty(): Boolean = size == 0

/**
* If the current set contains the same elements as another MultiSet, with the same number of occurrences per element.
*
* @param other [Any]?
* @return [Boolean]: `true` if [other] is a non-null MultiSet which contains the same values as the current set, `false` otherwise
*/
override fun equals(other: Any?): Boolean {
if (other == null || other !is MultiSet<*>) {
return false
}

@Suppress(Suppressions.UNCHECKED_CAST)
return tryOrDefault(false, listOf(ClassCastException::class)) {
if (other is ConstMultiSet<*>) {
other as ConstMultiSet<E>
counts == other.counts
} else {
other as MultiSet<E>
size == other.size && distinctValues.all { getCountOf(it) == other.getCountOf(it) }
}
}
}

/**
* Get a string representation of the set.
*
* @return [String]
*/
override fun toString(): String = string

/**
* Get an iterator for the elements in this set.
*
* @return [Iterator]<E>
*/
override fun iterator(): Iterator<E> = elements.iterator()

/**
* Get hash code for set.
*
* @return [Int]
*/
override fun hashCode(): Int {
val hashCode = counts.hashCode()
return 31 * hashCode + MultiSet::class.java.name.hashCode()
}

/**
* Generate a counts map from the initial elements
*
* @return [Map]<E, Int>: generated map
*/
protected fun initializeCounts(): Map<E, Int> {
if (initialCounts == null) {
initialCounts = createCountsMap(initialElements)
}

return initialCounts!!
}
abstract infix fun intersectC(other: ConstMultiSet<E>): ConstMultiSet<E>
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,40 @@
package xyz.lbres.kotlinutils.set.multiset.const

import xyz.lbres.kotlinutils.generic.ext.ifNull
import xyz.lbres.kotlinutils.set.multiset.MultiSet
import xyz.lbres.kotlinutils.set.multiset.utils.countsToString
import xyz.lbres.kotlinutils.set.multiset.utils.createCountsMap

// final implementation of ConstMultiSet
internal class ConstMultiSetImpl<E>(initialElements: Collection<E>, initialCounts: Map<E, Int>? = null) : ConstMultiSet<E>(initialElements, initialCounts)
internal class ConstMultiSetImpl<E>(private val elements: Collection<E>, initialCounts: Map<E, Int>? = null) : ConstMultiSet<E>(elements, initialCounts) {
private val manager: ConstMultiSetManager<E>
override val size: Int = elements.size
override val distinctValues: Set<E>
private val string: String
val counts: Map<E, Int>

init {
counts = initialCounts.ifNull { createCountsMap(elements) }
distinctValues = counts.keys
manager = ConstMultiSetManager(counts)
string = countsToString(counts)
}

override fun getCountOf(element: E): Int = manager.getCountOf(element)
override fun contains(element: E): Boolean = manager.contains(element)
override fun containsAll(elements: Collection<E>): Boolean = manager.containsAll(elements)

override fun plus(other: MultiSet<E>): MultiSet<E> = manager.plus(other)
override fun minus(other: MultiSet<E>): MultiSet<E> = manager.minus(other)
override fun intersect(other: MultiSet<E>): MultiSet<E> = manager.intersect(other)

override fun plusC(other: ConstMultiSet<E>): ConstMultiSet<E> = manager.plusC(other)
override fun minusC(other: ConstMultiSet<E>): ConstMultiSet<E> = manager.minusC(other)
override fun intersectC(other: ConstMultiSet<E>): ConstMultiSet<E> = manager.intersectC(other)

override fun isEmpty(): Boolean = manager.isEmpty()
override fun iterator(): Iterator<E> = elements.iterator()
override fun hashCode(): Int = manager.getHashCode()
override fun equals(other: Any?): Boolean = manager.equalsSet(other)
override fun toString(): String = string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package xyz.lbres.kotlinutils.set.multiset.const

import xyz.lbres.kotlinutils.general.simpleIf
import xyz.lbres.kotlinutils.set.multiset.MultiSet
import xyz.lbres.kotlinutils.set.multiset.impl.MultiSetImpl
import xyz.lbres.kotlinutils.set.multiset.utils.createCountsMap
import kotlin.math.min

/**
* Manager class for common ConstMultiSet code
*
* @param counts [Map]<E, Int>: counts map for set
*/
internal class ConstMultiSetManager<E>(private val counts: Map<E, Int>) {
fun getCountOf(element: E): Int = counts.getOrDefault(element, 0)
fun isEmpty(): Boolean = counts.isEmpty()

fun contains(element: E): Boolean = counts.contains(element)
fun containsAll(elements: Collection<E>): Boolean {
val otherCounts = createCountsMap(elements)

return otherCounts.all { (element, otherCount) ->
otherCount <= getCountOf(element)
}
}

fun plus(other: MultiSet<E>): MultiSet<E> {
return combineCounts(other, Int::plus, true, const = false)
}
fun minus(other: MultiSet<E>): MultiSet<E> {
return combineCounts(other, Int::minus, false, const = false)
}
fun intersect(other: MultiSet<E>): MultiSet<E> {
return combineCounts(other, ::min, false, const = false)
}
fun plusC(other: ConstMultiSet<E>): ConstMultiSet<E> {
return combineCounts(other, Int::plus, true, const = true) as ConstMultiSet<E>
}
fun minusC(other: ConstMultiSet<E>): ConstMultiSet<E> {
return combineCounts(other, Int::minus, false, const = true) as ConstMultiSet<E>
}
fun intersectC(other: ConstMultiSet<E>): ConstMultiSet<E> {
return combineCounts(other, ::min, false, const = true) as ConstMultiSet<E>
}

/**
* Combine counts with another MultiSet, using the given operation
*
* @param other [MultiSet]<E>: MultiSet to combine with
* @param operation (Int, Int) -> Int: combination function
* @param useAllValues [Boolean]: if all values from both sets should be used to generate the new set. If `false`, only the values from this set will be used.
* @param const [Boolean]: if the returned MultiSet should be a ConstMultiSet
* @return [MultiSet]<E>: new set where each element has the number of occurrences specified by the operation. If [const] is `true`, the set will be a const multi set
*/
private fun combineCounts(other: MultiSet<E>, operation: (count: Int, otherCount: Int) -> Int, useAllValues: Boolean, const: Boolean): MultiSet<E> {
val values: Set<E> = simpleIf(useAllValues, { counts.keys + other.distinctValues }, { counts.keys })
val newCounts: MutableMap<E, Int> = mutableMapOf()
val newElements: MutableList<E> = mutableListOf()

values.forEach { value ->
val count = operation(getCountOf(value), other.getCountOf(value))
if (count > 0) {
newCounts[value] = count
repeat(count) { newElements.add(value) }
}
}

return simpleIf(const, { ConstMultiSetImpl(newElements, newCounts) }, { MultiSetImpl(newElements) })
}

/**
* Check equality of the managed set to another MultiSet
*
* @param other [Any]?
* @return [Boolean] `true` if [other] is a MultiSet containing the same elements as the counts map, `false` otherwise
*/
fun equalsSet(other: Any?): Boolean {
return when (other) {
is ConstMultiSetImpl<*> -> counts == other.counts
is ConstMutableMultiSetImpl<*> -> counts == other.counts
is MultiSet<*> -> counts == createCountsMap(other)
else -> false
}
}

/**
* Get hash code for set.
*
* @return [Int]
*/
fun getHashCode(): Int {
val hashCode = counts.hashCode()
return 31 * hashCode + MultiSet::class.java.name.hashCode()
}
}
Loading

0 comments on commit 0f796e6

Please sign in to comment.