-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ae0311f
commit 565c8e3
Showing
2 changed files
with
211 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* | ||
* Copyright 2017-2019 Marconi Lanna | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package scail.commons.collection | ||
|
||
import scala.collection.AbstractSet | ||
import scala.collection.SetLike | ||
import scala.collection.breakOut | ||
|
||
/** | ||
* This class implements immutable indexed sets using a hash trie. | ||
* | ||
* An indexed set is like a set, however elements are indexed (hashed) by a given key. | ||
* A function `indexedBy: A => B` is used to map elements to their keys. | ||
* The key can be used to test for membership or retrieve the element, like in a map. | ||
* | ||
* Indexed sets guarantee no two elements share the same key. | ||
* | ||
* @tparam A the type of the elements contained in this indexed set | ||
* @tparam B the type of the keys in this indexed set | ||
*/ | ||
final class IndexedSet[A, B] private (map: Map[B, A], indexedBy: A => B) | ||
extends AbstractSet[A] | ||
with SetLike[A, IndexedSet[A, B]] { | ||
def contains(elem: A): Boolean = map.get(indexedBy(elem)).contains(elem) | ||
def iterator: Iterator[A] = map.valuesIterator | ||
// scalastyle:off method.name ensure.single.space.after.token | ||
def +(elem: A): IndexedSet[A, B] = new IndexedSet(map + (indexedBy(elem) -> elem), indexedBy) | ||
def -(elem: A): IndexedSet[A, B] = new IndexedSet(map - indexedBy(elem), indexedBy) | ||
// scalastyle:on | ||
|
||
override def empty: IndexedSet[A, B] = IndexedSet.empty(indexedBy) | ||
override def foreach[U](f: A => U): Unit = map foreach { case (_, v) => f(v) } | ||
override def size: Int = map.size | ||
|
||
/** | ||
* Tests if some key is contained in this set. | ||
* | ||
* @param key the key to test for membership | ||
* @return `true` if `key` is contained in this set, `false` otherwise | ||
*/ | ||
def containsKey(key: B): Boolean = map.contains(key) | ||
|
||
/** | ||
* Optionally returns the element associated with a key. | ||
* | ||
* @param key the key | ||
* @return the option value containing the element associated with `key` in this set, | ||
* or `None` if none exists | ||
*/ | ||
def get(key: B): Option[A] = map.get(key) | ||
} | ||
|
||
/** | ||
* This object provides a set of operations needed to create `IndexedSet` values. | ||
*/ | ||
object IndexedSet { | ||
/** | ||
* Creates a collection with the specified elements. | ||
* | ||
* @param indexedBy a function mapping an element to its key | ||
* @param elems the elements of the created collection | ||
* @tparam A the type of the elements contained in this indexed set | ||
* @tparam B the type of the keys in this indexed set | ||
* @return the new collection with elements elems | ||
*/ | ||
def apply[A, B](indexedBy: A => B)(elems: A*): IndexedSet[A, B] = { | ||
val map: Map[B, A] = elems.map { e => (indexedBy(e), e) } (breakOut) | ||
new IndexedSet(map, indexedBy) | ||
} | ||
|
||
/** | ||
* An empty collection of type Set[A]. | ||
* | ||
* @param indexedBy a function mapping an element to its key | ||
* @tparam A the type of the elements contained in this indexed set | ||
* @tparam B the type of the keys in this indexed set | ||
* @return the empty `IndexedSet` | ||
*/ | ||
def empty[A, B](indexedBy: A => B): IndexedSet[A, B] = new IndexedSet(Map.empty[B, A], indexedBy) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
/* | ||
* Copyright 2017-2019 Marconi Lanna | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package scail.commons.collection | ||
|
||
import scail.commons.Constants.Warts | ||
import scail.commons.Spec | ||
|
||
@SuppressWarnings(Array(Warts.GenTraversableLikeOps, Warts.TraversableOps)) | ||
class IndexedSetSpec extends Spec { | ||
"IndexedSet should:" - { | ||
"Create empty collection" in new Context { | ||
val result = IndexedSet.empty(f) | ||
|
||
assert(result.isEmpty) | ||
assert(result.iterator.isEmpty) | ||
assert(result.size == 0) | ||
} | ||
|
||
"Create collection from elements" in new Context { | ||
val result = IndexedSet(f)(elems: _*) | ||
|
||
assert(result.nonEmpty) | ||
assert(result.iterator.nonEmpty) | ||
assert(result.size == elems.size) | ||
|
||
elems foreach { e => | ||
f(e) was called | ||
} | ||
} | ||
|
||
"Tell whether collection contains element" in new Context { | ||
val result = IndexedSet(f)(elems: _*) | ||
|
||
elems foreach { e => | ||
assert(result.contains(e)) | ||
} | ||
|
||
assert(!result.contains(other)) | ||
} | ||
|
||
"Return collection iterator" in new Context { | ||
val result: Iterator[String] = IndexedSet(f)(elems: _*).iterator | ||
|
||
assert(result.nonEmpty) | ||
assert(result.toSet == elems.toSet) | ||
} | ||
|
||
"Add element to collection" in new Context { | ||
val result = IndexedSet(f)(elems: _*) + other | ||
|
||
assert(result.size == elems.size + 1) | ||
assert(result.toSet == elems.toSet + other) | ||
} | ||
|
||
"Remove element from collection" in new Context { | ||
val result = IndexedSet(f)(elems: _*) - elems.head | ||
|
||
assert(result.size == elems.size - 1) | ||
assert(result.toSet == elems.tail.toSet) | ||
} | ||
|
||
"Iterate through collection" in new Context { | ||
val result = IndexedSet(f)(elems: _*) | ||
|
||
val g = mock[String => Unit] | ||
|
||
result foreach g | ||
|
||
elems foreach { e => | ||
g(e) was called | ||
} | ||
} | ||
|
||
"Tell whether collection contains key" in new Context { | ||
val result = IndexedSet(f)(elems: _*) | ||
|
||
elems foreach { e => | ||
assert(result.containsKey(f(e))) | ||
} | ||
|
||
assert(!result.containsKey(f(other))) | ||
} | ||
|
||
"Return an element by its key" in new Context { | ||
val result = IndexedSet(f)(elems: _*) | ||
|
||
assert(result.get(f(elems.head)).contains(elems.head)) | ||
assert(result.get(f(other)).isEmpty) | ||
} | ||
} | ||
|
||
class Context { | ||
// shared objects | ||
val elems = Seq("a", "bc", "def") | ||
val other = "ghij" | ||
|
||
// shared mocks | ||
val f = mock[String => Int] | ||
|
||
// common expectations | ||
f(any[String]) answers { s: String => | ||
s.size | ||
} | ||
} | ||
} |