Browse Source

Merge pull request #13 from pmezard/add-option-to-sort-map-keys

config: add SortKeys option to sort native map keys before display
Dave Collins 12 years ago
parent
commit
f13f098c7a
5 changed files with 105 additions and 0 deletions
  1. 7 0
      spew/config.go
  2. 44 0
      spew/dump.go
  3. 42 0
      spew/dump_test.go
  4. 3 0
      spew/format.go
  5. 9 0
      spew/format_test.go

+ 7 - 0
spew/config.go

@@ -73,6 +73,13 @@ type ConfigState struct {
 	// NOTE: This flag does not have any effect if method invocation is disabled
 	// via the DisableMethods or DisablePointerMethods options.
 	ContinueOnMethod bool
+
+	// SortKeys specifies map keys should be sorted before being printed. Use
+	// this to have a more deterministic, diffable output. Note that only
+	// native types (bool, int, uint, floats, uintptr and string) are supported,
+	// other types will be sort according to the reflect.Value.String() output
+	// which guarantees display stability.
+	SortKeys bool
 }
 
 // Config is the active configuration of the top-level functions.

+ 44 - 0
spew/dump.go

@@ -24,6 +24,7 @@ import (
 	"os"
 	"reflect"
 	"regexp"
+	"sort"
 	"strconv"
 	"strings"
 )
@@ -241,6 +242,46 @@ func (d *dumpState) dumpSlice(v reflect.Value) {
 	}
 }
 
+type valuesSorter struct {
+	values []reflect.Value
+}
+
+func (s *valuesSorter) Len() int {
+	return len(s.values)
+}
+
+func (s *valuesSorter) Swap(i, j int) {
+	s.values[i], s.values[j] = s.values[j], s.values[i]
+}
+
+func (s *valuesSorter) Less(i, j int) bool {
+	switch s.values[i].Kind() {
+	case reflect.Bool:
+		return !s.values[i].Bool() && s.values[j].Bool()
+	case reflect.Float32, reflect.Float64:
+		return s.values[i].Float() < s.values[j].Float()
+	case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
+		return s.values[i].Int() < s.values[j].Int()
+	case reflect.String:
+		return s.values[i].String() < s.values[j].String()
+	case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
+		return s.values[i].Uint() < s.values[j].Uint()
+	case reflect.Uintptr:
+		return s.values[i].UnsafeAddr() < s.values[j].UnsafeAddr()
+	}
+	return s.values[i].String() < s.values[j].String()
+}
+
+// Generic sort function for native types: int, uint, bool, string and uintptr.
+// Other inputs are sort according to their Value.String() value to ensure
+// display stability.
+func SortValues(values []reflect.Value) {
+	if len(values) == 0 {
+		return
+	}
+	sort.Sort(&valuesSorter{values})
+}
+
 // dump is the main workhorse for dumping a value.  It uses the passed reflect
 // value to figure out what kind of object we are dealing with and formats it
 // appropriately.  It is a recursive function, however circular data structures
@@ -349,6 +390,9 @@ func (d *dumpState) dump(v reflect.Value) {
 		} else {
 			numEntries := v.Len()
 			keys := v.MapKeys()
+			if d.cs.SortKeys {
+				SortValues(keys)
+			}
 			for i, key := range keys {
 				d.dump(d.unpackValue(key))
 				d.w.Write(colonSpaceBytes)

+ 42 - 0
spew/dump_test.go

@@ -65,6 +65,7 @@ import (
 	"bytes"
 	"fmt"
 	"github.com/davecgh/go-spew/spew"
+	"reflect"
 	"testing"
 	"unsafe"
 )
@@ -896,3 +897,44 @@ func TestDump(t *testing.T) {
 		}
 	}
 }
+
+func TestSortValues(t *testing.T) {
+	v := reflect.ValueOf
+
+	a := v("a")
+	b := v("b")
+	c := v("c")
+	tests := []struct {
+		input    []reflect.Value
+		expected []reflect.Value
+	}{
+		{[]reflect.Value{v(2), v(1), v(3)},
+			[]reflect.Value{v(1), v(2), v(3)}},
+		{[]reflect.Value{v(2.), v(1.), v(3.)},
+			[]reflect.Value{v(1.), v(2.), v(3.)}},
+		{[]reflect.Value{v(false), v(true), v(false)},
+			[]reflect.Value{v(false), v(false), v(true)}},
+		{[]reflect.Value{b, a, c},
+			[]reflect.Value{a, b, c}},
+	}
+	for _, test := range tests {
+		spew.SortValues(test.input)
+		if !reflect.DeepEqual(test.input, test.expected) {
+			t.Errorf("Sort mismatch:\n %v != %v", test.input, test.expected)
+		}
+	}
+}
+
+func TestDumpSortedKeys(t *testing.T) {
+	cfg := spew.ConfigState{SortKeys: true}
+	s := cfg.Sdump(map[int]string{1: "1", 3: "3", 2: "2"})
+	expected := `(map[int]string) {
+(int) 1: (string) "1",
+(int) 2: (string) "2",
+(int) 3: (string) "3"
+}
+`
+	if s != expected {
+		t.Errorf("Sorted keys mismatch:\n  %v %v", s, expected)
+	}
+}

+ 3 - 0
spew/format.go

@@ -302,6 +302,9 @@ func (f *formatState) format(v reflect.Value) {
 			f.fs.Write(maxShortBytes)
 		} else {
 			keys := v.MapKeys()
+			if f.cs.SortKeys {
+				SortValues(keys)
+			}
 			for i, key := range keys {
 				if i > 0 {
 					f.fs.Write(spaceBytes)

+ 9 - 0
spew/format_test.go

@@ -1472,3 +1472,12 @@ func TestFormatter(t *testing.T) {
 		}
 	}
 }
+
+func TestPrintSortedKeys(t *testing.T) {
+	cfg := spew.ConfigState{SortKeys: true}
+	s := cfg.Sprint(map[int]string{1: "1", 3: "3", 2: "2"})
+	expected := "map[1:1 2:2 3:3]"
+	if s != expected {
+		t.Errorf("Sorted keys mismatch:\n  %v %v", s, expected)
+	}
+}