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