Skip to content

Commit

Permalink
Merge pull request #3129 from oasisprotocol/tjanez/prettier-unify-fra…
Browse files Browse the repository at this point in the history
…c-comp

Unify pretty printing of quantity.Quantity fractions
  • Loading branch information
tjanez authored Jul 24, 2020
2 parents 204b4f7 + ae3c937 commit 02a2481
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 37 deletions.
1 change: 1 addition & 0 deletions .changelog/3129.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go/common/prettyprint: Add `FractionBase10()`
33 changes: 33 additions & 0 deletions go/common/prettyprint/quantity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package prettyprint

import (
"fmt"
"math/big"
"strings"

"github.com/oasisprotocol/oasis-core/go/common/quantity"
)

// FractionBase10 returns a decimal representation of a fraction from fraction's
// numerator and denominator's base-10 exponent.
func FractionBase10(numerator quantity.Quantity, denominatorExp uint8) string {
denominator := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(denominatorExp)), nil)

// NOTE: We use DivMod() and manual string construction to avoid conversion
// to other types and support arbitrarily large amounts.
var quotient, remainder *big.Int
quotient, remainder = new(big.Int).DivMod(numerator.ToBigInt(), denominator, new(big.Int))

// Prefix the remainder with the appropriate number of zeros.
remainderStr := fmt.Sprintf("%0*s", denominatorExp, remainder)
// Trim trailing zeros from the remainder.
remainderStr = strings.TrimRight(remainderStr, "0")
// Ensure remainder is not empty.
if remainderStr == "" {
remainderStr = "0"
}

// Combine quotient and remainder to a string representing the decimal
// representation of the given fraction.
return fmt.Sprintf("%s.%s", quotient, remainderStr)
}
38 changes: 38 additions & 0 deletions go/common/prettyprint/quantity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package prettyprint

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/oasisprotocol/oasis-core/go/common/quantity"
)

func TestBase10Fraction(t *testing.T) {
require := require.New(t)

for _, t := range []struct {
expectedOutput string
numerator *quantity.Quantity
denominatorExp uint8
}{
{"10000000000.0", quantity.NewFromUint64(10000000000000000000), 9},
{"100.0", quantity.NewFromUint64(100000000000), 9},
{"7999217230.11968289", quantity.NewFromUint64(7999217230119682890), 9},
{"7999217230.1196", quantity.NewFromUint64(7999217230119600000), 9},
{"7999217230.1", quantity.NewFromUint64(7999217230100000000), 9},
{"0.0", quantity.NewFromUint64(0), 9},
// Checks for large and small denominator base-10 exponents.
{"0.010000000000000000001", quantity.NewFromUint64(10000000000000000001), 21},
{"10.0", quantity.NewFromUint64(10000000000000000000), 18},
{"10.000000000000000001", quantity.NewFromUint64(10000000000000000001), 18},
{"0.0000001", quantity.NewFromUint64(100000000000), 18},
{"0.0", quantity.NewFromUint64(0), 18},
{"10000000000000000000.0", quantity.NewFromUint64(10000000000000000000), 0},
{"10000000000000000001.0", quantity.NewFromUint64(10000000000000000001), 0},
{"0.0", quantity.NewFromUint64(0), 0},
} {
output := FractionBase10(*t.numerator, t.denominatorExp)
require.Equal(t.expectedOutput, output, "obtained pretty print didn't match expected value")
}
}
31 changes: 14 additions & 17 deletions go/staking/api/commission.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import (
"fmt"
"io"
"math/big"
"strconv"

"github.com/oasisprotocol/oasis-core/go/common/prettyprint"
"github.com/oasisprotocol/oasis-core/go/common/quantity"
epochtime "github.com/oasisprotocol/oasis-core/go/epochtime/api"
)

// commissionRateDenominatorExponent is the commission rate denominator's
// base-10 exponent.
//
// NOTE: Setting it to 5 means commission rates are denominated in 1000ths of a
// percent.
const commissionRateDenominatorExponent uint8 = 5

