diff --git a/commons/src/collection/IndexedSet.scala b/commons/src/collection/IndexedSet.scala new file mode 100644 index 0000000..f4d3d93 --- /dev/null +++ b/commons/src/collection/IndexedSet.scala @@ -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) +} diff --git a/commons/test/collection/IndexedSetSpec.scala b/commons/test/collection/IndexedSetSpec.scala new file mode 100644 index 0000000..fbe15d7 --- /dev/null +++ b/commons/test/collection/IndexedSetSpec.scala @@ -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 + } + } +}