Add Amount type to represent a monetary value.
ok @davecgh
This commit is contained in:
parent
4d8920c4dd
commit
fc6f0dee54
3 changed files with 302 additions and 21 deletions
94
amount.go
Normal file
94
amount.go
Normal 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
183
amount_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue