Skip to content
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

types: Make (Currency).String exact #146

Merged
merged 2 commits into from Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 {
lukechampine marked this conversation as resolved.
Show resolved Hide resolved
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())
}
75 changes: 36 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 Expand Up @@ -755,5 +747,10 @@ func TestParseCurrency(t *testing.T) {
} else if !got.Equals(tt.want) {
t.Errorf("ParseCurrency(%v) = %d, want %d", tt.s, got, tt.want)
}
if err := got.UnmarshalText([]byte(tt.s)); (err != nil) != tt.wantErr {
t.Errorf("UnmarshalText(%v) error = %v, wantErr %v", tt.s, err, tt.wantErr)
} else if !got.Equals(tt.want) {
t.Errorf("UnmarshalText(%v) = %d, want %d", tt.s, got, tt.want)
}
}
}