Skip to content
This repository has been archived by the owner on Jan 26, 2022. It is now read-only.

Add lite-delta module #29

Merged
merged 3 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Changes:
- `lite-pfix`: Initial release ([#27](https://github.com/MakeNowJust-Labo/lite/pull/27))
- `lite-shohw`: Use `lite-pfix` ([#27](https://github.com/MakeNowJust-Labo/lite/pull/27))
- `lite-crazy`: Initial release ([#28](https://github.com/MakeNowJust-Labo/lite/pull/28))
- `lite-delta` Initial release ([#29](https://github.com/MakeNowJust-Labo/lite/pull/29))
- `lite-show`: Add `isControl` parameter to `Pretty.Lit` ([#29](https://github.com/MakeNowJust-Labo/lite/pull/29))
- `lite-gestalt`: Rename from `lite-diff` ([#29](https://github.com/MakeNowJust-Labo/lite/pull/29))
- Support Scala 2.13.6
- Support Scala 3.0.1 ([#23](https://github.com/MakeNowJust-Labo/lite/pull/23))
- Update sbt to 1.5.5 ([#24](https://github.com/MakeNowJust-Labo/lite/pull/24))
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
## Libraries

- [**lite-crazy**](modules/lite-crazy): A lazy-evaluated value cell and time travelling state monads implementations.
- [**lite-diff**](modules/lite-diff): Computes a diff between two sequences.
- [**lite-gestalt**](modules/lite-gestalt): Computes a diff between two sequences by using Gestalt Pattern Matching.
- [**lite-gimei**](modules/lite-gimei): A generator of Japanese dummy names and addresses with furigana.
- [**lite-grapheme**](modules/lite-grapheme): Iterates the given string on each grapheme cluster.
- [**lite-pfix**](modules/lite-pfix): A partially defined fixpoint combinator.
Expand Down
54 changes: 45 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ ThisBuild / semanticdbVersion := scalafixSemanticdb.revision
ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.5.0"
ThisBuild / scalafixDependencies += "com.github.vovapolu" %% "scaluzzi" % "0.1.18"

val crossProjectNames = Seq("crazy", "diff", "gimei", "grapheme", "pfix", "romaji", "show")
val crossProjectNames = Seq("crazy", "delta", "gestalt", "gimei", "grapheme", "pfix", "romaji", "show")
val platformSuffices = Seq("JVM", "JS", "Native")
platformSuffices.flatMap { platform =>
addCommandAlias(s"test$platform", crossProjectNames.map(name => s"$name$platform/test").mkString("; "))
Expand All @@ -43,7 +43,8 @@ lazy val root = project
coverageEnabled := false
)
.aggregate(crazyJVM, crazyJS, crazyNative)
.aggregate(diffJVM, diffJS, diffNative)
.aggregate(deltaJVM, deltaJS, deltaNative)
.aggregate(gestaltJVM, gestaltJS, gestaltNative)
.aggregate(gimeiJVM, gimeiJS, gimeiNative)
.aggregate(graphemeJVM, graphemeJS, graphemeNative)
.aggregate(romajiJVM, romajiJS, romajiNative)
Expand Down Expand Up @@ -78,12 +79,46 @@ lazy val crazyJVM = crazy.jvm
lazy val crazyJS = crazy.js
lazy val crazyNative = crazy.native

lazy val diff = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.in(file("modules/lite-diff"))
lazy val delta = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.in(file("modules/lite-delta"))
.settings(
name := "lite-diff",
name := "lite-delta",
console / initialCommands :=
"""|import codes.quine.labo.lite.diff._
"""|import codes.quine.labo.lite.show._
|import codes.quine.labo.lite.show.Prettify.PrettifyGenOps
|
|import codes.quine.labo.lite.delta._
|import codes.quine.labo.lite.delta.Diff.DiffGenOps
|import codes.quine.labo.lite.delta.Key.KeyGenOps
|""".stripMargin,
Compile / console / scalacOptions -= "-Wunused",
Test / console / scalacOptions -= "-Wunused",
// Set URL mapping of scala standard API for Scaladoc.
apiMappings ++= scalaInstance.value.libraryJars
.filter(file => file.getName.startsWith("scala-library") && file.getName.endsWith(".jar"))
.map(_ -> url(s"http://www.scala-lang.org/api/${scalaVersion.value}/"))
.toMap,
// Settings for test:
libraryDependencies += "org.scalameta" %%% "munit" % "0.7.27" % Test,
testFrameworks += new TestFramework("munit.Framework")
)
.jsSettings(Test / scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) })
.nativeSettings(
crossScalaVersions := Seq("2.13.6"),
coverageEnabled := false
)
.dependsOn(gestalt, pfix, show)

lazy val deltaJVM = delta.jvm
lazy val deltaJS = delta.js
lazy val deltaNative = delta.native

lazy val gestalt = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.in(file("modules/lite-gestalt"))
.settings(
name := "lite-gestalt",
console / initialCommands :=
"""|import codes.quine.labo.lite.gestalt._
|""".stripMargin,
Compile / console / scalacOptions -= "-Wunused",
Test / console / scalacOptions -= "-Wunused",
Expand All @@ -102,9 +137,9 @@ lazy val diff = crossProject(JVMPlatform, JSPlatform, NativePlatform)
coverageEnabled := false
)

lazy val diffJVM = diff.jvm
lazy val diffJS = diff.js
lazy val diffNative = diff.native
lazy val gestaltJVM = gestalt.jvm
lazy val gestaltJS = gestalt.js
lazy val gestaltNative = gestalt.native

lazy val gimei = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.in(file("modules/lite-gimei"))
Expand Down Expand Up @@ -274,6 +309,7 @@ lazy val show = crossProject(JVMPlatform, JSPlatform, NativePlatform)
name := "lite-show",
console / initialCommands :=
"""|import codes.quine.labo.lite.show._
|import codes.quine.labo.lite.show.Prettify.PrettifyGenOps
|""".stripMargin,
Compile / console / scalacOptions -= "-Wunused",
Test / console / scalacOptions -= "-Wunused",
Expand Down
36 changes: 36 additions & 0 deletions modules/lite-delta/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# lite-delta

> Computes a difference between two any values.

[![Maven Central](https://img.shields.io/maven-central/v/codes.quine.labo/lite-delta_2.13?logo=scala&style=for-the-badge)](https://search.maven.org/artifact/codes.quine.labo/lite-delta_2.13)

## Install

Insert the following to your `build.sbt`.

```sbt
libraryDependencies += "codes.quine.labo" %% "lite-delta" % "<latest version>"
```

## Usage

`Delta.diff` computes a diff between two any values and returns a prettified string of this diff.

For example:

```scala
import codes.quine.labo.lite.delta.Delta

sealed abstract class FooBar
case class Foo(x: Int, y: Int) extends FooBar
case class Bar(z: String) extends FooBar

Delta.diff(
Seq(Foo(1, 2), Bar("foo"), Foo(3, 4), Foo(5, 6)),
Seq(Foo(1, 2), Bar("bar"), Foo(5, 6), Bar("foobar"))
)
```

Screenshot:

![screenshot](assets/screenshot.png)
Binary file added modules/lite-delta/assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package codes.quine.labo.lite.delta

import scala.io.AnsiColor

import codes.quine.labo.lite.delta.Diff.DiffGenOps
import codes.quine.labo.lite.show.Prettify
import codes.quine.labo.lite.show.Pretty
import codes.quine.labo.lite.show.Pretty._

/** Delta is an object for representing a difference between two values. */
sealed abstract class Delta extends Product with Serializable {

/** Checks whether or not this is identical. */
def isIdentical: Boolean

/** Renders this into pretty fragments for showing. */
def prettify(showIdentical: Boolean, prettify: Prettify): Seq[Pretty]
}

object Delta {

/** Computes a difference between two values and returns prettified string of this diff. */
def diff(left: Any, right: Any)(implicit diff: Diff = Diff.default().toDiff, opts: Diff.Opts = Diff.Opts()): String =
diff.diff(left, right)

/** Renders the given fragments as left item. */
private def buildLeft(l: Seq[Pretty]): Seq[Pretty] =
Seq(Lit(AnsiColor.RED, true)) ++ l ++ Seq(Lit(AnsiColor.RESET, true))

/** Renders the given fragments as right item. */
private def buildRight(r: Seq[Pretty]): Seq[Pretty] =
Seq(Lit(AnsiColor.GREEN, true)) ++ r ++ Seq(Lit(AnsiColor.RESET, true))

/** Renders items with ignoring by the given function. */
private def buildSeq[T](ts: Seq[T])(isIgnore: T => Boolean, show: T => Seq[Pretty]): LazyList[Seq[Pretty]] =
if (ts.isEmpty) LazyList.empty
else {
val (ls, rs) = ts.span(isIgnore)
val prefix = if (ls.isEmpty) LazyList.empty else LazyList(Seq(Lit("...")))
rs.headOption match {
case Some(r) =>
prefix ++ LazyList(show(r)) ++ buildSeq(rs.tail)(isIgnore, show)
case None => prefix
}
}

/** Apply is a delta object for case classes. */
final case class Case(name: String, fields: Seq[(String, Delta)]) extends Delta {
def isIdentical: Boolean = fields.forall(_._2.isIdentical)
def prettify(showIdentical: Boolean, prettify: Prettify): Seq[Pretty] = {
val fs = buildSeq(fields)(
{ f => !showIdentical && f._2.isIdentical },
{ f => List(Wide(s"${f._1} = ")) ++ f._2.prettify(showIdentical, prettify) }
)
Prettify.buildApply(name, fs)
}
}

/** Map is a delta object for mappings. */
final case class Mapping(name: String, entries: Seq[(Any, Delta)]) extends Delta {
def isIdentical: Boolean = entries.forall(e => e._2.isIdentical)
def prettify(showIdentical: Boolean, prettify: Prettify): Seq[Pretty] = {
val es = buildSeq(entries)(
{ e => !showIdentical && e._2.isIdentical },
{ e => prettify(e._1) ++ Seq(Lit(" -> ")) ++ e._2.prettify(showIdentical, prettify) }
)
Prettify.buildApply(name, es)
}
}

/** Set is a delta object for sequences. */
final case class Sequence(name: String, deltas: Seq[Delta]) extends Delta {
def isIdentical: Boolean = deltas.forall(_.isIdentical)
def prettify(showIdentical: Boolean, prettify: Prettify): Seq[Pretty] = {
val ds = buildSeq(deltas)(d => !showIdentical && d.isIdentical, _.prettify(showIdentical, prettify))
Prettify.buildApply(name, ds)
}
}

/** Identical is a delta object with an identical value. */
final case class Identical(value: Any) extends Delta {
def isIdentical: Boolean = true
def prettify(showIdentical: Boolean, prettify: Prettify): Seq[Pretty] =
prettify(value)
}

/** Changed is a delta object with changed values. */
final case class Changed(left: Any, right: Any) extends Delta {
def isIdentical: Boolean = false
def prettify(showIdentical: Boolean, prettify: Prettify): Seq[Pretty] =
buildLeft(prettify(left)) ++ Seq(Lit(" => ")) ++ buildRight(prettify(right))
}

/** Missing is a delta object with a missing value. */
final case class Missing(right: Any) extends Delta {
def isIdentical: Boolean = false
def prettify(showIdentical: Boolean, prettify: Prettify): Seq[Pretty] =
buildRight(prettify(right))
}

/** Missing is a delta object with an additional value. */
final case class Additional(left: Any) extends Delta {
def isIdentical: Boolean = false
def prettify(showIdentical: Boolean, prettify: Prettify): Seq[Pretty] =
buildLeft(prettify(left))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package codes.quine.labo.lite.delta

import codes.quine.labo.lite.delta.Key.KeyGenOps
import codes.quine.labo.lite.gestalt.Gestalt
import codes.quine.labo.lite.pfix.PFix
import codes.quine.labo.lite.show.Prettify
import codes.quine.labo.lite.show.Prettify.PrettifyGenOps
import codes.quine.labo.lite.show.Pretty

/** Diff is a function for computing a difference between two vales. */
trait Diff extends ((Any, Any) => Delta) {

/** Computes a difference between two values and returns prettified string of this diff. */
def diff(left: Any, right: Any)(implicit opts: Diff.Opts = Diff.Opts()): String = {
val delta = apply(left, right)
val frags = delta.prettify(opts.showIdentical, opts.prettify)
Pretty.render(frags, opts.width, opts.indentSize)
}
}

object Diff {

/** A generator function of a diff function. */
type Gen = PFix[(Any, Any), Delta]

object Gen {

/** Returns a new generator. */
def apply(f: ((Any, Any) => Delta) => PartialFunction[(Any, Any), Delta]): Gen =
PFix(rec => f((l, r) => rec((l, r))))

/** Converts a partial function into a generator. */
def from(pf: PartialFunction[(Any, Any), Delta]): Gen = PFix.from(pf)
}

/** GenOps provides `toDiff` method into `Gen` instance. */
implicit class DiffGenOps(private val g: Gen) extends AnyVal {

/** Converts a generator into a diff function. */
def toDiff: Diff = {
val f = g.toFunction { case (l, r) => if (l == r) Delta.Identical(l) else Delta.Changed(l, r) }
(l, r) => f((l, r))
}
}

/** Opts is a set of options for [[Diff.diff]] method. */
final case class Opts(
width: Int = 80,
indentSize: Int = 2,
showIdentical: Boolean = true,
prettify: Prettify = Prettify.default().toPrettify
)

/** A default diff function. */
def default(key: Key = Key.default.toKey): Gen =
`null`.orElse(map).orElse(set).orElse(seq(key)).orElse(product)

/** A diff function for null values. */
def `null`: Gen = Gen.from {
case (null, null) => Delta.Identical(null)
case (null, r) => Delta.Changed(null, r)
case (l, null) => Delta.Changed(l, null)
}

/** A diff function for mapping values. */
def map: Gen = Gen { rec =>
{ case (left: Map[_, _], right: Map[_, _]) =>
val name = Prettify.stringPrefix(left)
val keys =
(left.asInstanceOf[Map[Any, Any]].keySet | right.asInstanceOf[Map[Any, Any]].keySet).toSeq.sortBy(_.toString)
val entries = Seq.newBuilder[(Any, Delta)]
for (k <- keys) {
(left.asInstanceOf[Map[Any, Any]].get(k), right.asInstanceOf[Map[Any, Any]].get(k)) match {
case (Some(l), Some(r)) => entries.addOne(k -> rec(l, r))
case (None, Some(r)) => entries.addOne(k -> Delta.Missing(r))
case (Some(l), None) => entries.addOne(k -> Delta.Additional(l))
case (None, None) =>
// $COVERAGE-OFF$
sys.error("unreachable")
// $COVERAGE-ON$
}
}
Delta.Mapping(name, entries.result())
}
}

/** A diff function for sequence values. */
def seq(key: Key): Gen = Gen { rec =>
{ case (left: Seq[_], right: Seq[_]) =>
val name = Prettify.stringPrefix(left)
val leftISeq = left.toIndexedSeq
val rightISeq = right.toIndexedSeq
val leftKeys = leftISeq.map(key)
val rightKeys = rightISeq.map(key)
val hunks = Gestalt.diff(leftKeys, rightKeys).hunks

val deltas = Seq.newBuilder[Delta]
var leftIndex = 0
var rightIndex = 0
for (hunk <- hunks) {
for ((i, j) <- (leftIndex until hunk.leftStart).zip(rightIndex until hunk.rightStart))
deltas.addOne(rec(leftISeq(i), rightISeq(j)))
for (i <- hunk.leftStart until hunk.leftEnd) deltas.addOne(Delta.Additional(leftISeq(i)))
for (j <- hunk.rightStart until hunk.rightEnd) deltas.addOne(Delta.Missing(rightISeq(j)))
leftIndex = hunk.leftEnd
rightIndex = hunk.rightEnd
}
for ((i, j) <- (leftIndex until leftISeq.size).zip(rightIndex until rightISeq.size))
deltas.addOne(rec(leftISeq(i), rightISeq(j)))

Delta.Sequence(name, deltas.result())
}
}

/** A diff function for set values. */
def set: Gen = Gen.from { case (left: Set[_], right: Set[_]) =>
val name = Prettify.stringPrefix(left)
val is = left.asInstanceOf[Set[Any]] & right.asInstanceOf[Set[Any]]

val deltas = Seq.newBuilder[Delta]
for (x <- is) deltas.addOne(Delta.Identical(x))
for (l <- left.asInstanceOf[Set[Any]]; if !is.contains(l)) deltas.addOne(Delta.Additional(l))
for (r <- right.asInstanceOf[Set[Any]]; if !is.contains(r)) deltas.addOne(Delta.Missing(r))

Delta.Sequence(name, deltas.result())
}

/** A diff function for product values. */
def product: Gen = Gen { rec =>
{
case ((), ()) => Delta.Identical(())
case (left: Product, right: Product) if left.getClass == right.getClass =>
val isTuple = left.productPrefix.startsWith("Tuple")
val name = if (isTuple) "" else left.productPrefix
val fields = Seq.newBuilder[(String, Delta)]
for (((l, r), i) <- left.productIterator.zip(right.productIterator).zipWithIndex) {
val fieldName = if (isTuple) s"_${i + 1}" else left.productElementName(i)
fields.addOne(fieldName -> rec(l, r))
}
Delta.Case(name, fields.result())
}
}
}
Loading