Add Amount type to represent a monetary value.

ok @davecgh
This commit is contained in:
Josh Rickmar 2014-04-11 22:03:00 -05:00
parent 4d8920c4dd
commit fc6f0dee54
3 changed files with 302 additions and 21 deletions

94
amount.go Normal file
View file

@ -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
}

183
amount_test.go Normal file
View file

@ -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)
}
}
}

View file

@ -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)