report_slices.go 20 KB

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