diff --git a/util/collection/Relation.scala b/util/collection/Relation.scala new file mode 100644 index 000000000..c60925aba --- /dev/null +++ b/util/collection/Relation.scala @@ -0,0 +1,99 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt + +object Relation +{ + /** Constructs a new immutable, finite relation that is initially empty. */ + def empty[T]: Relation[T] = new MRelation[T](Map.empty, Map.empty) +} +/** Binary relation on T. It is a set of pairs (_1, _2) for _1, _2 in T. */ +trait Relation[T] +{ + /** Returns the set of all _2s such that (_1, _2) is in this relation. */ + def forward(_1: T): Set[T] + /** Returns the set of all _1s such that (_1, _2) is in this relation. */ + def reverse(_2: T): Set[T] + /** Includes the relation given by `pair`. */ + def +(pair: (T, T)): Relation[T] + /** Includes the relation (a, b). */ + def +(a: T, b: T): Relation[T] + /** Includes the relations (a, b) for all b in bs. */ + def +(a: T, bs: Iterable[T]): Relation[T] + /** Returns the union of the relation r with this relation. */ + def ++(r: Relation[T]): Relation[T] + /** Includes the given relations. */ + def ++(rs: Iterable[(T,T)]): Relation[T] + /** Removes all relations (_1, _2) for all _1 in _1s. */ + def --(_1s: Iterable[T]): Relation[T] + /** Removes all `pairs` from this relation. */ + def --(pairs: Traversable[(T,T)]): Relation[T] + /** Removes all pairs (_1, _2) from this relation. */ + def -(_1: T): Relation[T] + /** Removes `pair` from this relation. */ + def -(pair: (T,T)): Relation[T] + /** Returns the set of all _1s such that (_1, _2) is in this relation. */ + def _1s: collection.Set[T] + /** Returns the set of all _2s such that (_1, _2) is in this relation. */ + def _2s: collection.Set[T] + + /** Returns all pairs in this relation.*/ + def all: Traversable[(T,T)] + + def forwardMap: Map[T, Set[T]] + def reverseMap: Map[T, Set[T]] +} +private final class MRelation[T](fwd: Map[T, Set[T]], rev: Map[T, Set[T]]) extends Relation[T] +{ + type M = Map[T, Set[T]] + + def forwardMap = fwd + def reverseMap = rev + + def forward(t: T) = get(fwd, t) + def reverse(t: T) = get(rev, t) + + def _1s = fwd.keySet + def _2s = rev.keySet + + def all: Traversable[(T,T)] = fwd.iterator.flatMap { case (a, bs) => bs.iterator.map( b => (a,b) ) }.toTraversable + + def +(pair: (T, T)): Relation[T] = this + (pair._1, Set(pair._2)) + def +(from: T, to: T): Relation[T] = this + (from, Set(to)) + def +(from: T, to: Iterable[T]): Relation[T] = + new MRelation( add(fwd, from, to), (rev /: to) { (map, t) => add(map, t, Seq(from)) }) + + def ++(rs: Iterable[(T,T)]): Relation[T] = ((this: Relation[T]) /: rs) { _ + _ } + def ++(other: Relation[T]): Relation[T] = new MRelation[T]( combine(fwd, other.forwardMap), combine(rev, other.reverseMap) ) + + def --(ts: Iterable[T]): Relation[T] = ((this: Relation[T]) /: ts) { _ - _ } + def --(pairs: Traversable[(T,T)]): Relation[T] = ((this: Relation[T]) /: pairs) { _ - _ } + def -(pair: (T,T)): Relation[T] = + new MRelation( remove(fwd, pair._1, pair._2), remove(rev, pair._2, pair._1) ) + def -(t: T): Relation[T] = + fwd.get(t) match { + case Some(rs) => + val upRev = (rev /: rs) { (map, r) => remove(map, r, t) } + new MRelation(fwd - t, upRev) + case None => this + } + + private def remove(map: M, from: T, to: T): M = + map.get(from) match { + case Some(tos) => + val newSet = tos - to + if(newSet.isEmpty) map - from else map.updated(from, newSet) + case None => map + } + + private def combine(a: M, b: M): M = + (a /: b) { (map, mapping) => add(map, mapping._1, mapping._2) } + + private[this] def add(map: M, from: T, to: Iterable[T]): M = + map.updated(from, get(map, from) ++ to) + + private[this] def get(map: M, t: T): Set[T] = map.getOrElse(t, Set.empty[T]) + + override def toString = all.mkString("Relation [", ", ", "]") +} \ No newline at end of file diff --git a/util/collection/src/test/scala/RelationTest.scala b/util/collection/src/test/scala/RelationTest.scala new file mode 100644 index 000000000..d8a76bc17 --- /dev/null +++ b/util/collection/src/test/scala/RelationTest.scala @@ -0,0 +1,67 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt + +import org.scalacheck._ +import Prop._ + +object RelationTest extends Properties("Relation") +{ + property("Added entry check") = forAll { (pairs: List[(Int, Int)]) => + val r = Relation.empty[Int] ++ pairs + check(r, pairs) + } + def check(r: Relation[Int], pairs: Seq[(Int, Int)]) = + { + val _1s = pairs.map(_._1).toSet + val _2s = pairs.map(_._2).toSet + + r._1s == _1s && r.forwardMap.keySet == _1s && + r._2s == _2s && r.reverseMap.keySet == _2s && + pairs.forall { case (a, b) => + (r.forward(a) contains b) && + (r.reverse(b) contains a) && + (r.forwardMap(a) contains b) && + (r.reverseMap(b) contains a) + } + } + + property("Does not contain removed entries") = forAll { (pairs: List[(Int, Int, Boolean)]) => + val add = pairs.map { case (a,b,c) => (a,b) } + val added = Relation.empty[Int] ++ add + + val removeFine = pairs.collect { case (a,b,true) => (a,b) } + val removeCoarse = removeFine.map(_._1) + val r = added -- removeCoarse + + def notIn[T](map: Map[T, Set[T]], a: T, b: T) = map.get(a).forall(set => ! (set contains b) ) + + all(removeCoarse) { rem => + ("_1s does not contain removed" |: (!r._1s.contains(rem)) ) && + ("Forward does not contain removed" |: r.forward(rem).isEmpty ) && + ("Forward map does not contain removed" |: !r.forwardMap.contains(rem) ) && + ("Removed is not a value in reverse map" |: !r.reverseMap.values.toSet.contains(rem) ) + } && + all(removeFine) { case (a, b) => + ("Forward does not contain removed" |: ( !r.forward(a).contains(b) ) ) && + ("Reverse does not contain removed" |: ( !r.reverse(b).contains(a) ) ) && + ("Forward map does not contain removed" |: ( notIn(r.forwardMap, a, b) ) ) && + ("Reverse map does not contain removed" |: ( notIn(r.reverseMap, b, a) ) ) + } + } + def all[T](s: Seq[T])(p: T => Prop): Prop = + if(s.isEmpty) true else s.map(p).reduceLeft(_ && _) +} + +object EmptyRelationTest extends Properties("Empty relation") +{ + lazy val e = Relation.empty[Int] + + property("Forward empty") = forAll { (i: Int) => e.forward(i).isEmpty } + property("Reverse empty") = forAll { (i: Int) => e.reverse(i).isEmpty } + property("Forward map empty") = forAll { (i: Int) => e.forwardMap.isEmpty } + property("Reverse map empty") = forAll { (i: Int) => e.reverseMap.isEmpty } + property("_1 empty") = forAll { (i: Int) => e._1s.isEmpty } + property("_2 empty") = forAll { (i: Int) => e._2s.isEmpty } +} \ No newline at end of file