var (
// CommissionRateDenominator is the denominator for the commission rate.
CommissionRateDenominator *quantity.Quantity
Expand All @@ -21,17 +27,6 @@ var (
_ prettyprint.PrettyPrinter = (*CommissionSchedule)(nil)
)

// CommissionRatePercentage returns the string representing the commission rate
// in percentage for the given commission rate numerator.
func CommissionRatePercentage(rateNumerator quantity.Quantity) string {
rate := big.NewRat(rateNumerator.ToBigInt().Int64(), CommissionRateDenominator.ToBigInt().Int64())
// Multiply rate by 100 to convert it to percentage.
rate.Mul(rate, big.NewRat(100, 1))
// Return string representation of the rate that omits the trailing zeros.
rateFloat, _ := rate.Float64()
return strconv.FormatFloat(rateFloat, 'f', -1, 64)
}

// CommissionScheduleRules controls how commission schedule rates and rate
// bounds are allowed to be changed.
type CommissionScheduleRules struct {
Expand Down Expand Up @@ -59,7 +54,7 @@ type CommissionRateStep struct {
func (crs CommissionRateStep) PrettyPrint(ctx context.Context, prefix string, w io.Writer) {
fmt.Fprintf(w, "%s- Start: epoch %d\n", prefix, crs.Start)

fmt.Fprintf(w, "%s Rate: %s %%\n", prefix, CommissionRatePercentage(crs.Rate))
fmt.Fprintf(w, "%s Rate: %s\n", prefix, PrettyPrintCommissionRatePercentage(crs.Rate))
}

// PrettyType returns a representation of CommissionRateStep that can be used
Expand All @@ -84,8 +79,8 @@ type CommissionRateBoundStep struct {
func (crbs CommissionRateBoundStep) PrettyPrint(ctx context.Context, prefix string, w io.Writer) {
fmt.Fprintf(w, "%s- Start: epoch %d\n", prefix, crbs.Start)

fmt.Fprintf(w, "%s Minimum Rate: %s %%\n", prefix, CommissionRatePercentage(crbs.RateMin))
fmt.Fprintf(w, "%s Maximum Rate: %s %%\n", prefix, CommissionRatePercentage(crbs.RateMax))
fmt.Fprintf(w, "%s Minimum Rate: %s\n", prefix, PrettyPrintCommissionRatePercentage(crbs.RateMin))
fmt.Fprintf(w, "%s Maximum Rate: %s\n", prefix, PrettyPrintCommissionRatePercentage(crbs.RateMax))
}

// PrettyType returns a representation of CommissionRateBoundStep that can be
Expand Down Expand Up @@ -388,9 +383,11 @@ func (cs *CommissionSchedule) CurrentRate(now epochtime.EpochTime) *quantity.Qua
}

func init() {
// Denominated in 1000th of a percent.
// Compute CommissionRateDenominator from its base-10 exponent.
CommissionRateDenominator = quantity.NewQuantity()
err := CommissionRateDenominator.FromInt64(100_000)
err := CommissionRateDenominator.FromBigInt(
new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(commissionRateDenominatorExponent)), nil),
)
if err != nil {
panic(err)
}
Expand Down
36 changes: 16 additions & 20 deletions go/staking/api/prettyprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import (
"context"
"fmt"
"io"
"math/big"
"strings"

"github.com/oasisprotocol/oasis-core/go/common/prettyprint"
"github.com/oasisprotocol/oasis-core/go/common/quantity"
)

Expand All @@ -29,24 +28,7 @@ func ConvertToTokenAmount(amount quantity.Quantity, tokenValueExponent uint8) (s
return "", ErrInvalidTokenValueExponent
}

divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenValueExponent)), nil)

// NOTE: We use DivMod() and manual string construction to avoid conversion
// to other types and support arbitrarily large amounts.
var quotient, remainder *big.Int
quotient, remainder = new(big.Int).DivMod(amount.ToBigInt(), divisor, new(big.Int))

// Prefix the remainder with the appropriate number of zeros.
remainderStr := fmt.Sprintf("%0*s", tokenValueExponent, remainder)
// Trim trailing zeros from the remainder.
remainderStr = strings.TrimRight(remainderStr, "0")
// Ensure remainder is not empty.
if remainderStr == "" {
remainderStr = "0"
}

// Combine quotient and remainder to a string representing the token amount.
return fmt.Sprintf("%s.%s", quotient, remainderStr), nil
return prettyprint.FractionBase10(amount, tokenValueExponent), nil
}

// PrettyPrintAmount writes a pretty-printed representation of the given amount
Expand Down Expand Up @@ -82,3 +64,17 @@ func PrettyPrintAmount(ctx context.Context, amount quantity.Quantity, w io.Write
fmt.Fprintf(w, "%s %s", symbol, tokenAmount)
}
}

// PrettyPrintCommissionRatePercentage returns the string representing the
// commission rate (bound) in percentage for the given commission rate (bound)
// numerator.
func PrettyPrintCommissionRatePercentage(rateNumerator quantity.Quantity) string {
// Handle invalid commission rate (bound) numerator.
if rateNumerator.Cmp(CommissionRateDenominator) > 0 {
return "(invalid)"
}
// Reduce commission rate denominator's base-10 exponent by 2 to obtain the
// value in percentage.
denominatorExp := commissionRateDenominatorExponent - 2
return fmt.Sprintf("%s%%", prettyprint.FractionBase10(rateNumerator, denominatorExp))
}
22 changes: 22 additions & 0 deletions go/staking/api/prettyprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,25 @@ func TestPrettyPrintAmount(t *testing.T) {
"pretty printing stake amount didn't return the expected result")
}
}

func TestPrettyPrintCommissionRatePercentage(t *testing.T) {
require := require.New(t)

for _, t := range []struct {
expectedRate string
rateNumerator *quantity.Quantity
}{
{"0.0%", quantity.NewFromUint64(0)},
{"50.0%", quantity.NewFromUint64(50_000)},
{"100.0%", quantity.NewFromUint64(100_000)},
{"20.2%", quantity.NewFromUint64(20_200)},
{"30.03%", quantity.NewFromUint64(30_030)},
{"12.345%", quantity.NewFromUint64(12_345)},
// Checks for invalid commission rate numerators.
{"(invalid)", quantity.NewFromUint64(100_001)},
{"(invalid)", quantity.NewFromUint64(123_456)},
} {
rate := PrettyPrintCommissionRatePercentage(*t.rateNumerator)
require.Equal(t.expectedRate, rate, "obtained pretty print didn't match expected value")
}
}

0 comments on commit 02a2481

Please sign in to comment.