Skip to content

Commit

Permalink
collection.IndexedSet
Browse files Browse the repository at this point in the history
  • Loading branch information
marconilanna committed Aug 25, 2019
1 parent ae0311f commit 565c8e3
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 0 deletions.
93 changes: 93 additions & 0 deletions commons/src/collection/IndexedSet.scala
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)
}
118 changes: 118 additions & 0 deletions commons/test/collection/IndexedSetSpec.scala
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
}
}
}

0 comments on commit 565c8e3

Please sign in to comment.