Add NonEmptySet

Add a new set-like collection which is constrained to ensure it's never
empty.
This commit is contained in:
Ross Nicoll 2016-05-20 14:10:34 +01:00
parent 4065cb25c0
commit 4744740913
3 changed files with 186 additions and 0 deletions

View File

@ -31,6 +31,9 @@ dependencies {
testCompile 'org.assertj:assertj-core:3.4.1'
testCompile "commons-fileupload:commons-fileupload:1.3.1"
// Guava: Google test library (collections test suite)
testCompile "com.google.guava:guava-testlib:19.0"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"

View File

@ -0,0 +1,83 @@
package com.r3corda.core.utilities
/**
* A set which is constrained to ensure it can never be empty. An initial value must be provided at
* construction, and attempting to remove the last element will cause an IllegalStateException.
*/
class NonEmptySet<T>(initial: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> {
init {
require (set.isEmpty()) { "Provided set must be empty." }
set.add(initial)
}
override val size: Int
get() = set.size
override fun add(element: T): Boolean = set.add(element)
override fun addAll(elements: Collection<T>): Boolean = set.addAll(elements)
override fun clear() = throw UnsupportedOperationException()
override fun contains(element: T): Boolean = set.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = set.containsAll(elements)
override fun isEmpty(): Boolean = false
override fun iterator(): MutableIterator<T> = Iterator<T>(set.iterator())
override fun remove(element: T): Boolean =
// Test either there's more than one element, or the removal is a no-op
if (size > 1)
set.remove(element)
else if (!contains(element))
false
else
throw IllegalStateException()
override fun removeAll(elements: Collection<T>): Boolean =
if (size > elements.size)
set.removeAll(elements)
else if (!containsAll(elements))
// Remove the common elements
set.removeAll(elements)
else
throw IllegalStateException()
override fun retainAll(elements: Collection<T>): Boolean {
val iterator = iterator()
val ret = false
// The iterator will throw an IllegalStateException if we try removing the last element
while (iterator.hasNext()) {
if (!elements.contains(iterator.next())) {
iterator.remove()
}
}
return ret
}
override fun equals(other: Any?): Boolean =
if (other is Set<*>)
// Delegate down to the wrapped set's equals() function
set.equals(other)
else
false
override fun hashCode(): Int = set.hashCode()
override fun toString(): String = set.toString()
inner class Iterator<T>(val iterator: MutableIterator<T>) : MutableIterator<T> {
override fun hasNext(): Boolean = iterator.hasNext()
override fun next(): T = iterator.next()
override fun remove() =
if (set.size > 1)
iterator.remove()
else
throw IllegalStateException()
}
}
fun <T> nonEmptySetOf(initial: T, vararg elements: T): NonEmptySet<T> {
val set = NonEmptySet<T>(initial)
// We add the first element twice, but it's a set, so who cares
set.addAll(elements)
return set
}

View File

@ -0,0 +1,100 @@
package com.r3corda.core.utilities
import com.google.common.collect.testing.SetTestSuiteBuilder
import com.google.common.collect.testing.TestIntegerSetGenerator
import com.google.common.collect.testing.features.CollectionFeature
import com.google.common.collect.testing.features.CollectionSize
import com.google.common.collect.testing.testers.CollectionAddAllTester
import com.google.common.collect.testing.testers.CollectionClearTester
import com.google.common.collect.testing.testers.CollectionRemoveAllTester
import com.google.common.collect.testing.testers.CollectionRetainAllTester
import junit.framework.TestSuite
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Suite
import kotlin.test.assertEquals
@RunWith(Suite::class)
@Suite.SuiteClasses(
NonEmptySetTest.Guava::class,
NonEmptySetTest.Remove::class
)
class NonEmptySetTest {
/**
* Guava test suite generator for NonEmptySet.
*/
class Guava {
companion object {
@JvmStatic
fun suite(): TestSuite
= SetTestSuiteBuilder
.using(NonEmptySetGenerator())
.named("test NonEmptySet with several values")
.withFeatures(
CollectionSize.SEVERAL,
CollectionFeature.ALLOWS_NULL_VALUES,
CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION,
CollectionFeature.GENERAL_PURPOSE
)
// Kotlin throws the wrong exception in this cases
.suppressing(CollectionAddAllTester::class.java.getMethod("testAddAll_nullCollectionReference"))
// Disable tests that try to remove everything:
.suppressing(CollectionRemoveAllTester::class.java.getMethod("testRemoveAll_nullCollectionReferenceNonEmptySubject"))
.suppressing(CollectionClearTester::class.java.getMethods().toList())
.suppressing(CollectionRetainAllTester::class.java.getMethods().toList())
.createTestSuite()
}
}
/**
* Test removal, which Guava's standard tests can't cover for us.
*/
class Remove {
@Test
fun `construction`() {
val expected = 17
val basicSet = nonEmptySetOf(expected)
val actual = basicSet.first()
assertEquals(expected, actual)
}
@Test(expected = IllegalStateException::class)
fun `remove sole element`() {
val basicSet = nonEmptySetOf(-17)
basicSet.remove(-17)
}
@Test
fun `remove one of two elements`() {
val basicSet = nonEmptySetOf(-17, 17)
basicSet.remove(-17)
}
@Test
fun `remove element which does not exist`() {
val basicSet = nonEmptySetOf(-17)
basicSet.remove(-5)
assertEquals(1, basicSet.size)
}
@Test(expected = IllegalStateException::class)
fun `remove via iterator`() {
val basicSet = nonEmptySetOf(-17, 17)
val iterator = basicSet.iterator()
while (iterator.hasNext()) {
iterator.remove()
}
}
}
}
/**
* Generator of non empty set instances needed for testing.
*/
class NonEmptySetGenerator : TestIntegerSetGenerator() {
override fun create(elements: Array<out Int?>?): NonEmptySet<Int?>? {
val set = nonEmptySetOf(elements!!.first())
set.addAll(elements.toList())
return set
}
}