Skip to content

Commit 49650d2

Browse files
committed
Merge pull request #2 from xdotai/fixes
Fixes & readme
2 parents 8b50190 + 86a1bd9 commit 49650d2

File tree

5 files changed

+162
-20
lines changed

5 files changed

+162
-20
lines changed

README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
## diff
2+
3+
A tool to visually compare Scala data structures with out of the box support for arbitrary case classes.
4+
5+
### SBT Dependency
6+
7+
`"ai.x" %% "diff" % "1.0"`
8+
9+
### Usage
10+
11+
```scala
12+
println( DiffShow.diff( before, after ).string )
13+
```
14+
15+
#### Output
16+
17+
<img width="422" alt="example-output" src="https://cloud.githubusercontent.com/assets/274947/15580477/e46957e6-2336-11e6-919c-3eaf00f60cff.png">
18+
19+
#### Code
20+
21+
```scala
22+
sealed trait Parent
23+
case class Bar( s: String, i: Int ) extends Parent
24+
case class Foo( bar: Bar, b: List[Int], parent: Option[Parent] ) extends Parent
25+
26+
val before: Foo = Foo(
27+
Bar( "asdf", 5 ),
28+
List( 123, 1234 ),
29+
Some( Bar( "asdf", 5 ) )
30+
)
31+
val after: Foo = Foo(
32+
Bar( "asdf", 66 ),
33+
List( 1234 ),
34+
Some( Bar( "qwer", 5 ) )
35+
)
36+
```
37+
38+
### SBT Dependency
39+
40+
#### Custom comparison
41+
42+
Sometimes you may need to write your own type class instances. For example for non-case classes that don't compare well using ==.
43+
44+
```scala
45+
implicit def localTimeDiffShow = {
46+
val show = ( d: org.joda.time.LocalTime ) => "LocalTime(" + d.toString + ")"
47+
DiffShow.create[org.joda.time.LocalTime]( show, ( l, r ) =>
48+
if ( l isEqual r ) Identical( show( l ) ) else Different( showChange( show( l ), show( r ) ) ) )
49+
}
50+
```
51+
52+
#### Ignore parts of data
53+
54+
Sometimes you may want to ignore some parts of your data during comparison.
55+
You can do so by type, e.g. for non-deterministic parts like ObjectId, which always differ.
56+
57+
```scala
58+
def ignore[T] = new DiffShow[T] {
59+
def show( t: T ) = t.toString
60+
def diff( left: T, right: T ) = Identical( "<not compared>" )
61+
override def diffable( left: T, right: T ) = true
62+
}
63+
implicit def LocationIdShow = ignore[LocationId]
64+
```
65+
66+
#### Influence comparison in collections
67+
68+
When comparing collections you can influence if two elements should be compared or treated as completely different.
69+
Comparing elements shows their partial differences. Not comparing them shows them as added or removed.
70+
71+
```scala
72+
implicit def PersonDiffShow[L <: HList](
73+
implicit
74+
labelled: LabelledGeneric.Aux[Person, L],
75+
hlistShow: Lazy[DiffShowFields[L]]
76+
) = new CaseClassDiffShow[Person, L] {
77+
override def diffable( left: Person, right: Person ) = left._id === right._id
78+
}
79+
```

build/build.scala

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@ import java.net._
33
import java.io.File
44
import scala.collection.immutable.Seq
55

