xref: /aosp_15_r20/external/go-cmp/cmp/report_text.go (revision 88d15eac089d7f20c739ff1001d56b91872b21a1)
1*88d15eacSSasha Smundak// Copyright 2019, The Go Authors. All rights reserved.
2*88d15eacSSasha Smundak// Use of this source code is governed by a BSD-style
3*88d15eacSSasha Smundak// license that can be found in the LICENSE file.
4*88d15eacSSasha Smundak
5*88d15eacSSasha Smundakpackage cmp
6*88d15eacSSasha Smundak
7*88d15eacSSasha Smundakimport (
8*88d15eacSSasha Smundak	"bytes"
9*88d15eacSSasha Smundak	"fmt"
10*88d15eacSSasha Smundak	"math/rand"
11*88d15eacSSasha Smundak	"strings"
12*88d15eacSSasha Smundak	"time"
13*88d15eacSSasha Smundak	"unicode/utf8"
14*88d15eacSSasha Smundak
15*88d15eacSSasha Smundak	"github.com/google/go-cmp/cmp/internal/flags"
16*88d15eacSSasha Smundak)
17*88d15eacSSasha Smundak
18*88d15eacSSasha Smundakvar randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
19*88d15eacSSasha Smundak
20*88d15eacSSasha Smundakconst maxColumnLength = 80
21*88d15eacSSasha Smundak
22*88d15eacSSasha Smundaktype indentMode int
23*88d15eacSSasha Smundak
24*88d15eacSSasha Smundakfunc (n indentMode) appendIndent(b []byte, d diffMode) []byte {
25*88d15eacSSasha Smundak	// The output of Diff is documented as being unstable to provide future
26*88d15eacSSasha Smundak	// flexibility in changing the output for more humanly readable reports.
27*88d15eacSSasha Smundak	// This logic intentionally introduces instability to the exact output
28*88d15eacSSasha Smundak	// so that users can detect accidental reliance on stability early on,
29*88d15eacSSasha Smundak	// rather than much later when an actual change to the format occurs.
30*88d15eacSSasha Smundak	if flags.Deterministic || randBool {
31*88d15eacSSasha Smundak		// Use regular spaces (U+0020).
32*88d15eacSSasha Smundak		switch d {
33*88d15eacSSasha Smundak		case diffUnknown, diffIdentical:
34*88d15eacSSasha Smundak			b = append(b, "  "...)
35*88d15eacSSasha Smundak		case diffRemoved:
36*88d15eacSSasha Smundak			b = append(b, "- "...)
37*88d15eacSSasha Smundak		case diffInserted:
38*88d15eacSSasha Smundak			b = append(b, "+ "...)
39*88d15eacSSasha Smundak		}
40*88d15eacSSasha Smundak	} else {
41*88d15eacSSasha Smundak		// Use non-breaking spaces (U+00a0).
42*88d15eacSSasha Smundak		switch d {
43*88d15eacSSasha Smundak		case diffUnknown, diffIdentical:
44*88d15eacSSasha Smundak			b = append(b, "  "...)
45*88d15eacSSasha Smundak		case diffRemoved:
46*88d15eacSSasha Smundak			b = append(b, "- "...)
47*88d15eacSSasha Smundak		case diffInserted:
48*88d15eacSSasha Smundak			b = append(b, "+ "...)
49*88d15eacSSasha Smundak		}
50*88d15eacSSasha Smundak	}
51*88d15eacSSasha Smundak	return repeatCount(n).appendChar(b, '\t')
52*88d15eacSSasha Smundak}
53*88d15eacSSasha Smundak
54*88d15eacSSasha Smundaktype repeatCount int
55*88d15eacSSasha Smundak
56*88d15eacSSasha Smundakfunc (n repeatCount) appendChar(b []byte, c byte) []byte {
57*88d15eacSSasha Smundak	for ; n > 0; n-- {
58*88d15eacSSasha Smundak		b = append(b, c)
59*88d15eacSSasha Smundak	}
60*88d15eacSSasha Smundak	return b
61*88d15eacSSasha Smundak}
62*88d15eacSSasha Smundak
63*88d15eacSSasha Smundak// textNode is a simplified tree-based representation of structured text.
64*88d15eacSSasha Smundak// Possible node types are textWrap, textList, or textLine.
65*88d15eacSSasha Smundaktype textNode interface {
66*88d15eacSSasha Smundak	// Len reports the length in bytes of a single-line version of the tree.
67*88d15eacSSasha Smundak	// Nested textRecord.Diff and textRecord.Comment fields are ignored.
68*88d15eacSSasha Smundak	Len() int
69*88d15eacSSasha Smundak	// Equal reports whether the two trees are structurally identical.
70*88d15eacSSasha Smundak	// Nested textRecord.Diff and textRecord.Comment fields are compared.
71*88d15eacSSasha Smundak	Equal(textNode) bool
72*88d15eacSSasha Smundak	// String returns the string representation of the text tree.
73*88d15eacSSasha Smundak	// It is not guaranteed that len(x.String()) == x.Len(),
74*88d15eacSSasha Smundak	// nor that x.String() == y.String() implies that x.Equal(y).
75*88d15eacSSasha Smundak	String() string
76*88d15eacSSasha Smundak
77*88d15eacSSasha Smundak	// formatCompactTo formats the contents of the tree as a single-line string
78*88d15eacSSasha Smundak	// to the provided buffer. Any nested textRecord.Diff and textRecord.Comment
79*88d15eacSSasha Smundak	// fields are ignored.
80*88d15eacSSasha Smundak	//
81*88d15eacSSasha Smundak	// However, not all nodes in the tree should be collapsed as a single-line.
82*88d15eacSSasha Smundak	// If a node can be collapsed as a single-line, it is replaced by a textLine
83*88d15eacSSasha Smundak	// node. Since the top-level node cannot replace itself, this also returns
84*88d15eacSSasha Smundak	// the current node itself.
85*88d15eacSSasha Smundak	//
86*88d15eacSSasha Smundak	// This does not mutate the receiver.
87*88d15eacSSasha Smundak	formatCompactTo([]byte, diffMode) ([]byte, textNode)
88*88d15eacSSasha Smundak	// formatExpandedTo formats the contents of the tree as a multi-line string
89*88d15eacSSasha Smundak	// to the provided buffer. In order for column alignment to operate well,
90*88d15eacSSasha Smundak	// formatCompactTo must be called before calling formatExpandedTo.
91*88d15eacSSasha Smundak	formatExpandedTo([]byte, diffMode, indentMode) []byte
92*88d15eacSSasha Smundak}
93*88d15eacSSasha Smundak
94*88d15eacSSasha Smundak// textWrap is a wrapper that concatenates a prefix and/or a suffix
95*88d15eacSSasha Smundak// to the underlying node.
96*88d15eacSSasha Smundaktype textWrap struct {
97*88d15eacSSasha Smundak	Prefix   string      // e.g., "bytes.Buffer{"
98*88d15eacSSasha Smundak	Value    textNode    // textWrap | textList | textLine
99*88d15eacSSasha Smundak	Suffix   string      // e.g., "}"
100*88d15eacSSasha Smundak	Metadata interface{} // arbitrary metadata; has no effect on formatting
101*88d15eacSSasha Smundak}
102*88d15eacSSasha Smundak
103*88d15eacSSasha Smundakfunc (s *textWrap) Len() int {
104*88d15eacSSasha Smundak	return len(s.Prefix) + s.Value.Len() + len(s.Suffix)
105*88d15eacSSasha Smundak}
106*88d15eacSSasha Smundakfunc (s1 *textWrap) Equal(s2 textNode) bool {
107*88d15eacSSasha Smundak	if s2, ok := s2.(*textWrap); ok {
108*88d15eacSSasha Smundak		return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix
109*88d15eacSSasha Smundak	}
110*88d15eacSSasha Smundak	return false
111*88d15eacSSasha Smundak}
112*88d15eacSSasha Smundakfunc (s *textWrap) String() string {
113*88d15eacSSasha Smundak	var d diffMode
114*88d15eacSSasha Smundak	var n indentMode
115*88d15eacSSasha Smundak	_, s2 := s.formatCompactTo(nil, d)
116*88d15eacSSasha Smundak	b := n.appendIndent(nil, d)      // Leading indent
117*88d15eacSSasha Smundak	b = s2.formatExpandedTo(b, d, n) // Main body
118*88d15eacSSasha Smundak	b = append(b, '\n')              // Trailing newline
119*88d15eacSSasha Smundak	return string(b)
120*88d15eacSSasha Smundak}
121*88d15eacSSasha Smundakfunc (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
122*88d15eacSSasha Smundak	n0 := len(b) // Original buffer length
123*88d15eacSSasha Smundak	b = append(b, s.Prefix...)
124*88d15eacSSasha Smundak	b, s.Value = s.Value.formatCompactTo(b, d)
125*88d15eacSSasha Smundak	b = append(b, s.Suffix...)
126*88d15eacSSasha Smundak	if _, ok := s.Value.(textLine); ok {
127*88d15eacSSasha Smundak		return b, textLine(b[n0:])
128*88d15eacSSasha Smundak	}
129*88d15eacSSasha Smundak	return b, s
130*88d15eacSSasha Smundak}
131*88d15eacSSasha Smundakfunc (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
132*88d15eacSSasha Smundak	b = append(b, s.Prefix...)
133*88d15eacSSasha Smundak	b = s.Value.formatExpandedTo(b, d, n)
134*88d15eacSSasha Smundak	b = append(b, s.Suffix...)
135*88d15eacSSasha Smundak	return b
136*88d15eacSSasha Smundak}
137*88d15eacSSasha Smundak
138*88d15eacSSasha Smundak// textList is a comma-separated list of textWrap or textLine nodes.
139*88d15eacSSasha Smundak// The list may be formatted as multi-lines or single-line at the discretion
140*88d15eacSSasha Smundak// of the textList.formatCompactTo method.
141*88d15eacSSasha Smundaktype textList []textRecord
142*88d15eacSSasha Smundaktype textRecord struct {
143*88d15eacSSasha Smundak	Diff       diffMode     // e.g., 0 or '-' or '+'
144*88d15eacSSasha Smundak	Key        string       // e.g., "MyField"
145*88d15eacSSasha Smundak	Value      textNode     // textWrap | textLine
146*88d15eacSSasha Smundak	ElideComma bool         // avoid trailing comma
147*88d15eacSSasha Smundak	Comment    fmt.Stringer // e.g., "6 identical fields"
148*88d15eacSSasha Smundak}
149*88d15eacSSasha Smundak
150*88d15eacSSasha Smundak// AppendEllipsis appends a new ellipsis node to the list if none already
151*88d15eacSSasha Smundak// exists at the end. If cs is non-zero it coalesces the statistics with the
152*88d15eacSSasha Smundak// previous diffStats.
153*88d15eacSSasha Smundakfunc (s *textList) AppendEllipsis(ds diffStats) {
154*88d15eacSSasha Smundak	hasStats := !ds.IsZero()
155*88d15eacSSasha Smundak	if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {
156*88d15eacSSasha Smundak		if hasStats {
157*88d15eacSSasha Smundak			*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds})
158*88d15eacSSasha Smundak		} else {
159*88d15eacSSasha Smundak			*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true})
160*88d15eacSSasha Smundak		}
161*88d15eacSSasha Smundak		return
162*88d15eacSSasha Smundak	}
163*88d15eacSSasha Smundak	if hasStats {
164*88d15eacSSasha Smundak		(*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds)
165*88d15eacSSasha Smundak	}
166*88d15eacSSasha Smundak}
167*88d15eacSSasha Smundak
168*88d15eacSSasha Smundakfunc (s textList) Len() (n int) {
169*88d15eacSSasha Smundak	for i, r := range s {
170*88d15eacSSasha Smundak		n += len(r.Key)
171*88d15eacSSasha Smundak		if r.Key != "" {
172*88d15eacSSasha Smundak			n += len(": ")
173*88d15eacSSasha Smundak		}
174*88d15eacSSasha Smundak		n += r.Value.Len()
175*88d15eacSSasha Smundak		if i < len(s)-1 {
176*88d15eacSSasha Smundak			n += len(", ")
177*88d15eacSSasha Smundak		}
178*88d15eacSSasha Smundak	}
179*88d15eacSSasha Smundak	return n
180*88d15eacSSasha Smundak}
181*88d15eacSSasha Smundak
182*88d15eacSSasha Smundakfunc (s1 textList) Equal(s2 textNode) bool {
183*88d15eacSSasha Smundak	if s2, ok := s2.(textList); ok {
184*88d15eacSSasha Smundak		if len(s1) != len(s2) {
185*88d15eacSSasha Smundak			return false
186*88d15eacSSasha Smundak		}
187*88d15eacSSasha Smundak		for i := range s1 {
188*88d15eacSSasha Smundak			r1, r2 := s1[i], s2[i]
189*88d15eacSSasha Smundak			if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) {
190*88d15eacSSasha Smundak				return false
191*88d15eacSSasha Smundak			}
192*88d15eacSSasha Smundak		}
193*88d15eacSSasha Smundak		return true
194*88d15eacSSasha Smundak	}
195*88d15eacSSasha Smundak	return false
196*88d15eacSSasha Smundak}
197*88d15eacSSasha Smundak
198*88d15eacSSasha Smundakfunc (s textList) String() string {
199*88d15eacSSasha Smundak	return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String()
200*88d15eacSSasha Smundak}
201*88d15eacSSasha Smundak
202*88d15eacSSasha Smundakfunc (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
203*88d15eacSSasha Smundak	s = append(textList(nil), s...) // Avoid mutating original
204*88d15eacSSasha Smundak
205*88d15eacSSasha Smundak	// Determine whether we can collapse this list as a single line.
206*88d15eacSSasha Smundak	n0 := len(b) // Original buffer length
207*88d15eacSSasha Smundak	var multiLine bool
208*88d15eacSSasha Smundak	for i, r := range s {
209*88d15eacSSasha Smundak		if r.Diff == diffInserted || r.Diff == diffRemoved {
210*88d15eacSSasha Smundak			multiLine = true
211*88d15eacSSasha Smundak		}
212*88d15eacSSasha Smundak		b = append(b, r.Key...)
213*88d15eacSSasha Smundak		if r.Key != "" {
214*88d15eacSSasha Smundak			b = append(b, ": "...)
215*88d15eacSSasha Smundak		}
216*88d15eacSSasha Smundak		b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff)
217*88d15eacSSasha Smundak		if _, ok := s[i].Value.(textLine); !ok {
218*88d15eacSSasha Smundak			multiLine = true
219*88d15eacSSasha Smundak		}
220*88d15eacSSasha Smundak		if r.Comment != nil {
221*88d15eacSSasha Smundak			multiLine = true
222*88d15eacSSasha Smundak		}
223*88d15eacSSasha Smundak		if i < len(s)-1 {
224*88d15eacSSasha Smundak			b = append(b, ", "...)
225*88d15eacSSasha Smundak		}
226*88d15eacSSasha Smundak	}
227*88d15eacSSasha Smundak	// Force multi-lined output when printing a removed/inserted node that
228*88d15eacSSasha Smundak	// is sufficiently long.
229*88d15eacSSasha Smundak	if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength {
230*88d15eacSSasha Smundak		multiLine = true
231*88d15eacSSasha Smundak	}
232*88d15eacSSasha Smundak	if !multiLine {
233*88d15eacSSasha Smundak		return b, textLine(b[n0:])
234*88d15eacSSasha Smundak	}
235*88d15eacSSasha Smundak	return b, s
236*88d15eacSSasha Smundak}
237*88d15eacSSasha Smundak
238*88d15eacSSasha Smundakfunc (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
239*88d15eacSSasha Smundak	alignKeyLens := s.alignLens(
240*88d15eacSSasha Smundak		func(r textRecord) bool {
241*88d15eacSSasha Smundak			_, isLine := r.Value.(textLine)
242*88d15eacSSasha Smundak			return r.Key == "" || !isLine
243*88d15eacSSasha Smundak		},
244*88d15eacSSasha Smundak		func(r textRecord) int { return utf8.RuneCountInString(r.Key) },
245*88d15eacSSasha Smundak	)
246*88d15eacSSasha Smundak	alignValueLens := s.alignLens(
247*88d15eacSSasha Smundak		func(r textRecord) bool {
248*88d15eacSSasha Smundak			_, isLine := r.Value.(textLine)
249*88d15eacSSasha Smundak			return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil
250*88d15eacSSasha Smundak		},
251*88d15eacSSasha Smundak		func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) },
252*88d15eacSSasha Smundak	)
253*88d15eacSSasha Smundak
254*88d15eacSSasha Smundak	// Format lists of simple lists in a batched form.
255*88d15eacSSasha Smundak	// If the list is sequence of only textLine values,
256*88d15eacSSasha Smundak	// then batch multiple values on a single line.
257*88d15eacSSasha Smundak	var isSimple bool
258*88d15eacSSasha Smundak	for _, r := range s {
259*88d15eacSSasha Smundak		_, isLine := r.Value.(textLine)
260*88d15eacSSasha Smundak		isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil
261*88d15eacSSasha Smundak		if !isSimple {
262*88d15eacSSasha Smundak			break
263*88d15eacSSasha Smundak		}
264*88d15eacSSasha Smundak	}
265*88d15eacSSasha Smundak	if isSimple {
266*88d15eacSSasha Smundak		n++
267*88d15eacSSasha Smundak		var batch []byte
268*88d15eacSSasha Smundak		emitBatch := func() {
269*88d15eacSSasha Smundak			if len(batch) > 0 {
270*88d15eacSSasha Smundak				b = n.appendIndent(append(b, '\n'), d)
271*88d15eacSSasha Smundak				b = append(b, bytes.TrimRight(batch, " ")...)
272*88d15eacSSasha Smundak				batch = batch[:0]
273*88d15eacSSasha Smundak			}
274*88d15eacSSasha Smundak		}
275*88d15eacSSasha Smundak		for _, r := range s {
276*88d15eacSSasha Smundak			line := r.Value.(textLine)
277*88d15eacSSasha Smundak			if len(batch)+len(line)+len(", ") > maxColumnLength {
278*88d15eacSSasha Smundak				emitBatch()
279*88d15eacSSasha Smundak			}
280*88d15eacSSasha Smundak			batch = append(batch, line...)
281*88d15eacSSasha Smundak			batch = append(batch, ", "...)
282*88d15eacSSasha Smundak		}
283*88d15eacSSasha Smundak		emitBatch()
284*88d15eacSSasha Smundak		n--
285*88d15eacSSasha Smundak		return n.appendIndent(append(b, '\n'), d)
286*88d15eacSSasha Smundak	}
287*88d15eacSSasha Smundak
288*88d15eacSSasha Smundak	// Format the list as a multi-lined output.
289*88d15eacSSasha Smundak	n++
290*88d15eacSSasha Smundak	for i, r := range s {
291*88d15eacSSasha Smundak		b = n.appendIndent(append(b, '\n'), d|r.Diff)
292*88d15eacSSasha Smundak		if r.Key != "" {
293*88d15eacSSasha Smundak			b = append(b, r.Key+": "...)
294*88d15eacSSasha Smundak		}
295*88d15eacSSasha Smundak		b = alignKeyLens[i].appendChar(b, ' ')
296*88d15eacSSasha Smundak
297*88d15eacSSasha Smundak		b = r.Value.formatExpandedTo(b, d|r.Diff, n)
298*88d15eacSSasha Smundak		if !r.ElideComma {
299*88d15eacSSasha Smundak			b = append(b, ',')
300*88d15eacSSasha Smundak		}
301*88d15eacSSasha Smundak		b = alignValueLens[i].appendChar(b, ' ')
302*88d15eacSSasha Smundak
303*88d15eacSSasha Smundak		if r.Comment != nil {
304*88d15eacSSasha Smundak			b = append(b, " // "+r.Comment.String()...)
305*88d15eacSSasha Smundak		}
306*88d15eacSSasha Smundak	}
307*88d15eacSSasha Smundak	n--
308*88d15eacSSasha Smundak
309*88d15eacSSasha Smundak	return n.appendIndent(append(b, '\n'), d)
310*88d15eacSSasha Smundak}
311*88d15eacSSasha Smundak
312*88d15eacSSasha Smundakfunc (s textList) alignLens(
313*88d15eacSSasha Smundak	skipFunc func(textRecord) bool,
314*88d15eacSSasha Smundak	lenFunc func(textRecord) int,
315*88d15eacSSasha Smundak) []repeatCount {
316*88d15eacSSasha Smundak	var startIdx, endIdx, maxLen int
317*88d15eacSSasha Smundak	lens := make([]repeatCount, len(s))
318*88d15eacSSasha Smundak	for i, r := range s {
319*88d15eacSSasha Smundak		if skipFunc(r) {
320*88d15eacSSasha Smundak			for j := startIdx; j < endIdx && j < len(s); j++ {
321*88d15eacSSasha Smundak				lens[j] = repeatCount(maxLen - lenFunc(s[j]))
322*88d15eacSSasha Smundak			}
323*88d15eacSSasha Smundak			startIdx, endIdx, maxLen = i+1, i+1, 0
324*88d15eacSSasha Smundak		} else {
325*88d15eacSSasha Smundak			if maxLen < lenFunc(r) {
326*88d15eacSSasha Smundak				maxLen = lenFunc(r)
327*88d15eacSSasha Smundak			}
328*88d15eacSSasha Smundak			endIdx = i + 1
329*88d15eacSSasha Smundak		}
330*88d15eacSSasha Smundak	}
331*88d15eacSSasha Smundak	for j := startIdx; j < endIdx && j < len(s); j++ {
332*88d15eacSSasha Smundak		lens[j] = repeatCount(maxLen - lenFunc(s[j]))
333*88d15eacSSasha Smundak	}
334*88d15eacSSasha Smundak	return lens
335*88d15eacSSasha Smundak}
336*88d15eacSSasha Smundak
337*88d15eacSSasha Smundak// textLine is a single-line segment of text and is always a leaf node
338*88d15eacSSasha Smundak// in the textNode tree.
339*88d15eacSSasha Smundaktype textLine []byte
340*88d15eacSSasha Smundak
341*88d15eacSSasha Smundakvar (
342*88d15eacSSasha Smundak	textNil      = textLine("nil")
343*88d15eacSSasha Smundak	textEllipsis = textLine("...")
344*88d15eacSSasha Smundak)
345*88d15eacSSasha Smundak
346*88d15eacSSasha Smundakfunc (s textLine) Len() int {
347*88d15eacSSasha Smundak	return len(s)
348*88d15eacSSasha Smundak}
349*88d15eacSSasha Smundakfunc (s1 textLine) Equal(s2 textNode) bool {
350*88d15eacSSasha Smundak	if s2, ok := s2.(textLine); ok {
351*88d15eacSSasha Smundak		return bytes.Equal([]byte(s1), []byte(s2))
352*88d15eacSSasha Smundak	}
353*88d15eacSSasha Smundak	return false
354*88d15eacSSasha Smundak}
355*88d15eacSSasha Smundakfunc (s textLine) String() string {
356*88d15eacSSasha Smundak	return string(s)
357*88d15eacSSasha Smundak}
358*88d15eacSSasha Smundakfunc (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
359*88d15eacSSasha Smundak	return append(b, s...), s
360*88d15eacSSasha Smundak}
361*88d15eacSSasha Smundakfunc (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte {
362*88d15eacSSasha Smundak	return append(b, s...)
363*88d15eacSSasha Smundak}
364*88d15eacSSasha Smundak
365*88d15eacSSasha Smundaktype diffStats struct {
366*88d15eacSSasha Smundak	Name         string
367*88d15eacSSasha Smundak	NumIgnored   int
368*88d15eacSSasha Smundak	NumIdentical int
369*88d15eacSSasha Smundak	NumRemoved   int
370*88d15eacSSasha Smundak	NumInserted  int
371*88d15eacSSasha Smundak	NumModified  int
372*88d15eacSSasha Smundak}
373*88d15eacSSasha Smundak
374*88d15eacSSasha Smundakfunc (s diffStats) IsZero() bool {
375*88d15eacSSasha Smundak	s.Name = ""
376*88d15eacSSasha Smundak	return s == diffStats{}
377*88d15eacSSasha Smundak}
378*88d15eacSSasha Smundak
379*88d15eacSSasha Smundakfunc (s diffStats) NumDiff() int {
380*88d15eacSSasha Smundak	return s.NumRemoved + s.NumInserted + s.NumModified
381*88d15eacSSasha Smundak}
382*88d15eacSSasha Smundak
383*88d15eacSSasha Smundakfunc (s diffStats) Append(ds diffStats) diffStats {
384*88d15eacSSasha Smundak	assert(s.Name == ds.Name)
385*88d15eacSSasha Smundak	s.NumIgnored += ds.NumIgnored
386*88d15eacSSasha Smundak	s.NumIdentical += ds.NumIdentical
387*88d15eacSSasha Smundak	s.NumRemoved += ds.NumRemoved
388*88d15eacSSasha Smundak	s.NumInserted += ds.NumInserted
389*88d15eacSSasha Smundak	s.NumModified += ds.NumModified
390*88d15eacSSasha Smundak	return s
391*88d15eacSSasha Smundak}
392*88d15eacSSasha Smundak
393*88d15eacSSasha Smundak// String prints a humanly-readable summary of coalesced records.
394*88d15eacSSasha Smundak//
395*88d15eacSSasha Smundak// Example:
396*88d15eacSSasha Smundak//
397*88d15eacSSasha Smundak//	diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields"
398*88d15eacSSasha Smundakfunc (s diffStats) String() string {
399*88d15eacSSasha Smundak	var ss []string
400*88d15eacSSasha Smundak	var sum int
401*88d15eacSSasha Smundak	labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"}
402*88d15eacSSasha Smundak	counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified}
403*88d15eacSSasha Smundak	for i, n := range counts {
404*88d15eacSSasha Smundak		if n > 0 {
405*88d15eacSSasha Smundak			ss = append(ss, fmt.Sprintf("%d %v", n, labels[i]))
406*88d15eacSSasha Smundak		}
407*88d15eacSSasha Smundak		sum += n
408*88d15eacSSasha Smundak	}
409*88d15eacSSasha Smundak
410*88d15eacSSasha Smundak	// Pluralize the name (adjusting for some obscure English grammar rules).
411*88d15eacSSasha Smundak	name := s.Name
412*88d15eacSSasha Smundak	if sum > 1 {
413*88d15eacSSasha Smundak		name += "s"
414*88d15eacSSasha Smundak		if strings.HasSuffix(name, "ys") {
415*88d15eacSSasha Smundak			name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries"
416*88d15eacSSasha Smundak		}
417*88d15eacSSasha Smundak	}
418*88d15eacSSasha Smundak
419*88d15eacSSasha Smundak	// Format the list according to English grammar (with Oxford comma).
420*88d15eacSSasha Smundak	switch n := len(ss); n {
421*88d15eacSSasha Smundak	case 0:
422*88d15eacSSasha Smundak		return ""
423*88d15eacSSasha Smundak	case 1, 2:
424*88d15eacSSasha Smundak		return strings.Join(ss, " and ") + " " + name
425*88d15eacSSasha Smundak	default:
426*88d15eacSSasha Smundak		return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name
427*88d15eacSSasha Smundak	}
428*88d15eacSSasha Smundak}
429*88d15eacSSasha Smundak
430*88d15eacSSasha Smundaktype commentString string
431*88d15eacSSasha Smundak
432*88d15eacSSasha Smundakfunc (s commentString) String() string { return string(s) }
433