...

Source file src/github.com/google/go-cmp/cmp/report_slices.go

Documentation: github.com/google/go-cmp/cmp

     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 cmp
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"math"
    11  	"reflect"
    12  	"strconv"
    13  	"strings"
    14  	"unicode"
    15  	"unicode/utf8"
    16  
    17  	"github.com/google/go-cmp/cmp/internal/diff"
    18  )
    19  
    20  // CanFormatDiffSlice reports whether we support custom formatting for nodes
    21  // that are slices of primitive kinds or strings.
    22  func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool {
    23  	switch {
    24  	case opts.DiffMode != diffUnknown:
    25  		return false // Must be formatting in diff mode
    26  	case v.NumDiff == 0:
    27  		return false // No differences detected
    28  	case !v.ValueX.IsValid() || !v.ValueY.IsValid():
    29  		return false // Both values must be valid
    30  	case v.NumIgnored > 0:
    31  		return false // Some ignore option was used
    32  	case v.NumTransformed > 0:
    33  		return false // Some transform option was used
    34  	case v.NumCompared > 1:
    35  		return false // More than one comparison was used
    36  	case v.NumCompared == 1 && v.Type.Name() != "":
    37  		// The need for cmp to check applicability of options on every element
    38  		// in a slice is a significant performance detriment for large []byte.
    39  		// The workaround is to specify Comparer(bytes.Equal),
    40  		// which enables cmp to compare []byte more efficiently.
    41  		// If they differ, we still want to provide batched diffing.
    42  		// The logic disallows named types since they tend to have their own
    43  		// String method, with nicer formatting than what this provides.
    44  		return false
    45  	}
    46  
    47  	// Check whether this is an interface with the same concrete types.
    48  	t := v.Type
    49  	vx, vy := v.ValueX, v.ValueY
    50  	if t.Kind() == reflect.Interface && !vx.IsNil() && !vy.IsNil() && vx.Elem().Type() == vy.Elem().Type() {
    51  		vx, vy = vx.Elem(), vy.Elem()
    52  		t = vx.Type()
    53  	}
    54  
    55  	// Check whether we provide specialized diffing for this type.
    56  	switch t.Kind() {
    57  	case reflect.String:
    58  	case reflect.Array, reflect.Slice:
    59  		// Only slices of primitive types have specialized handling.
    60  		switch t.Elem().Kind() {
    61  		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
    62  			reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
    63  			reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
    64  		default:
    65  			return false
    66  		}
    67  
    68  		// Both slice values have to be non-empty.
    69  		if t.Kind() == reflect.Slice && (vx.Len() == 0 || vy.Len() == 0) {
    70  			return false
    71  		}
    72  
    73  		// If a sufficient number of elements already differ,
    74  		// use specialized formatting even if length requirement is not met.
    75  		if v.NumDiff > v.NumSame {
    76  			return true
    77  		}
    78  	default:
    79  		return false
    80  	}
    81  
    82  	// Use specialized string diffing for longer slices or strings.
    83  	const minLength = 32
    84  	return vx.Len() >= minLength && vy.Len() >= minLength
    85  }
    86  
    87  // FormatDiffSlice prints a diff for the slices (or strings) represented by v.
    88  // This provides custom-tailored logic to make printing of differences in
    89  // textual strings and slices of primitive kinds more readable.
    90  func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
    91  	assert(opts.DiffMode == diffUnknown)
    92  	t, vx, vy := v.Type, v.ValueX, v.ValueY
    93  	if t.Kind() == reflect.Interface {
    94  		vx, vy = vx.Elem(), vy.Elem()
    95  		t = vx.Type()
    96  		opts = opts.WithTypeMode(emitType)
    97  	}
    98  
    99  	// Auto-detect the type of the data.
   100  	var sx, sy string
   101  	var ssx, ssy []string
   102  	var isString, isMostlyText, isPureLinedText, isBinary bool
   103  	switch {
   104  	case t.Kind() == reflect.String:
   105  		sx, sy = vx.String(), vy.String()
   106  		isString = true
   107  	case t.Kind() == reflect.Slice && t.Elem() == byteType:
   108  		sx, sy = string(vx.Bytes()), string(vy.Bytes())
   109  		isString = true
   110  	case t.Kind() == reflect.Array:
   111  		// Arrays need to be addressable for slice operations to work.
   112  		vx2, vy2 := reflect.New(t).Elem(), reflect.New(t).Elem()
   113  		vx2.Set(vx)
   114  		vy2.Set(vy)
   115  		vx, vy = vx2, vy2
   116  	}
   117  	if isString {
   118  		var numTotalRunes, numValidRunes, numLines, lastLineIdx, maxLineLen int
   119  		for i, r := range sx + sy {
   120  			numTotalRunes++
   121  			if (unicode.IsPrint(r) || unicode.IsSpace(r)) && r != utf8.RuneError {
   122  				numValidRunes++
   123  			}
   124  			if r == '\n' {
   125  				if maxLineLen < i-lastLineIdx {
   126  					maxLineLen = i - lastLineIdx
   127  				}
   128  				lastLineIdx = i + 1
   129  				numLines++
   130  			}
   131  		}
   132  		isPureText := numValidRunes == numTotalRunes
   133  		isMostlyText = float64(numValidRunes) > math.Floor(0.90*float64(numTotalRunes))
   134  		isPureLinedText = isPureText && numLines >= 4 && maxLineLen <= 1024
   135  		isBinary = !isMostlyText
   136  
   137  		// Avoid diffing by lines if it produces a significantly more complex
   138  		// edit script than diffing by bytes.
   139  		if isPureLinedText {
   140  			ssx = strings.Split(sx, "\n")
   141  			ssy = strings.Split(sy, "\n")
   142  			esLines := diff.Difference(len(ssx), len(ssy), func(ix, iy int) diff.Result {
   143  				return diff.BoolResult(ssx[ix] == ssy[iy])
   144  			})
   145  			esBytes := diff.Difference(len(sx), len(sy), func(ix, iy int) diff.Result {
   146  				return diff.BoolResult(sx[ix] == sy[iy])
   147  			})
   148  			efficiencyLines := float64(esLines.Dist()) / float64(len(esLines))
   149  			efficiencyBytes := float64(esBytes.Dist()) / float64(len(esBytes))
   150  			quotedLength := len(strconv.Quote(sx + sy))
   151  			unquotedLength := len(sx) + len(sy)
   152  			escapeExpansionRatio := float64(quotedLength) / float64(unquotedLength)
   153  			isPureLinedText = efficiencyLines < 4*efficiencyBytes || escapeExpansionRatio > 1.1
   154  		}
   155  	}
   156  
   157  	// Format the string into printable records.
   158  	var list textList
   159  	var delim string
   160  	switch {
   161  	// If the text appears to be multi-lined text,
   162  	// then perform differencing across individual lines.
   163  	case isPureLinedText:
   164  		list = opts.formatDiffSlice(
   165  			reflect.ValueOf(ssx), reflect.ValueOf(ssy), 1, "line",
   166  			func(v reflect.Value, d diffMode) textRecord {
   167  				s := formatString(v.Index(0).String())
   168  				return textRecord{Diff: d, Value: textLine(s)}
   169  			},
   170  		)
   171  		delim = "\n"
   172  
   173  		// If possible, use a custom triple-quote (""") syntax for printing
   174  		// differences in a string literal. This format is more readable,
   175  		// but has edge-cases where differences are visually indistinguishable.
   176  		// This format is avoided under the following conditions:
   177  		//   - A line starts with `"""`
   178  		//   - A line starts with "..."
   179  		//   - A line contains non-printable characters
   180  		//   - Adjacent different lines differ only by whitespace
   181  		//
   182  		// For example:
   183  		//
   184  		//		"""
   185  		//		... // 3 identical lines
   186  		//		foo
   187  		//		bar
   188  		//	-	baz
   189  		//	+	BAZ
   190  		//		"""
   191  		isTripleQuoted := true
   192  		prevRemoveLines := map[string]bool{}
   193  		prevInsertLines := map[string]bool{}
   194  		var list2 textList
   195  		list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true})
   196  		for _, r := range list {
   197  			if !r.Value.Equal(textEllipsis) {
   198  				line, _ := strconv.Unquote(string(r.Value.(textLine)))
   199  				line = strings.TrimPrefix(strings.TrimSuffix(line, "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support
   200  				normLine := strings.Map(func(r rune) rune {
   201  					if unicode.IsSpace(r) {
   202  						return -1 // drop whitespace to avoid visually indistinguishable output
   203  					}
   204  					return r
   205  				}, line)
   206  				isPrintable := func(r rune) bool {
   207  					return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable
   208  				}
   209  				isTripleQuoted = !strings.HasPrefix(line, `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == ""
   210  				switch r.Diff {
   211  				case diffRemoved:
   212  					isTripleQuoted = isTripleQuoted && !prevInsertLines[normLine]
   213  					prevRemoveLines[normLine] = true
   214  				case diffInserted:
   215  					isTripleQuoted = isTripleQuoted && !prevRemoveLines[normLine]
   216  					prevInsertLines[normLine] = true
   217  				}
   218  				if !isTripleQuoted {
   219  					break
   220  				}
   221  				r.Value = textLine(line)
   222  				r.ElideComma = true
   223  			}
   224  			if !(r.Diff == diffRemoved || r.Diff == diffInserted) { // start a new non-adjacent difference group
   225  				prevRemoveLines = map[string]bool{}
   226  				prevInsertLines = map[string]bool{}
   227  			}
   228  			list2 = append(list2, r)
   229  		}
   230  		if r := list2[len(list2)-1]; r.Diff == diffIdentical && len(r.Value.(textLine)) == 0 {
   231  			list2 = list2[:len(list2)-1] // elide single empty line at the end
   232  		}
   233  		list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true})
   234  		if isTripleQuoted {
   235  			var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"}
   236  			switch t.Kind() {
   237  			case reflect.String:
   238  				if t != stringType {
   239  					out = opts.FormatType(t, out)
   240  				}
   241  			case reflect.Slice:
   242  				// Always emit type for slices since the triple-quote syntax
   243  				// looks like a string (not a slice).
   244  				opts = opts.WithTypeMode(emitType)
   245  				out = opts.FormatType(t, out)
   246  			}
   247  			return out
   248  		}
   249  
   250  	// If the text appears to be single-lined text,
   251  	// then perform differencing in approximately fixed-sized chunks.
   252  	// The output is printed as quoted strings.
   253  	case isMostlyText:
   254  		list = opts.formatDiffSlice(
   255  			reflect.ValueOf(sx), reflect.ValueOf(sy), 64, "byte",
   256  			func(v reflect.Value, d diffMode) textRecord {
   257  				s := formatString(v.String())
   258  				return textRecord{Diff: d, Value: textLine(s)}
   259  			},
   260  		)
   261  
   262  	// If the text appears to be binary data,
   263  	// then perform differencing in approximately fixed-sized chunks.
   264  	// The output is inspired by hexdump.
   265  	case isBinary:
   266  		list = opts.formatDiffSlice(
   267  			reflect.ValueOf(sx), reflect.ValueOf(sy), 16, "byte",
   268  			func(v reflect.Value, d diffMode) textRecord {
   269  				var ss []string
   270  				for i := 0; i < v.Len(); i++ {
   271  					ss = append(ss, formatHex(v.Index(i).Uint()))
   272  				}
   273  				s := strings.Join(ss, ", ")
   274  				comment := commentString(fmt.Sprintf("%c|%v|", d, formatASCII(v.String())))
   275  				return textRecord{Diff: d, Value: textLine(s), Comment: comment}
   276  			},
   277  		)
   278  
   279  	// For all other slices of primitive types,
   280  	// then perform differencing in approximately fixed-sized chunks.
   281  	// The size of each chunk depends on the width of the element kind.
   282  	default:
   283  		var chunkSize int
   284  		if t.Elem().Kind() == reflect.Bool {
   285  			chunkSize = 16
   286  		} else {
   287  			switch t.Elem().Bits() {
   288  			case 8:
   289  				chunkSize = 16
   290  			case 16:
   291  				chunkSize = 12
   292  			case 32:
   293  				chunkSize = 8
   294  			default:
   295  				chunkSize = 8
   296  			}
   297  		}
   298  		list = opts.formatDiffSlice(
   299  			vx, vy, chunkSize, t.Elem().Kind().String(),
   300  			func(v reflect.Value, d diffMode) textRecord {
   301  				var ss []string
   302  				for i := 0; i < v.Len(); i++ {
   303  					switch t.Elem().Kind() {
   304  					case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
   305  						ss = append(ss, fmt.Sprint(v.Index(i).Int()))
   306  					case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
   307  						ss = append(ss, fmt.Sprint(v.Index(i).Uint()))
   308  					case reflect.Uint8, reflect.Uintptr:
   309  						ss = append(ss, formatHex(v.Index(i).Uint()))
   310  					case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
   311  						ss = append(ss, fmt.Sprint(v.Index(i).Interface()))
   312  					}
   313  				}
   314  				s := strings.Join(ss, ", ")
   315  				return textRecord{Diff: d, Value: textLine(s)}
   316  			},
   317  		)
   318  	}
   319  
   320  	// Wrap the output with appropriate type information.
   321  	var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
   322  	if !isMostlyText {
   323  		// The "{...}" byte-sequence literal is not valid Go syntax for strings.
   324  		// Emit the type for extra clarity (e.g. "string{...}").
   325  		if t.Kind() == reflect.String {
   326  			opts = opts.WithTypeMode(emitType)
   327  		}
   328  		return opts.FormatType(t, out)
   329  	}
   330  	switch t.Kind() {
   331  	case reflect.String:
   332  		out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
   333  		if t != stringType {
   334  			out = opts.FormatType(t, out)
   335  		}
   336  	case reflect.Slice:
   337  		out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
   338  		if t != bytesType {
   339  			out = opts.FormatType(t, out)
   340  		}
   341  	}
   342  	return out
   343  }
   344  
   345  // formatASCII formats s as an ASCII string.
   346  // This is useful for printing binary strings in a semi-legible way.
   347  func formatASCII(s string) string {
   348  	b := bytes.Repeat([]byte{'.'}, len(s))
   349  	for i := 0; i < len(s); i++ {
   350  		if ' ' <= s[i] && s[i] <= '~' {
   351  			b[i] = s[i]
   352  		}
   353  	}
   354  	return string(b)
   355  }
   356  
   357  func (opts formatOptions) formatDiffSlice(
   358  	vx, vy reflect.Value, chunkSize int, name string,
   359  	makeRec func(reflect.Value, diffMode) textRecord,
   360  ) (list textList) {
   361  	eq := func(ix, iy int) bool {
   362  		return vx.Index(ix).Interface() == vy.Index(iy).Interface()
   363  	}
   364  	es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result {
   365  		return diff.BoolResult(eq(ix, iy))
   366  	})
   367  
   368  	appendChunks := func(v reflect.Value, d diffMode) int {
   369  		n0 := v.Len()
   370  		for v.Len() > 0 {
   371  			n := chunkSize
   372  			if n > v.Len() {
   373  				n = v.Len()
   374  			}
   375  			list = append(list, makeRec(v.Slice(0, n), d))
   376  			v = v.Slice(n, v.Len())
   377  		}
   378  		return n0 - v.Len()
   379  	}
   380  
   381  	var numDiffs int
   382  	maxLen := -1
   383  	if opts.LimitVerbosity {
   384  		maxLen = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc...
   385  		opts.VerbosityLevel--
   386  	}
   387  
   388  	groups := coalesceAdjacentEdits(name, es)
   389  	groups = coalesceInterveningIdentical(groups, chunkSize/4)
   390  	groups = cleanupSurroundingIdentical(groups, eq)
   391  	maxGroup := diffStats{Name: name}
   392  	for i, ds := range groups {
   393  		if maxLen >= 0 && numDiffs >= maxLen {
   394  			maxGroup = maxGroup.Append(ds)
   395  			continue
   396  		}
   397  
   398  		// Print equal.
   399  		if ds.NumDiff() == 0 {
   400  			// Compute the number of leading and trailing equal bytes to print.
   401  			var numLo, numHi int
   402  			numEqual := ds.NumIgnored + ds.NumIdentical
   403  			for numLo < chunkSize*numContextRecords && numLo+numHi < numEqual && i != 0 {
   404  				numLo++
   405  			}
   406  			for numHi < chunkSize*numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
   407  				numHi++
   408  			}
   409  			if numEqual-(numLo+numHi) <= chunkSize && ds.NumIgnored == 0 {
   410  				numHi = numEqual - numLo // Avoid pointless coalescing of single equal row
   411  			}
   412  
   413  			// Print the equal bytes.
   414  			appendChunks(vx.Slice(0, numLo), diffIdentical)
   415  			if numEqual > numLo+numHi {
   416  				ds.NumIdentical -= numLo + numHi
   417  				list.AppendEllipsis(ds)
   418  			}
   419  			appendChunks(vx.Slice(numEqual-numHi, numEqual), diffIdentical)
   420  			vx = vx.Slice(numEqual, vx.Len())
   421  			vy = vy.Slice(numEqual, vy.Len())
   422  			continue
   423  		}
   424  
   425  		// Print unequal.
   426  		len0 := len(list)
   427  		nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved)
   428  		vx = vx.Slice(nx, vx.Len())
   429  		ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted)
   430  		vy = vy.Slice(ny, vy.Len())
   431  		numDiffs += len(list) - len0
   432  	}
   433  	if maxGroup.IsZero() {
   434  		assert(vx.Len() == 0 && vy.Len() == 0)
   435  	} else {
   436  		list.AppendEllipsis(maxGroup)
   437  	}
   438  	return list
   439  }
   440  
   441  // coalesceAdjacentEdits coalesces the list of edits into groups of adjacent
   442  // equal or unequal counts.
   443  //
   444  // Example:
   445  //
   446  //	Input:  "..XXY...Y"
   447  //	Output: [
   448  //		{NumIdentical: 2},
   449  //		{NumRemoved: 2, NumInserted 1},
   450  //		{NumIdentical: 3},
   451  //		{NumInserted: 1},
   452  //	]
   453  func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) {
   454  	var prevMode byte
   455  	lastStats := func(mode byte) *diffStats {
   456  		if prevMode != mode {
   457  			groups = append(groups, diffStats{Name: name})
   458  			prevMode = mode
   459  		}
   460  		return &groups[len(groups)-1]
   461  	}
   462  	for _, e := range es {
   463  		switch e {
   464  		case diff.Identity:
   465  			lastStats('=').NumIdentical++
   466  		case diff.UniqueX:
   467  			lastStats('!').NumRemoved++
   468  		case diff.UniqueY:
   469  			lastStats('!').NumInserted++
   470  		case diff.Modified:
   471  			lastStats('!').NumModified++
   472  		}
   473  	}
   474  	return groups
   475  }
   476  
   477  // coalesceInterveningIdentical coalesces sufficiently short (<= windowSize)
   478  // equal groups into adjacent unequal groups that currently result in a
   479  // dual inserted/removed printout. This acts as a high-pass filter to smooth
   480  // out high-frequency changes within the windowSize.
   481  //
   482  // Example:
   483  //
   484  //	WindowSize: 16,
   485  //	Input: [
   486  //		{NumIdentical: 61},              // group 0
   487  //		{NumRemoved: 3, NumInserted: 1}, // group 1
   488  //		{NumIdentical: 6},               // ├── coalesce
   489  //		{NumInserted: 2},                // ├── coalesce
   490  //		{NumIdentical: 1},               // ├── coalesce
   491  //		{NumRemoved: 9},                 // └── coalesce
   492  //		{NumIdentical: 64},              // group 2
   493  //		{NumRemoved: 3, NumInserted: 1}, // group 3
   494  //		{NumIdentical: 6},               // ├── coalesce
   495  //		{NumInserted: 2},                // ├── coalesce
   496  //		{NumIdentical: 1},               // ├── coalesce
   497  //		{NumRemoved: 7},                 // ├── coalesce
   498  //		{NumIdentical: 1},               // ├── coalesce
   499  //		{NumRemoved: 2},                 // └── coalesce
   500  //		{NumIdentical: 63},              // group 4
   501  //	]
   502  //	Output: [
   503  //		{NumIdentical: 61},
   504  //		{NumIdentical: 7, NumRemoved: 12, NumInserted: 3},
   505  //		{NumIdentical: 64},
   506  //		{NumIdentical: 8, NumRemoved: 12, NumInserted: 3},
   507  //		{NumIdentical: 63},
   508  //	]
   509  func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats {
   510  	groups, groupsOrig := groups[:0], groups
   511  	for i, ds := range groupsOrig {
   512  		if len(groups) >= 2 && ds.NumDiff() > 0 {
   513  			prev := &groups[len(groups)-2] // Unequal group
   514  			curr := &groups[len(groups)-1] // Equal group
   515  			next := &groupsOrig[i]         // Unequal group
   516  			hadX, hadY := prev.NumRemoved > 0, prev.NumInserted > 0
   517  			hasX, hasY := next.NumRemoved > 0, next.NumInserted > 0
   518  			if ((hadX || hasX) && (hadY || hasY)) && curr.NumIdentical <= windowSize {
   519  				*prev = prev.Append(*curr).Append(*next)
   520  				groups = groups[:len(groups)-1] // Truncate off equal group
   521  				continue
   522  			}
   523  		}
   524  		groups = append(groups, ds)
   525  	}
   526  	return groups
   527  }
   528  
   529  // cleanupSurroundingIdentical scans through all unequal groups, and
   530  // moves any leading sequence of equal elements to the preceding equal group and
   531  // moves and trailing sequence of equal elements to the succeeding equal group.
   532  //
   533  // This is necessary since coalesceInterveningIdentical may coalesce edit groups
   534  // together such that leading/trailing spans of equal elements becomes possible.
   535  // Note that this can occur even with an optimal diffing algorithm.
   536  //
   537  // Example:
   538  //
   539  //	Input: [
   540  //		{NumIdentical: 61},
   541  //		{NumIdentical: 1 , NumRemoved: 11, NumInserted: 2}, // assume 3 leading identical elements
   542  //		{NumIdentical: 67},
   543  //		{NumIdentical: 7, NumRemoved: 12, NumInserted: 3},  // assume 10 trailing identical elements
   544  //		{NumIdentical: 54},
   545  //	]
   546  //	Output: [
   547  //		{NumIdentical: 64}, // incremented by 3
   548  //		{NumRemoved: 9},
   549  //		{NumIdentical: 67},
   550  //		{NumRemoved: 9},
   551  //		{NumIdentical: 64}, // incremented by 10
   552  //	]
   553  func cleanupSurroundingIdentical(groups []diffStats, eq func(i, j int) bool) []diffStats {
   554  	var ix, iy int // indexes into sequence x and y
   555  	for i, ds := range groups {
   556  		// Handle equal group.
   557  		if ds.NumDiff() == 0 {
   558  			ix += ds.NumIdentical
   559  			iy += ds.NumIdentical
   560  			continue
   561  		}
   562  
   563  		// Handle unequal group.
   564  		nx := ds.NumIdentical + ds.NumRemoved + ds.NumModified
   565  		ny := ds.NumIdentical + ds.NumInserted + ds.NumModified
   566  		var numLeadingIdentical, numTrailingIdentical int
   567  		for j := 0; j < nx && j < ny && eq(ix+j, iy+j); j++ {
   568  			numLeadingIdentical++
   569  		}
   570  		for j := 0; j < nx && j < ny && eq(ix+nx-1-j, iy+ny-1-j); j++ {
   571  			numTrailingIdentical++
   572  		}
   573  		if numIdentical := numLeadingIdentical + numTrailingIdentical; numIdentical > 0 {
   574  			if numLeadingIdentical > 0 {
   575  				// Remove leading identical span from this group and
   576  				// insert it into the preceding group.
   577  				if i-1 >= 0 {
   578  					groups[i-1].NumIdentical += numLeadingIdentical
   579  				} else {
   580  					// No preceding group exists, so prepend a new group,
   581  					// but do so after we finish iterating over all groups.
   582  					defer func() {
   583  						groups = append([]diffStats{{Name: groups[0].Name, NumIdentical: numLeadingIdentical}}, groups...)
   584  					}()
   585  				}
   586  				// Increment indexes since the preceding group would have handled this.
   587  				ix += numLeadingIdentical
   588  				iy += numLeadingIdentical
   589  			}
   590  			if numTrailingIdentical > 0 {
   591  				// Remove trailing identical span from this group and
   592  				// insert it into the succeeding group.
   593  				if i+1 < len(groups) {
   594  					groups[i+1].NumIdentical += numTrailingIdentical
   595  				} else {
   596  					// No succeeding group exists, so append a new group,
   597  					// but do so after we finish iterating over all groups.
   598  					defer func() {
   599  						groups = append(groups, diffStats{Name: groups[len(groups)-1].Name, NumIdentical: numTrailingIdentical})
   600  					}()
   601  				}
   602  				// Do not increment indexes since the succeeding group will handle this.
   603  			}
   604  
   605  			// Update this group since some identical elements were removed.
   606  			nx -= numIdentical
   607  			ny -= numIdentical
   608  			groups[i] = diffStats{Name: ds.Name, NumRemoved: nx, NumInserted: ny}
   609  		}
   610  		ix += nx
   611  		iy += ny
   612  	}
   613  	return groups
   614  }
   615  

View as plain text