6+
// cbt:https://github.com/cvogt/cbt.git#cf7a66b7dfde8bb4386efe93a41f68996335db35
67
class Build(context: cbt.Context) extends cbt.PublishBuild(context){
7-
override def scalaVersion = "2.11.7"
8+
override def defaultScalaVersion = "2.11.8"
89

9-
override def version = "1.0-SNAPSHOT"
10-
override def artifactId = "diff_2.11"
10+
override def defaultVersion = "1.0"
11+
override def artifactId = "diff"
1112
override def groupId = "ai.x"
1213

1314
override def runClass: String = "ai.x.diff.Test"
1415

15-
override def dependencies = super.dependencies :+ MavenRepository.central.resolve(
16-
"com.chuusai" %% "shapeless" % "2.1.0",
17-
"org.cvogt" %% "scala-extensions" % "0.4.1"
18-
)
16+
override def dependencies = super.dependencies ++
17+
Resolver( mavenCentral ).bind(
18+
"com.chuusai" %% "shapeless" % "2.3.1",
19+
"org.cvogt" %% "scala-extensions" % "0.4.1"
20+
)
1921
override def scalacOptions = Seq( "-language:experimental.macros" )
2022

2123
override def url = new URL("http://github.com/xdotai/diff")

diff.scala

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package ai.x.diff
2-
import scala.collection.immutable.SortedMap
32
import shapeless._, record._, shapeless.syntax._, labelled._, ops.record._, ops.hlist._
43
import org.cvogt.scala.string._
54

65
object `package` {
7-
def red( s: String ) = Console.RED ++ s ++ Console.RESET
8-
def green( s: String ) = Console.GREEN ++ s ++ Console.RESET
9-
def blue( s: String ) = Console.BLUE ++ s ++ Console.RESET
10-
def pad( s: Any, i: Int = 5 ) = ( " " * ( i - s.toString.size ) ) ++ s.toString
11-
def arrow( l: String, r: String ) = l ++ " -> " ++ r
12-
def showChange( l: String, r: String ) = red( l ) ++ " -> " ++ green( r )
6+
def red( s: String ) = Console.RED + s + Console.RESET
7+
def green( s: String ) = Console.GREEN + s + Console.RESET
8+
def blue( s: String ) = Console.BLUE + s + Console.RESET
9+
def pad( s: Any, i: Int = 5 ) = ( " " * ( i - s.toString.size ) ) + s
10+
def arrow( l: String, r: String ) = l + " -> " + r
11+
def showChange( l: String, r: String ) = red( l ) + " -> " + green( r )
1312
}
13+
/*
14+
Loose TODO:
15+
- replace many of the manual type classes with shapeless type class derivation
16+
- introduce intermediate representation to allow alternative String renderings and allow easy testability of added/removed
17+
- split Show and Diff
18+
*/
1419

1520
abstract class Comparison {
1621
def string: String
@@ -164,16 +169,17 @@ abstract class DiffShowInstances extends DiffShowInstancesLowPriority {
164169
val ( left, right ) = ( _left.toList, _right.toList )
165170
val removed = left.filterNot( l => right.exists( r => DiffShow.diffable( l, r ) ) )
166171
val added = right.filterNot( r => left.exists( l => DiffShow.diffable( l, r ) ) )
167-
val comparable = left.filter( l => right.exists( r => DiffShow.diffable( l, r ) ) )
172+
val comparableLeft = left.filter( l => right.exists( r => DiffShow.diffable( l, r ) ) )
173+
val comparableRight = right.filter( l => left.exists( r => DiffShow.diffable( l, r ) ) )
168174
val identical = for {
169-
l <- comparable
170-
r <- comparable if DiffShow.diffable( l, r )
175+
l <- comparableLeft
176+
r <- comparableRight if DiffShow.diffable( l, r )
171177
Identical( s ) <- DiffShow.diff( l, r ) :: Nil
172178
} yield l
173179

174180
val changed = for {
175-
l <- comparable diff identical
176-
r <- comparable diff identical if DiffShow.diffable( l, r )
181+
l <- comparableLeft diff identical
182+
r <- comparableRight diff identical if DiffShow.diffable( l, r )
177183
// The pattern match here is fishy. It should really only contain Different ones, but might not if == is screwed up.
178184
Different( s ) <- DiffShow.diff( l, r ) :: Nil
179185
} yield s
@@ -221,7 +227,6 @@ abstract class DiffShowInstances extends DiffShowInstancesLowPriority {
221227
show( added ).map( ( arrow _ ).tupled ).map( green )
222228
).flatten.map( s => Option( ( "", s ) ) )
223229
)
224-
225230
if ( removed.isEmpty && added.isEmpty && changed.isEmpty )
226231
Identical( string )
227232
else

example-output.png

28.4 KB
Loading

test/Main.scala

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ai.x.diff._
2+
import scala.collection.immutable.SortedMap
23
object Main extends App {
34
sealed trait Parent
45
case class Bar( s: String, i: Int ) extends Parent
@@ -18,4 +19,59 @@ object Main extends App {
1819
println(
1920
DiffShow.diff( before, after ).string
2021
)
22+
23+
{
24+
implicit def StringDiffShow = new DiffShow[String] {
25+
def show( t: String ) = t
26+
def diff( left: String, right: String ) = if(left == right) Identical(left) else Different(left, right)
27+
override def diffable( left: String, right: String ) = left.lift(0) == right.lift(0)
28+
}
29+
30+
println(
31+
DiffShow.diff(
32+
"adsf" :: "qwer" :: Nil,
33+
"axx" :: "yxcv" :: Nil
34+
).string
35+
)
36+
}
37+
38+
/*
39+
40+
//import pprint.Config.Defaults._
41+
42+
val actual = compare( before, after )
43+
val expected = Different(
44+
Tree( before ),
45+
Tree( after ),
46+
SortedMap(
47+
"bar" -> Different(
48+
Tree( Bar( "asdf", 5 ) ),
49+
Tree( Bar( "asdf", 66 ) ),
50+
SortedMap(
51+
"s" -> Identical( Leaf( "asdf" ) ),
52+
"i" -> Different(
53+
Leaf( 5 ), Leaf( 66 ), SortedMap()
54+
)
55+
)
56+
),
57+
"b" -> Different(
58+
Leaf( 123 :: 1234 :: Nil ), Leaf( 1234 :: Nil ), SortedMap()
59+
)
60+
)
61+
)
62+
//pprint.pprintln( ( Generic[Bar] to Bar( "asdf", 5 ) ) delta ( Generic[Bar] to Bar( "asdf", 66 ) ) )
63+
assert( actual == expected, "expected\n:" + pprint.tokenize( expected ).mkString + "\n\nactual:\n" + pprint.tokenize( actual ).mkString )
64+
65+
println( expected.show() )
66+
67+
"""
68+
Foo(
69+
b = List( 123 +, 1234 ),
70+
bar = Bar(
71+
s = asdf,
72+
i = -5 +66
73+
)
74+
)
75+
"""
76+
*/
2177
}

0 commit comments

Comments
 (0)