diff --git a/examples/gno.land/p/demo/ufmt/ufmt.gno b/examples/gno.land/p/demo/ufmt/ufmt.gno index 68bc92e55dd..8ba1bcb1e38 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt.gno @@ -8,8 +8,10 @@ package ufmt import ( "errors" "io" + "math" "strconv" "strings" + "unicode" "unicode/utf8" ) @@ -32,13 +34,134 @@ func (b *buffer) writeRune(r rune) { *b = utf8.AppendRune(*b, r) } +// the different flags +const ( + flagHashtag uint8 = 1 + flagZero uint8 = 1 << 1 + flagMinus uint8 = 1 << 2 + flagSpace uint8 = 1 << 3 + flagPlus uint8 = 1 << 4 +) + +// the different len modifiers +const ( + lenNo uint8 = iota + lenHH + lenH + lenL + lenLL + lenZ +) + +type printerMeta struct { + flags uint8 + padding uint + length uint8 + width uint + precision int +} + // printer holds state for formatting operations. type printer struct { - buf buffer + buf buffer + meta printerMeta +} + +func strtoi(s string) (int, string) { + i := 0 + for i < len(s) && unicode.IsDigit(rune(s[i])) { + i++ + } + if i == 0 { + return 0, s + } + num, err := strconv.Atoi(s[:i]) + if err != nil { + return 0, s + } + return num, s[i:] +} + +func ptrForward(ptr *int, runes []rune, inc int) { + if inc <= 0 { + return + } + if *ptr + inc > len(runes) { + return + } + *ptr += inc +} + +var c2f = map[rune]uint8{ + '-': flagMinus, + '+': flagPlus, + ' ': flagSpace, + '0': flagZero, +} + +var lenMap = map[string]uint8{ + "ll": lenLL, + "l": lenL, + "h": lenH, + "hh": lenHH, + "z": lenZ, +} + +// parse the flags +func (p *printer) parse(runes []rune, index *int) { + ptrForward(index, runes, 1) + if runes[*index] == '#' { + p.meta.flags |= flagHashtag + ptrForward(index, runes, 1) + } + stop := false + for { + x, ok := c2f[runes[*index]] + switch { + case ok: + p.meta.flags |= x + ptrForward(index, runes, 1) + case unicode.IsNumber(runes[*index]): + strRunes := string(runes[*index:]) + n, rest := strtoi(strRunes) + if rest == strRunes { + continue + } + p.meta.padding = uint(n) + ptrForward(index, runes, len(strRunes) - len(rest)) + case runes[*index] == '.': + ptrForward(index, runes, 1) + strRunes := string(runes[*index:]) + n, rest := strtoi(strRunes) + p.meta.precision = n // 0 by default, not an error + if rest == strRunes { + continue + } + ptrForward(index, runes, len(strRunes) - len(rest)) + default: + stop = true + } + if stop { + break + } + } + for k, v := range lenMap { + if string(runes[*index:*index+len(k)-1]) == k { + p.meta.length = v + ptrForward(index, runes, len(k)) + break + } + } + *index-- } func newPrinter() *printer { - return &printer{} + return &printer{ + buf: buffer{}, + meta: printerMeta{ + precision: 6, + }, + } } // Sprint formats using the default formats for its operands and returns the resulting string. @@ -110,7 +233,7 @@ func (p *printer) doPrintln(a []any) { // %G: Formats a float value with %G for large exponents, and %F with full precision for smaller numbers // %t: Formats a boolean value to "true" or "false". // %x: Formats an integer value as a hexadecimal string. -// Currently supports only uint8, []uint8, [32]uint8. +// Currently supports only int, uint, uint8, 16, 32, 64 and []uint8. // %c: Formats a rune value as a string. // Currently supports only rune, int. // %q: Formats a string value as a quoted string. @@ -150,6 +273,8 @@ func (p *printer) doPrintf(format string, args []any) { continue } + p.parse(sTor, &i) + verb := sTor[i+1] if verb == '%' { p.buf.writeRune('%') @@ -170,14 +295,14 @@ func (p *printer) doPrintf(format string, args []any) { writeString(p, verb, arg) case 'c': writeChar(p, verb, arg) - case 'd': + case 'd', 'i': writeInt(p, verb, arg) case 'e', 'E', 'f', 'F', 'g', 'G': writeFloat(p, verb, arg) case 't': writeBool(p, verb, arg) - case 'x': - writeHex(p, verb, arg) + case 'x', 'X': + writeHex(p, verb, arg, verb == 'X') case 'q': writeQuotedString(p, verb, arg) case 'T': @@ -276,53 +401,69 @@ func writeChar(p *printer, verb rune, arg any) { } } -// writeInt handles %d formatting +// writeInt handles %d & %i formatting func writeInt(p *printer, verb rune, arg any) { + var subBuf string switch v := arg.(type) { case int: - p.buf.writeString(strconv.Itoa(v)) + subBuf = strconv.Itoa(v) case int8: - p.buf.writeString(strconv.Itoa(int(v))) + subBuf = strconv.Itoa(int(v)) case int16: - p.buf.writeString(strconv.Itoa(int(v))) + subBuf = strconv.Itoa(int(v)) case int32: - p.buf.writeString(strconv.Itoa(int(v))) + subBuf = strconv.Itoa(int(v)) case int64: - p.buf.writeString(strconv.Itoa(int(v))) + subBuf = strconv.Itoa(int(v)) case uint: - p.buf.writeString(strconv.FormatUint(uint64(v), 10)) + subBuf = strconv.FormatUint(uint64(v), 10) case uint8: - p.buf.writeString(strconv.FormatUint(uint64(v), 10)) + subBuf = strconv.FormatUint(uint64(v), 10) case uint16: - p.buf.writeString(strconv.FormatUint(uint64(v), 10)) + subBuf = strconv.FormatUint(uint64(v), 10) case uint32: - p.buf.writeString(strconv.FormatUint(uint64(v), 10)) + subBuf = strconv.FormatUint(uint64(v), 10) case uint64: - p.buf.writeString(strconv.FormatUint(v, 10)) + subBuf = strconv.FormatUint(v, 10) default: - p.buf.writeString(fallback(verb, v)) + subBuf = fallback(verb, v) + } + paddingCount := int(p.meta.padding) - len(subBuf) + if paddingCount < 0 { + paddingCount = 0 + } + if p.meta.flags&flagSpace != 0 { + subBuf = strings.Repeat(" ", paddingCount) + subBuf + } else if p.meta.flags&flagZero != 0 { + subBuf = strings.Repeat("0", paddingCount) + subBuf } + p.buf.writeString(subBuf) } // writeFloat handles floating-point formatting verbs func writeFloat(p *printer, verb rune, arg any) { + var subBuf string + bits := 64 + n := 0. switch v := arg.(type) { case float64: - switch verb { - case 'e': - p.buf.writeString(strconv.FormatFloat(v, 'e', -1, 64)) - case 'E': - p.buf.writeString(strconv.FormatFloat(v, 'E', -1, 64)) - case 'f', 'F': - p.buf.writeString(strconv.FormatFloat(v, 'f', 6, 64)) - case 'g': - p.buf.writeString(strconv.FormatFloat(v, 'g', -1, 64)) - case 'G': - p.buf.writeString(strconv.FormatFloat(v, 'G', -1, 64)) - } + bits = 64 + n = v + case float32: + bits = 32 + n = float64(v) default: p.buf.writeString(fallback(verb, v)) + return + } + prec := -1 + verbChar := byte(verb) + if verbChar == 'f' || verbChar == 'F' { + prec = p.meta.precision + verbChar = 'f' } + subBuf = strconv.FormatFloat(n, verbChar, prec, bits) + p.buf.writeString(subBuf) } // writeBool handles %t formatting @@ -339,14 +480,46 @@ func writeBool(p *printer, verb rune, arg any) { } } -// writeHex handles %x formatting -func writeHex(p *printer, verb rune, arg any) { +// writeHex handles %x & %X formatting +func writeHex(p *printer, verb rune, arg any, big bool) { + var subBuf string switch v := arg.(type) { + case int: + subBuf += strconv.FormatUint(uint64(v), 16) + case uint: + subBuf += strconv.FormatUint(uint64(v), 16) case uint8: - p.buf.writeString(strconv.FormatUint(uint64(v), 16)) + subBuf += strconv.FormatUint(uint64(v), 16) + case uint16: + subBuf += strconv.FormatUint(uint64(v), 16) + case uint32: + subBuf += strconv.FormatUint(uint64(v), 16) + case uint64: + subBuf += strconv.FormatUint(v, 16) + case []uint8: + for _, v := range v { + x := strconv.FormatUint(uint64(v), 16) + if len(x) == 1 { + x = "0" + x + } + subBuf += x + } default: p.buf.writeString("(unhandled)") + return + } + if p.meta.flags&flagSpace != 0 { + subBuf = strings.Repeat(" ", int(math.Max(0, float64(int(p.meta.padding)-len(subBuf))))) + subBuf + } else if p.meta.flags&flagZero != 0 { + subBuf = strings.Repeat("0", int(math.Max(0, float64(int(p.meta.padding)-len(subBuf))))) + subBuf + } + if p.meta.flags&flagHashtag != 0 { + subBuf = "0x" + subBuf + } + if big { + subBuf = strings.ToUpper(subBuf) } + p.buf.writeString(subBuf) } // writeQuotedString handles %q formatting diff --git a/examples/gno.land/p/demo/ufmt/ufmt_test.gno b/examples/gno.land/p/demo/ufmt/ufmt_test.gno index c443914adaa..6f294feb722 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt_test.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt_test.gno @@ -20,12 +20,28 @@ func TestSprintf(t *testing.T) { values []any expectedOutput string }{ + // normal usage {"hello %s!", []any{"planet"}, "hello planet!"}, {"hello %v!", []any{"planet"}, "hello planet!"}, {"hi %%%s!", []any{"worl%d"}, "hi %worl%d!"}, {"%s %c %d %t", []any{"foo", 'α', 421, true}, "foo α 421 true"}, {"string [%s]", []any{"foo"}, "string [foo]"}, + + // ints {"int [%d]", []any{int(42)}, "int [42]"}, + {"int [%03d]", []any{int(42)}, "int [042]"}, + {"int [%010d]", []any{int(42)}, "int [0000000042]"}, + {"int [% 10d]", []any{int(42)}, "int [ 42]"}, + {"hex [%x]", []any{int(42)}, "hex [2a]"}, + {"hex [%X]", []any{int(42)}, "hex [2A]"}, + {"hex [%02x]", []any{int(42)}, "hex [2a]"}, + {"hex [%03x]", []any{int(42)}, "hex [02a]"}, + {"hex [%#03x]", []any{int(42)}, "hex [0x02a]"}, + {"hex [%#03X]", []any{int(42)}, "hex [0X02A]"}, + {"hex [%#X]", []any{[]uint8{1, 2, 42}}, "hex [0X01022A]"}, + {"hex [%x]", []any{[]uint8("hello")}, "hex [68656c6c6f]"}, + {"hex [%x]", []any{[]uint8{0, 0}}, "hex [0000]"}, + {"hex [%x]", []any{uint8(0)}, "hex [0]"}, {"int [%v]", []any{int(42)}, "int [42]"}, {"int8 [%d]", []any{int8(8)}, "int8 [8]"}, {"int8 [%v]", []any{int8(8)}, "int8 [8]"}, @@ -45,16 +61,28 @@ func TestSprintf(t *testing.T) { {"uint32 [%v]", []any{uint32(32)}, "uint32 [32]"}, {"uint64 [%d]", []any{uint64(64)}, "uint64 [64]"}, {"uint64 [%v]", []any{uint64(64)}, "uint64 [64]"}, + + // floats {"float64 [%e]", []any{float64(64.1)}, "float64 [6.41e+01]"}, {"float64 [%E]", []any{float64(64.1)}, "float64 [6.41E+01]"}, {"float64 [%f]", []any{float64(64.1)}, "float64 [64.100000]"}, + {"float64 [%.2f]", []any{float64(64.1)}, "float64 [64.10]"}, + {"float32 [%.1f]", []any{float32(64.1)}, "float32 [64.1]"}, + {"float64 [%.f]", []any{float64(64.1)}, "float64 [64]"}, + {"float64 [%.0f]", []any{float64(64.1)}, "float64 [64]"}, + {"float64 [%.1f]", []any{float64(64.1)}, "float64 [64.1]"}, {"float64 [%F]", []any{float64(64.1)}, "float64 [64.100000]"}, {"float64 [%g]", []any{float64(64.1)}, "float64 [64.1]"}, {"float64 [%G]", []any{float64(64.1)}, "float64 [64.1]"}, + {"float64 [%.000001f]", []any{float64(64.1)}, "float64 [64.1]"}, + + // bool {"bool [%t]", []any{true}, "bool [true]"}, {"bool [%v]", []any{true}, "bool [true]"}, {"bool [%t]", []any{false}, "bool [false]"}, {"bool [%v]", []any{false}, "bool [false]"}, + + // errros/special cases {"no args", nil, "no args"}, {"finish with %", nil, "finish with %"}, {"stringer [%s]", []any{stringer{}}, "stringer [I'm a stringer]"},