Skip to content

feat(ufmt): enhance printf functionnalities by adding flags #4136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 39 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
502c85b
feat(examples): add 'pol' example page
paulogarithm Apr 7, 2025
f72fd4c
chore: remove the unusep p/pol/gnomining package
paulogarithm Apr 7, 2025
10c4960
feat(gnominers): nerf opal price & spawn rate
paulogarithm Apr 7, 2025
30780d2
Merge branch 'gnolang:master' into master
paulogarithm Apr 8, 2025
d935be7
feat(gnominers): begin dao impl in gnominers
paulogarithm Apr 8, 2025
966de11
fix(gnominers): finish impl dao
paulogarithm Apr 8, 2025
4923018
feat(gnominers): now you can generate a DAO request
paulogarithm Apr 8, 2025
80bf496
fix(gnominers): bad formatting
paulogarithm Apr 8, 2025
20260f7
feat: add uufmt (with printf with flags) + nfmt (number format)
paulogarithm Apr 9, 2025
5455127
Merge branch 'gnolang:master' into master
paulogarithm Apr 9, 2025
5d71b3a
feat: add flag parsing in ufmt.printf
paulogarithm Apr 10, 2025
f396fe4
fix(nfmt): number format fix .2f instead of .1f
paulogarithm Apr 10, 2025
a91aac5
fix(nfmt): number format func name
paulogarithm Apr 10, 2025
efb7a3e
fix(ufmt)!: flag parsing using []rune instead of string
paulogarithm Apr 11, 2025
c5d0400
feat(gnominers): add nfmt to shop
paulogarithm Apr 11, 2025
9451a3b
chore: remove tofix folder remotely
paulogarithm Apr 11, 2025
8289230
fix(#4109): html escape string global realm error
paulogarithm Apr 11, 2025
f2359c8
chore: add some gno-columns everywhere so its cleaner
paulogarithm Apr 11, 2025
ce15ebf
feat(gnominers): add the leaderboard system
paulogarithm Apr 11, 2025
9755a09
feat(ux): improved user experience + use md lib
paulogarithm Apr 14, 2025
6decc3f
feat(gnominers): change everything to use md lib
paulogarithm Apr 14, 2025
6b914d3
feat: add releases to gnominers
paulogarithm Apr 14, 2025
d83de10
feat: implement gnominers releases in home
paulogarithm Apr 14, 2025
6ea1081
chore: fixup render gno unused packages
paulogarithm Apr 14, 2025
76d2680
feat(home): make my home page use md lib
paulogarithm Apr 14, 2025
67b2a21
Merge branch 'master' into master
paulogarithm Apr 14, 2025
d172d78
fix: remove uufmt
paulogarithm Apr 14, 2025
feaffc2
test(ufmt/sprintf): add more tests to printf
paulogarithm Apr 14, 2025
fd9e9fb
feat(ufmt/printf): add %x features for []uint8{}
paulogarithm Apr 14, 2025
442a327
chore: remove r/pol & p/pol for clean PR
paulogarithm Apr 14, 2025
6c44073
chore: remove #4109 fix html
paulogarithm Apr 14, 2025
2dc5f13
Merge branch 'master' into ufmt-enhancement
paulogarithm Apr 14, 2025
47b20da
Update examples/gno.land/p/demo/ufmt/ufmt.gno
paulogarithm Apr 15, 2025
a9633ce
Update examples/gno.land/p/demo/ufmt/ufmt.gno
paulogarithm Apr 15, 2025
53a20d1
chore: formatting & typos
paulogarithm Apr 15, 2025
b6c03bf
fix: increase index pointer not based on number size
paulogarithm Apr 15, 2025
d79fd53
feat: ptrForward to safely move forward + fix! (see comment)
paulogarithm Apr 15, 2025
6353239
chore: move map to upvalues for better memory efficiency
paulogarithm Apr 15, 2025
1d29c1c
fix: remove test on leon's home
paulogarithm Apr 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 206 additions & 33 deletions examples/gno.land/p/demo/ufmt/ufmt.gno
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ package ufmt
import (
"errors"
"io"
"math"
"strconv"
"strings"
"unicode"
"unicode/utf8"
)

Expand All @@ -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
)
Comment on lines +37 to +44
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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 flags
const (
flagHashtag uint8 = 1 << iota
flagZero
flagMinus
flagSpace
flagPlus
)


// 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]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hiding the reference inside a map complicates readability and performance. Why not use a direct switch case like this:

switch *index {
case '+'
case '-':
...
}

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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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('%')
Expand All @@ -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':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%i doesn't seems to exist in go, I don't think we should port it in gno

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':
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions examples/gno.land/p/demo/ufmt/ufmt_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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]"},
Expand All @@ -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]"},
Expand Down
Loading