├── go.mod ├── diff_test.go ├── LICENSE └── diff.go /go.mod: -------------------------------------------------------------------------------- 1 | module rsc.io/diff 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /diff_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package diff 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | var formatTests = []struct { 13 | text1 string 14 | text2 string 15 | diff string 16 | }{ 17 | {"a b c", "a b d e f", "a b -c +d +e +f"}, 18 | {"", "a b c", "+a +b +c"}, 19 | {"a b c", "", "-a -b -c"}, 20 | {"a b c", "d e f", "-a -b -c +d +e +f"}, 21 | {"a b c d e f", "a b d e f", "a b -c d e f"}, 22 | {"a b c e f", "a b c d e f", "a b c +d e f"}, 23 | } 24 | 25 | func TestFormat(t *testing.T) { 26 | for _, tt := range formatTests { 27 | // Turn spaces into \n. 28 | text1 := strings.ReplaceAll(tt.text1, " ", "\n") 29 | if text1 != "" { 30 | text1 += "\n" 31 | } 32 | text2 := strings.ReplaceAll(tt.text2, " ", "\n") 33 | if text2 != "" { 34 | text2 += "\n" 35 | } 36 | out := Format(text1, text2) 37 | // Cut final \n, cut spaces, turn remaining \n into spaces. 38 | out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ") 39 | if out != tt.diff { 40 | t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /diff.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package diff provides basic text comparison (like Unix's diff(1)). 6 | package diff 7 | 8 | import ( 9 | "fmt" 10 | "strings" 11 | ) 12 | 13 | // Format returns a formatted diff of the two texts, 14 | // showing the entire text and the minimum line-level 15 | // additions and removals to turn text1 into text2. 16 | // (That is, lines only in text1 appear with a leading -, 17 | // and lines only in text2 appear with a leading +.) 18 | func Format(text1, text2 string) string { 19 | if text1 != "" && !strings.HasSuffix(text1, "\n") { 20 | text1 += "(missing final newline)" 21 | } 22 | lines1 := strings.Split(text1, "\n") 23 | lines1 = lines1[:len(lines1)-1] // remove empty string after final line 24 | if text2 != "" && !strings.HasSuffix(text2, "\n") { 25 | text2 += "(missing final newline)" 26 | } 27 | lines2 := strings.Split(text2, "\n") 28 | lines2 = lines2[:len(lines2)-1] // remove empty string after final line 29 | 30 | // Naive dynamic programming algorithm for edit distance. 31 | // https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm 32 | // dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j] 33 | // (The reversed indices make following the minimum cost path 34 | // visit lines in the same order as in the text.) 35 | dist := make([][]int, len(lines1)+1) 36 | for i := range dist { 37 | dist[i] = make([]int, len(lines2)+1) 38 | if i == 0 { 39 | for j := range dist[0] { 40 | dist[0][j] = j 41 | } 42 | continue 43 | } 44 | for j := range dist[i] { 45 | if j == 0 { 46 | dist[i][0] = i 47 | continue 48 | } 49 | cost := dist[i][j-1] + 1 50 | if cost > dist[i-1][j]+1 { 51 | cost = dist[i-1][j] + 1 52 | } 53 | if lines1[len(lines1)-i] == lines2[len(lines2)-j] { 54 | if cost > dist[i-1][j-1] { 55 | cost = dist[i-1][j-1] 56 | } 57 | } 58 | dist[i][j] = cost 59 | } 60 | } 61 | 62 | var buf strings.Builder 63 | i, j := len(lines1), len(lines2) 64 | for i > 0 || j > 0 { 65 | cost := dist[i][j] 66 | if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] { 67 | fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i]) 68 | i-- 69 | j-- 70 | } else if i > 0 && cost == dist[i-1][j]+1 { 71 | fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i]) 72 | i-- 73 | } else { 74 | fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j]) 75 | j-- 76 | } 77 | } 78 | return buf.String() 79 | } 80 | --------------------------------------------------------------------------------