Skip to content
This repository was archived by the owner on Jan 24, 2025. It is now read-only.

Commit adfc4d8

Browse files
committed
Merge branch 'master' into update/refined-0.9.27
2 parents c2641c8 + 45cfefb commit adfc4d8

File tree

19 files changed

+893
-84
lines changed

19 files changed

+893
-84
lines changed

build.sbt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import com.softwaremill.UpdateVersionInDocs
22
import sbt.Def
33
import sbt.Reference.display
4+
import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings
5+
import com.softwaremill.Publish.{ossPublishSettings, updateDocs}
46

57
val scala212 = "2.12.14"
68
val scala213 = "2.13.6"
@@ -122,7 +124,7 @@ lazy val munit = (projectMatrix in file("munit"))
122124
.settings(
123125
name := "diffx-munit",
124126
libraryDependencies ++= Seq(
125-
"org.scalameta" %%% "munit" % "0.7.26"
127+
"org.scalameta" %%% "munit" % "0.7.27"
126128
),
127129
testFrameworks += new TestFramework("munit.Framework")
128130
)

core/src/main/scala/com/softwaremill/diffx/DiffResult.scala

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ trait DiffResult extends Product with Serializable {
1515

1616
object DiffResult {
1717
private[diffx] final val indentLevel = 5
18+
private[diffx] def mergeChunks(diffs: List[DiffResult]) = {
19+
diffs
20+
.foldLeft(List.empty[DiffResult]) { (acc, item) =>
21+
(acc.lastOption, item) match {
22+
case (Some(d: DiffResultMissingChunk), di: DiffResultMissingChunk) =>
23+
acc.dropRight(1) :+ d.copy(value = d.value + di.value)
24+
case (Some(d: DiffResultAdditionalChunk), di: DiffResultAdditionalChunk) =>
25+
acc.dropRight(1) :+ d.copy(value = d.value + di.value)
26+
case (Some(d: DiffResultChunk), di: DiffResultChunk) =>
27+
acc.dropRight(1) :+ d.copy(left = d.left + di.left, right = d.right + di.right)
28+
case _ => acc :+ item
29+
}
30+
}
31+
}
32+
1833
val Ignored: IdenticalValue[Any] = IdenticalValue("<ignored>")
1934
}
2035

@@ -102,6 +117,38 @@ case class DiffResultString(diffs: List[DiffResult]) extends DiffResult {
102117
override def isIdentical: Boolean = diffs.forall(_.isIdentical)
103118
}
104119

120+
case class DiffResultStringLine(diffs: List[DiffResult]) extends DiffResult {
121+
override private[diffx] def showIndented(indent: Int, renderIdentical: Boolean)(implicit
122+
c: ConsoleColorConfig
123+
): String = {
124+
mergeChunks(diffs)
125+
.map(_.showIndented(indent, renderIdentical))
126+
.mkString
127+
}
128+
129+
override def isIdentical: Boolean = diffs.forall(_.isIdentical)
130+
}
131+
132+
case class DiffResultStringWord(diffs: List[DiffResult]) extends DiffResult {
133+
override private[diffx] def showIndented(indent: Int, renderIdentical: Boolean)(implicit
134+
c: ConsoleColorConfig
135+
): String = {
136+
mergeChunks(diffs)
137+
.map(_.showIndented(indent, renderIdentical))
138+
.mkString
139+
}
140+
141+
override def isIdentical: Boolean = diffs.forall(_.isIdentical)
142+
}
143+
144+
case class DiffResultChunk(left: String, right: String) extends DiffResult {
145+
override def isIdentical: Boolean = false
146+
147+
override private[diffx] def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig) = {
148+
arrowColor("[") + showChange(s"$left", s"$right") + arrowColor("]")
149+
}
150+
}
151+
105152
case class DiffResultValue[T](left: T, right: T) extends DiffResult {
106153
override def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String =
107154
showChange(s"$left", s"$right")
@@ -118,14 +165,28 @@ case class IdenticalValue[T](value: T) extends DiffResult {
118165

119166
case class DiffResultMissing[T](value: T) extends DiffResult {
120167
override def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String = {
121-
rightColor(s"$value")
168+
leftColor(s"$value")
169+
}
170+
override def isIdentical: Boolean = false
171+
}
172+
173+
case class DiffResultMissingChunk(value: String) extends DiffResult {
174+
override def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String = {
175+
leftColor(s"[$value]")
122176
}
123177
override def isIdentical: Boolean = false
124178
}
125179

126180
case class DiffResultAdditional[T](value: T) extends DiffResult {
127181
override def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String = {
128-
leftColor(s"$value")
182+
rightColor(s"$value")
183+
}
184+
override def isIdentical: Boolean = false
185+
}
186+
187+
case class DiffResultAdditionalChunk(value: String) extends DiffResult {
188+
override def showIndented(indent: Int, renderIdentical: Boolean)(implicit c: ConsoleColorConfig): String = {
189+
rightColor(s"[$value]")
129190
}
130191
override def isIdentical: Boolean = false
131192
}
Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,115 @@
11
package com.softwaremill.diffx.instances
22

33
import com.softwaremill.diffx._
4+
import com.softwaremill.diffx.instances.string.DiffRow.Tag
5+
import com.softwaremill.diffx.instances.string.{DiffRow, DiffRowGenerator}
46

5-
private[diffx] class DiffForString extends Diff[String] {
6-
override def apply(left: String, right: String, context: DiffContext): DiffResult = nullGuard(left, right) {
7-
(left, right) =>
8-
val leftLines = left.split("\n").toList
9-
val rightLines = right.split("\n").toList
10-
val leftAsMap = leftLines.lift
11-
val rightAsMap = rightLines.lift
12-
val maxSize = Math.max(leftLines.length, rightLines.length)
13-
val partialResults = (0 until maxSize).map { i =>
14-
(leftAsMap(i), rightAsMap(i)) match {
15-
case (Some(lv), Some(rv)) =>
16-
if (lv == rv) {
17-
IdenticalValue(lv)
7+
class DiffForString(similarityThreshold: Double = 0.5) extends Diff[String] {
8+
private val generator = DiffRowGenerator.create
9+
10+
override def apply(left: String, right: String, context: DiffContext): DiffResult =
11+
nullGuard(left, right) { (left, right) =>
12+
val rows = generator.generateDiffRows(splitIntoLines(left), splitIntoLines(right))
13+
val lineResults = processLineDiffs(rows)
14+
if (lineResults.forall(_.isIdentical)) {
15+
IdenticalValue(left)
16+
} else {
17+
DiffResultString(lineResults)
18+
}
19+
}
20+
21+
private def processLineDiffs(rows: List[DiffRow]) = {
22+
rows.map { row =>
23+
row.tag match {
24+
case Tag.INSERT => DiffResultMissing(row.newLine)
25+
case Tag.DELETE => DiffResultAdditional(row.oldLine)
26+
case Tag.CHANGE =>
27+
if (row.newLine.isEmpty) {
28+
DiffResultAdditional(row.oldLine)
29+
} else if (row.oldLine.isEmpty) {
30+
DiffResultMissing(row.newLine)
31+
} else {
32+
val oldSplit = tokenize(row.oldLine)
33+
val newSplit = tokenize(row.newLine)
34+
val wordDiffs = generator.generateDiffRows(
35+
oldSplit,
36+
newSplit
37+
)
38+
val words = processWordDiffs(wordDiffs)
39+
DiffResultStringLine(words)
40+
}
41+
case Tag.EQUAL =>
42+
IdenticalValue(row.newLine)
43+
}
44+
}
45+
}
46+
47+
private def tokenize(line: String): List[String] = {
48+
line
49+
.foldLeft(List.empty[List[Char]]) { (acc, item) =>
50+
acc.lastOption match {
51+
case Some(word) =>
52+
if (item == ' ') {
53+
acc ++ List(List(item))
1854
} else {
19-
DiffResultValue(lv, rv)
55+
if (word.lastOption.contains(' ')) {
56+
acc :+ List(item)
57+
} else {
58+
acc.dropRight(1) :+ (word ++ List(item))
59+
}
2060
}
21-
case (Some(lv), None) => DiffResultAdditional(lv)
22-
case (None, Some(rv)) => DiffResultMissing(rv)
23-
case (None, None) => throw new IllegalStateException("That should never happen")
61+
case None => acc :+ List(item)
2462
}
25-
}.toList
26-
if (partialResults.forall(_.isIdentical)) {
27-
IdenticalValue(left)
28-
} else {
29-
DiffResultString(partialResults)
3063
}
64+
.map(_.mkString)
65+
}
66+
67+
private def processWordDiffs(words: List[DiffRow]): List[DiffResult] = {
68+
words.map { wordDiff =>
69+
wordDiff.tag match {
70+
case Tag.INSERT => DiffResultMissingChunk(wordDiff.newLine)
71+
case Tag.DELETE => DiffResultAdditionalChunk(wordDiff.oldLine)
72+
case Tag.CHANGE =>
73+
if (wordDiff.newLine.isEmpty) {
74+
DiffResultAdditionalChunk(wordDiff.oldLine)
75+
} else if (wordDiff.oldLine.isEmpty) {
76+
DiffResultMissingChunk(wordDiff.newLine)
77+
} else {
78+
val charDiff = generator.generateDiffRows(
79+
wordDiff.oldLine.toList.map(_.toString),
80+
wordDiff.newLine.toList.map(_.toString)
81+
)
82+
val similarity = charDiff.count(_.tag == Tag.EQUAL).toDouble / charDiff.size
83+
if (similarity < similarityThreshold) {
84+
DiffResultValue(wordDiff.oldLine, wordDiff.newLine)
85+
} else {
86+
DiffResultStringWord(processCharDiffs(charDiff))
87+
}
88+
}
89+
case Tag.EQUAL => IdenticalValue(wordDiff.newLine)
90+
}
91+
}
92+
}
93+
94+
private def processCharDiffs(chars: List[DiffRow]): List[DiffResult] = {
95+
chars.map { charDiff =>
96+
charDiff.tag match {
97+
case Tag.INSERT => DiffResultMissingChunk(charDiff.newLine)
98+
case Tag.DELETE => DiffResultAdditionalChunk(charDiff.oldLine)
99+
case Tag.CHANGE =>
100+
if (charDiff.newLine.isEmpty) {
101+
DiffResultAdditionalChunk(charDiff.oldLine)
102+
} else if (charDiff.oldLine.isEmpty) {
103+
DiffResultMissingChunk(charDiff.newLine)
104+
} else {
105+
DiffResultChunk(charDiff.oldLine, charDiff.newLine)
106+
}
107+
case Tag.EQUAL => IdenticalValue(charDiff.newLine)
108+
}
109+
}
110+
}
111+
112+
private def splitIntoLines(string: String) = {
113+
string.replace("\r\n", "\n").split("\n").toList
31114
}
32115
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.softwaremill.diffx.instances.string
2+
3+
case class Chunk[T](position: Int, lines: List[T]) {
4+
def size: Int = lines.size
5+
def last: Int = position + size - 1
6+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.softwaremill.diffx.instances.string
2+
3+
import com.softwaremill.diffx.instances.string.Delta.TYPE
4+
5+
sealed abstract class Delta[T](original: Chunk[T], revised: Chunk[T]) {
6+
7+
def getType: TYPE
8+
def getOriginal: Chunk[T] = original
9+
def getRevised: Chunk[T] = revised
10+
def getSource: Chunk[T] = original
11+
def getTarget: Chunk[T] = revised
12+
override def toString: String = s"Delta($getType, $getOriginal, $getRevised)"
13+
}
14+
15+
object Delta {
16+
sealed abstract class TYPE
17+
object TYPE {
18+
case object CHANGE extends TYPE
19+
case object DELETE extends TYPE
20+
case object INSERT extends TYPE
21+
}
22+
}
23+
class ChangeDelta[T](original: Chunk[T], revised: Chunk[T]) extends Delta(original, revised) {
24+
override def getType: TYPE = TYPE.CHANGE
25+
}
26+
class InsertDelta[T](original: Chunk[T], revised: Chunk[T]) extends Delta(original, revised) {
27+
override def getType: TYPE = TYPE.INSERT
28+
}
29+
class DeleteDelta[T](original: Chunk[T], revised: Chunk[T]) extends Delta(original, revised) {
30+
override def getType: TYPE = TYPE.DELETE
31+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.softwaremill.diffx.instances.string
2+
3+
object DiffRow {
4+
5+
sealed trait Tag
6+
object Tag {
7+
case object INSERT extends Tag
8+
case object DELETE extends Tag
9+
case object CHANGE extends Tag
10+
case object EQUAL extends Tag
11+
}
12+
}
13+
14+
case class DiffRow(tag: DiffRow.Tag, oldLine: String, newLine: String)

0 commit comments

Comments
 (0)