Skip to content

Commit

Permalink
issues/60: Add ability to diff items (#67)
Browse files Browse the repository at this point in the history
- Fixes: #60

Thanks to;
- https://github.com/rsc/diff
  • Loading branch information
komuw authored Feb 22, 2024
1 parent f213215 commit 2c684e6
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Most recent version is listed first.


# v0.0.18
- Add ability to diff items: https://github.com/komuw/kama/pull/67

# v0.0.17
- Add ability to dump items with circular references: https://github.com/komuw/kama/pull/66

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,4 @@ go test -race ./... -count=1
5. https://github.com/alecthomas/repr
6. https://github.com/k0kubun/pp
7. https://github.com/jba/printsrc
8. https://github.com/kylelemons/godebug
82 changes: 82 additions & 0 deletions diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package diff provides basic text comparison (like Unix's diff(1)).
package kama

import (
"fmt"
"strings"
)

// Most of the code here is insipired by(or taken from):
// (a) https://github.com/rsc/diff whose license(BSD 3-Clause "New" or "Revised" License) can be found here: https://github.com/rsc/diff/blob/master/LICENSE

// diff returns a formatted diff of the two texts,
// showing the entire text and the minimum line-level
// additions and removals to turn text1 into text2.
// (That is, lines only in text1 appear with a leading -,
// and lines only in text2 appear with a leading +.)
func diff(text1, text2 string) string {
if text1 != "" && !strings.HasSuffix(text1, "\n") {
text1 += "(missing final newline)"
}
lines1 := strings.Split(text1, "\n")
lines1 = lines1[:len(lines1)-1] // remove empty string after final line
if text2 != "" && !strings.HasSuffix(text2, "\n") {
text2 += "(missing final newline)"
}
lines2 := strings.Split(text2, "\n")
lines2 = lines2[:len(lines2)-1] // remove empty string after final line

// Naive dynamic programming algorithm for edit distance.
// https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm
// dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j]
// (The reversed indices make following the minimum cost path
// visit lines in the same order as in the text.)
dist := make([][]int, len(lines1)+1)
for i := range dist {
dist[i] = make([]int, len(lines2)+1)
if i == 0 {
for j := range dist[0] {
dist[0][j] = j
}
continue
}
for j := range dist[i] {
if j == 0 {
dist[i][0] = i
continue
}
cost := dist[i][j-1] + 1
if cost > dist[i-1][j]+1 {
cost = dist[i-1][j] + 1
}
if lines1[len(lines1)-i] == lines2[len(lines2)-j] {
if cost > dist[i-1][j-1] {
cost = dist[i-1][j-1]
}
}
dist[i][j] = cost
}
}

var buf strings.Builder
i, j := len(lines1), len(lines2)
for i > 0 || j > 0 {
cost := dist[i][j]
if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] {
fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i])
i--
j--
} else if i > 0 && cost == dist[i-1][j]+1 {
fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i])
i--
} else {
fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j])
j--
}
}
return buf.String()
}
45 changes: 45 additions & 0 deletions diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package kama

import (
"strings"
"testing"
)

func TestSmallDiff(t *testing.T) {
t.Parallel()

tests := []struct {
text1 string
text2 string
diff string
}{
{"a b c", "a b d e f", "a b -c +d +e +f"},
{"", "a b c", "+a +b +c"},
{"a b c", "", "-a -b -c"},
{"a b c", "d e f", "-a -b -c +d +e +f"},
{"a b c d e f", "a b d e f", "a b -c d e f"},
{"a b c e f", "a b c d e f", "a b c +d e f"},
}

for _, tt := range tests {
// Turn spaces into \n.
text1 := strings.ReplaceAll(tt.text1, " ", "\n")
if text1 != "" {
text1 += "\n"
}
text2 := strings.ReplaceAll(tt.text2, " ", "\n")
if text2 != "" {
text2 += "\n"
}
out := diff(text1, text2)
// Cut final \n, cut spaces, turn remaining \n into spaces.
out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ")
if out != tt.diff {
t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff)
}
}
}
15 changes: 15 additions & 0 deletions kama.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,18 @@ func Dir(i interface{}, c ...Config) string {
func Stackp() {
stackp()
}

// Diffp prints a formatted diff showing the minimum line-level additions and removals that would turn old into new.
func Diffp(old, new interface{}, c ...Config) {
fmt.Println(
Diff(old, new, c...),
)
}

// Diff returns a formatted diff showing the minimum line-level additions and removals that would turn old into new.
func Diff(old, new interface{}, c ...Config) string {
return diff(
Dir(old, c...),
Dir(new, c...),
)
}
40 changes: 40 additions & 0 deletions kama_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,43 @@ func TestReadmeExamples(t *testing.T) {
})
}
}

