Skip to content

Commit

Permalink
types: Make (Currency).String exact
Browse files Browse the repository at this point in the history
  • Loading branch information
lukechampine committed Mar 11, 2024
1 parent 092850c commit 3f36536
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 86 deletions.
78 changes: 24 additions & 54 deletions types/currency.go
Expand Up @@ -226,72 +226,42 @@ func (c Currency) Big() *big.Int {
return new(big.Int).SetBytes(b)
}

// ExactString returns the base-10 representation of c as a string.
func (c Currency) ExactString() string {
if c.IsZero() {
return "0"
}
buf := []byte("0000000000000000000000000000000000000000") // log10(2^128) < 40
for i := len(buf); ; i -= 19 {
q, r := c.quoRem64(1e19) // largest power of 10 that fits in a uint64
var n int
for ; r != 0; r /= 10 {
n++
buf[i-n] += byte(r % 10)
}
if q.IsZero() {
return string(buf[i-n:])
}
c = q
}
}

// String returns base-10 representation of c with a unit suffix. The value may
// be rounded. To avoid loss of precision, use ExactString.
func (c Currency) String() string {
pico := Siacoins(1).Div64(1e12)
if c.Cmp(pico) < 0 {
return c.ExactString() + " H"
if c.IsZero() {
return "0 SC"
}

// iterate until we find a unit greater than c
//
// NOTE: MaxCurrency is ~340.3 TS
mag := pico
unit := ""
for _, unit = range []string{"pS", "nS", "uS", "mS", "SC", "KS", "MS", "GS", "TS"} {
j, overflow := mag.Mul64WithOverflow(1000)
if overflow || c.Cmp(j) < 0 || unit == "TS" {
break
}
mag = j
s := c.Big().String()
u := (len(s) - 1) / 3
if u < 4 {
return s + " H"
} else if u > 12 {
u = 12
}

f, _ := new(big.Rat).SetFrac(c.Big(), mag.Big()).Float64()
s := fmt.Sprintf("%.4g %s", f, unit)
// test for exactness
if p, _ := ParseCurrency(s); !p.Equals(c) {
s = "~" + s
mant := s[:len(s)-u*3]
if frac := strings.TrimRight(s[len(s)-u*3:], "0"); len(frac) > 0 {
mant += "." + frac
}
return s
unit := []string{"pS", "nS", "uS", "mS", "SC", "KS", "MS", "GS", "TS"}[u-4]
return mant + " " + unit
}

// Siacoins converts the value of c from Hastings to Siacoins (SC) and returns
// it as a float64.
// Siacoins converts c to a floating-point number of siacoins. This may result
// in a loss of precision.
func (c Currency) Siacoins() float64 {
f, _ := new(big.Rat).SetFrac(c.Big(), HastingsPerSiacoin.Big()).Float64()
return f
}

// Format implements fmt.Formatter. It accepts the following formats:
//
// d: raw integer (equivalent to ExactString())
// s: rounded integer with unit suffix (equivalent to String())
// v: same as s
// s, v: decimal with unit suffix (equivalent to String())
// b, d, o, O, x, X: raw integer, formatted via c.Big().Format
func (c Currency) Format(f fmt.State, v rune) {
switch v {
case 'd':
io.WriteString(f, c.ExactString())
case 'b', 'd', 'o', 'O', 'x', 'X':
c.Big().Format(f, v)
case 's', 'v':
io.WriteString(f, c.String())
default:
Expand All @@ -301,16 +271,16 @@ func (c Currency) Format(f fmt.State, v rune) {

// MarshalJSON implements json.Marshaler.
func (c Currency) MarshalJSON() ([]byte, error) {
return []byte(`"` + c.ExactString() + `"`), nil
return []byte(`"` + c.Big().String() + `"`), nil
}

// UnmarshalJSON implements json.Unmarshaler.
func (c *Currency) UnmarshalJSON(b []byte) (err error) {
*c, err = parseExactCurrency(strings.Trim(string(b), `"`))
*c, err = parseHastings(strings.Trim(string(b), `"`))
return
}

func parseExactCurrency(s string) (Currency, error) {
func parseHastings(s string) (Currency, error) {
i, ok := new(big.Int).SetString(s, 10)
if !ok {
return ZeroCurrency, errors.New("not an integer")
Expand Down Expand Up @@ -347,7 +317,7 @@ func ParseCurrency(s string) (Currency, error) {
}
n, unit := s[:i], strings.TrimSpace(s[i:])
if unit == "" || unit == "H" {
return parseExactCurrency(n)
return parseHastings(n)
}
// parse numeric part as a big.Rat
r, ok := new(big.Rat).SetString(n)
Expand All @@ -364,5 +334,5 @@ func ParseCurrency(s string) (Currency, error) {
if !r.IsInt() {
return ZeroCurrency, errors.New("not an integer")
}
return parseExactCurrency(r.RatString())
return parseHastings(r.RatString())
}
59 changes: 27 additions & 32 deletions types/currency_test.go
Expand Up @@ -531,51 +531,42 @@ func TestCurrencyDiv64(t *testing.T) {
}
}

func TestCurrencyExactString(t *testing.T) {
func TestCurrencyString(t *testing.T) {
tests := []struct {
val Currency
want string
}{
{
ZeroCurrency,
"0",
"0 SC",
},
{
Siacoins(128),
"128000000000000000000000000",
NewCurrency64(10000),
"10000 H",
},
{
NewCurrency64(math.MaxUint64),
"18446744073709551615",
Siacoins(1).Div64(1e12),
"1 pS",
},
{
NewCurrency(8262254095159001088, 2742357),
"50587566000000000000000000",
Siacoins(1),
"1 SC",
},
{
MaxCurrency,
"340282366920938463463374607431768211455",
Siacoins(10),
"10 SC",
},
}
for _, tt := range tests {
if got := tt.val.ExactString(); got != tt.want {
t.Errorf("Currency.ExactString() = %v, want %v", got, tt.want)
}
}
}

func TestCurrencyString(t *testing.T) {
tests := []struct {
val Currency
want string
}{
{
ZeroCurrency,
"0 H",
Siacoins(100),
"100 SC",
},
{
NewCurrency64(10000),
"10000 H",
Siacoins(1000),
"1 KS",
},
{
Siacoins(1).Mul64(1e12),
"1 TS",
},
{
Siacoins(10).Sub(Siacoins(1)),
Expand All @@ -595,24 +586,28 @@ func TestCurrencyString(t *testing.T) {
},
{
Siacoins(10).Sub(Siacoins(1).Div64(10000)),
"~10 SC",
"9.9999 SC",
},
{
Siacoins(10).Sub(NewCurrency64(1)),
"~10 SC",
"9.999999999999999999999999 SC",
},
{
NewCurrency(8262254095159001088, 2742357),
"~50.59 SC",
"50.587566 SC",
},
{
NewCurrency(2174395257947586975, 137),
"~2.529 mS",
"2.529378333356156158367 mS",
},
{
NewCurrency(math.MaxUint64, math.MaxUint64),
"340.282366920938463463374607431768211455 TS",
},
}
for _, tt := range tests {
if got := tt.val.String(); got != tt.want {
t.Errorf("Currency.String() = %v (%d), want %v", got, tt.val, tt.want)
t.Errorf("Currency.String() = %v (%d H), want %v", got, tt.val, tt.want)
}
}
}
Expand Down

0 comments on commit 3f36536

Please sign in to comment.