-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
feat: Value Renderers for number and coin #10038
Changes from 32 commits
2a08eb1
599fadf
4b4ee3a
4231817
f6f698f
00824fb
53003ed
36ae1e2
f32a13c
4d9e3bd
06d9ae5
ab7d6fb
24ae2f4
b1d15ff
0c5e2b1
812bcd2
5090714
700fab9
ec13baf
b8953f7
41394fc
fa8c8e3
f0b5a1d
784eee9
ccf9b4d
dcefbf5
c57115c
15992db
03f6041
ed27445
fa9b039
5c0a6cd
c8fc3a0
6860bde
fbe7e94
8c29035
1264277
28b5890
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
package valuerenderer | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"math" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/dustin/go-humanize" | ||
|
||
"github.com/cosmos/cosmos-sdk/types" | ||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" | ||
) | ||
|
||
// ValueRenderer defines an interface to produce formated output for Int,Dec,Coin types as well as parse a string to Coin or Uint. | ||
type ValueRenderer interface { | ||
Format(context.Context, interface{}) (string, error) | ||
Parse(context.Context, string) (interface{}, error) | ||
} | ||
|
||
// denomQuerierFunc takes a context and a denom as arguments and returns metadata, error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's update a comment to something more meaningful. We know the types, instead, describe what the function should do. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hope it sounds better: |
||
type denomQuerierFunc func(context.Context, string) (banktypes.Metadata, error) | ||
|
||
// DefaultValueRenderer defines a struct that implements ValueRenderer interface | ||
type DefaultValueRenderer struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need to repeat the package name in the struct name. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does |
||
denomQuerier denomQuerierFunc | ||
} | ||
|
||
var _ ValueRenderer = &DefaultValueRenderer{} | ||
|
||
// NewDefaultValueRenderer initiates denomToMetadataMap field and returns DefaultValueRenderer struct | ||
func NewDefaultValueRenderer(denomQuerier denomQuerierFunc) DefaultValueRenderer { | ||
return DefaultValueRenderer{ | ||
denomQuerier: denomQuerier, | ||
} | ||
} | ||
|
||
// Format converts an empty interface into a string depending on interface type. | ||
func (dvr DefaultValueRenderer) Format(c context.Context, x interface{}) (string, error) { | ||
var sb strings.Builder | ||
|
||
switch v := x.(type) { | ||
case types.Dec: | ||
s := v.String() | ||
if len(s) == 0 { | ||
return "", errors.New("empty string") | ||
} | ||
|
||
i := strings.Index(s, ".") | ||
first, second := s[:i], s[i+1:] | ||
first64, err := strconv.ParseInt(first, 10, 64) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
sb.WriteString(humanize.Comma(first64)) | ||
sb.WriteString(".") | ||
sb.WriteString(removeTrailingZeroes(second)) | ||
|
||
case types.Int: | ||
s := v.String() | ||
if len(s) == 0 { | ||
return "", errors.New("empty string") | ||
} | ||
|
||
sb.WriteString(humanize.Comma(v.Int64())) | ||
|
||
case types.Coin: | ||
metadata, err := dvr.denomQuerier(c, convertToBaseDenom(v.Denom)) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
expSub := computeExponentSubtraction(v.Denom, metadata) | ||
|
||
formatedAmount := dvr.ComputeAmount(v.Amount.Int64(), expSub) | ||
|
||
sb.WriteString(formatedAmount) | ||
sb.WriteString(metadata.Display) | ||
|
||
default: | ||
panic("type is invalid") | ||
} | ||
|
||
return sb.String(), nil | ||
} | ||
|
||
// TODO address the cass where denom starts with "u" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this TODO still relevant? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, there is a case, where |
||
func convertToBaseDenom(denom string) string { | ||
cyberbono3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
switch { | ||
// e.g. uregen => uregen | ||
case strings.HasPrefix(denom, "u"): | ||
cyberbono3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return denom | ||
// e.g. mregen => uregen | ||
case strings.HasPrefix(denom, "m"): | ||
return "u" + denom[1:] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. m stands for mili. Why are you overwriting this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have to convert it to |
||
// has no prefix regen => uregen | ||
default: | ||
return "u" + denom | ||
} | ||
} | ||
|
||
// computeExponentSubtraction iterates over metadata.DenomUnits and computes the subtraction of exponents | ||
func computeExponentSubtraction(denom string, metadata banktypes.Metadata) float64 { | ||
var coinExp, displayExp int64 | ||
for _, denomUnit := range metadata.DenomUnits { | ||
if denomUnit.Denom == denom { | ||
coinExp = int64(denomUnit.Exponent) | ||
} | ||
|
||
if denomUnit.Denom == metadata.Display { | ||
displayExp = int64(denomUnit.Exponent) | ||
} | ||
} | ||
|
||
return float64(coinExp - displayExp) | ||
cyberbono3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
// removeTrailingZeroes removes trailing zeroes from a string | ||
func removeTrailingZeroes(str string) string { | ||
index := len(str)-1 | ||
for i := len(str) - 1; i > 0; i-- { | ||
if rune(str[i]) == rune('0') { | ||
continue | ||
} | ||
|
||
index = i | ||
break | ||
} | ||
|
||
return str[:index+1] | ||
} | ||
cyberbono3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// countTrailingZeroes counts the amount of trailing zeroes in a string | ||
func countTrailingZeroes(str string) int { | ||
counter := 0 | ||
for i := len(str) - 1; i > 0; i-- { | ||
if rune(str[i]) == rune('0') { | ||
counter++ | ||
} else { | ||
break | ||
} | ||
} | ||
cyberbono3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return counter | ||
} | ||
|
||
// ComputeAmount calculates an amount to produce formated output | ||
func (dvr DefaultValueRenderer) ComputeAmount(amount int64, expSub float64) string { | ||
|
||
switch { | ||
// negative , convert mregen to regen less zeroes 23 => 0,023, expSub -3 | ||
case math.Signbit(expSub): | ||
|
||
stringValue := strconv.FormatInt(amount, 10) | ||
count := countTrailingZeroes(stringValue) | ||
if count >= int(math.Abs(expSub)) { | ||
// case 1 if number of zeroes >= Abs(expSub) 23000, -3 => 23 (int64) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. case 1 : |
||
x := amount / int64(math.Pow(10, math.Abs(expSub))) | ||
return humanize.Comma(x) | ||
} else { | ||
// case 2 number of trailing zeroes < abs(expSub) 23, -3,=> 0.023(float64) | ||
x := float64(float64(amount) / math.Pow(10, math.Abs(expSub))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. case 2 : |
||
return humanize.Ftoa(x) | ||
} | ||
// positive, e.g.convert mregen to uregen | ||
case !math.Signbit(expSub): | ||
x := amount * int64(math.Pow(10, expSub)) | ||
return humanize.Comma(x) | ||
// == 0, convert regen to regen, amount does not change | ||
default: | ||
return humanize.Comma(amount) | ||
} | ||
} | ||
|
||
// Parse parses a string and takes a decision whether to convert it into Coin or Uint | ||
cyberbono3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
func (dvr DefaultValueRenderer) Parse(ctx context.Context, s string) (interface{}, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. returning an empty interface is a bad idea. Why do we need a single function to work both with numbers and coins? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Can we split
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that would be a better idea. |
||
if s == "" { | ||
return nil, errors.New("unable to parse empty string") | ||
} | ||
// remove all commas | ||
str := strings.ReplaceAll(s, ",", "") | ||
re := regexp.MustCompile(`(\d+)(\w+)`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should compile the regexp once, and store it as a module private variable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rather make it global since |
||
// case 1: "1000000regen" => Coin | ||
if re.MatchString(str) { | ||
var amountStr, denomStr string | ||
s1 := re.FindAllStringSubmatch(str, -1) // [[1000000regen 1000000 regen]] | ||
amountStr, denomStr = s1[0][1], s1[0][2] | ||
|
||
amount, err := strconv.ParseInt(amountStr, 10, 64) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return types.NewInt64Coin(denomStr, amount), nil | ||
} | ||
|
||
// case2: convert it to Uint | ||
i, err := strconv.ParseUint(str, 10, 64) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return types.NewUint(i), nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have found
humanize
library with 4k github stars.