func TestDiff(t *testing.T) {
t.Parallel()

tt := []struct {
tName string
old interface{}
new interface{}
}{
{
tName: "package compress/flate",
old: "compress/flate",
new: "compress/flate",
},
{
tName: "errors",
old: "errors",
new: "github.com/pkg/errors",
},
{
tName: "http Request",
old: http.Request{Method: "GET"},
new: http.Request{Method: "POST"},
},
}

for _, v := range tt {
v := v
tName := fmt.Sprintf("TestDiff-%s", v.tName)

t.Run(tName, func(t *testing.T) {
t.Parallel()

res := Diff(v.old, v.new)

path := getDataPath(t, "kama_test.go", tName)
dealWithTestData(t, path, res)
})
}
}
29 changes: 29 additions & 0 deletions testdata/kama_test/TestDiff-errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

[
-NAME: errors
+NAME: github.com/pkg/errors
CONSTANTS: []
VARIABLES: []
FUNCTIONS: [
- As(err error, target any) bool
+ As(err error, target interface{}) bool
+ Cause(err error) error
+ Errorf(format string, args ...interface{}) error
Is(err error, target error) bool
- Join(errs ...error) error
- New(text string) error
+ New(message string) error
Unwrap(err error) error
+ WithMessage(err error, message string) error
+ WithMessagef(err error, format string, args ...interface{}) error
+ WithStack(err error) error
+ Wrap(err error, message string) error
+ Wrapf(err error, format string, args ...interface{}) error
]
-TYPES: []
+TYPES: [
+ Frame uintptr
+ (Frame) Format(s fmt.State, verb rune)
+ (Frame) MarshalText() ([]byte, error)
+ StackTrace []Frame
+ (StackTrace) Format(s fmt.State, verb rune)]
76 changes: 76 additions & 0 deletions testdata/kama_test/TestDiff-http_Request.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

[
NAME: net/http.Request
KIND: struct
SIGNATURE: [http.Request *http.Request]
FIELDS: [
Method string
URL *url.URL
Proto string
ProtoMajor int
ProtoMinor int
Header http.Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer http.Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct {}
Response *http.Response
]
METHODS: [
AddCookie func(*http.Request, *http.Cookie)
BasicAuth func(*http.Request) (string, string, bool)
Clone func(*http.Request, context.Context) *http.Request
Context func(*http.Request) context.Context
Cookie func(*http.Request, string) (*http.Cookie, error)
Cookies func(*http.Request) []*http.Cookie
FormFile func(*http.Request, string) (multipart.File, *multipart.FileHeader, error)
FormValue func(*http.Request, string) string
MultipartReader func(*http.Request) (*multipart.Reader, error)
ParseForm func(*http.Request) error
ParseMultipartForm func(*http.Request, int64) error
PathValue func(*http.Request, string) string
PostFormValue func(*http.Request, string) string
ProtoAtLeast func(*http.Request, int, int) bool
Referer func(*http.Request) string
SetBasicAuth func(*http.Request, string, string)
SetPathValue func(*http.Request, string, string)
UserAgent func(*http.Request) string
WithContext func(*http.Request, context.Context) *http.Request
Write func(*http.Request, io.Writer) error
WriteProxy func(*http.Request, io.Writer) error
]
SNIPPET: Request{
- Method: "GET",
+ Method: "POST",
URL: *url.URL(nil),
Proto: "",
ProtoMajor: int(0),
ProtoMinor: int(0),
Header: http.Header{(nil)},
Body: io.ReadCloser nil,
GetBody: func() (io.ReadCloser, error),
ContentLength: int64(0),
TransferEncoding: []string{(nil)},
Close: false,
Host: "",
Form: url.Values{(nil)},
PostForm: url.Values{(nil)},
MultipartForm: *multipart.Form(nil),
Trailer: http.Header{(nil)},
RemoteAddr: "",
RequestURI: "",
TLS: *tls.ConnectionState(nil),
Cancel: <-chan struct {} (len=0, cap=0),
Response: *http.Response(nil),
}
]
36 changes: 36 additions & 0 deletions testdata/kama_test/TestDiff-package_compress_flate.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

[
NAME: compress/flate
CONSTANTS: [
BestCompression untyped int
BestSpeed untyped int
DefaultCompression untyped int
HuffmanOnly untyped int
NoCompression untyped int
]
VARIABLES: []
FUNCTIONS: [
NewReader(r io.Reader) io.ReadCloser
NewReaderDict(r io.Reader, dict []byte) io.ReadCloser
NewWriter(w io.Writer, level int) (*Writer, error)
NewWriterDict(w io.Writer, level int, dict []byte) (*Writer, error)
]
TYPES: [
CorruptInputError int64
(CorruptInputError) Error() string
InternalError string
(InternalError) Error() string
ReadError struct
(*ReadError) Error() string
Reader interface
(Reader) Read(p []byte) (n int, err error)
(Reader) ReadByte() (byte, error)
Resetter interface
(Resetter) Reset(r io.Reader, dict []byte) error
WriteError struct
(*WriteError) Error() string
Writer struct
(*Writer) Close() error
(*Writer) Flush() error
(*Writer) Reset(dst io.Writer)
(*Writer) Write(data []byte) (n int, err error)]

0 comments on commit 2c684e6

Please sign in to comment.