From fc6f0dee54e6ee599508e31d36f554ab208cb2b3 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Fri, 11 Apr 2014 22:03:00 -0500 Subject: [PATCH] Add Amount type to represent a monetary value. ok @davecgh --- amount.go | 94 ++++++++++++++++++++++++ amount_test.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++ test_coverage.txt | 46 ++++++------ 3 files changed, 302 insertions(+), 21 deletions(-) create mode 100644 amount.go create mode 100644 amount_test.go diff --git a/amount.go b/amount.go new file mode 100644 index 0000000..d14b97a --- /dev/null +++ b/amount.go @@ -0,0 +1,94 @@ +// Copyright (c) 2013, 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcutil + +import ( + "errors" + "math" + "strconv" +) + +// AmountUnit describes a method of converting an Amount to something +// other than the base unit of a bitcoin. The value of the AmountUnit +// is the exponent component of the decadic multiple to convert from +// an amount in bitcoin to an amount counted in units. +type AmountUnit int + +// These constants define the various standard units used when describing +// a bitcoin monetary amount. +const ( + AmountMegaBitcoin AmountUnit = 6 + AmountKiloBitcoin AmountUnit = 3 + AmountBitcoin AmountUnit = 0 + AmountMilliBitcoin AmountUnit = -3 + AmountMicroBitcoin AmountUnit = -6 + AmountBaseBitcoin AmountUnit = -8 +) + +// String returns the unit as a string. For recognized units, the SI +// prefix is used, or "Satoshi" for the base unit. For all unrecognized +// units, "1eN BTC" is returned, where N is the AmountUnit. +func (u AmountUnit) String() string { + switch u { + case AmountMegaBitcoin: + return "MBTC" + case AmountKiloBitcoin: + return "kBTC" + case AmountBitcoin: + return "BTC" + case AmountMilliBitcoin: + return "mBTC" + case AmountMicroBitcoin: + return "μBTC" + case AmountBaseBitcoin: + return "Satoshi" + default: + return "1e" + strconv.FormatInt(int64(u), 10) + " BTC" + } +} + +// Amount represents the base bitcoin monetary unit (colloquially referred +// to as a `Satoshi'). A single Amount is equal to 1e-8 of a bitcoin. +type Amount int64 + +// NewAmount creates an Amount from a floating point value representing +// some value in bitcoin. +func NewAmount(f float64) (Amount, error) { + a := f * float64(SatoshiPerBitcoin) + + // The amount is only valid if it does not exceed the total amount + // of bitcoin producable, and is not a floating point number that + // would otherwise fail that check such as NaN or +-Inf. + switch abs := math.Abs(a); { + case abs > float64(MaxSatoshi): + fallthrough + case math.IsNaN(abs) || math.IsInf(abs, 1): + return 0, errors.New("invalid bitcoin amount") + } + + // Depending on the sign, add or subtract 0.5 and rely on integer + // truncation to correctly round the value up or down. + if a < 0 { + a = a - 0.5 + } else { + a = a + 0.5 + } + return Amount(a), nil +} + +// ToUnit converts a monetary amount counted in bitcoin base units to a +// floating point value representing an amount of bitcoin. +func (a Amount) ToUnit(u AmountUnit) float64 { + return float64(a) / math.Pow10(int(u+8)) +} + +// Format formats a monetary amount counted in bitcoin base units as a +// string for a given unit. The conversion will succeed for any unit, +// however, known units will be formated with an appended label describing +// the units with SI notation. +func (a Amount) Format(u AmountUnit) string { + units := " " + u.String() + return strconv.FormatFloat(a.ToUnit(u), 'f', -int(u+8), 64) + units +} diff --git a/amount_test.go b/amount_test.go new file mode 100644 index 0000000..f6d6d79 --- /dev/null +++ b/amount_test.go @@ -0,0 +1,183 @@ +// Copyright (c) 2013, 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcutil_test + +import ( + . "github.com/conformal/btcutil" + "math" + "testing" +) + +func TestAmountCreation(t *testing.T) { + tests := []struct { + name string + amount float64 + valid bool + expected Amount + }{ + // Positive tests. + { + name: "zero", + amount: 0, + valid: true, + expected: 0, + }, + { + name: "max", + amount: 21e6, + valid: true, + expected: Amount(MaxSatoshi), + }, + { + name: "min", + amount: -21e6, + valid: true, + expected: Amount(-MaxSatoshi), + }, + { + name: "one hundred", + amount: 100, + valid: true, + expected: Amount(100 * SatoshiPerBitcoin), + }, + { + name: "fraction", + amount: 0.01234567, + valid: true, + expected: Amount(1234567), + }, + { + name: "rounding up", + amount: 54.999999999999943157, + valid: true, + expected: Amount(55 * SatoshiPerBitcoin), + }, + { + name: "rounding down", + amount: 55.000000000000056843, + valid: true, + expected: Amount(55 * SatoshiPerBitcoin), + }, + + // Negative tests. + { + name: "exceeds max", + amount: 21e6 + 1, + valid: false, + }, + { + name: "exceeds min", + amount: -21e6 - 1, + valid: false, + }, + { + name: "not-a-number", + amount: math.NaN(), + valid: false, + }, + { + name: "-infinity", + amount: math.Inf(-1), + valid: false, + }, + { + name: "+infinity", + amount: math.Inf(1), + valid: false, + }, + } + + for _, test := range tests { + a, err := NewAmount(test.amount) + switch { + case test.valid && err != nil: + t.Errorf("%v: Positive test Amount creation failed with: %v", test.name, err) + continue + case !test.valid && err == nil: + t.Errorf("%v: Negative test Amount creation succeeded (value %v) when should fail", test.name, a) + continue + } + + if a != test.expected { + t.Errorf("%v: Created amount %v does not match expected %v", test.name, a, test.expected) + } + } +} + +func TestAmountUnitConversions(t *testing.T) { + tests := []struct { + name string + amount Amount + unit AmountUnit + converted float64 + s string + }{ + { + name: "MBTC", + amount: Amount(MaxSatoshi), + unit: AmountMegaBitcoin, + converted: 21, + s: "21 MBTC", + }, + { + name: "kBTC", + amount: Amount(44433322211100), + unit: AmountKiloBitcoin, + converted: 444.33322211100, + s: "444.333222111 kBTC", + }, + { + name: "BTC", + amount: Amount(44433322211100), + unit: AmountBitcoin, + converted: 444333.22211100, + s: "444333.222111 BTC", + }, + { + name: "mBTC", + amount: Amount(44433322211100), + unit: AmountMilliBitcoin, + converted: 444333222.11100, + s: "444333222.111 mBTC", + }, + { + + name: "μBTC", + amount: Amount(44433322211100), + unit: AmountMicroBitcoin, + converted: 444333222111.00, + s: "444333222111 μBTC", + }, + { + + name: "satoshi", + amount: Amount(44433322211100), + unit: AmountBaseBitcoin, + converted: 44433322211100, + s: "44433322211100 Satoshi", + }, + { + + name: "non-standard unit", + amount: Amount(44433322211100), + unit: AmountUnit(-1), + converted: 4443332.2211100, + s: "4443332.22111 1e-1 BTC", + }, + } + + for _, test := range tests { + f := test.amount.ToUnit(test.unit) + if f != test.converted { + t.Errorf("%v: converted value %v does not match expected %v", test.name, f, test.converted) + continue + } + + s := test.amount.Format(test.unit) + if s != test.s { + t.Errorf("%v: format '%v' does not match expected '%v'", test.name, s, test.s) + } + } +} diff --git a/test_coverage.txt b/test_coverage.txt index 7b99711..41764c6 100644 --- a/test_coverage.txt +++ b/test_coverage.txt @@ -4,32 +4,36 @@ github.com/conformal/btcutil/base58.go Base58Encode 100.00% (15/15) github.com/conformal/btcutil/block.go Block.Tx 100.00% (12/12) github.com/conformal/btcutil/block.go Block.Transactions 100.00% (11/11) github.com/conformal/btcutil/address.go encodeAddress 100.00% (9/9) -github.com/conformal/btcutil/tx.go NewTxFromBytes 100.00% (7/7) +github.com/conformal/btcutil/amount.go AmountUnit.String 100.00% (8/8) +github.com/conformal/btcutil/amount.go NewAmount 100.00% (8/8) github.com/conformal/btcutil/block.go NewBlockFromBytes 100.00% (7/7) -github.com/conformal/btcutil/block.go Block.Sha 100.00% (5/5) +github.com/conformal/btcutil/tx.go NewTxFromBytes 100.00% (7/7) github.com/conformal/btcutil/tx.go Tx.Sha 100.00% (5/5) +github.com/conformal/btcutil/block.go Block.Sha 100.00% (5/5) +github.com/conformal/btcutil/amount.go Amount.Format 100.00% (2/2) github.com/conformal/btcutil/address.go NewAddressScriptHash 100.00% (2/2) github.com/conformal/btcutil/hash160.go calcHash 100.00% (2/2) -github.com/conformal/btcutil/address.go AddressScriptHash.ScriptAddress 100.00% (1/1) -github.com/conformal/btcutil/address.go AddressPubKey.EncodeAddress 100.00% (1/1) -github.com/conformal/btcutil/address.go AddressScriptHash.String 100.00% (1/1) -github.com/conformal/btcutil/tx.go Tx.MsgTx 100.00% (1/1) -github.com/conformal/btcutil/address.go AddressPubKeyHash.String 100.00% (1/1) -github.com/conformal/btcutil/tx.go Tx.SetIndex 100.00% (1/1) -github.com/conformal/btcutil/address.go AddressPubKey.ScriptAddress 100.00% (1/1) -github.com/conformal/btcutil/address.go AddressPubKey.String 100.00% (1/1) -github.com/conformal/btcutil/address.go AddressPubKeyHash.ScriptAddress 100.00% (1/1) -github.com/conformal/btcutil/block.go NewBlock 100.00% (1/1) -github.com/conformal/btcutil/block.go Block.SetHeight 100.00% (1/1) -github.com/conformal/btcutil/block.go Block.Height 100.00% (1/1) github.com/conformal/btcutil/hash160.go Hash160 100.00% (1/1) github.com/conformal/btcutil/address.go AddressPubKeyHash.EncodeAddress 100.00% (1/1) -github.com/conformal/btcutil/tx.go NewTx 100.00% (1/1) -github.com/conformal/btcutil/block.go NewBlockFromBlockAndBytes 100.00% (1/1) +github.com/conformal/btcutil/address.go AddressPubKeyHash.ScriptAddress 100.00% (1/1) +github.com/conformal/btcutil/address.go AddressPubKeyHash.String 100.00% (1/1) github.com/conformal/btcutil/address.go AddressScriptHash.EncodeAddress 100.00% (1/1) -github.com/conformal/btcutil/tx.go Tx.Index 100.00% (1/1) +github.com/conformal/btcutil/address.go AddressScriptHash.ScriptAddress 100.00% (1/1) +github.com/conformal/btcutil/address.go AddressScriptHash.String 100.00% (1/1) +github.com/conformal/btcutil/address.go AddressPubKey.EncodeAddress 100.00% (1/1) +github.com/conformal/btcutil/address.go AddressPubKey.ScriptAddress 100.00% (1/1) +github.com/conformal/btcutil/address.go AddressPubKey.String 100.00% (1/1) +github.com/conformal/btcutil/amount.go Amount.ToUnit 100.00% (1/1) github.com/conformal/btcutil/block.go OutOfRangeError.Error 100.00% (1/1) github.com/conformal/btcutil/block.go Block.MsgBlock 100.00% (1/1) +github.com/conformal/btcutil/block.go Block.Height 100.00% (1/1) +github.com/conformal/btcutil/block.go Block.SetHeight 100.00% (1/1) +github.com/conformal/btcutil/block.go NewBlock 100.00% (1/1) +github.com/conformal/btcutil/block.go NewBlockFromBlockAndBytes 100.00% (1/1) +github.com/conformal/btcutil/tx.go Tx.MsgTx 100.00% (1/1) +github.com/conformal/btcutil/tx.go Tx.Index 100.00% (1/1) +github.com/conformal/btcutil/tx.go Tx.SetIndex 100.00% (1/1) +github.com/conformal/btcutil/tx.go NewTx 100.00% (1/1) github.com/conformal/btcutil/address.go DecodeAddress 95.65% (22/23) github.com/conformal/btcutil/appdata.go appDataDir 92.00% (23/25) github.com/conformal/btcutil/address.go NewAddressPubKeyHash 91.67% (11/12) @@ -38,17 +42,17 @@ github.com/conformal/btcutil/address.go EncodePrivateKey 90.91% (20/22) github.com/conformal/btcutil/block.go Block.TxLoc 88.89% (8/9) github.com/conformal/btcutil/block.go Block.Bytes 88.89% (8/9) github.com/conformal/btcutil/address.go AddressPubKey.serialize 85.71% (6/7) +github.com/conformal/btcutil/address.go DecodePrivateKey 83.33% (20/24) github.com/conformal/btcutil/address.go NewAddressPubKey 83.33% (15/18) github.com/conformal/btcutil/address.go checkBitcoinNet 83.33% (5/6) -github.com/conformal/btcutil/address.go DecodePrivateKey 82.61% (19/23) github.com/conformal/btcutil/block.go Block.TxSha 75.00% (3/4) github.com/conformal/btcutil/address.go AddressPubKeyHash.IsForNet 60.00% (3/5) -github.com/conformal/btcutil/address.go AddressPubKey.IsForNet 60.00% (3/5) github.com/conformal/btcutil/address.go AddressScriptHash.IsForNet 60.00% (3/5) +github.com/conformal/btcutil/address.go AddressPubKey.IsForNet 60.00% (3/5) github.com/conformal/btcutil/certgen.go NewTLSCertPair 0.00% (0/50) github.com/conformal/btcutil/address.go AddressPubKey.AddressPubKeyHash 0.00% (0/3) -github.com/conformal/btcutil/address.go AddressPubKey.SetFormat 0.00% (0/1) github.com/conformal/btcutil/appdata.go AppDataDir 0.00% (0/1) github.com/conformal/btcutil/address.go AddressPubKey.Format 0.00% (0/1) -github.com/conformal/btcutil ------------------------------- 77.25% (275/356) +github.com/conformal/btcutil/address.go AddressPubKey.SetFormat 0.00% (0/1) +github.com/conformal/btcutil ------------------------------- 78.46% (295/376)