Skip to content
Browse files

Zipper Optimizations

  • Loading branch information...
1 parent 37d2328 commit 495b3407a1af227b968b3222a47670089630d781 @josharnold52 josharnold52 committed Oct 18, 2011
View
66 src/main/scala/com/codecommit/antixml/Group.scala
@@ -32,6 +32,7 @@ package antixml
import util._
import scala.annotation.unchecked.uncheckedVariance
+import scala.annotation.tailrec
import scala.collection.{IndexedSeqLike, TraversableLike, GenTraversableOnce}
import scala.collection.generic.{CanBuildFrom, HasNewBuilder}
@@ -132,6 +133,10 @@ class Group[+A <: Node] private[antixml] (private[antixml] val nodes: VectorCase
override def head = nodes.head
+ override def foreach[U](f: A => U) {
+ nodes.foreach(f)
+ }
+
override def init = new Group(nodes.init)
override def iterator = nodes.iterator
@@ -154,6 +159,67 @@ class Group[+A <: Node] private[antixml] (private[antixml] val nodes: VectorCase
override def take(n: Int) = new Group(nodes take n)
override def takeRight(n: Int) = new Group(nodes takeRight n)
+
+ /** Optionally replaces each node with 0 to many nodes.
+ *
+ * This is used by `Zipper.unselect` to update groups. It is an optimized version of:
+ *
+ * {{{
+ * group.zipWithIndex.flatMap {case (n,i) => f(n,i).getOrElse(Seq(group(i))) }
+ * }}}
+ *
+ */
+ private [antixml] def conditionalFlatMapWithIndex[B >: A <: Node] (f: (A, Int) => Option[Seq[B]]): Group[B] = {
+ /*
+ * The key observation is that most of the time we are only updating a few of the group's nodes.
+ * So instead of rebuilding a Group from scratch, we try to just call `updated` on the nodes
+ * that are changing. However, we must fall back to a complete rebuild if we discover
+ * a node being replaced by 0 or many nodes.
+ */
+
+ //Optimistic function that uses `update`
+ @tailrec
+ def update(g: VectorCase[B], index: Int): VectorCase[B] = {
+ if (index>=g.length)
+ g
+ else {
+ val node = nodes(index)
+ val fval = f(node, index)
+ if (!fval.isDefined)
+ update(g,index + 1)
+ else {
+ val replacements = fval.get
+ if (replacements.lengthCompare(1)==0) {
+ val newNode = replacements.head
+ if (newNode eq node)
+ update(g, index + 1)
+ else
+ update(g.updated(index, replacements.head), index + 1)
+ } else {
+ build(g, replacements, index)
+ }
+ }
+ }
+ }
+
+ //Fallback function that uses a builder
+ def build(g: VectorCase[B], currentReplacements: Seq[B], index: Int): VectorCase[B] = {
+ val b = VectorCase.newBuilder[B] ++= g.view.take(index)
+ b ++= currentReplacements
+ for(i <- (index + 1) until g.length) {
+ val n = nodes(i)
+ val fv = f(n,i)
+ if (fv.isDefined)
+ b ++= fv.get
+ else
+ b += n
+ }
+ b.result
+ }
+
+
+ new Group(update(nodes, 0))
+ }
/**
* Merges adjacent [[com.codecommit.antixml.Text]] as well as adjacent
View
433 src/main/scala/com/codecommit/antixml/Zipper.scala
@@ -29,9 +29,10 @@
package com.codecommit.antixml
import com.codecommit.antixml.util.VectorCase
-import scala.collection.{IndexedSeqLike, GenTraversableOnce}
+import scala.annotation.tailrec
+import scala.collection.{immutable, mutable, IndexedSeqLike, GenTraversableOnce}
import scala.collection.generic.{CanBuildFrom, FilterMonadic}
-import scala.collection.immutable.{SortedMap, IndexedSeq, Seq}
+import scala.collection.immutable.{SortedMap, IndexedSeq}
import scala.collection.mutable.Builder
import Zipper._
@@ -138,40 +139,59 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se
* - A method such as `++`, is used to "add" nodes to a zipper without replacing existing nodes.
*
**/
- def parent: Option[Zipper[Node]]
+ val parent: Option[Zipper[Node]]
private def parentOrError = parent getOrElse sys.error("Root has no parent")
/** Context information corresponding to each node in the zipper. */
- protected def metas: IndexedSeq[(Path, Time)]
-
- /**
- * Information corresponding to each path in the zipper. The map's values consist of the indices of the corresponding nodes,
- * along with a master update time for the path. An empty sequence indicates the path has been elided and should be
- * removed upon `unselect`. In this case, the update time indicates the time of elision.
- *
- * The map is sorted lexicographically by the path primarily to facilitate the `isBeneathPath` method.
- */
- protected def pathIndex: SortedMap[Path,(IndexedSeq[Int], Time)]
+ protected def metas: VectorCase[(ZipperPath, Time)]
+ /** Additional (path,time) pairs associated with the zipper. This is mainly used to
+ * keep track of holes that have had all of their correpsonding nodes deleted, but
+ * it's fine for it to contain paths that are also associated with nodes. During
+ * unselect, each hole will be associated with the latest associated update time
+ * found in this list or in `metas`.
+ */
+ protected def additionalHoles: immutable.Seq[(ZipperPath,Time)]
+
override protected[this] def newBuilder = Zipper.newBuilder[A]
- override def updated[B >: A <: Node](index: Int, node: B): Zipper[B] = {
- val updatedTime = time + 1
- val (updatedPath,_) = metas(index)
- val (updatePathIndices,_) = pathIndex(updatedPath)
-
- new Group(super.updated(index, node).toVectorCase) with Zipper[B] {
- def parent = self.parent
- val time = updatedTime
- val metas = self.metas.updated(index, (updatedPath, updatedTime))
- val pathIndex = self.pathIndex.updated(updatedPath,(updatePathIndices, updatedTime))
+ override def updated[B >: A <: Node](index: Int, node: B): Zipper[B] = parent match {
+ case Some(_) => {
+ val updatedTime = time + 1
+ val (updatedPath,_) = metas(index)
+
+ new Group(nodes.updated(index, node)) with Zipper[B] {
+ val parent = self.parent
+ val time = updatedTime
+ val metas = self.metas.updated(index, (updatedPath, updatedTime))
+ val additionalHoles = self.additionalHoles
+ }
}
+ case None => brokenZipper(nodes.updated(index,node))
}
- override def slice(from: Int, until: Int): Zipper[A] = flatMapWithIndex {
- case (e, i) if i >= from && i < until => VectorCase(e)
- case (e, _) => VectorCase()
+ override def slice(from: Int, until: Int): Zipper[A] = parent match {
+ case Some(_) => {
+ val lo = math.min(math.max(from, 0), nodes.length)
+ val hi = math.min(math.max(until, lo), nodes.length)
+ val cnt = hi - lo
+
+ val ahs = new AdditionalHolesBuilder()
+ ahs ++= additionalHoles
+ for(i <- 0 until lo)
+ ahs += ((metas(i)._1, time + 1 + i))
+ for(i <- hi until nodes.length)
+ ahs += ((metas(i)._1, time + 1 + i - cnt))
+
+ new Group(nodes.slice(from,until)) with Zipper[A] {
+ val parent = self.parent
+ val time = self.time + self.length - cnt
+ val metas = self.metas.slice(from,until)
+ val additionalHoles = ahs.result()
+ }
+ }
+ case None => brokenZipper(nodes.slice(from,until))
}
override def drop(n: Int) = slice(n, size)
@@ -192,15 +212,23 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se
flatMap(liftedF)(cbf)
}
- override def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit cbf: CanBuildFrom[Zipper[A], B, That]): That = {
- cbf match {
- // subtypes of this are the only expected types, hence ignoring type erasure
- case cbf: CanProduceZipper[Zipper[A], B, That] => {
- val liftedF = (x: (A, Int)) => f(x._1)
- flatMapWithIndex(liftedF)(cbf.lift)
+ override def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit cbf: CanBuildFrom[Zipper[A], B, That]): That = cbf match {
+ case cpz: CanProduceZipper[Zipper[A], B, That] if parent.isDefined => {
+ val b = cpz.lift(parent, this)
+ for(i <- 0 until nodes.length) {
+ val (path,time) = metas(i)
+ b += ElemsWithContext(path, time, f(nodes(i)))
}
-
- case _ => super.flatMap(f)(cbf)
+ for ((path,time) <- additionalHoles) {
+ b += ElemsWithContext[B](path,time,util.Vector0)
+ }
+ b.result()
+ }
+ case _ => {
+ val b = cbf(this)
+ for(n <- nodes)
+ b ++= f(n).seq
+ b.result()
}
}
@@ -228,107 +256,160 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se
*/
def stripZipper = new Group(toVectorCase)
- /** A specialized flatMap where the mapping function receives the index of the
- * current element as an argument. */
- private def flatMapWithIndex[B, That](f: ((A, Int)) => GenTraversableOnce[B])(implicit cbfwdz: CanBuildFromWithZipper[Zipper[A], B, That]): That = {
- val result = toVector.zipWithIndex map {x => (f(x),x._2)}
-
- val builder = cbfwdz(parent, this)
- for ( (items, index) <- result) {
- val (path, _) = metas(index)
- builder += ElemsWithContext[B](path, time+index+1, items)
- }
- //Add any paths that had been previously emptied out. (TODO - Can optimize this)
- for ( (path,(inds,time)) <- pathIndex) {
- if (inds.isEmpty)
- builder += ElemsWithContext[B](path,time,VectorCase.empty)
- }
- builder.result
- }
-
- /** Returns true iff the specified path is one of the contexts maintained by the zipper. */
- private[antixml] def containsPath(p: Path) = pathIndex.contains(p)
-
- /** Returns true iff the specified path is the ancestor of one of the contexts maintained by the zipper. */
- private[antixml] def isBeneathPath(p: Path) = {
- //This would be easier if OrderedMap had a variant of the `from` method that was exclusive.
- val ifp = pathIndex.keySet.from(p)
- val result = for {
- h <- ifp.headOption
- h2 <- if (h != p) Some(h) else ifp.take(2).tail.headOption
- } yield h2.startsWith(p) && h2.length != p.length
- result.getOrElse(false)
- }
-
- /**
- * Returns the direct updates for the specified path. The result is unspecified if the path is not contained
- * in the zipper.
- * @return the time of last update to the path followed by a sequence of the direct update nodes and their update times.
- **/
- private def directUpdatesFor(p: Path): (IndexedSeq[(A,Time)], Time) = {
- val (indices, time) = pathIndex(p)
- (indices map {x => (self(x), metas(x)._2)}, time)
+ /**
+ * Optionally replaces each node with 0 to many nodes. Used by `unselect`. See same-named function in `Group` for more details.
+ */
+ private [antixml] override def conditionalFlatMapWithIndex[B >: A <: Node] (f: (A, Int) => Option[scala.collection.Seq[B]]): Zipper[B] = {
+ /* See the Group implementation for information about how this function is optimized. */
+ parent match {
+ case None => brokenZipper(new Group(nodes).conditionalFlatMapWithIndex(f).nodes)
+ case Some(_) => {
+ //Optimistic function that uses `update`
+ @tailrec
+ def update(z: Zipper[B], index: Int): Zipper[B] = {
+ if (index>=z.length)
+ z
+ else {
+ val node = nodes(index)
+ val fval = f(node, index)
+ if (!fval.isDefined)
+ update(z,index + 1)
+ else {
+ val replacements = fval.get
+ if (replacements.lengthCompare(1)==0) {
+ val newNode = replacements.head
+ if (newNode eq node)
+ update(z, index + 1)
+ else
+ update(z.updated(index, replacements.head), index + 1)
+ } else {
+ build(z, replacements, index)
+ }
+ }
+ }
+ }
+
+ //Fallback function that uses a builder
+ def build(z: Zipper[B], currentReplacements: Seq[B], index: Int): Zipper[B] = {
+ val b = newZipperContextBuilder[B](parent)
+
+ for(i <- 0 until index) {
+ val (p,t) = z.metas(i)
+ b += ElemsWithContext(p,t,util.Vector1(z(i)))
+ }
+ b += ElemsWithContext(metas(index)._1, z.time+1,currentReplacements)
+ for(i <- (index + 1) until nodes.length) {
+ val n = nodes(i)
+ val fv = f(n,i)
+ b += ElemsWithContext(metas(i)._1, z.time + 1 + i - index, fv.getOrElse(util.Vector1(n)))
+ }
+ for((p,t) <- additionalHoles) {
+ b += ElemsWithContext(p,t,util.Vector0)
+ }
+
+ b.result
+ }
+
+ update(this, 0)
+ }
+ }
}
/** Applies the node updates to the parent and returns the result. */
- def unselect(implicit mergeStrategy: ZipperMergeStrategy): Zipper[Node] = {
- //TODO - Should we pull back update times as well as nodes?
- parentOrError flatMapWithIndex {
- case (node,index) => pullBack(node, VectorCase(index), mergeStrategy)._1
+ def unselect(implicit zms: ZipperMergeStrategy): Zipper[Node] =
+ new Unselector(zms).unselect
+
+ /** Utility class to perform unselect. */
+ private[this] class Unselector(mergeStrategy: ZipperMergeStrategy) {
+
+ /** Each hole is associated with a list of node/time pairs as well as a master update time */
+ type HoleInfo = ZipperHoleMap[(VectorCase[(A,Time)],Time)]
+
+ private val topLevelHoleInfo: HoleInfo = {
+ val init:(VectorCase[(A,Time)],Time) = (util.Vector0,0)
+ val hm0: HoleInfo = ZipperHoleMap.empty
+ val hm1 = (hm0 /: (0 until self.length)) { (hm, i) =>
+ val item = self(i)
+ val (path,time) = metas(i)
+ val (oldItems, oldTime) = hm.getDeep(path).getOrElse(init)
+ val newItems = oldItems :+ (item, time)
+ val newTime = math.max(oldTime, time)
+ hm.updatedDeep(path, (newItems, newTime))
+ }
+ (hm1 /: additionalHoles) { case (hm,(path,time)) =>
+ val (oldItems, oldTime) = hm.getDeep(path).getOrElse(init)
+ val newTime = math.max(oldTime, time)
+ hm.updatedDeep(path, (oldItems, newTime))
+ }
}
- }
- /**
- * Returns the pullback of a path.
- * @param node the node that is at the specified path in the zipper's parent
- * @param path the path
- * @return the pullback nodes along with the path's latest update time.
- */
- private def pullBack(node: Node, path: Path, mergeStrategy: ZipperMergeStrategy): (IndexedSeq[Node], Time) = node match {
- case elem: Elem if isBeneathPath(path) => {
- val childPullBacks @ (childGroup, childTime) = pullBackChildren(elem.children, path, mergeStrategy)
- val indirectUpdate = elem.copy(children = childGroup)
- if (containsPath(path)) {
- mergeConflicts(elem, directUpdatesFor(path), (indirectUpdate, childTime), mergeStrategy)
+ /** Applies the node updates to the parent and returns the result. */
+ def unselect: Zipper[Node] =
+ pullBackGroup(parentOrError, topLevelHoleInfo)._1.asInstanceOf[Zipper[Node]]
+
+ /**
+ * Returns the pullback of the nodes in the specified group.
+ * @param nodes the group containing the nodes to pull back.
+ * @param holeInfo the HoleInfo corresponding to the group.
+ * @return the pullBacks of the groups children, concatenated together, along with the latest update
+ * time.
+ */
+ private[this] def pullBackGroup(nodes: Group[Node], holeInfo: HoleInfo): (Group[Node], Time) = {
+ var mxt: Int = 0
+ val updatedGroup = nodes.conditionalFlatMapWithIndex[Node] { (node,index) => node match {
+ case elem:Elem if (holeInfo.hasChildrenAt(index)) => {
+ val (newNodes, time) = pullUp(elem, index, holeInfo)
+ mxt = math.max(mxt,time)
+ Some(newNodes)
+ }
+ case _ if holeInfo.contains(index) => {
+ val (newNodes, time) = holeInfo(index)
+ mxt = math.max(mxt, time)
+ Some(newNodes.map {_._1})
+ }
+ case _ => None
+ }}
+ (updatedGroup,mxt)
+ }
+
+ /**
+ * Returns the pullback of an element that is known to be above a hole (and thus has
+ * child updates that need to be pulled up).
+ *
+ * @param elem the element
+ * @param indexInParent the index of the element in its parent
+ * @param holeInfo the HoleInfo corresponding to the parent group
+ * @return the pulled back nodes and their combined update time
+ *
+ * @note assumes `holeInfo.hasChildrenAt(indexInParent) == true`
+ */
+ private[this] def pullUp(elem: Elem, indexInParent: Int, holeInfo: HoleInfo): (VectorCase[Node], Time) = {
+ //Recursively pull back children
+ val (childGrp, childTime) = pullBackGroup(elem.children, holeInfo.children(indexInParent))
+ val indirectUpdate = elem.copy(children = childGrp)
+ if (holeInfo.contains(indexInParent)) {
+ //This is a conflicted hole, so merge.
+ mergeConflicts(elem, holeInfo(indexInParent), (indirectUpdate, childTime))
} else {
+ //No conflicts, just let the child updates bubble up
(VectorCase(indirectUpdate), childTime)
}
}
- case _ if containsPath(path) => {
- val (items, time) = directUpdatesFor(path)
- (items.map(_._1), time)
+
+ /**
+ * Merges updates at a conflicted node in the tree. See the unselection algorithm, above, for more information.
+ * @param node the conflicted node
+ * @param directUpdates the direct updates to `node`.
+ * @param indirectUpdate the indirectUpdate to `node`.
+ * @return the sequence of nodes to replace `node`, along with an overall update time for `node`.
+ */
+ private def mergeConflicts(node: Elem, directUpdates: (IndexedSeq[(Node,Time)], Time) , indirectUpdate: (Node, Time)): (VectorCase[Node], Time) = {
+ val mergeContext = ZipperMergeContext(original=node, lastDirectUpdate = directUpdates._2, directUpdate = directUpdates._1,
+ indirectUpdate = indirectUpdate)
+
+ val result = mergeStrategy(mergeContext)
+ (VectorCase.fromSeq(result), math.max(directUpdates._2, indirectUpdate._2))
}
- case _ => (VectorCase(node), 0)
- }
-
- /**
- * Returns the pullback of the children of a path in the zipper's parent tree.
- * @param node the node that is at the specified path in the zipper's parent
- * @param path the path
- * @return the pullBacks of the path's children, concatenated together, along with the latest update
- * time of the child paths.
- */
- private def pullBackChildren(nodes: IndexedSeq[Node], path: Path, mergeStrategy: ZipperMergeStrategy): (Group[Node], Time) = {
- val childPullbacks = nodes.zipWithIndex.map {
- case (node, index) => pullBack(node, path :+ index, mergeStrategy)
- }
- (childPullbacks.flatMap[Node,Group[Node]](_._1), childPullbacks.maxBy(_._2)._2)
- }
-
- /**
- * Merges updates at a conflicted node in the tree. See the unselection algorithm, above, for more information.
- * @param node the conflicted node
- * @param directUpdates the direct updates to `node`.
- * @param indirectUpdate the indirectUpdate to `node`.
- * @param mergeStrategy the merge strategy
- * @return the sequence of nodes to replace `node`, along with an overall update time for `node`.
- */
- private def mergeConflicts(node: Elem, directUpdates: (IndexedSeq[(Node,Time)], Time) , indirectUpdate: (Node, Time), mergeStrategy: ZipperMergeStrategy): (IndexedSeq[Node], Time) = {
- val mergeContext = ZipperMergeContext(original=node, lastDirectUpdate = directUpdates._2, directUpdate = directUpdates._1,
- indirectUpdate = indirectUpdate)
-
- val result = mergeStrategy(mergeContext)
- (VectorCase.fromSeq(result), math.max(directUpdates._2, indirectUpdate._2))
}
}
@@ -339,17 +420,9 @@ object Zipper {
/** The units in which time is measured in the zipper. Assumed non negative. */
private type Time = Int
- /** A top-down path used to represent a location in the Group tree.*/
- private type Path = VectorCase[Int]
-
- private implicit object PathOrdering extends Ordering[Path] {
- override def compare(x: Path, y: Path) =
- Ordering.Iterable[Int].compare(x,y)
- }
-
implicit def canBuildFromWithZipper[A <: Node] = {
new CanBuildFromWithZipper[Traversable[_], A, Zipper[A]] {
- override def apply(parent: Option[Zipper[Node]]): Builder[ElemsWithContext[A],Zipper[A]] = new WithZipperBuilder[A](parent)
+ override def apply(parent: Option[Zipper[Node]]) = newZipperContextBuilder(parent)
}
}
@@ -362,63 +435,101 @@ object Zipper {
}
}
- def newBuilder[A <: Node] = VectorCase.newBuilder[A].mapResult({new Group(_).toZipper})
+ /** Returns a builder that produces a zipper without a parent */
+ def newBuilder[A <: Node] = VectorCase.newBuilder[A].mapResult(brokenZipper(_))
+
+ /** Returns a builder that produces a zipper with a full zipper context */
+ private def newZipperContextBuilder[A <: Node](parent: Option[Zipper[Node]]) = parent match {
+ case Some(_) => new WithZipperBuilder[A](parent)
+ case None => brokenZipperBuilder[A]
+ }
/** Returns a "broken" zipper which contains the specified nodes but cannot be unselected */
private[antixml] def brokenZipper[A <: Node](nodes: VectorCase[A]): Zipper[A] = {
- val fakePath = VectorCase(0)
new Group[A](nodes) with Zipper[A] {
- override def parent = None
+ override val parent = None
override val time = 0
- override val metas = constant((fakePath,0), nodes.length)
- override val pathIndex = SortedMap( fakePath -> (0 until nodes.length, 0))
+ override val metas = null //Should never be accesseed
+ override val additionalHoles = null //Should never be accessed
}
}
- private def constant[A](a: A, sz: Int) = new IndexedSeq[A] {
- override def apply(i: Int) = a
- override def length = sz
- }
-
+ /** Ignores context and builds a "broken" zipper */
+ private def brokenZipperBuilder[A <: Node]: Builder[ElemsWithContext[A],Zipper[A]] =
+ CanBuildFromWithZipper.identityCanBuildFrom(VectorCase.canBuildFrom[A])(None) mapResult(brokenZipper(_))
+
/**
* The primary builder class used to construct Zippers.
*/
private class WithZipperBuilder[A <: Node](parent: Option[Zipper[Node]]) extends Builder[ElemsWithContext[A],Zipper[A]] { self =>
- private val innerBuilder = VectorCase.newBuilder[(Path, Time, A)]
- private var pathIndex = SortedMap.empty[Path,(IndexedSeq[Int], Time)]
+
+ import scala.collection.mutable.HashMap
+
+ private val itemsBuilder = VectorCase.newBuilder[A]
+ private val metasBuilder = VectorCase.newBuilder[(ZipperPath,Time)]
+ private val additionalHolesBuilder = new AdditionalHolesBuilder()
private var size = 0
private var maxTime = 0
override def += (ewc: ElemsWithContext[A]) = {
val ElemsWithContext(pseq, time, ns) = ewc
- val path = VectorCase.fromSeq(pseq)
-
- val items = ns.seq.toSeq.map(x => (path, time, x))(VectorCase.canBuildFrom)
- innerBuilder ++= items
+ val path: ZipperPath = ZipperPath.fromSeq(pseq)
+ val pathTime = (path, time)
- val (oldIndices, oldTime) = pathIndex.getOrElse(path, (VectorCase.empty,0))
- val (newIndices, newTime) = (math.max(oldTime, time), oldIndices ++ (size until (size + items.length)))
- pathIndex = pathIndex.updated(path, (newTime,newIndices))
+ var nsz = 0
+ for(n <- ns) {
+ itemsBuilder += n
+ metasBuilder += pathTime
+ nsz += 1
+ }
+ if (nsz==0) {
+ additionalHolesBuilder += pathTime
+ }
- size += items.length
+ size += nsz
maxTime = math.max(maxTime, time)
this
}
override def clear() {
- innerBuilder.clear()
- pathIndex = SortedMap.empty
+ itemsBuilder.clear()
+ metasBuilder.clear()
+ additionalHolesBuilder.clear()
size = 0
maxTime = 0
}
override def result(): Zipper[A] = {
- val res = innerBuilder.result()
- new Group[A](res map {case (_,_,node) => node}) with Zipper[A] {
- override def parent = self.parent
+ val itemsResult = itemsBuilder.result()
+ val metasResult = metasBuilder.result()
+ val additionalHolesResult = additionalHolesBuilder.result()
+ maxTime = math.max(maxTime, size)
+
+ new Group[A](itemsResult) with Zipper[A] {
+ override val parent = self.parent
override val time = maxTime
- override val metas = res map {case (path,time,_) => (path,time)}
- override val pathIndex = self.pathIndex
+ override val metas = metasResult
+ override val additionalHoles = additionalHolesResult
}
}
}
+ /** Builder for the `additionalHoles` list. This builder ensures that the result has at most one
+ * entry for any given ZipperPath. Although this isn't necessary for correctness, it ensures that
+ * the `additionalHoles` list remains bounded in size by the total number of holes.
+ */
+ private class AdditionalHolesBuilder extends Builder[(ZipperPath,Time), immutable.Seq[(ZipperPath,Time)]] {0
+ private val hm = mutable.HashMap[ZipperPath,Time]()
+
+ def += (elem: (ZipperPath,Time)) = {
+ val (p,t) = elem
+ val t2 = hm.getOrElse(p,0)
+ hm.put(p,math.max(t,t2))
+ this
+ }
+ def clear() {
+ hm.clear
+ }
+ def result =
+ if (hm.size==0) util.Vector0
+ else (VectorCase.newBuilder[(ZipperPath,Time)] ++= hm).result
+ }
}
View
147 src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2011, Daniel Spiewak
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * - Neither the name of "Anti-XML" nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.codecommit.antixml
+
+import util.{VectorCase}
+import scala.annotation.tailrec
+import scala.collection.immutable.{Map, IndexedSeq}
+import scala.collection.{Traversable}
+
+/** Used by `Zipper` to associate information with its "holes".
+ *
+ * The `ZipperHoleMap` has a similar tree structure to `Group` and is used to maintain
+ * information about selected locations within the `Group` tree. It differs from `Group`
+ * in the following respects:
+ * - It allows arbitrary element types.
+ * - It maintains its children separately from its elements
+ * - It is sparse; Only a subset of it's indices are valued.
+ *
+ * This structure can also be viewed as a map from `ZipperPath` to `B`, although it does
+ * not actually implement the `Map` trait.
+ *
+ * @tparam the element type of the `ZipperHoleMap`.
+ * @see [[com.codecommit.antixml.Zipper]]
+ */
+private[antixml] final class ZipperHoleMap[+B] private (items: Map[Int, ZipperHoleMap.HoleMapNode[B]]) { self =>
+ import ZipperHoleMap._
+
+ private def find(i: Int): HoleMapNode[B] =
+ items.getOrElse(i,unusedLocation)
+
+ /** Returns the value at the specified position or throws an exception if there is no
+ * such value (if `contains(loc) == false`).
+ */
+ def apply(loc: Int): B = find(loc).value.get
+
+ /** Returns the value at the specified position or `None` if there is no
+ * such value (if `contains(loc) == false`).
+ */
+ def get(loc: Int): Option[B] = find(loc).value
+
+ /** Tests whether the hole map contains a value at the specified position */
+ def contains(loc: Int): Boolean = find(loc).value.isDefined
+
+ /** Returns the children at the specified position or throws an exception if there are
+ * no children there (if `hasChildrenAt(loc) == false`).
+ */
+ def children(loc: Int):ZipperHoleMap[B] = find(loc).children.get
+
+ /** Tests whether the hole map has children at the specified position */
+ def hasChildrenAt(loc: Int): Boolean = find(loc).children.isDefined
+
+ /** Returns the value at the deep location at the specified path, or `None` if there is
+ * no such value.
+ */
+ def getDeep(path: ZipperPath): Option[B] = getDeep(path, 0)
+
+ @tailrec
+ private def getDeep(path: ZipperPath, from: Int): Option[B] = {
+ if (from==(path.length - 1))
+ get(path.valueAt(from))
+ else find(path(from)) match {
+ case HoleMapNode(_, Some(c)) => c.getDeep(path,from+1)
+ case _ => None
+ }
+ }
+
+ /** Creates a new `ZipperHoleMap` by updating the value at the specified deep location. */
+ final def updatedDeep[B2 >: B](path: ZipperPath, value: B2): ZipperHoleMap[B2] = updatedDeep(path,0,value)
+
+ private def updatedDeep[B2 >: B](path: ZipperPath, from: Int, value: B2): ZipperHoleMap[B2] = {
+ val loc = path.valueAt(from)
+ val HoleMapNode(v,c) = find(loc)
+ if (from == (path.length - 1))
+ new ZipperHoleMap(items.updated(loc, HoleMapNode(Some(value), c)))
+ else {
+ val updatedChildren = c.getOrElse(empty).updatedDeep(path,from+1,value)
+ new ZipperHoleMap(items.updated(loc, HoleMapNode(v, Some(updatedChildren))))
+ }
+ }
+
+ /** Returns a traversable represented the tree's contents in pre-order (lexicographically by path).
+ * Note that this has not been optimized, as it is only currenly used by toString and for testing.
+ */
+ def depthFirst: Traversable[(ZipperPath, B)] =
+ new Traversable[(ZipperPath, B)] {
+ override def foreach[U] (f: ((ZipperPath, B)) => U) {
+ self.traverseDepthFirst(Nil, f)
+ }
+ }
+
+ private def traverseDepthFirst[U](parents : List[Int], f: ((ZipperPath, B)) => U) {
+ val sortedItems = items.toSeq.sortBy(_._1)
+ sortedItems foreach {case (loc,node) =>
+ val p2 = loc :: parents
+ if (node.value.isDefined)
+ f((ZipperPath.reversed(p2), node.value.get))
+ if (node.children.isDefined)
+ node.children.get.traverseDepthFirst(p2,f)
+ }
+ }
+
+ override def toString = {
+ val els = depthFirst.view map { case (p,v) =>
+ p.mkString("[",",","]") + " -> "+v
+ }
+ els.mkString("[",", ","]")
+ }
+}
+
+private[antixml] object ZipperHoleMap {
+ def apply[B](items: (ZipperPath,B)*) = {
+ val e: ZipperHoleMap[B] = empty
+ (e /: items) { (mp,i) => mp.updatedDeep(i._1, 0, i._2) }
+ }
+
+ private [antixml] case class HoleMapNode[+B](value: Option[B], children: Option[ZipperHoleMap[B]])
+
+ private val unusedLocation = HoleMapNode(None,None)
+
+ val empty: ZipperHoleMap[Nothing] = new ZipperHoleMap(Map.empty[Int,Nothing])
+}
View
115 src/main/scala/com/codecommit/antixml/ZipperPath.scala
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2011, Daniel Spiewak
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * - Neither the name of "Anti-XML" nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.codecommit.antixml
+
+import scala.collection.{IndexedSeqOptimized,Seq, LinearSeq}
+import scala.collection.generic.CanBuildFrom
+import scala.collection.mutable.{ArrayBuilder, Builder}
+import scala.collection.immutable.{IndexedSeq}
+
+/** The `IndexedSeq[Int]` implementation used by `Zipper` to represent paths.
+ *
+ * This implementation is optimized specifically for Zipper. It is backed by
+ * an `Array[Int]` and is both space efficient and fast for reading. "Modify" operations are `O(n)`,
+ * in general, but with a small constant factor and are acceptably fast for paths coming from
+ * reasonable XML documents. `Zipper` doesn't ever modify paths in any case.
+ *
+ * @see [[com.codecommit.antixml.Zipper]]
+ */
+private[antixml] final class ZipperPath private (private val arr: Array[Int]) extends IndexedSeq[Int] with IndexedSeqOptimized[Int, ZipperPath] {
+ override def apply(i: Int) = arr(i)
+ override def length = arr.length
+ override def newBuilder = ZipperPath.newBuilder
+ def valueAt(i: Int): Int = arr(i)
+
+ //TODO : Needed?
+ def :+(value: Int): ZipperPath = {
+ val ln = arr.length
+ val a2 = new Array[Int](ln + 1)
+ Array.copy(arr,0,a2,0,ln)
+ a2(ln) = value
+ new ZipperPath(a2)
+ }
+}
+
+
+private[antixml] object ZipperPath {
+
+ def apply(is: Int*) = fromSeq(is)
+
+ def fromSeq(s: Seq[Int]): ZipperPath = s match {
+ case iv: ZipperPath => iv
+ case _ => new ZipperPath(toArray(s))
+ }
+
+ def reversed(s: Seq[Int]): ZipperPath = {
+ val a = toArray(s)
+ val len = a.length
+ val mid = len >> 1
+ for(low <- 0 until mid) {
+ val high = len - low - 1
+ val tmp = a(low)
+ a(low) = a(high)
+ a(high) = tmp
+ }
+ new ZipperPath(a)
+ }
+
+ private def toArray(s: Seq[Int]): Array[Int] = s match {
+ case _: collection.IndexedSeq[_] => {
+ val len = s.length
+ val a = new Array[Int](len)
+ s.copyToArray(a,0,len)
+ a
+ }
+ case _ => {
+ if (s.isEmpty)
+ new Array[Int](0)
+ else if (s.lengthCompare(1)==0) {
+ val a = new Array[Int](1)
+ a(0) = s.head
+ a
+ } else
+ ((new ArrayBuilder.ofInt) ++= s).result()
+ }
+ }
+
+ def empty = new ZipperPath(new Array[Int](0))
+ def newBuilder: Builder[Int,ZipperPath] =
+ new ArrayBuilder.ofInt mapResult (a => if (a.length==0) empty else new ZipperPath(a))
+
+ object BuilderFactory extends CanBuildFrom[Any,Int,ZipperPath] {
+ def apply() = newBuilder
+ def apply(from: Any) = newBuilder
+ }
+
+ implicit def canBuildFrom: CanBuildFrom[ZipperPath,Int,ZipperPath] = BuilderFactory
+
+}
+
View
26 src/main/scala/com/codecommit/antixml/util/vectorCases.scala
@@ -132,6 +132,8 @@ private[antixml] case object Vector0 extends VectorCase[Nothing] {
override def iterator = Iterator.empty
+ override def foreach[U](f: Nothing => U) {}
+
def toVector = Vector()
}
@@ -164,6 +166,10 @@ private[antixml] case class Vector1[+A](_1: A) extends VectorCase[A] {
VectorN(_1 +: that.toVector)
}
+ override def foreach[U](f: A => U) {
+ f(_1)
+ }
+
// TODO more methods
def toVector = Vector(_1)
@@ -195,6 +201,11 @@ private[antixml] case class Vector2[+A](_1: A, _2: A) extends VectorCase[A] {
VectorN(Vector(_1, _2) ++ that.toVector)
}
+ override def foreach[U](f: A => U) {
+ f(_1)
+ f(_2)
+ }
+
// TODO more methods
def toVector = Vector(_1, _2)
@@ -227,6 +238,12 @@ private[antixml] case class Vector3[+A](_1: A, _2: A, _3: A) extends VectorCase[
VectorN(Vector(_1, _2, _3) ++ that.toVector)
}
+ override def foreach[U](f: A => U) {
+ f(_1)
+ f(_2)
+ f(_3)
+ }
+
// TODO more methods
def toVector = Vector(_1, _2, _3)
@@ -260,6 +277,13 @@ private[antixml] case class Vector4[+A](_1: A, _2: A, _3: A, _4: A) extends Vect
VectorN(Vector(_1, _2, _3, _4) ++ that.toVector)
}
+ override def foreach[U](f: A => U) {
+ f(_1)
+ f(_2)
+ f(_3)
+ f(_4)
+ }
+
// TODO more methods
def toVector = Vector(_1, _2, _3, _4)
@@ -361,5 +385,7 @@ private[antixml] case class VectorN[+A](vector: Vector[A]) extends VectorCase[A]
// note: this actually defeats a HotSpot optimization in trivial micro-benchmarks
override def iterator = vector.iterator
+ override def foreach[U](f: A => U) {vector.foreach(f)}
+
def toVector = vector
}
View
46 src/test/scala/com/codecommit/antixml/GroupSpecs.scala
@@ -223,6 +223,52 @@ class GroupSpecs extends Specification with ScalaCheck with XMLGenerators with U
func(xml) mustEqual xml
}
+
+ "foreach should traverse group" in check { (xml: Group[Node]) =>
+ val b = Vector.newBuilder[Node]
+ xml foreach {b += _}
+ val v = b.result
+ val results = for(i <- 0 until xml.length) yield v(i) mustEqual xml(i)
+ (v.length mustEqual xml.length) +: results
+ }
+ }
+
+ "Group.conditionalFlatMapWithIndex" should {
+
+ "work with simple replacements" in check { (xml: Group[Node]) =>
+ def f(n: Node, i: Int): Option[Seq[Node]] = n match {
+ case n if (i & 1) == 0 => None
+ case e: Elem => Some(Seq(e.copy(name=e.name.toUpperCase)))
+ case n => None
+ }
+ val cfmwi = xml.conditionalFlatMapWithIndex(f)
+ val equiv = xml.zipWithIndex.flatMap {case (n,i) => f(n,i).getOrElse(Seq(n))}
+
+ Seq(
+ Vector(cfmwi:_*) mustEqual Vector(equiv:_*),
+ cfmwi.length mustEqual xml.length
+ )
+ }
+
+ "work with complex replacements" in check { (xml: Group[Node]) =>
+ def f(n: Node, i: Int): Option[Seq[Node]] = n match {
+ case n if (i & 1) == 0 => None
+ case _ if (i & 2) == 0 => Some(Seq())
+ case e: Elem => Some(Seq(e.copy(name=e.name+"MODIFIED"), e, e))
+ case n => Some(Seq(n, n, n))
+ }
+ val cfmwi = xml.conditionalFlatMapWithIndex(f)
+ val equiv = xml.zipWithIndex.flatMap {case (n,i) => f(n,i).getOrElse(Seq(n))}
+
+ val expectedDels = (xml.length + 2) >>> 2
+ val expectedTripples = (xml.length) >>> 2
+ val expectedLength = xml.length - expectedDels + 2*expectedTripples
+
+ Seq(
+ Vector(cfmwi:_*) mustEqual Vector(equiv:_*),
+ cfmwi.length mustEqual expectedLength
+ )
+ }
}
"canonicalization" should {
View
338 src/test/scala/com/codecommit/antixml/ZipperHoleMapSpecs.scala
@@ -0,0 +1,338 @@
+/*
+ * Copyright (c) 2011, Daniel Spiewak
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * - Neither the name of "Anti-XML" nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.codecommit.antixml
+
+import org.specs2.mutable._
+import org.specs2.ScalaCheck
+import org.specs2.execute.Result
+import org.specs2.matcher.Parameters
+import org.scalacheck.{Arbitrary, Prop, Gen, Choose}
+import Prop._
+import org.specs2.matcher.ScalaCheckMatchers._
+import scala.math.Ordering
+
+class ZipperHoleMapSpecs extends Specification with ScalaCheck {
+
+ implicit object ZipperPathLexOrder extends Ordering[ZipperPath] {
+ private val delg = Ordering.Iterable[Int]
+ def compare(x: ZipperPath, y: ZipperPath) = delg.compare(x,y)
+ }
+
+ def p(i: Int*) = ZipperPath(i:_*)
+
+ def toMap[B](zp: ZipperHoleMap[B]) = Map(zp.depthFirst.toSeq:_*)
+
+ def extensionsOf(zp: ZipperPath) = new {
+ def in[B](m: Map[ZipperPath,B]): Map[ZipperPath,B] = m.collect {
+ case (k,v) if k.startsWith(zp) && k.length > zp.length => (k.drop(zp.length), v)
+ }
+ }
+
+ "ZipperHoleMap.depthFirst" should {
+ "traverse lexicographically" in {
+ forAll(Gen.listOf(saneEntries[Int])) {entries =>
+ val df = ZipperHoleMap(entries:_*).depthFirst
+ val expectedOrder = Map(entries:_*).toSeq.sortBy(_._1)
+ List(df.toSeq:_*) mustEqual List(expectedOrder:_*)
+ }
+ }
+ }
+
+ "ZipperHoleMap.apply" should {
+ val hm = ZipperHoleMap(p(1)->"a", p(2)->"b", p(1,2,3)->"c", p(3,0,0)->"d")
+ "find leaf values" in {
+ hm(2) mustEqual("b")
+ }
+ "find intermediate values" in {
+ hm(1) mustEqual("a")
+ }
+ "throw on non-existent positions" in {
+ hm(-1) must throwA[Throwable]
+ hm(0) must throwA[Throwable]
+ hm(4) must throwA[Throwable]
+ }
+ "throw on non-valued intermediate nodes" in {
+ hm(3) must throwA[Throwable]
+ }
+ "work with arbitrary entries" in {
+ forAll(Gen.listOf(saneEntries[Int])) { entries =>
+ val hm = ZipperHoleMap(entries:_*)
+ val m = Map(entries:_*)
+ val r:Result = for(i <- (minSaneLoc - 5) to (maxSaneLoc + 5)) yield {
+ if (m.contains(p(i))) {
+ hm(i) mustEqual m(p(i))
+ } else {
+ hm(i) must throwA[Throwable]
+ }
+ }
+ r
+ }
+ }
+ }
+
+ "ZipperHoleMap.get" should {
+ val hm = ZipperHoleMap(p(1)->"a", p(2)->"b", p(1,2,3)->"c", p(3,0,0)->"d")
+ "find leaf values" in {
+ hm.get(2) mustEqual(Some("b"))
+ }
+ "find intermediate values" in {
+ hm.get(1) mustEqual(Some("a"))
+ }
+ "return None on non-existent positions" in {
+ hm.get(-1) mustEqual None
+ hm.get(0) mustEqual None
+ hm.get(4) mustEqual None
+ }
+ "return None on non-valued intermediate nodes" in {
+ hm.get(3) mustEqual None
+ }
+ "work with arbitrary entries" in {
+ forAll(Gen.listOf(saneEntries[Int])) { entries =>
+ val hm = ZipperHoleMap(entries:_*)
+ val m = Map(entries:_*)
+ val r:Result = for(i <- (minSaneLoc - 5) to (maxSaneLoc + 5)) yield {
+ hm.get(i) mustEqual m.get(p(i))
+ }
+ r
+ }
+ }
+ }
+
+ "ZipperHoleMap.contains" should {
+ val hm = ZipperHoleMap(p(1)->"a", p(2)->"b", p(1,2,3)->"c", p(3,0,0)->"d")
+ "find leaf values" in {
+ hm.contains(2) must beTrue
+ }
+ "find intermediate values" in {
+ hm.contains(1) must beTrue
+ }
+ "return None on non-existent positions" in {
+ hm.contains(-1) must beFalse
+ hm.contains(0) must beFalse
+ hm.contains(4) must beFalse
+ }
+ "return None on non-valued intermediate nodes" in {
+ hm.contains(3) must beFalse
+ }
+ "work with arbitrary entries" in {
+ forAll(Gen.listOf(saneEntries[Int])) { entries =>
+ val hm = ZipperHoleMap(entries:_*)
+ val m = Map(entries:_*)
+ val r:Result = for(i <- (minSaneLoc - 5) to (maxSaneLoc + 5)) yield {
+ hm.contains(i) mustEqual m.contains(p(i))
+ }
+ r
+ }
+ }
+ }
+
+ "ZipperHoleMap.children" should {
+ val hm = ZipperHoleMap(p(1)->"a", p(2)->"b", p(1,1)->"c", p(1,2,3)->"d", p(1,2,4)->"e", p(3,0,0)->"f")
+ "throw on leaf values" in {
+ hm.children(2) must throwA[Throwable]
+ }
+ "find intermediate valued nodes" in {
+ val c = hm.children(1)
+ toMap(c) mustEqual Map(p(1)->"c",p(2,3)->"d",p(2,4)->"e")
+ }
+ "find intermediate non-valued nodes" in {
+ val c = hm.children(3)
+ toMap(c) mustEqual Map(p(0,0)->"f")
+ }
+ "throw on non-existent positions" in {
+ hm.children(-1) must throwA[Throwable]
+ hm.children(0) must throwA[Throwable]
+ hm.children(4) must throwA[Throwable]
+ }
+ "work with arbitrary entries" in {
+ forAll(Gen.listOf(saneEntries[Int])) { entries =>
+ val hm = ZipperHoleMap(entries:_*)
+ val m = Map(entries:_*)
+ val r:Result = for(i <- (minSaneLoc - 5) to (maxSaneLoc + 5)) yield {
+ val expect = extensionsOf(p(i)).in(m)
+ if (expect.isEmpty)
+ hm.children(i) must throwA[Throwable]
+ else
+ toMap(hm.children(i)) mustEqual expect
+ }
+ r
+ }
+ }
+ }
+
+ "ZipperHoleMap.hasChildrenAt" should {
+ val hm = ZipperHoleMap(p(1)->"a", p(2)->"b", p(1,1)->"c", p(1,2,3)->"d", p(1,2,4)->"e", p(3,0,0)->"f")
+ "return false on leaf values" in {
+ hm.hasChildrenAt(2) must beFalse
+ }
+ "return true on intermediate valued nodes" in {
+ hm.hasChildrenAt(1) must beTrue
+ }
+ "return true on intermediate non-valued nodes" in {
+ hm.hasChildrenAt(3) must beTrue
+ }
+ "return false on non-existent positions" in {
+ hm.hasChildrenAt(-1) must beFalse
+ hm.hasChildrenAt(0) must beFalse
+ hm.hasChildrenAt(4) must beFalse
+ }
+ "work with arbitrary entries" in {
+ forAll(Gen.listOf(saneEntries[Int])) { entries =>
+ val hm = ZipperHoleMap(entries:_*)
+ val m = Map(entries:_*)
+ val r:Result = for(i <- (minSaneLoc - 5) to (maxSaneLoc + 5)) yield {
+ val expect = extensionsOf(p(i)).in(m)
+ hm.hasChildrenAt(i) mustEqual (!expect.isEmpty)
+ }
+ r
+ }
+ }
+ }
+
+ "ZipperHoleMap.getDeep" should {
+ val hm = ZipperHoleMap(p(1)->"a", p(2)->"b", p(1,2)->"c", p(1,2,3)->"d", p(1,2,4)->"e", p(3,0,0)->"f")
+ val hmMap = toMap(hm)
+ "find leaf values" in Seq(
+ hm.getDeep(p(2)) mustEqual Some("b"),
+ hm.getDeep(p(1,2,3)) mustEqual Some("d"),
+ hm.getDeep(p(1,2,4)) mustEqual Some("e"),
+ hm.getDeep(p(3,0,0)) mustEqual Some("f")
+ )
+
+ "find intermediate valued nodes" in Seq(
+ hm.getDeep(p(1)) mustEqual Some("a"),
+ hm.getDeep(p(1,2)) mustEqual Some("c")
+ )
+ "return None on intermediate non-valued nodes" in Seq(
+ hm.getDeep(p(3)) mustEqual None,
+ hm.getDeep(p(3,0)) mustEqual None
+ )
+ "return None on non-existent positions" in Seq(
+ hm.getDeep(p(-1)) mustEqual None,
+ hm.getDeep(p(-1,0)) mustEqual None,
+ hm.getDeep(p(0)) mustEqual None,
+ hm.getDeep(p(0,1,2,3)) mustEqual None,
+ hm.getDeep(p(1,2,3,4)) mustEqual None,
+ hm.getDeep(p(4)) mustEqual None,
+ hm.getDeep(p(4,4,4,4,4)) mustEqual None
+ )
+ "work with arbitrary entries and paths" in {
+ forAll(Gen.listOf(saneEntries[Int]), sanePaths) { (entries,path) =>
+ val hm = ZipperHoleMap(entries:_*)
+ val m = Map(entries:_*)
+ hm.getDeep(path) mustEqual m.get(path)
+ }
+ }
+ "find all of its arbitrary entries" in {
+ forAll(Gen.listOf(saneEntries[Int])) { entries =>
+ val hm = ZipperHoleMap(entries:_*)
+ val m = Map(entries:_*)
+ val r:Result = if (!entries.isEmpty) {
+ for(path <- m.keys.toSeq) yield hm.getDeep(path) mustEqual Some(m(path))
+ } else {
+ //specs chokes on an empty Result sequence, so do something else for the empty case
+ hm.getDeep(p(0)) mustEqual None
+ }
+ r
+ }
+ }
+ }
+
+ "ZipperHoleMap.updatedDeep" should {
+ val hm = ZipperHoleMap(p(1)->"a", p(2)->"b", p(1,2)->"c", p(1,2,3)->"d", p(1,2,4)->"e", p(3,0,0)->"f")
+ val hmMap = toMap(hm)
+ "replace leaf values" in Seq(
+ toMap(hm.updatedDeep(p(2),"XYZ")) mustEqual hmMap.updated(p(2),"XYZ"),
+ toMap(hm.updatedDeep(p(1,2,3),"123")) mustEqual hmMap.updated(p(1,2,3),"123")
+ )
+ "set intermediate valued nodes" in Seq(
+ toMap(hm.updatedDeep(p(1),"IM1")) mustEqual hmMap.updated(p(1),"IM1"),
+ toMap(hm.updatedDeep(p(1,2),"IM12")) mustEqual hmMap.updated(p(1,2),"IM12")
+ )
+ "set intermediate non-valued nodes" in Seq(
+ toMap(hm.updatedDeep(p(3),"NV3")) mustEqual hmMap.updated(p(3),"NV3"),
+ toMap(hm.updatedDeep(p(3,0),"NV30")) mustEqual hmMap.updated(p(3,0),"NV30")
+ )
+ "set non-existent positions" in Seq(
+ toMap(hm.updatedDeep(p(-1),"QQQ")) mustEqual hmMap.updated(p(-1),"QQQ"),
+ toMap(hm.updatedDeep(p(-1,0),"QQQ")) mustEqual hmMap.updated(p(-1,0),"QQQ"),
+ toMap(hm.updatedDeep(p(0),"QQQ")) mustEqual hmMap.updated(p(0),"QQQ"),
+ toMap(hm.updatedDeep(p(0,1,2,3),"QQQ")) mustEqual hmMap.updated(p(0,1,2,3),"QQQ"),
+ toMap(hm.updatedDeep(p(1,2,3,4),"QQQ")) mustEqual hmMap.updated(p(1,2,3,4),"QQQ"),
+ toMap(hm.updatedDeep(p(4),"QQQ")) mustEqual hmMap.updated(p(4),"QQQ"),
+ toMap(hm.updatedDeep(p(4,4,4,4),"QQQ")) mustEqual hmMap.updated(p(4,4,4,4),"QQQ"),
+ toMap(hm.updatedDeep(p(2,99),"QQQ")) mustEqual hmMap.updated(p(2,99),"QQQ")
+ )
+ "work with arbitrary entries, paths, and values" in {
+ forAll(Gen.listOf(saneEntries[Int]), sanePaths, Arbitrary.arbInt.arbitrary) { (entries,path,value) =>
+ val hm = ZipperHoleMap(entries:_*)
+ val m = Map(entries:_*)
+ toMap(hm.updatedDeep(path,value)) mustEqual m.updated(path,value)
+ }
+ }
+ }
+
+ "ZipperHoleMap.toString" should {
+ "be non-empty" in {
+ forAll(Gen.listOf(saneEntries[Int])) { entries =>
+ val hm = ZipperHoleMap(entries:_*)
+ val s = hm.toString
+ s.length must beGreaterThan(0)
+ }
+ }
+ }
+
+ "ZipperHoleMap companion" should {
+ "have an empty empty" in {
+ val hm:ZipperHoleMap[Int] = ZipperHoleMap.empty
+ toMap(hm).size mustEqual 0
+ }
+ "build ZipperHoleMaps using apply" in {
+ forAll(Gen.listOf(saneEntries[Int])) { entries =>
+ val hm = ZipperHoleMap(entries:_*)
+ toMap(hm) mustEqual Map(entries:_*)
+ }
+ }
+ }
+
+ def saneEntries[B](implicit valGen: Arbitrary[B]): Gen[(ZipperPath, B)] = for {
+ path <- sanePaths
+ value <- valGen.arbitrary
+ } yield (path,value)
+
+ def sanePaths: Gen[ZipperPath] = for {
+ items <- Gen.listOf(saneLocations)
+ head <- saneLocations
+ } yield ZipperPath((head :: items):_*)
+
+ //Using a small range to ensure some overlapping prefixes
+ private final val minSaneLoc = 0
+ private final val maxSaneLoc = 10
+ def saneLocations: Gen[Int] = Choose.chooseInt.choose(minSaneLoc,maxSaneLoc)
+}
View
405 src/test/scala/com/codecommit/antixml/ZipperPathSpecs.scala
@@ -0,0 +1,405 @@
+/*
+ * Copyright (c) 2011, Daniel Spiewak
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * - Neither the name of "Anti-XML" nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.codecommit.antixml
+
+import org.specs2.mutable._
+import org.specs2.ScalaCheck
+import org.specs2.matcher.Parameters
+import org.scalacheck.{Arbitrary, Prop}
+import Prop._
+import org.specs2.matcher.ScalaCheckMatchers._
+
+class ZipperPathSpecs extends Specification with ScalaCheck {
+ import math._
+
+ //Shaemelessly stolen from VectorCaseSpecs
+
+ val emptyPath = ZipperPath()
+
+ "ZipperPath companion" should {
+
+ "build using apply" in check { (items: List[Int]) =>
+ val zp = ZipperPath(items:_*)
+ items mustEqual List(zp:_*)
+ }
+
+ "build from a ZipperPath using fromSeq" in check { (items: ZipperPath) =>
+ val zp = ZipperPath.fromSeq(items)
+ List(items:_*) mustEqual List(zp:_*)
+ }
+
+ "build from an IndexedSeq using fromSeq" in check { (items: Vector[Int]) =>
+ val zp = ZipperPath.fromSeq(items)
+ List(items:_*) mustEqual List(zp:_*)
+ }
+
+ "build from a LinearSeq using fromSeq" in check { (items: List[Int]) =>
+ val zp = ZipperPath.fromSeq(items)
+ List(items:_*) mustEqual List(zp:_*)
+ }
+
+ "build from a ZipperPath using reversed" in check { (items: ZipperPath) =>
+ val zp = ZipperPath.reversed(items)
+ List(items:_*).reverse mustEqual List(zp:_*)
+ }
+
+ "build from an IndexedSeq using reversed" in check { (items: Vector[Int]) =>
+ val zp = ZipperPath.reversed(items)
+ List(items:_*).reverse mustEqual List(zp:_*)
+ }
+
+ "build from a LinearSeq using reversed" in check { (items: List[Int]) =>
+ val zp = ZipperPath.reversed(items)
+ List(items:_*).reverse mustEqual List(zp:_*)
+ }
+
+ "should have an empty empty" in {
+ ZipperPath.empty.length mustEqual 0
+ }
+
+ "should produce builders" in check { (items: List[Int]) =>
+ val zp = (ZipperPath.newBuilder ++= items).result
+ List(items:_*) mustEqual List(zp:_*)
+ }
+
+ }
+
+ "ZipperPath" should {
+ "store a single element" in {
+ val v2 = emptyPath :+ 42
+ v2(0) mustEqual 42
+ }
+
+ "implement +:" in check { (zp: ZipperPath, i: Int) =>
+ val zp2 = i +: zp
+ zp2.length mustEqual (zp.length + 1)
+ zp2.head mustEqual i
+ zp.zipWithIndex forall {
+ case (x, i) => zp2(i + 1) mustEqual x
+ }
+ }
+
+ "implement :+" in check { (zp: ZipperPath, i: Int) =>
+ val zp2 = zp :+ i
+ zp2.length mustEqual (zp.length + 1)
+ zp2.last mustEqual i
+ zp.zipWithIndex forall {
+ case (x, i) => zp2(i) mustEqual x
+ }
+ }
+
+ "implement ++" in check { (zp1: ZipperPath, zp2: ZipperPath) =>
+ val result1 = zp1 ++ zp2
+ val result2 = zp2 ++ zp1
+
+ result1.length mustEqual (zp1.length + zp2.length)
+ result1.length mustEqual result2.length
+
+ zp1.zipWithIndex forall {
+ case (x, i) => {
+ result1(i) mustEqual x
+ result2(i + zp2.length) mustEqual x
+ }
+ }
+
+ zp2.zipWithIndex forall {
+ case (x, i) => {
+ result2(i) mustEqual x
+ result1(i + zp1.length) mustEqual x
+ }
+ }
+ }
+
+ "implement length" in check { list: List[Int] =>
+ val zp = list.foldLeft(ZipperPath()) { _ :+ _ }
+ zp.length === list.length
+ }
+
+ "replace single element" in check { (zp: ZipperPath, i: Int) =>
+ (zp.nonEmpty && i > Int.MinValue) ==> {
+ val idx = abs(i) % zp.length
+ val newVectorCase = zp.updated(idx, "test").updated(idx, "newTest")
+ newVectorCase(idx) mustEqual "newTest"
+ }
+ }
+ "fail on apply out-of-bounds" in check { (zp: ZipperPath, i: Int) =>
+ !((0 until zp.length) contains i) ==> { zp(i) must throwA[Throwable] }
+ }
+
+ "store multiple elements in order" in check { list: List[Int] =>
+ val newVectorCase = list.foldLeft(emptyPath) { _ :+ _ }
+ val res = for (i <- 0 until list.length) yield newVectorCase(i) == list(i)
+
+ res must not contain (false)
+ }
+
+ "store lots of elements" in {
+ val LENGTH = 100000
+ val emptyPath = (0 until LENGTH).foldLeft(ZipperPath()) { _ :+ _ }
+
+ emptyPath.length mustEqual LENGTH
+ ((i:Int) => emptyPath(i) mustEqual i).forall(0 until LENGTH)
+ }
+
+ "maintain both old and new versions after conj" in check { zp: ZipperPath =>
+ val zp2 = zp :+ 42
+ for (i <- 0 until zp.length) {
+ zp2(i) aka ("Index " + i + " in derivative") mustEqual zp(i) aka ("Index " + i + " in origin")
+ }
+ zp2.last mustEqual 42
+ }.set(maxSize -> 3000, minTestsOk -> 1000, workers -> numProcessors)
+
+ "maintain both old and new versions after update" in check { (zp: ZipperPath, i: Int) =>
+ (!zp.isEmpty && i > Int.MinValue) ==> {
+ val idx = abs(i) % zp.length
+ val zp2 = zp.updated(idx, 42)
+ for (i <- 0 until zp.length if i != idx) {
+ zp2(i) aka ("Index " + i + " in derivative") mustEqual zp(i) aka ("Index " + i + " in origin")
+ }
+ zp2(idx) mustEqual 42
+ }
+ }
+
+ "implement drop matching Vector semantics (in general case)" in check { (zp: ZipperPath, len: Int) =>
+ toVector(zp drop len) mustEqual (toVector(zp) drop len)
+ }
+
+ "implement dropRight matching Vector semantics" in check { (zp: ZipperPath, len: Int) =>
+ //IndexedSeqOptimized doesn't match Vector for negative lengths
+ val l2 = max(0,len)
+ toVector(zp dropRight l2) mustEqual (toVector(zp) dropRight l2)
+ }
+
+ "implement filter" in check { (zp: ZipperPath, f: (Int)=>Boolean) =>
+ val filtered = zp filter f
+
+ var back = filtered forall f
+ for (e <- zp) {
+ if (f(e)) {
+ back &&= filtered.contains(e)
+ }
+ }
+ back
+ }
+
+ "implement foldLeft" in check { list: List[Int] =>
+ val zp = list.foldLeft(ZipperPath()) { _ :+ _ }
+ zp.foldLeft(0) { _ + _ } === list.foldLeft(0) { _ + _ }
+ }
+
+ "implement foreach" in check { zp: ZipperPath =>
+ val b = Vector.newBuilder[Int]
+ for(i <- zp)
+ b += i
+ val v = b.result()
+ v mustEqual zp
+ }
+
+ "implement forall" in check { (zp: ZipperPath, f: (Int)=>Boolean) =>
+ val bool = zp forall f
+
+ var back = true
+ for (e <- zp) {
+ back &&= f(e)
+ }
+
+ (back && bool) || (!back && !bool)
+ }
+
+ "implement flatMap" in check { (zp: ZipperPath, f: (Int)=>ZipperPath) =>
+ val mapped = zp flatMap f
+
+ var back = true
+
+ var i = 0
+ var n = 0
+
+ while (i < zp.length) {
+ val res = f(zp(i))
+
+ var inner = 0
+ while (inner < res.length) {
+ back &&= mapped(n) == res(inner)
+
+ inner += 1
+ n += 1
+ }
+
+ i += 1
+ }
+
+ back
+ }
+
+ "implement init matching Vector semantics" in check { zp: ZipperPath =>
+ !zp.isEmpty ==> {
+ toVector(zp.init) mustEqual toVector(zp).init
+ }
+ }
+
+ "implement map" in check { (zp: ZipperPath, f: (Int)=>Int) =>
+ val mapped = zp map f
+
+ var back = zp.length == mapped.length
+ for (i <- 0 until zp.length) {
+ back &&= mapped(i) == f(zp(i))
+ }
+ back
+ }
+
+ "implement reverse" in check { v: ZipperPath =>
+ val reversed = v.reverse
+
+ var back = v.length == reversed.length
+ for (i <- 0 until v.length) {
+ back &&= reversed(i) == v(v.length - i - 1)
+ }
+ back
+ }
+
+ "append to reverse" in check { (v: ZipperPath, n: Int) =>
+ val rev = v.reverse
+ val add = rev :+ n
+
+ var back = add.length == rev.length + 1
+ for (i <- 0 until rev.length) {
+ back &&= add(i) == rev(i)
+ }
+ back && add(rev.length) === n
+ }
+
+ "map on reverse" in check { (v: ZipperPath, f: (Int)=>Int) =>
+ val rev = v.reverse
+ val mapped = rev map f
+
+ var back = mapped.length == rev.length
+ for (i <- 0 until rev.length) {
+ back &&= mapped(i) == f(rev(i))
+ }
+ back
+ }
+
+ /* "implement slice matching Vector semantics" in check { (zp: ZipperPath, from: Int, until: Int) =>
+ // skip("Vector slice semantics are inconsistent with Traversable")
+ zp.slice(from, until).toVector mustEqual zp.toVector.slice(from, until)
+ } */
+
+ "implement splitAt matching Vector semantics" in check { (zp: ZipperPath, i: Int) =>
+ val (left, right) = zp splitAt i
+ val (expectLeft, expectRight) = toVector(zp) splitAt i
+
+ toVector(left) mustEqual expectLeft
+ toVector(right) mustEqual expectRight
+ }
+
+ "implement tail matching Vector semantics" in check { zp: ZipperPath =>
+ !zp.isEmpty ==> {
+ toVector(zp.tail) mustEqual toVector(zp).tail
+ }
+ }
+
+ "implement take matching Vector semantics" in check { (zp: ZipperPath, len: Int) =>
+ toVector(zp take len) mustEqual (toVector(zp) take len)
+ }
+
+ "implement takeRight matching Vector semantics" in check { (zp: ZipperPath, len: Int) =>
+ //scala.collection.IndexedSeqOptimized differs from Vector on negative lengths
+ val l2 = max(0,len)
+ toVector(zp takeRight l2) mustEqual (toVector(zp) takeRight l2)
+ }
+
+ "implement zip" in check { (first: ZipperPath, second: ZipperPath) =>
+ val zip = first zip second
+
+ var back = zip.length == min(first.length, second.length)
+ for (i <- 0 until zip.length) {
+ var (left, right) = zip(i)
+ back &&= (left == first(i) && right == second(i))
+ }
+ back
+ }
+
+ "implement zipWithIndex" in check { zp: ZipperPath =>
+ val zip = zp.zipWithIndex
+
+ var back = zip.length == zp.length
+ for (i <- 0 until zip.length) {
+ val (elem, index) = zip(i)
+ back &&= (index == i && elem == zp(i))
+ }
+ back
+ }
+
+ "implement equals" >> {
+ "1." in check { list: List[Int] =>
+ val zpA = list.foldLeft(ZipperPath()) { _ :+ _ }
+ val zpB = list.foldLeft(ZipperPath()) { _ :+ _ }
+ zpA === zpB
+ }
+ "2." in check { (zpA: ZipperPath, zpB: ZipperPath) =>
+ zpA.length != zpB.length ==> (zpA != zpB)
+ }
+ "3." in check { (listA: List[Int], listB: List[Int]) =>
+ val zpA = listA.foldLeft(ZipperPath()) { _ :+ _ }
+ val zpB = listB.foldLeft(ZipperPath()) { _ :+ _ }
+
+ listA != listB ==> (zpA != zpB)
+ }
+ "4." in check { (zp: ZipperPath, data: Int) => zp !== data }
+ }
+
+ "implement hashCode" in check { list: List[Int] =>
+ val zpA = list.foldLeft(ZipperPath()) { _ :+ _ }
+ val zpB = list.foldLeft(ZipperPath()) { _ :+ _ }
+ zpA.hashCode === zpB.hashCode
+ }
+ }
+
+ implicit def arbitraryZipperPath: Arbitrary[ZipperPath] = {
+ Arbitrary(for {
+ data <- Arbitrary.arbitrary[List[Int]]
+ } yield ZipperPath.fromSeq(data))
+ }
+ implicit def arbitraryVector[A](implicit arb: Arbitrary[A]): Arbitrary[Vector[A]] = {
+ Arbitrary(for {
+ data <- Arbitrary.arbitrary[List[A]]
+ } yield Vector(data:_*))
+ }
+
+ def toVector(zp: ZipperPath): Vector[Int] = {
+ (Vector[Int]() /: (0 until zp.length)) { (v,i) =>
+ v :+ zp(i)
+ }
+ }
+
+ val numProcessors = Runtime.getRuntime.availableProcessors
+ implicit val params: Parameters = set(workers -> numProcessors)
+}
+
+
View
211 src/test/scala/com/codecommit/antixml/ZipperSpecs.scala
@@ -670,6 +670,213 @@ class ZipperSpecs extends SpecificationWithJUnit with ScalaCheck with XMLGenera
}
+ "Zipper.conditionalFlatMapWithIndex" should {
+
+ "work with simple replacements" in check { (xml: Group[Node]) =>
+ val zipper = xml select *
+ def f(n: Node, i: Int): Option[Seq[Node]] = n match {
+ case n if (i & 1) == 0 => None
+ case e: Elem => Some(Seq(e.copy(name=e.name.toUpperCase)))
+ case n => None
+ }
+ val cfmwi = zipper.conditionalFlatMapWithIndex(f)
+ val equiv = xml.zipWithIndex.flatMap {case (n,i) => f(n,i).getOrElse(Seq(n))}
+
+ Seq(
+ Vector(cfmwi:_*) mustEqual Vector(equiv:_*),
+ cfmwi.length mustEqual xml.length
+ )
+ }
+
+ "work with complex replacements" in check { (xml: Group[Node]) =>
+ def f(n: Node, i: Int): Option[Seq[Node]] = n match {
+ case n if (i & 1) == 0 => None
+ case _ if (i & 2) == 0 => Some(Seq())
+ case e: Elem => Some(Seq(e.copy(name=e.name+"MODIFIED"), e, e))
+ case n => Some(Seq(n, n, n))
+ }
+ val zipper = xml select *
+ val cfmwi = zipper.conditionalFlatMapWithIndex(f)
+ val equiv = xml.zipWithIndex.flatMap {case (n,i) => f(n,i).getOrElse(Seq(n))}
+
+ val expectedDels = (xml.length + 2) >>> 2
+ val expectedTripples = (xml.length) >>> 2
+ val expectedLength = xml.length - expectedDels + 2*expectedTripples
+
+ Seq(
+ Vector(cfmwi:_*) mustEqual Vector(equiv:_*),
+ cfmwi.length mustEqual expectedLength
+ )
+ }
+
+ "preserve zipper context with simple replacements" in {
+ def f(t: Text, i: Int): Option[Seq[Text]] = {
+ if ((i&1)==0) None else Some(Seq(Text(t.text + i)))
+ }
+ val texts = Group.fromSeq(for (i <- 0 until 100) yield Text(i.toString))
+ val elems = for(t <- texts) yield elem("Text"+t.text,t)
+
+ val zipper = elems \ textNode(".*".r)
+ zipper.toVectorCase mustEqual texts.toVectorCase //sanity check
+
+ val z2 = zipper.conditionalFlatMapWithIndex(f)
+ z2.length mustEqual 100
+ z2.toVectorCase mustEqual zipper.toVectorCase.zipWithIndex.flatMap {
+ case (t,i) => f(t,i).getOrElse(Seq(t))
+ }
+
+ val result = z2.unselect
+ result.length mustEqual 100
+ result.forall(_.children.length == 1) must beTrue
+
+ val rc = result flatMap {n => n.children}
+ Vector(rc:_*) mustEqual Vector(z2:_*)
+ }
+
+ "preserve zipper context with complex replacements" in {
+ //Replaces every 4 nodes with 5 nodes
+ def f(t: Text, i: Int): Option[Seq[Text]] = {
+ if ((i&1)==0) None
+ else if ((i&2)==0) Some(Seq(t,t,t))
+ else Some(Seq())
+ }
+
+ val texts = Group.fromSeq(for (i <- 0 until 100) yield Text(i.toString))
+ val elems = for(t <- texts) yield elem("Text"+t.text,t)
+
+ val zipper = elems \ textNode(".*".r)
+ zipper.toVectorCase mustEqual texts.toVectorCase //sanity check
+
+ val z2 = zipper.conditionalFlatMapWithIndex(f)
+ z2.length mustEqual 125
+ z2.toVectorCase mustEqual zipper.toVectorCase.zipWithIndex.flatMap {
+ case (t,i) => f(t,i).getOrElse(Seq(t))
+ }
+
+ val result = z2.unselect
+ result.length mustEqual 100
+
+ val rc = result flatMap {n => n.children}
+ rc.length mustEqual 125
+ rc.toVectorCase mustEqual z2.toVectorCase
+ }
+
+ "preserve zipper context with complex replacements and empty holes" in {
+ //Replaces every 4 nodes with 5 nodes
+ def f(t: Text, i: Int): Option[Seq[Text]] = {
+ if ((i&1)==0) None
+ else if ((i&2)==0) Some(Seq(t,t,t))
+ else Some(Seq())
+ }
+
+ val texts = Group.fromSeq(for (i <- 0 until 200) yield Text(i.toString))
+ val elems = for(t <- texts) yield elem("Text"+t.text,t)
+
+ val zipper = elems \ textNode(".*".r)
+ zipper.toVectorCase mustEqual texts.toVectorCase //sanity check
+
+ val z2 = zipper.slice(50,150).conditionalFlatMapWithIndex(f)
+ z2.length mustEqual 125
+ z2.toVectorCase mustEqual zipper.toVectorCase.slice(50,150).zipWithIndex.flatMap {
+ case (t,i) => f(t,i).getOrElse(Seq(t))
+ }
+
+ val result = z2.unselect
+ result.length mustEqual 200
+ result.slice(0,50).forall(_.children.isEmpty) must beTrue
+ result.slice(150,200).forall(_.children.isEmpty) must beTrue
+
+ val rc = result flatMap {n => n.children}
+ rc.length mustEqual 125
+ rc.toVectorCase mustEqual z2.toVectorCase
+ }
+
+ "preserve zipper context with half simple, half complex" in {
+ def f(t: Text, i: Int): Option[Seq[Text]] = {
+ if (i<25) None
+ else if (i<50) Some(Seq(Text(t.text.toUpperCase)))
+ else Some(Seq(t,t,t))
+ }
+
+ val texts = Group.fromSeq(for (i <- 0 until 100) yield Text(i.toString))
+ val elems = for(t <- texts) yield elem("Text"+t.text,t)
+
+ val zipper = elems \ textNode(".*".r)
+ zipper.toVectorCase mustEqual texts.toVectorCase //sanity check
+
+ val z2 = zipper.conditionalFlatMapWithIndex(f)
+ z2.length mustEqual 200
+ z2.toVectorCase mustEqual zipper.toVectorCase.zipWithIndex.flatMap {
+ case (t,i) => f(t,i).getOrElse(Seq(t))
+ }
+
+ val result = z2.unselect
+ result.length mustEqual 100
+ result.slice(0,50).forall(_.children.length == 1) must beTrue
+
+ val rc = result flatMap {n => n.children}
+ rc.length mustEqual 200
+ rc.toVectorCase mustEqual z2.toVectorCase
+ }
+ }
+
+ "Zipper.slice" should {
+ val texts = Group.fromSeq(for (i <- 0 until 100) yield Text(i.toString))
+ val elems = for(t <- texts) yield elem("Text"+t.text,t)
+ val zipper = elems \ textNode(".*".r)
+
+ "match the behavior of Group.slice for ranges in bounds" in check { (from: Int, to: Int) =>
+ val f = (from & Int.MaxValue) % zipper.length
+ val t = (to & Int.MaxValue) % zipper.length
+ zipper.slice(f,t).toVectorCase mustEqual texts.slice(f,t).toVectorCase
+ }
+ "match the behavior of Group.slice for any range" in check { (f: Int, t: Int) =>
+ zipper.slice(f,t).toVectorCase mustEqual texts.slice(f,t).toVectorCase
+ }
+
+ "update the zipper context correctly for ranges in bounds" in check { (from: Int, to: Int) =>
+ val f = (from & Int.MaxValue) % zipper.length
+ val t = (to & Int.MaxValue) % zipper.length
+
+ val unselected = zipper.slice(f,t).unselect
+ unselected.length mustEqual elems.length
+
+ val uc = unselected flatMap {n => n.children}
+ uc.toVectorCase mustEqual texts.slice(f,t).toVectorCase
+ }
+
+ "update the zipper context correctly for any range" in check { (f: Int, t: Int) =>
+ val unselected = zipper.slice(f,t).unselect
+ unselected.length mustEqual elems.length
+
+ val uc = unselected flatMap {n => n.children}
+ uc.toVectorCase mustEqual texts.slice(f,t).toVectorCase
+ }
+
+ "update the zipper context correctly for multiple slices" in check { (f1: Int, t1: Int, f2: Int, t2: Int) =>
+
+ val unselected = zipper.slice(f1,t1).slice(f2,t2).unselect
+ unselected.length mustEqual elems.length
+
+ val uc = unselected flatMap {n => n.children}
+ uc.toVectorCase mustEqual texts.slice(f1,t1).slice(f2,t2).toVectorCase
+ }
+
+ "update the zipper context correctly for multiple slices in bounds" in check { (from1: Int, to1: Int, from2: Int, to2: Int) =>
+ val f1 = (from1 & Int.MaxValue) % zipper.length
+ val t1 = (to1 & Int.MaxValue) % zipper.length
+ val slice1 = zipper.slice(f1,t1)
+
+ val f2 = if (slice1.length == 0) 0 else (from2 & Int.MaxValue) % slice1.length
+ val t2 = if (slice1.length == 0) 0 else (to2 & Int.MaxValue) % slice1.length
+
+ val unselected = slice1.slice(f2,t2).unselect
+ unselected.length mustEqual elems.length
+
+ val uc = unselected flatMap {n => n.children}
+ uc.toVectorCase mustEqual texts.slice(f1,t1).slice(f2,t2).toVectorCase
+ }
+ }
def validate[Expected] = new {
def apply[A](a: A)(implicit evidence: A =:= Expected) = evidence must not beNull
@@ -680,5 +887,9 @@ class ZipperSpecs extends SpecificationWithJUnit with ScalaCheck with XMLGenera
def elem(name: String) = Elem(None, name, Attributes(), Map(), Group())
def elem(name: String, children: Node*) = Elem(None, name, Attributes(), Map(), Group(children: _*))
+
+ def textNode(s: scala.util.matching.Regex) = Selector[Text]({
+ case t: Text if s.pattern.matcher(t.text).matches() => t
+ })
}
View
13 src/test/scala/com/codecommit/antixml/performance/Performance.scala
@@ -59,7 +59,8 @@ object Performance {
ShallowZipperOps,
DeepZipperOps,
HugeZipperOps,
- HugeDeadZipperOps
+ HugeDeadZipperOps,
+ HugeZipperWithEmptyHolesOps
)
/* === Load trials === */
@@ -246,4 +247,14 @@ object Performance {
override def unselectCount = 0
}
+ object HugeZipperWithEmptyHolesOps extends Trial('zipperHugeFiltered, "operations on a BIG zipper that was partially filtered") with ZipperOpsTrial {
+ override val classifiers = Set('zipperOps, 'shallow, 'small)
+ override def xmlResource = getClass.getResource("/spending.xml")
+ override def createZipper(xml: Elem) = {
+ val z = xml \\ anyElem
+ z.slice(z.length/4, z.length/4 + z.length/2)
+ }
+
+ override def unselectCount = 1
+ }
}
View
8 src/test/scala/com/codecommit/antixml/util/VectorCaseSpecs.scala
@@ -179,6 +179,14 @@ class VectorCaseSpecs extends Specification with ScalaCheck {
vec.foldLeft(0) { _ + _ } === list.foldLeft(0) { _ + _ }
}
+ "implement foreach" in check { vec: VectorCase[Int] =>
+ val b = Vector.newBuilder[Int]
+ for(i <- vec)
+ b += i
+ val v = b.result()
+ v mustEqual vec
+ }
+
"implement forall" in check { (vec: VectorCase[Int], f: (Int)=>Boolean) =>
val bool = vec forall f

0 comments on commit 495b340

Please sign in to comment.
Something went wrong with that request. Please try again.