Skip to content

Commit

Permalink
Pulling Zipper hole shifting and axes into separate traits.
Browse files Browse the repository at this point in the history
This removes the need in implicit conversion for Zipper axes support.
  • Loading branch information
ncreep committed Dec 13, 2011
1 parent e20bf45 commit f566b66
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 92 deletions.
89 changes: 8 additions & 81 deletions src/main/scala/com/codecommit/antixml/zipper/Zipper.scala
Expand Up @@ -35,7 +35,6 @@ import scala.collection.generic.{CanBuildFrom, FilterMonadic}
import scala.collection.immutable.{SortedMap, IndexedSeq, SortedSet}
import scala.collection.mutable.Builder
import Zipper._
import Zipper.ZipperPathOrdering
import CanBuildFromWithZipper._

/**
Expand Down Expand Up @@ -123,7 +122,9 @@ import CanBuildFromWithZipper._
*/
trait Zipper[+A <: Node] extends Group[A]
with IndexedSeqLike[A, Zipper[A]]
with ZipperGroupOverrides[A] { self =>
with ZipperGroupOverrides[A]
with ZipperHoleShifting
with ZipperAxes { self =>

/**
* Returns the original group that was selected upon when the Zipper was created. A value of `None` indicates that
Expand All @@ -140,77 +141,6 @@ trait Zipper[+A <: Node] extends Group[A]
/** The zipper context or None if this is a broken zipper. */
private[antixml] val context: Option[Context]

/** Shifts the focus of the zipper to another set of holes.
*
* The shifting is performed using a shifting function which is applied to each
* path visible in the zipper and produces a new sequence of paths.
* These paths are sorted lexicographically and duplicates are removed.
*
* A new zipper displaying the above paths is returned while internally maintaining data
* about any previously contained paths.
*
* The values which are attached to the paths come from two sources:
* - If the zipper previously contained the path, the data attached to it is used.
* - If the path is new, the data is fetched directly from the parent of the zipper.
*
* In case a hole was previously multiplied (e.g. using flatMap) it is placed
* as is in the resulting zipper.
*
* Note: shifting is not supported for parentless (broken) zippers.
*
* @param shiftFunc A function to be supplied with the parent of the zipper
* and applied to the indexed contents of the zipper.
* Assumed to produce valid paths with regard to the supplied parent. */
private[antixml] def shiftHoles(shiftFunc: Group[Node] => ZipperPath => Seq[ZipperPath]): Zipper[Node] = context match {
case Some(context @ Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => {
implicit val lexicographic = ZipperPathOrdering

val shift = shiftFunc(parent)
val unsoretedPaths = for {
m <- metas
path <- shift(m._1) if path != ZipperPath.empty // ignoring empty paths
} yield path

// not allowing duplicates and empty paths and sorting lexicographically
val newPaths = SortedSet(unsoretedPaths: _*)
val holeInfo = new HoleMapper(context).holeInfo

val b = newZipperContextBuilder[Node](Some(parent))
val pathsInit: VectorCase[ElemsWithContextVisible[Node]] = util.Vector0

val (unusedPaths, usedPaths) = // leaving paths that were never used before
holeInfo.depthFirst.foldLeft((newPaths, pathsInit)) { case ((nPaths, used), hole) =>
val (path, (nodesTimes, masterTime)) = hole

val holes =
if (nodesTimes.isEmpty) Seq((path, masterTime, util.Vector0))
else nodesTimes.map { case (n, t) => (path, t, Seq(n)) } // this contains duplicates for multiplied locations

val visible = (ElemsWithContextVisible.apply[Node] _).tupled
val hidden = (ElemsWithContextHidden.apply _).tupled

if (nPaths contains path) {
(nPaths - path, used ++ holes.map(visible))
} else {
b ++= holes.map(hidden)
(nPaths, used)
}
}

val initTime = 0 // these paths were never modified
val unusedElems = unusedPaths.toList map { p =>
ElemsWithContextVisible[Node](p, initTime, PathFetcher.getNode(parent)(p))
}

// this can contain duplicate locations from the previously used paths
val visibleElems = (unusedElems ++ usedPaths).sortBy(_.path)
b ++= visibleElems

b.result
}
case None => sys.error("Cannot shift root.")
}

/**
* Returns a `Group` containing the same nodes as this Zipper, but without any Zipper context, and in particular,
* without any implict references to the zipper's parent.
Expand All @@ -224,7 +154,10 @@ trait Zipper[+A <: Node] extends Group[A]
}

/** Each hole is associated with a list of node/time pairs as well as a master update time */
private type HoleInfo = ZipperHoleMap[(VectorCase[(Node,Time)],Time)]
private[antixml] type HoleInfo = ZipperHoleMap[(VectorCase[(Node,Time)],Time)]

/** Converts the given context object into a hole info instance. */
private[antixml] def toHoleInfo(context: Context) = new HoleMapper(context).holeInfo

/** A utility class to convert the contents of the zipper into a hole map. */
private[this] class HoleMapper(context: Context) {
Expand Down Expand Up @@ -278,7 +211,7 @@ trait Zipper[+A <: Node] extends Group[A]
/** Utility class to perform unselect. */
private[this] class Unselector(context: Context, mergeStrategy: ZipperMergeStrategy) {

private val topLevelHoleInfo = new HoleMapper(context).holeInfo
private val topLevelHoleInfo = toHoleInfo(context)

/** Applies the node updates to the parent and returns the result. */
def unselect: Zipper[Node] =
Expand Down Expand Up @@ -391,12 +324,6 @@ object Zipper {
}
}

/** Lexicographic ordering for path objects. */
private object ZipperPathOrdering extends Ordering[ZipperPath] {
override def compare(x: ZipperPath, y: ZipperPath) =
Ordering.Iterable[Int].compare(x,y)
}

/** Returns a builder that produces a zipper without a parent */
def newBuilder[A <: Node] = VectorCase.newBuilder[A].mapResult(brokenZipper(_))

Expand Down
14 changes: 4 additions & 10 deletions src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala
Expand Up @@ -2,7 +2,7 @@ package com.codecommit.antixml
import scala.annotation.tailrec

/**
* Wraps [[com.codecommit.antixml.Zipper]] instances with some XPath like axes.
* Adds some XPath like axes to [[com.codecommit.antixml.Zipper]] instances.
*
* Note1: the axes are applied to each node in a zipper individually and the result
* is a new zipper with the nodes concatenated and sorted lexicographically by
Expand All @@ -11,10 +11,10 @@ import scala.annotation.tailrec
* Note2: the axes are calculated using holes in the zipper, hence for a modified
* zipper some nodes may be multiplied or elided.
*/
class ZipperAxes(zipper: Zipper[Node]) {
trait ZipperAxes { self: Zipper[Node] =>
/** Returns the direct parent of a node. */
def directParent = {
zipper shiftHoles (g => (PathTransformer(g).shiftUp(_)).andThen(_.toList))
shiftHoles (g => (PathTransformer(g).shiftUp(_)).andThen(_.toList))
}

/** Returns the ancestors of a node. */
Expand All @@ -40,7 +40,7 @@ class ZipperAxes(zipper: Zipper[Node]) {
* @param appendSource True if the initial path should be part of the result.
*/
private def transFuncToShift(func: PathTransformer => ZipperPath => Option[ZipperPath], withSource: Boolean) = {
zipper shiftHoles { g =>
shiftHoles { g =>
val pathToOpt = func(PathTransformer(g))

@tailrec
Expand All @@ -62,10 +62,4 @@ class ZipperAxes(zipper: Zipper[Node]) {
}
}

}

object ZipperAxes {
/** Pimps a plain zipper to have axes selection methods.
* TODO move to package object? */
implicit def zipperToAxes(zipper: Zipper[Node]) = new ZipperAxes(zipper)
}
@@ -0,0 +1,92 @@
package com.codecommit.antixml

import Zipper._
import ZipperHoleShifting._
import util.VectorCase
import scala.collection.immutable.SortedSet
import CanBuildFromWithZipper._

/** Responsible for zipper hole shifting support. */
trait ZipperHoleShifting { self: Zipper[Node] =>

/** Shifts the focus of the zipper to another set of holes.
*
* The shifting is performed using a shifting function which is applied to each
* path visible in the zipper and produces a new sequence of paths.
* These paths are sorted lexicographically and duplicates are removed.
*
* A new zipper displaying the above paths is returned while internally maintaining data
* about any previously contained paths.
*
* The values which are attached to the paths come from two sources:
* - If the zipper previously contained the path, the data attached to it is used.
* - If the path is new, the data is fetched directly from the parent of the zipper.
*
* In case a hole was previously multiplied (e.g. using flatMap) it is placed
* as is in the resulting zipper.
*
* Note: shifting is not supported for parentless (broken) zippers.
*
* @param shiftFunc A function to be supplied with the parent of the zipper
* and applied to the indexed contents of the zipper.
* Assumed to produce valid paths with regard to the supplied parent.
*/
private[antixml] def shiftHoles(shiftFunc: Group[Node] => ZipperPath => Seq[ZipperPath]): Zipper[Node] = context match {
case Some(context @ Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => {
implicit val lexicographic = ZipperPathOrdering

val shift = shiftFunc(parent)
val unsoretedPaths = for {
m <- metas
path <- shift(m._1) if path != ZipperPath.empty // ignoring empty paths
} yield path

// not allowing duplicates and empty paths and sorting lexicographically
val newPaths = SortedSet(unsoretedPaths: _*)
val holeInfo = toHoleInfo(context)

val b = newZipperContextBuilder[Node](Some(parent))
val pathsInit: VectorCase[ElemsWithContextVisible[Node]] = util.Vector0

val (unusedPaths, usedPaths) = // leaving paths that were never used before
holeInfo.depthFirst.foldLeft((newPaths, pathsInit)) {
case ((nPaths, used), hole) =>
val (path, (nodesTimes, masterTime)) = hole

val holes =
if (nodesTimes.isEmpty) Seq((path, masterTime, util.Vector0))
else nodesTimes.map { case (n, t) => (path, t, Seq(n)) } // this contains duplicates for multiplied locations

val visible = (ElemsWithContextVisible.apply[Node] _).tupled
val hidden = (ElemsWithContextHidden.apply _).tupled

if (nPaths contains path) {
(nPaths - path, used ++ holes.map(visible))
} else {
b ++= holes.map(hidden)
(nPaths, used)
}
}

val initTime = 0 // these paths were never modified
val unusedElems = unusedPaths.toList map { p =>
ElemsWithContextVisible[Node](p, initTime, PathFetcher.getNode(parent)(p))
}

// this can contain duplicate locations from the previously used paths
val visibleElems = (unusedElems ++ usedPaths).sortBy(_.path)
b ++= visibleElems

b.result
}
case None => sys.error("Cannot shift root.")
}
}

object ZipperHoleShifting {
/** Lexicographic ordering for path objects. */
private object ZipperPathOrdering extends Ordering[ZipperPath] {
override def compare(x: ZipperPath, y: ZipperPath) =
Ordering.Iterable[Int].compare(x,y)
}
}
Expand Up @@ -2,7 +2,6 @@ package com.codecommit.antixml

import org.specs2.mutable._
import XML._
import ZipperAxes._
import scala.collection.immutable.SortedSet

class ZipperAxesSpecs extends SpecificationWithJUnit {
Expand Down

0 comments on commit f566b66

Please sign in to comment.