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 15, 2024
1 parent 092850c commit 28768ae
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 95 deletions.
86 changes: 30 additions & 56 deletions types/currency.go
Expand Up @@ -228,89 +228,63 @@ func (c Currency) Big() *big.Int {

// 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
}
return fmt.Sprintf("%d", c)
}

// String returns base-10 representation of c with a unit suffix. The value may
// be rounded. To avoid loss of precision, use ExactString.
// String returns the base-10 representation of c with a unit suffix.
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:
fmt.Fprintf(f, "%%!%c(unsupported,Currency=%d)", v, c)
}
}

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

// UnmarshalJSON implements json.Unmarshaler.
func (c *Currency) UnmarshalJSON(b []byte) (err error) {
*c, err = parseExactCurrency(strings.Trim(string(b), `"`))
// UnmarshalText implements encoding.TextUnmarshaler.
func (c *Currency) UnmarshalText(b []byte) (err error) {
*c, err = ParseCurrency(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 +321,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 +338,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())
}
70 changes: 31 additions & 39 deletions types/currency_test.go
@@ -1,6 +1,7 @@
package types

import (
"encoding/json"
"math"
"testing"
)
Expand Down Expand Up @@ -531,51 +532,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 +587,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 Expand Up @@ -640,15 +636,11 @@ func TestCurrencyJSON(t *testing.T) {
},
}
for _, tt := range tests {
// MarshalJSON cannot error
buf, _ := tt.val.MarshalJSON()
var c Currency
buf, _ := json.Marshal(tt.val)
if string(buf) != tt.want {
t.Errorf("Currency.MarshalJSON(%d) = %s, want %s", tt.val, buf, tt.want)
continue
}

var c Currency
if err := c.UnmarshalJSON(buf); err != nil {
} else if err := json.Unmarshal(buf, &c); err != nil {
t.Errorf("Currency.UnmarshalJSON(%s) err = %v", buf, err)
} else if !c.Equals(tt.val) {
t.Errorf("Currency.UnmarshalJSON(%s) = %d, want %d", buf, c, tt.val)
Expand Down

0 comments on commit 28768ae

Please sign in to comment.