Skip to content

Commit 8a87bd0

Browse files
committed
add case-insensitive mutable string set
1 parent 62159ad commit 8a87bd0

File tree

5 files changed

+209
-12
lines changed

5 files changed

+209
-12
lines changed

runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMap.kt

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,6 @@ package aws.smithy.kotlin.runtime.collections
66

77
import aws.smithy.kotlin.runtime.InternalApi
88

9-
private class CaseInsensitiveString(val original: String) {
10-
val normalized = original.lowercase()
11-
override fun hashCode() = normalized.hashCode()
12-
override fun equals(other: Any?) = other is CaseInsensitiveString && normalized == other.normalized
13-
override fun toString() = original
14-
}
15-
16-
private fun String.toInsensitive(): CaseInsensitiveString =
17-
CaseInsensitiveString(this)
18-
199
/**
2010
* Map of case-insensitive [String] to [Value]
2111
*/
@@ -30,7 +20,7 @@ internal class CaseInsensitiveMap<Value> : MutableMap<String, Value> {
3020

3121
override fun containsValue(value: Value): Boolean = impl.containsValue(value)
3222

33-
override fun get(key: String): Value? = impl.get(key.toInsensitive())
23+
override fun get(key: String): Value? = impl[key.toInsensitive()]
3424

3525
override fun isEmpty(): Boolean = impl.isEmpty()
3626

@@ -40,7 +30,7 @@ internal class CaseInsensitiveMap<Value> : MutableMap<String, Value> {
4030
}.toMutableSet()
4131

4232
override val keys: MutableSet<String>
43-
get() = impl.keys.map { it.normalized }.toMutableSet()
33+
get() = CaseInsensitiveMutableStringSet(impl.keys)
4434

4535
override val values: MutableCollection<Value>
4636
get() = impl.values
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package aws.smithy.kotlin.runtime.collections
2+
3+
internal class CaseInsensitiveMutableStringSet(
4+
initialValues: Iterable<CaseInsensitiveString> = setOf(),
5+
) : MutableSet<String> {
6+
private val delegate = initialValues.toMutableSet()
7+
8+
override fun add(element: String) = delegate.add(element.toInsensitive())
9+
override fun clear() = delegate.clear()
10+
override fun contains(element: String) = delegate.contains(element.toInsensitive())
11+
override fun containsAll(elements: Collection<String>) = elements.all { it in this }
12+
override fun equals(other: Any?) = other is CaseInsensitiveMutableStringSet && delegate == other.delegate
13+
override fun hashCode() = delegate.hashCode()
14+
override fun isEmpty() = delegate.isEmpty()
15+
override fun remove(element: String) = delegate.remove(element.toInsensitive())
16+
override val size: Int get() = delegate.size
17+
override fun toString() = delegate.toString()
18+
19+
override fun addAll(elements: Collection<String>) =
20+
elements.fold(false) { modified, item -> add(item) || modified }
21+
22+
override fun iterator() = object : MutableIterator<String> {
23+
val delegate = this@CaseInsensitiveMutableStringSet.delegate.iterator()
24+
override fun hasNext() = delegate.hasNext()
25+
override fun next() = delegate.next().normalized
26+
override fun remove() = delegate.remove()
27+
}
28+
29+
override fun removeAll(elements: Collection<String>) =
30+
elements.fold(false) { modified, item -> remove(item) || modified }
31+
32+
override fun retainAll(elements: Collection<String>): Boolean {
33+
val insensitiveElements = elements.map { it.toInsensitive() }.toSet()
34+
val toRemove = delegate.filterNot { it in insensitiveElements }
35+
return toRemove.fold(false) { modified, item -> delegate.remove(item) || modified }
36+
}
37+
}
38+
39+
internal fun CaseInsensitiveMutableStringSet(initialValues: Iterable<String>) =
40+
CaseInsensitiveMutableStringSet(initialValues.map { it.toInsensitive() })
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package aws.smithy.kotlin.runtime.collections
2+
3+
internal class CaseInsensitiveString(val original: String) {
4+
val normalized = original.lowercase()
5+
override fun hashCode() = normalized.hashCode()
6+
override fun equals(other: Any?) = other is CaseInsensitiveString && normalized == other.normalized
7+
override fun toString() = original
8+
}
9+
10+
internal fun String.toInsensitive(): CaseInsensitiveString =
11+
CaseInsensitiveString(this)

runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMapTest.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
*/
55
package aws.smithy.kotlin.runtime.collections
66

7+
import org.junit.jupiter.api.Assertions.assertFalse
78
import kotlin.test.Test
89
import kotlin.test.assertEquals
10+
import kotlin.test.assertTrue
911

1012
class CaseInsensitiveMapTest {
1113
@Test
@@ -19,6 +21,31 @@ class CaseInsensitiveMapTest {
1921
assertEquals("json", map["CONTENT-TYPE"])
2022
}
2123

24+
@Test
25+
fun testContains() {
26+
val map = CaseInsensitiveMap<String>()
27+
map["A"] = "apple"
28+
map["B"] = "banana"
29+
map["C"] = "cherry"
30+
31+
assertTrue("C" in map)
32+
assertTrue("c" in map)
33+
assertFalse("D" in map)
34+
}
35+
36+
@Test
37+
fun testKeysContains() {
38+
val map = CaseInsensitiveMap<String>()
39+
map["A"] = "apple"
40+
map["B"] = "banana"
41+
map["C"] = "cherry"
42+
val keys = map.keys
43+
44+
assertTrue("C" in keys)
45+
assertTrue("c" in keys)
46+
assertFalse("D" in keys)
47+
}
48+
2249
@Test
2350
fun testEquality() {
2451
val left = CaseInsensitiveMap<String>()
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package aws.smithy.kotlin.runtime.collections
2+
3+
import org.junit.jupiter.api.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFalse
6+
import kotlin.test.assertNotEquals
7+
import kotlin.test.assertTrue
8+
9+
private val input = setOf("APPLE", "banana", "cHeRrY")
10+
private val variations = (input + input.map { it.lowercase() } + input.map { it.uppercase() })
11+
private val disjoint = setOf("durIAN", "ELdeRBerRY", "FiG")
12+
private val subset = input - "APPLE"
13+
private val intersecting = subset + disjoint
14+
15+
class CaseInsensitiveMutableStringSetTest {
16+
private fun assertSize(size: Int, set: CaseInsensitiveMutableStringSet) {
17+
assertEquals(size, set.size)
18+
val emptyAsserter: (Boolean) -> Unit = if (size == 0) ::assertTrue else ::assertFalse
19+
emptyAsserter(set.isEmpty())
20+
}
21+
22+
@Test
23+
fun testInitialization() {
24+
val set = CaseInsensitiveMutableStringSet(input)
25+
assertSize(3, set)
26+
}
27+
28+
@Test
29+
fun testAdd() {
30+
val set = CaseInsensitiveMutableStringSet(input)
31+
set += "durIAN"
32+
assertSize(4, set)
33+
}
34+
35+
@Test
36+
fun testAddAll() {
37+
val set = CaseInsensitiveMutableStringSet(input)
38+
assertFalse(set.addAll(set))
39+
40+
val intersecting = input + "durian"
41+
assertTrue(set.addAll(intersecting))
42+
assertSize(4, set)
43+
}
44+
45+
@Test
46+
fun testClear() {
47+
val set = CaseInsensitiveMutableStringSet(input)
48+
set.clear()
49+
assertSize(0, set)
50+
}
51+
52+
@Test
53+
fun testContains() {
54+
val set = CaseInsensitiveMutableStringSet(input)
55+
variations.forEach { assertTrue("Set should contain element $it") { it in set } }
56+
57+
assertFalse("durian" in set)
58+
}
59+
60+
@Test
61+
fun testContainsAll() {
62+
val set = CaseInsensitiveMutableStringSet(input)
63+
assertTrue(set.containsAll(variations))
64+
65+
val intersecting = input + "durian"
66+
assertFalse(set.containsAll(intersecting))
67+
}
68+
69+
@Test
70+
fun testEquality() {
71+
val left = CaseInsensitiveMutableStringSet(input)
72+
val right = CaseInsensitiveMutableStringSet(input)
73+
assertEquals(left, right)
74+
75+
left -= "apple"
76+
assertNotEquals(left, right)
77+
78+
right -= "ApPlE"
79+
assertEquals(left, right)
80+
}
81+
82+
@Test
83+
fun testIterator() {
84+
val set = CaseInsensitiveMutableStringSet(input)
85+
val iterator = set.iterator()
86+
87+
assertTrue(iterator.hasNext())
88+
assertEquals("apple", iterator.next())
89+
90+
assertTrue(iterator.hasNext())
91+
assertEquals("banana", iterator.next())
92+
iterator.remove()
93+
assertSize(2, set)
94+
95+
assertTrue(iterator.hasNext())
96+
assertEquals("cherry", iterator.next())
97+
98+
assertFalse(iterator.hasNext())
99+
assertTrue(set.containsAll(input - "banana"))
100+
}
101+
102+
@Test
103+
fun testRemove() {
104+
val set = CaseInsensitiveMutableStringSet(input)
105+
set -= "BANANA"
106+
assertSize(2, set)
107+
}
108+
109+
@Test
110+
fun testRemoveAll() {
111+
val set = CaseInsensitiveMutableStringSet(input)
112+
assertFalse(set.removeAll(disjoint))
113+
114+
assertTrue(set.removeAll(intersecting))
115+
assertSize(1, set)
116+
assertTrue("apple" in set)
117+
}
118+
119+
@Test
120+
fun testRetainAll() {
121+
val set = CaseInsensitiveMutableStringSet(input)
122+
assertFalse(set.retainAll(set))
123+
assertSize(3, set)
124+
125+
assertTrue(set.retainAll(intersecting))
126+
assertSize(2, set)
127+
assertTrue(set.containsAll(subset))
128+
}
129+
}

0 commit comments

Comments
 (0)