diff --git a/examples/gno.land/p/demo/ufmt/ufmt.gno b/examples/gno.land/p/demo/ufmt/ufmt.gno index a7cd8550fff..55494e32cec 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt.gno @@ -4,6 +4,7 @@ package ufmt import ( + "errors" "strconv" "strings" ) @@ -17,16 +18,20 @@ func Println(args ...interface{}) { switch v := arg.(type) { case string: strs = append(strs, v) + case (interface{ String() string }): + strs = append(strs, v.String()) + case error: + strs = append(strs, v.Error()) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: strs = append(strs, Sprintf("%d", v)) case bool: if v { strs = append(strs, "true") - - continue + } else { + strs = append(strs, "false") } - - strs = append(strs, "false") + case nil: + strs = append(strs, "") default: strs = append(strs, "(unhandled)") } @@ -46,7 +51,9 @@ func Println(args ...interface{}) { // // %s: places a string value directly. // If the value implements the interface interface{ String() string }, -// the String() method is called to retrieve the value. +// the String() method is called to retrieve the value. Same about Error() +// string. +// %c: formats the character represented by Unicode code point // %d: formats an integer value using package "strconv". // Currently supports only uint, uint64, int, int64. // %t: formats a boolean value to "true" or "false". @@ -88,10 +95,32 @@ func Sprintf(format string, args ...interface{}) string { switch v := arg.(type) { case interface{ String() string }: buf += v.String() + case error: + buf += v.Error() case string: buf += v default: - buf += "(unhandled)" + buf += fallback(verb, v) + } + case "c": + switch v := arg.(type) { + // rune is int32. Exclude overflowing numeric types and dups (byte, int32): + case rune: + buf += string(v) + case int: + buf += string(v) + case int8: + buf += string(v) + case int16: + buf += string(v) + case uint: + buf += string(v) + case uint8: + buf += string(v) + case uint16: + buf += string(v) + default: + buf += fallback(verb, v) } case "d": switch v := arg.(type) { @@ -116,7 +145,7 @@ func Sprintf(format string, args ...interface{}) string { case uint64: buf += strconv.FormatUint(v, 10) default: - buf += "(unhandled)" + buf += fallback(verb, v) } case "t": switch v := arg.(type) { @@ -127,11 +156,11 @@ func Sprintf(format string, args ...interface{}) string { buf += "false" } default: - buf += "(unhandled)" + buf += fallback(verb, v) } // % handled before, as it does not consume an argument default: - buf += "(unhandled)" + buf += "(unhandled verb: %" + verb + ")" } i += 2 @@ -142,6 +171,85 @@ func Sprintf(format string, args ...interface{}) string { return buf } +// This function is used to mimic Go's fmt.Sprintf +// specific behaviour of showing verb/type mismatches, +// where for example: +// +// fmt.Sprintf("%d", "foo") gives "%!d(string=foo)" +// +// Here: +// +// fallback("s", 8) -> "%!s(int=8)" +// fallback("d", nil) -> "%!d()", and so on. +func fallback(verb string, arg interface{}) string { + var s string + switch v := arg.(type) { + case string: + s = "string=" + v + case (interface{ String() string }): + s = "string=" + v.String() + case error: + // note: also "string=" in Go fmt + s = "string=" + v.Error() + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + // note: rune, byte would be dups, being aliases + if typename, e := typeToString(v); e != nil { + panic("should not happen") + } else { + s = typename + "=" + Sprintf("%d", v) + } + case bool: + if v { + s = "bool=true" + } else { + s = "bool=false" + } + case nil: + s = "" + default: + s = "(unhandled)" + } + return "%!" + verb + "(" + s + ")" +} + +// Get the name of the type of `v` as a string. +// The recognized type of v is currently limited to native non-composite types. +// An error is returned otherwise. +func typeToString(v interface{}) (string, error) { + switch v.(type) { + case string: + return "string", nil + case int: + return "int", nil + case int8: + return "int8", nil + case int16: + return "int16", nil + case int32: + return "int32", nil + case int64: + return "int64", nil + case uint: + return "uint", nil + case uint8: + return "uint8", nil + case uint16: + return "uint16", nil + case uint32: + return "uint32", nil + case uint64: + return "uint64", nil + case float32: + return "float32", nil + case float64: + return "float64", nil + case bool: + return "bool", nil + default: + return "", errors.New("(unsupported type)") + } +} + // errMsg implements the error interface. type errMsg struct { msg string @@ -165,7 +273,8 @@ func (e *errMsg) Error() string { // // %s: places a string value directly. // If the value implements the interface interface{ String() string }, -// the String() method is called to retrieve the value. +// the String() method is called to retrieve the value. Same for error. +// %c: formats the character represented by Unicode code point // %d: formats an integer value using package "strconv". // Currently supports only uint, uint64, int, int64. // %t: formats a boolean value to "true" or "false". diff --git a/examples/gno.land/p/demo/ufmt/ufmt_test.gno b/examples/gno.land/p/demo/ufmt/ufmt_test.gno index 94d32372d30..d53fb39bc44 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt_test.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt_test.gno @@ -1,6 +1,7 @@ package ufmt import ( + "errors" "fmt" "testing" ) @@ -12,6 +13,7 @@ func (stringer) String() string { } func TestSprintf(t *testing.T) { + tru := true cases := []struct { format string values []interface{} @@ -19,6 +21,7 @@ func TestSprintf(t *testing.T) { }{ {"hello %s!", []interface{}{"planet"}, "hello planet!"}, {"hi %%%s!", []interface{}{"worl%d"}, "hi %worl%d!"}, + {"%s %c %d %t", []interface{}{"foo", 'ฮฑ', 421, true}, "foo ฮฑ 421 true"}, {"string [%s]", []interface{}{"foo"}, "string [foo]"}, {"int [%d]", []interface{}{int(42)}, "int [42]"}, {"int8 [%d]", []interface{}{int8(8)}, "int8 [8]"}, @@ -32,15 +35,36 @@ func TestSprintf(t *testing.T) { {"uint64 [%d]", []interface{}{uint64(64)}, "uint64 [64]"}, {"bool [%t]", []interface{}{true}, "bool [true]"}, {"bool [%t]", []interface{}{false}, "bool [false]"}, - {"invalid bool [%t]", []interface{}{"invalid"}, "invalid bool [(unhandled)]"}, - {"invalid integer [%d]", []interface{}{"invalid"}, "invalid integer [(unhandled)]"}, - {"invalid string [%s]", []interface{}{1}, "invalid string [(unhandled)]"}, {"no args", nil, "no args"}, {"finish with %", nil, "finish with %"}, {"stringer [%s]", []interface{}{stringer{}}, "stringer [I'm a stringer]"}, {"รข", nil, "รข"}, {"Hello, World! ๐Ÿ˜Š", nil, "Hello, World! ๐Ÿ˜Š"}, {"unicode formatting: %s", []interface{}{"๐Ÿ˜Š"}, "unicode formatting: ๐Ÿ˜Š"}, + // mismatch printing + {"%s", []interface{}{nil}, "%!s()"}, + {"%s", []interface{}{421}, "%!s(int=421)"}, + {"%s", []interface{}{"z"}, "z"}, + {"%s", []interface{}{tru}, "%!s(bool=true)"}, + {"%s", []interface{}{'z'}, "%!s(int32=122)"}, + + {"%c", []interface{}{nil}, "%!c()"}, + {"%c", []interface{}{421}, "ฦฅ"}, + {"%c", []interface{}{"z"}, "%!c(string=z)"}, + {"%c", []interface{}{tru}, "%!c(bool=true)"}, + {"%c", []interface{}{'z'}, "z"}, + + {"%d", []interface{}{nil}, "%!d()"}, + {"%d", []interface{}{421}, "421"}, + {"%d", []interface{}{"z"}, "%!d(string=z)"}, + {"%d", []interface{}{tru}, "%!d(bool=true)"}, + {"%d", []interface{}{'z'}, "122"}, + + {"%t", []interface{}{nil}, "%!t()"}, + {"%t", []interface{}{421}, "%!t(int=421)"}, + {"%t", []interface{}{"z"}, "%!t(string=z)"}, + {"%t", []interface{}{tru}, "true"}, + {"%t", []interface{}{'z'}, "%!t(int32=122)"}, } for _, tc := range cases { @@ -103,6 +127,14 @@ func TestErrorf(t *testing.T) { } } +func TestPrintErrors(t *testing.T) { + got := Sprintf("error: %s", errors.New("can I be printed?")) + expectedOutput := "error: can I be printed?" + if got != expectedOutput { + t.Errorf("got %q, want %q.", got, expectedOutput) + } +} + // NOTE: Currently, there is no way to get the output of Println without using os.Stdout, // so we can only test that it doesn't panic and print arguments well. func TestPrintln(t *testing.T) {