diff --git a/.travis.yml b/.travis.yml index 2508665..27824bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,10 @@ language: scala before_script: - sudo chmod +x /usr/local/bin/sbt +scala: + - 2.11.8 + - 2.12.4 + # only trigger builds on master branches: only: diff --git a/build.sbt b/build.sbt index 1c3491d..90ce4b7 100644 --- a/build.sbt +++ b/build.sbt @@ -1,11 +1,13 @@ +import scalariform.formatter.preferences._ + lazy val Benchmark = config("bench") extend Test lazy val commonSettings = Seq( organization := "org.gnieh", name := "tekstlib", version := "0.1.2-SNAPSHOT", - scalaVersion := "2.12.2", - crossScalaVersions := Seq("2.12.2", "2.11.8"), + scalaVersion := "2.12.4", + crossScalaVersions := Seq("2.12.4", "2.11.8"), libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.3" % "test", libraryDependencies += "org.scodec" %% "scodec-bits" % "1.1.4", scalacOptions in (Compile, doc) ++= Seq("-doc-root-content", "rootdoc.txt"), @@ -14,9 +16,9 @@ lazy val commonSettings = Seq( homepage := Some(url("https://github.com/gnieh/tekstlib"))) lazy val root = project.in(file(".")) + .enablePlugins(SbtOsgi) .settings(commonSettings) .settings(osgiSettings) - .settings(scalariformSettings) .settings( resourceDirectories in Compile := List(), OsgiKeys.exportPackage := Seq( @@ -26,12 +28,12 @@ lazy val root = project.in(file(".")) "Bundle-Name" -> "Gnieh Text and Document Manipulation"), OsgiKeys.bundleSymbolicName := "org.gnieh.tekstlib", OsgiKeys.privatePackage := Seq(), - ScalariformKeys.preferences := { - import scalariform.formatter.preferences._ - ScalariformKeys.preferences.value + scalariformAutoformat := true, + scalariformPreferences := { + scalariformPreferences.value .setPreference(AlignSingleLineCaseStatements, true) - .setPreference(DoubleIndentClassDeclaration, true) - .setPreference(PreserveDanglingCloseParenthesis, true) + .setPreference(DoubleIndentConstructorArguments, true) + .setPreference(DanglingCloseParenthesis, Preserve) .setPreference(MultilineScaladocCommentsStartOnFirstLine, true) }, publishMavenStyle := true, diff --git a/project/build.properties b/project/build.properties index 64317fd..9abea12 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.15 +sbt.version=1.0.3 diff --git a/project/plugins.sbt b/project/plugins.sbt index aa7e995..d24a9ee 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ -addSbtPlugin("com.typesafe.sbt" % "sbt-osgi" % "0.6.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-osgi" % "0.9.2") -addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.2.1") +addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.2") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") diff --git a/src/main/scala/gnieh/Indexable.scala b/src/main/scala/gnieh/Indexable.scala new file mode 100644 index 0000000..5723a0d --- /dev/null +++ b/src/main/scala/gnieh/Indexable.scala @@ -0,0 +1,126 @@ +/* +* Copyright (c) 2017 Lucas Satabin +* +* 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 gnieh + +import matching._ + +import scala.language.higherKinds + +import scala.annotation.tailrec + +abstract class Indexable[Coll, Elem] { + + private implicit val self = this + + def apply(coll: Coll, idx: Int): Elem + + def isEmpty(coll: Coll): Boolean + + def size(coll: Coll): Int + + def slice(coll: Coll, start: Int, end: Int): Coll + + def indexOfSlice(s1: Coll, s2: Coll)(implicit equiv: Equiv[Elem]): Int = + KMP.search(s1, s2) + + def startsWith(s1: Coll, s2: Coll)(implicit equiv: Equiv[Elem]): Boolean = { + @tailrec + def loop(idx: Int): Boolean = + if (idx < size(s1) && idx < size(s2)) { + if (equiv.equiv(apply(s1, idx), apply(s2, idx))) { + loop(idx + 1) + } else { + false + } + } else if (idx >= size(s2)) { + true + } else { + false + } + loop(0) + } + + def equivalent(coll: Coll, that: Coll)(implicit equiv: Equiv[Elem]): Boolean = { + val s1 = size(coll) + val s2 = size(that) + + if (s1 == s2) { + // same size, there is a chance they are equivalent + @tailrec + def loop(idx: Int): Boolean = + if (idx < s1) { + if (equiv.equiv(apply(coll, idx), apply(that, idx))) { + // elements are equivalent, continue + loop(idx + 1) + } else { + // non equivalent elements, stop + false + } + } else { + // end of collection for both, they are equivalent + true + } + loop(0) + } else { + false + } + } + +} + +trait IndexableInstances { + + implicit object IndexableString extends Indexable[String, Char] { + + @inline + def apply(s: String, idx: Int) = + s.charAt(idx) + + @inline + def isEmpty(s: String) = + s == null || s.length == 0 + + @inline + def size(s: String): Int = + s.length + + @inline + def slice(s: String, start: Int, end: Int) = + s.slice(start, end) + + } + + implicit def IndexableIndexedSeq[T]: Indexable[IndexedSeq[T], T] = new Indexable[IndexedSeq[T], T] { + + @inline + def apply(s: IndexedSeq[T], idx: Int) = + s(idx) + + @inline + def isEmpty(s: IndexedSeq[T]) = + s.isEmpty + + @inline + def size(s: IndexedSeq[T]): Int = + s.size + + @inline + def slice(s: IndexedSeq[T], start: Int, end: Int) = + s.slice(start, end) + + } + +} diff --git a/src/main/scala/gnieh/diff/Diff.scala b/src/main/scala/gnieh/diff/Diff.scala index d43d8e2..e2842ec 100644 --- a/src/main/scala/gnieh/diff/Diff.scala +++ b/src/main/scala/gnieh/diff/Diff.scala @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package gnieh.diff +package gnieh +package diff import scala.annotation.tailrec -class LcsDiff[T](lcsalg: Lcs[T]) { +class LcsDiff(lcsalg: Lcs) { - def diff(s1: IndexedSeq[T], s2: IndexedSeq[T]): List[Diff] = { + def diff[Coll, T](s1: Coll, s2: Coll)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): List[Diff] = { val lcs = lcsalg.lcs(s1, s2) @tailrec def loop(lcs: List[Common], idx1: Int, idx2: Int, acc: List[Diff]): List[Diff] = @@ -34,7 +35,6 @@ class LcsDiff[T](lcsalg: Lcs[T]) { else acc.reverse case Common(start1, start2, _) :: _ if idx1 < start1 || idx2 < start2 => - // assert(idx1 < s1.size && idx2 < s2.size) if (idx1 < start1 && idx2 < start2) loop(lcs, start1, start2, Second(idx2, start2) :: First(idx1, start1) :: acc) else if (idx1 < start1) @@ -42,7 +42,6 @@ class LcsDiff[T](lcsalg: Lcs[T]) { else loop(lcs, idx1, start2, Second(idx2, start2) :: acc) case Common(start1, start2, length) :: rest if length > 0 => - // assert(start1 == idx1 && start2 == idx2) loop(rest, start1 + length, start2 + length, Both(start1, start1 + length, start2, start2 + length) :: acc) case Common(start1, start2, _) :: rest => loop(rest, start1, start2, acc) diff --git a/src/main/scala/gnieh/diff/DynamicProgLcs.scala b/src/main/scala/gnieh/diff/DynamicProgLcs.scala index 690b121..e66e324 100644 --- a/src/main/scala/gnieh/diff/DynamicProgLcs.scala +++ b/src/main/scala/gnieh/diff/DynamicProgLcs.scala @@ -11,7 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package gnieh.diff +package gnieh +package diff import scala.annotation.tailrec @@ -19,15 +20,15 @@ import scala.annotation.tailrec * * @author Lucas Satabin */ -class DynamicProgLcs[T] extends Lcs[T] { +class DynamicProgLcs extends Lcs { - def lcsInner(seq1: IndexedSeq[T], low1: Int, seq2: IndexedSeq[T], low2: Int): List[Common] = { + def lcsInner[Coll, T](seq1: Coll, low1: Int, seq2: Coll, low2: Int)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): List[Common] = { val lengths = Array.ofDim[Int](seq1.size + 1, seq2.size + 1) // fill up the length matrix for { i <- 0 until seq1.size j <- 0 until seq2.size - } if (seq1(i) == seq2(j)) + } if (equiv.equiv(seq1(i), seq2(j))) lengths(i + 1)(j + 1) = lengths(i)(j) + 1 else lengths(i + 1)(j + 1) = math.max(lengths(i + 1)(j), lengths(i)(j + 1)) diff --git a/src/main/scala/gnieh/diff/Lcs.scala b/src/main/scala/gnieh/diff/Lcs.scala index feff6f6..883b67e 100644 --- a/src/main/scala/gnieh/diff/Lcs.scala +++ b/src/main/scala/gnieh/diff/Lcs.scala @@ -11,7 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package gnieh.diff +package gnieh +package diff import scala.annotation.tailrec @@ -20,27 +21,27 @@ import scala.annotation.tailrec * * @author Lucas Satabin */ -abstract class Lcs[T] { +abstract class Lcs { /** Computes the longest commons subsequence between both inputs. * Returns an ordered list containing the indices in the first sequence and in the second sequence. */ @inline - def lcs(seq1: IndexedSeq[T], seq2: IndexedSeq[T]): List[Common] = + def lcs[Coll, T](seq1: Coll, seq2: Coll)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): List[Common] = lcs(seq1, seq2, 0, seq1.size, 0, seq2.size) /** Computest the longest common subsequence between both input slices. * Returns an ordered list containing the indices in the first sequence and in the second sequence. * Before calling the actual lcs algorithm, it performs some preprocessing to detect trivial solutions. */ - def lcs(s1: IndexedSeq[T], s2: IndexedSeq[T], low1: Int, high1: Int, low2: Int, high2: Int): List[Common] = { + def lcs[Coll, T](s1: Coll, s2: Coll, low1: Int, high1: Int, low2: Int, high2: Int)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): List[Common] = { val seq1 = s1.slice(low1, high1) val seq2 = s2.slice(low2, high2) if (seq1.isEmpty || seq2.isEmpty) { // shortcut if at least on sequence is empty, the lcs, is empty as well Nil - } else if (seq1 == seq2) { + } else if (indexable.equivalent(seq1, seq2)) { // both sequences are equal, the lcs is either of them List(Common(low1, low2, seq1.size)) } else if (seq1.startsWith(seq2)) { @@ -93,16 +94,16 @@ abstract class Lcs[T] { /** Computest the longest common subsequence between both input slices. * Returns an ordered list containing the indices in the first sequence and in the second sequence. */ - def lcsInner(s1: IndexedSeq[T], low1: Int, s2: IndexedSeq[T], low2: Int): List[Common] + def lcsInner[Coll, T](s1: Coll, low1: Int, s2: Coll, low2: Int)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): List[Common] /* Extract common prefix and suffix from both sequences */ - private def splitPrefixSuffix(seq1: IndexedSeq[T], seq2: IndexedSeq[T], low1: Int, low2: Int): (Option[Common], IndexedSeq[T], IndexedSeq[T], Option[Common]) = { + private def splitPrefixSuffix[Coll, T](seq1: Coll, seq2: Coll, low1: Int, low2: Int)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): (Option[Common], Coll, Coll, Option[Common]) = { val size1 = seq1.size val size2 = seq2.size val size = math.min(size1, size2) @tailrec def prefixLoop(idx: Int): Option[Common] = - if (idx >= size || seq1(idx) != seq2(idx)) { + if (idx >= size || !equiv.equiv(seq1(idx), seq2(idx))) { if (idx == 0) { None } else { @@ -112,10 +113,10 @@ abstract class Lcs[T] { prefixLoop(idx + 1) } val prefix = prefixLoop(0) - val (endPrefix1, endPrefix2) = prefix.map { case Common(s1, s2, l) => (s1 + l, s2 + l) } getOrElse ((0, 0)) + val (prefixEnd1, prefixEnd2) = prefix.map { case Common(s1, s2, l) => (s1 + l, s2 + l) } getOrElse ((0, 0)) @tailrec def suffixLoop(idx1: Int, idx2: Int, l: Int): Option[Common] = - if (idx1 < endPrefix1 || idx2 < endPrefix2 || seq1(idx1) != seq2(idx2)) { + if (idx1 < prefixEnd1 || idx2 < prefixEnd2 || !equiv.equiv(seq1(idx1), seq2(idx2))) { if (l == 0) { None } else { @@ -127,7 +128,7 @@ abstract class Lcs[T] { val suffix = suffixLoop(size1 - 1, size2 - 1, 0) val psize = prefix.map(_.length).getOrElse(0) val ssize = suffix.map(_.length).getOrElse(0) - (prefix, seq1.drop(psize).dropRight(ssize), seq2.drop(psize).dropRight(ssize), suffix) + (prefix, seq1.slice(psize, seq1.size - ssize), seq2.slice(psize, seq2.size - ssize), suffix) } protected def push(idx1: Int, idx2: Int, commons: List[Common], back: Boolean): List[Common] = diff --git a/src/main/scala/gnieh/diff/MyersLcs.scala b/src/main/scala/gnieh/diff/MyersLcs.scala index c0506ea..957603c 100644 --- a/src/main/scala/gnieh/diff/MyersLcs.scala +++ b/src/main/scala/gnieh/diff/MyersLcs.scala @@ -11,15 +11,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package gnieh.diff +package gnieh +package diff import scala.annotation.tailrec import scala.collection.mutable.ListBuffer -class MyersLcs[T] extends Lcs[T] { +class MyersLcs extends Lcs { - def lcsInner(seq1: IndexedSeq[T], low1: Int, seq2: IndexedSeq[T], low2: Int): List[Common] = { + def lcsInner[Coll, T](seq1: Coll, low1: Int, seq2: Coll, low2: Int)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): List[Common] = { val size1 = seq1.size val size2 = seq2.size val max = 1 + size1 + size2 @@ -36,7 +37,7 @@ class MyersLcs[T] extends Lcs[T] { else v(max + k - 1) + 1 var y = x - k - while (x < size1 && y < size2 && seq1(x) == seq2(y)) { + while (x < size1 && y < size2 && equiv.equiv(seq1(x), seq2(y))) { acc = push(x + low1, y + low2, acc, false) x += 1 y += 1 diff --git a/src/main/scala/gnieh/diff/Patience.scala b/src/main/scala/gnieh/diff/Patience.scala index 53939a8..6d499d5 100644 --- a/src/main/scala/gnieh/diff/Patience.scala +++ b/src/main/scala/gnieh/diff/Patience.scala @@ -11,12 +11,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package gnieh.diff +package gnieh +package diff import scala.annotation.tailrec -import scala.collection.SeqView - /** Implementation of the patience algorithm [1] to compute the longest common subsequence * * [1] http://alfedenzo.livejournal.com/170301.html @@ -24,19 +23,19 @@ import scala.collection.SeqView * @param withFallback whether to fallback to classic LCS when patience could not find the LCS * @author Lucas Satabin */ -class Patience[T](withFallback: Boolean = true) extends Lcs[T] { +class Patience(withFallback: Boolean = true) extends Lcs { // algorithm we fall back to when patience algorithm is unable to find the LCS - private val classicLcs: Option[Lcs[T]] = - if (withFallback) Some(new MyersLcs[T]) else None + private val classicLcs: Option[Lcs] = + if (withFallback) Some(new MyersLcs) else None /** An occurrence of a value associated to its index */ - type Occurrence = (T, Int) + type Occurrence[T] = (T, Int) /** Returns occurrences that appear only once in the list, associated with their index */ - private def uniques(l: SeqView[T, IndexedSeq[T]]): List[Occurrence] = { + private def uniques[Coll, T](l: Coll)(implicit indexable: Indexable[Coll, T]): List[Occurrence[T]] = { @tailrec - def loop(idx: Int, acc: Map[T, Int]): List[Occurrence] = + def loop(idx: Int, acc: Map[T, Int]): List[Occurrence[T]] = if (idx >= l.size) { acc.toList } else { @@ -52,12 +51,12 @@ class Patience[T](withFallback: Boolean = true) extends Lcs[T] { } /** Takes all occurences from the first sequence and order them as in the second sequence if it is present */ - private def common(l1: List[Occurrence], l2: List[Occurrence]): List[(Occurrence, Int)] = { + private def common[T](l1: List[Occurrence[T]], l2: List[Occurrence[T]])(implicit equiv: Equiv[T]): List[(Occurrence[T], Int)] = { @tailrec - def loop(l: List[Occurrence], acc: List[(Occurrence, Int)]): List[(Occurrence, Int)] = l match { + def loop(l: List[Occurrence[T]], acc: List[(Occurrence[T], Int)]): List[(Occurrence[T], Int)] = l match { case occ :: tl => // find the element in the second sequence if present - l2.find(_._1 == occ._1) match { + l2.find(e => equiv.equiv(e._1, occ._1)) match { case Some((_, idx2)) => loop(tl, (occ -> idx2) :: acc) case None => loop(tl, acc) } @@ -69,7 +68,7 @@ class Patience[T](withFallback: Boolean = true) extends Lcs[T] { } /** Returns the list of elements that appear only once in both l1 and l2 ordered as they appear in l2 with their index in l1 */ - private def uniqueCommons(seq1: SeqView[T, IndexedSeq[T]], seq2: SeqView[T, IndexedSeq[T]]): List[(Occurrence, Int)] = { + private def uniqueCommons[Coll, T](seq1: Coll, seq2: Coll)(implicit indexable: Indexable[Coll, T]): List[(Occurrence[T], Int)] = { // the values that occur only once in the first sequence val uniques1 = uniques(seq1) // the values that occur only once in the second sequence @@ -79,7 +78,7 @@ class Patience[T](withFallback: Boolean = true) extends Lcs[T] { } /** Returns the longest sequence */ - private def longest(l: List[(Occurrence, Int)]): List[Common] = { + private def longest[T](l: List[(Occurrence[T], Int)]): List[Common] = { if (l.isEmpty) { Nil } else { @@ -98,7 +97,7 @@ class Patience[T](withFallback: Boolean = true) extends Lcs[T] { // this case should NEVER happen throw new Exception("No empty stack must exist") } - def sort(l: List[(Occurrence, Int)]): List[List[Stacked]] = + def sort(l: List[(Occurrence[T], Int)]): List[List[Stacked]] = l.foldLeft(List[List[Stacked]]()) { case (acc, ((_, idx1), idx2)) => push(idx1, idx2, acc, None, Nil) @@ -114,7 +113,7 @@ class Patience[T](withFallback: Boolean = true) extends Lcs[T] { /** Computes the longest common subsequence between both sequences. * It is encoded as the list of common indices in the first and the second sequence. */ - def lcsInner(seq1: IndexedSeq[T], glow1: Int, seq2: IndexedSeq[T], glow2: Int): List[Common] = { + def lcsInner[Coll, T](seq1: Coll, glow1: Int, seq2: Coll, glow2: Int)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): List[Common] = { // fill the holes with possibly common (not unique) elements def loop(low1: Int, low2: Int, high1: Int, high2: Int, acc: List[Common]): List[Common] = if (low1 >= high1 || low2 >= high2) { @@ -123,7 +122,7 @@ class Patience[T](withFallback: Boolean = true) extends Lcs[T] { var lastPos1 = low1 - 1 var lastPos2 = low2 - 1 var answer = acc - for (Common(p1, p2, l) <- longest(uniqueCommons(seq1.view(low1, high1), seq2.view(low2, high2)))) { + for (Common(p1, p2, l) <- longest(uniqueCommons(seq1.slice(low1, high1), seq2.slice(low2, high2)))) { // recurse between lines which are unique in each sequence val pos1 = p1 + low1 val pos2 = p2 + low2 @@ -138,21 +137,21 @@ class Patience[T](withFallback: Boolean = true) extends Lcs[T] { // the size of the accumulator increased, find // matches between the last match and the end loop(lastPos1 + 1, lastPos2 + 1, high1, high2, answer) - } else if (seq1(low1) == seq2(low2)) { + } else if (equiv.equiv(seq1(low1), seq2(low2))) { // find lines that match at the beginning var newLow1 = low1 var newLow2 = low2 - while (newLow1 < high1 && newLow2 < high2 && seq1(newLow1) == seq2(newLow2)) { + while (newLow1 < high1 && newLow2 < high2 && equiv.equiv(seq1(newLow1), seq2(newLow2))) { answer = push(newLow1 + glow1, newLow2 + glow2, answer, false) newLow1 += 1 newLow2 += 1 } loop(newLow1, newLow2, high1, high2, answer) - } else if (seq1(high1 - 1) == seq2(high2 - 1)) { + } else if (equiv.equiv(seq1(high1 - 1), seq2(high2 - 1))) { // find lines that match at the end var newHigh1 = high1 - 1 var newHigh2 = high2 - 1 - while (newHigh1 > low1 && newHigh2 > low2 && seq1(newHigh1 - 1) == seq2(newHigh2 - 1)) { + while (newHigh1 > low1 && newHigh2 > low2 && equiv.equiv(seq1(newHigh1 - 1), seq2(newHigh2 - 1))) { newHigh1 -= 1 newHigh2 -= 1 } diff --git a/src/main/scala/gnieh/matching/Bitap.scala b/src/main/scala/gnieh/matching/Bitap.scala index b2b2692..6e98c1d 100644 --- a/src/main/scala/gnieh/matching/Bitap.scala +++ b/src/main/scala/gnieh/matching/Bitap.scala @@ -24,18 +24,41 @@ import scodec.bits._ * * @author Lucas Satabin */ -trait Bitap { +class Bitap { /** Searches for the exact given `pattern` in sequence `s` and returns * the index of the first match or `-1`. Maximum `k` errors are allowed. */ - def search[T](pattern: IndexedSeq[T], s: IndexedSeq[T], from: Int = 0, k: Int = 0): Int = { + def search[Coll, T](pattern: Coll, s: Coll, from: Int = 0, k: Int = 0)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): Int = { + + class Wrapped(val value: T) { + override def hashCode = value.hashCode + override def equals(o: Any) = o match { + case that: Wrapped => equiv.equiv(this.value, that.value) + case _ => false + } + } + val m = pattern.size val n = s.size val notOne = BitVector.high(m + 1).update(m, false) val r = Vector.fill(k + 1)(notOne) val one = BitVector.low(m + 1).update(m, true) - val patternMask = patternMap(pattern, m, one) + + def patternMap[Coll, T]: Map[Wrapped, BitVector] = { + @tailrec + def loop(idx: Int, acc: Map[Wrapped, BitVector]): Map[Wrapped, BitVector] = + if (idx == m) { + acc + } else { + val t = new Wrapped(pattern(idx)) + loop(idx + 1, acc.updated(t, acc(t) & ~(one << idx))) + } + val ones = BitVector.high(m + 1) + loop(0, Map.empty.withDefaultValue(ones)) + } + + val patternMask = patternMap val oneShiftM = BitVector.low(m + 1).update(0, true) val zeros = BitVector.low(m + 1) @@ -46,7 +69,7 @@ trait Bitap { -1 } else { val t = s(idx) - val r01 = r(0) | patternMask(t) + val r01 = r(0) | patternMask(new Wrapped(t)) val r02 = r01 << 1 val r1 = r.updated(0, r02) @@ -56,9 +79,10 @@ trait Bitap { if (d > k) { r } else { - val sub = (old & (r(d) | patternMask(t))) << 1 - val ins = old & ((r(d) | patternMask(t)) << 1) - val del = (nextOld & (r(d) | patternMask(t))) << 1 + val t1 = new Wrapped(t) + val sub = (old & (r(d) | patternMask(t1))) << 1 + val ins = old & ((r(d) | patternMask(t1)) << 1) + val del = (nextOld & (r(d) | patternMask(t1))) << 1 val r1 = r.updated(d, sub & ins & del) dloop(d + 1, r1, r(d), r1(d)) } @@ -74,19 +98,6 @@ trait Bitap { loop(from, r) } - private def patternMap[T](pattern: IndexedSeq[T], m: Int, one: BitVector): Map[T, BitVector] = { - @tailrec - def loop(idx: Int, acc: Map[T, BitVector]): Map[T, BitVector] = - if (idx == m) { - acc - } else { - val t = pattern(idx) - loop(idx + 1, acc.updated(t, acc(t) & ~(one << idx))) - } - val ones = BitVector.high(m + 1) - loop(0, Map.empty.withDefaultValue(ones)) - } - } object Bitap extends Bitap diff --git a/src/main/scala/gnieh/matching/KMP.scala b/src/main/scala/gnieh/matching/KMP.scala new file mode 100644 index 0000000..712494a --- /dev/null +++ b/src/main/scala/gnieh/matching/KMP.scala @@ -0,0 +1,64 @@ +/* +* Copyright (c) 2017 Lucas Satabin +* +* 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 gnieh +package matching + +import scala.annotation.tailrec + +object KMP { + + private sealed trait Result + private case class Found(idx: Int) extends Result + private case class Interrupted(idx: Int) extends Result + + private def assertInterrupted(r: Result): Int = r match { + case Interrupted(j) => j + case Found(_) => throw new Exception("This is a bug") + } + + def search[Coll, T](seq: Coll, pattern: Coll)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): Int = { + + @tailrec + def loop(pattern: Coll, m: Int, seq: Coll, n: Int, table: Vector[Int], j: Int, k: Int): Result = + if (j == m) + Found(k - j) + else if (k == n) + Interrupted(j) + else if (pattern(j) ~= seq(k)) + loop(pattern, m, seq, n, table, j + 1, k + 1) + else if (j == 0) + loop(pattern, m, seq, n, table, 0, k + 1) + else + loop(pattern, m, seq, n, table, table(j), k) + + @tailrec + def init(j: Int, table: Vector[Int]): Vector[Int] = + if (j >= pattern.size) { + table + } else { + val s = assertInterrupted(loop(pattern, pattern.size, pattern, j, table, table(j - 1), j - 1)) + init(j + 1, table.updated(j, s)) + } + + val table = init(2, Vector.fill(pattern.size)(0)) + + loop(pattern, pattern.size, seq, seq.size, table, 0, 0) match { + case Found(i) => i + case Interrupted(_) => -1 + } + } + +} diff --git a/src/main/scala/gnieh/mustache/MustacheProcessor.scala b/src/main/scala/gnieh/mustache/MustacheProcessor.scala index f42705e..ef1e55f 100644 --- a/src/main/scala/gnieh/mustache/MustacheProcessor.scala +++ b/src/main/scala/gnieh/mustache/MustacheProcessor.scala @@ -124,14 +124,16 @@ class MustacheProcessor(loader: MustacheLoader, resident: Boolean = false) { else acc - private def renderSection(acc: StringBuilder, + private def renderSection( + acc: StringBuilder, name: String, content: List[Statement], inverted: Boolean, value: Map[String, Any])( - implicit mtag: ClassTag[Map[String, Any]], - mtagl: ClassTag[Seq[Map[String, Any]]], - ftag: ClassTag[(List[Statement], Map[String, Any], (List[Statement], Map[String, Any]) => String) => String]): StringBuilder = + implicit + mtag: ClassTag[Map[String, Any]], + mtagl: ClassTag[Seq[Map[String, Any]]], + ftag: ClassTag[(List[Statement], Map[String, Any], (List[Statement], Map[String, Any]) => String) => String]): StringBuilder = value.get(name) match { case Some(false) | Some(Seq()) | Some(null) | None if inverted => // it is undefined or false of empty and inverted, render content diff --git a/src/main/scala/gnieh/package.scala b/src/main/scala/gnieh/package.scala new file mode 100644 index 0000000..f72a985 --- /dev/null +++ b/src/main/scala/gnieh/package.scala @@ -0,0 +1,58 @@ +/* +* Copyright (c) 2017 Lucas Satabin +* +* 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 object gnieh extends IndexableInstances { + + implicit class IndexableOps[Coll, T](val coll: Coll) extends AnyVal { + + @inline + def isEmpty(implicit indexable: Indexable[Coll, T]): Boolean = + indexable.isEmpty(coll) + + @inline + def indexOfSlice(that: Coll)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): Int = + indexable.indexOfSlice(coll, that) + + @inline + def startsWith(that: Coll)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): Boolean = + indexable.startsWith(coll, that) + + @inline + def size(implicit indexable: Indexable[Coll, T]): Int = + indexable.size(coll) + + @inline + def slice(start: Int, end: Int)(implicit indexable: Indexable[Coll, T]): Coll = + indexable.slice(coll, start, end) + + @inline + def apply(idx: Int)(implicit indexable: Indexable[Coll, T]): T = + indexable.apply(coll, idx) + + @inline + def equivalent(that: Coll)(implicit indexable: Indexable[Coll, T], equiv: Equiv[T]): Boolean = + indexable.equivalent(coll, that) + + } + + implicit class EquivOps[T](val v1: T) extends AnyVal { + + @inline + def ~=(v2: T)(implicit equiv: Equiv[T]): Boolean = + equiv.equiv(v1, v2) + + } + +} diff --git a/src/main/scala/gnieh/pp/Renderer.scala b/src/main/scala/gnieh/pp/Renderer.scala index c2036cc..f75e919 100644 --- a/src/main/scala/gnieh/pp/Renderer.scala +++ b/src/main/scala/gnieh/pp/Renderer.scala @@ -1,12 +1,12 @@ /* * This file is part of the gnieh-pp project. - * + * * 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. diff --git a/src/main/scala/gnieh/pp/SimpleDoc.scala b/src/main/scala/gnieh/pp/SimpleDoc.scala index 0f7222c..d8f9b02 100644 --- a/src/main/scala/gnieh/pp/SimpleDoc.scala +++ b/src/main/scala/gnieh/pp/SimpleDoc.scala @@ -1,12 +1,12 @@ /* * This file is part of the gnieh-pp project. - * + * * 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. diff --git a/src/main/scala/gnieh/regex/tdfa/TDfa.scala b/src/main/scala/gnieh/regex/tdfa/TDfa.scala index 749e17a..e4132ad 100644 --- a/src/main/scala/gnieh/regex/tdfa/TDfa.scala +++ b/src/main/scala/gnieh/regex/tdfa/TDfa.scala @@ -16,7 +16,8 @@ package gnieh.regex package tdfa -class TDfa[Symbol](val initialState: State, +class TDfa[Symbol]( + val initialState: State, val states: Set[State], val initializer: Vector[Command], val finishers: Map[State, Vector[Command]], diff --git a/src/main/scala/gnieh/regex/tdfa/TNfa.scala b/src/main/scala/gnieh/regex/tdfa/TNfa.scala index 34c2be0..4cdd4a8 100644 --- a/src/main/scala/gnieh/regex/tdfa/TNfa.scala +++ b/src/main/scala/gnieh/regex/tdfa/TNfa.scala @@ -25,7 +25,8 @@ final case class EpsilonTransition(priority: Int, tags: Set[Int], target: State) final case class SymbolTransition[Symbol](sym: Symbol, target: State) extends Transition[Symbol] final case class AnyTransition(target: State) extends Transition[Nothing] -class TNfa[Symbol](val initialState: State, +class TNfa[Symbol]( + val initialState: State, val states: Set[State], val finalStates: Map[State, Int], val transitions: Map[State, Seq[Transition[Symbol]]]) { diff --git a/src/test/scala/gnieh/diff/TestDynLcs.scala b/src/test/scala/gnieh/diff/TestDynLcs.scala index 1cb2ef8..1b9f560 100644 --- a/src/test/scala/gnieh/diff/TestDynLcs.scala +++ b/src/test/scala/gnieh/diff/TestDynLcs.scala @@ -3,6 +3,6 @@ package test class TestDynLcs extends TestLcs { - val lcsImpl = new DynamicProgLcs[Char] + val lcsImpl = new DynamicProgLcs } diff --git a/src/test/scala/gnieh/diff/TestLcs.scala b/src/test/scala/gnieh/diff/TestLcs.scala index 93eaa71..0642647 100644 --- a/src/test/scala/gnieh/diff/TestLcs.scala +++ b/src/test/scala/gnieh/diff/TestLcs.scala @@ -5,7 +5,7 @@ import org.scalatest._ abstract class TestLcs extends FlatSpec with Matchers { - val lcsImpl: Lcs[Char] + val lcsImpl: Lcs "the lcs of an empty sequence and another sequence" should "be the empty sequence" in { @@ -126,4 +126,10 @@ abstract class TestLcs extends FlatSpec with Matchers { lcsImpl.lcs(s1, s2) should be(List(Common(0, 1, 1))) } + it should "be correctly computed if one sequence is in the middle of the other" in { + val s1 = "abcdefghijkl" + val s2 = "abdefkl" + lcsImpl.lcs(s1, s2) should be(List(Common(0, 0, 2), Common(3, 2, 3), Common(10, 5, 2))) + } + } diff --git a/src/test/scala/gnieh/diff/TestMyers.scala b/src/test/scala/gnieh/diff/TestMyers.scala index 9e8d10f..0a2a97e 100644 --- a/src/test/scala/gnieh/diff/TestMyers.scala +++ b/src/test/scala/gnieh/diff/TestMyers.scala @@ -3,6 +3,6 @@ package test class TestMyers extends TestLcs { - val lcsImpl = new MyersLcs[Char] + val lcsImpl = new MyersLcs } diff --git a/src/test/scala/gnieh/diff/TestPatience.scala b/src/test/scala/gnieh/diff/TestPatience.scala index 547276c..f8bf447 100644 --- a/src/test/scala/gnieh/diff/TestPatience.scala +++ b/src/test/scala/gnieh/diff/TestPatience.scala @@ -3,6 +3,6 @@ package test class TestPatience extends TestLcs { - val lcsImpl = new Patience[Char] + val lcsImpl = new Patience } diff --git a/src/test/scala/gnieh/pp/tests/AlignTest.scala b/src/test/scala/gnieh/pp/tests/AlignTest.scala index 65156e3..5ce9dbf 100644 --- a/src/test/scala/gnieh/pp/tests/AlignTest.scala +++ b/src/test/scala/gnieh/pp/tests/AlignTest.scala @@ -1,12 +1,12 @@ /* * This file is part of the gnieh-pp project. - * + * * 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. diff --git a/src/test/scala/gnieh/pp/tests/CompactRendererTest.scala b/src/test/scala/gnieh/pp/tests/CompactRendererTest.scala index c830910..db3b4c3 100644 --- a/src/test/scala/gnieh/pp/tests/CompactRendererTest.scala +++ b/src/test/scala/gnieh/pp/tests/CompactRendererTest.scala @@ -1,12 +1,12 @@ /* * This file is part of the gnieh-pp project. - * + * * 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. diff --git a/src/test/scala/gnieh/pp/tests/ConstructorTest.scala b/src/test/scala/gnieh/pp/tests/ConstructorTest.scala index 27409f8..8405d55 100644 --- a/src/test/scala/gnieh/pp/tests/ConstructorTest.scala +++ b/src/test/scala/gnieh/pp/tests/ConstructorTest.scala @@ -1,12 +1,12 @@ /* * This file is part of the gnieh-pp project. - * + * * 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. diff --git a/src/test/scala/gnieh/pp/tests/FillTest.scala b/src/test/scala/gnieh/pp/tests/FillTest.scala index fd5a957..a4a70e3 100644 --- a/src/test/scala/gnieh/pp/tests/FillTest.scala +++ b/src/test/scala/gnieh/pp/tests/FillTest.scala @@ -1,12 +1,12 @@ /* * This file is part of the gnieh-pp project. - * + * * 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. diff --git a/src/test/scala/gnieh/pp/tests/LineReplacementTest.scala b/src/test/scala/gnieh/pp/tests/LineReplacementTest.scala index cbb6327..d6c2576 100644 --- a/src/test/scala/gnieh/pp/tests/LineReplacementTest.scala +++ b/src/test/scala/gnieh/pp/tests/LineReplacementTest.scala @@ -1,12 +1,12 @@ /* * This file is part of the gnieh-pp project. - * + * * 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. diff --git a/src/test/scala/gnieh/pp/tests/ListCombinatorsTest.scala b/src/test/scala/gnieh/pp/tests/ListCombinatorsTest.scala index 4fa851c..1149792 100644 --- a/src/test/scala/gnieh/pp/tests/ListCombinatorsTest.scala +++ b/src/test/scala/gnieh/pp/tests/ListCombinatorsTest.scala @@ -1,12 +1,12 @@ /* * This file is part of the gnieh-pp project. - * + * * 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. diff --git a/src/test/scala/gnieh/pp/tests/OperatorUnitTest.scala b/src/test/scala/gnieh/pp/tests/OperatorUnitTest.scala index 2da2d3d..89d2f3d 100644 --- a/src/test/scala/gnieh/pp/tests/OperatorUnitTest.scala +++ b/src/test/scala/gnieh/pp/tests/OperatorUnitTest.scala @@ -1,12 +1,12 @@ /* * This file is part of the gnieh-pp project. - * + * * 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.