internal/number: add number formatting
Now supports formatting of decimal,
scientific/ engineering and percent.
Change-Id: If94b0684dc1b1bd66106a07828d3b7f695f12e2d
Reviewed-on: https://go-review.googlesource.com/45494
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/internal/number/format.go b/internal/number/format.go
new file mode 100755
index 0000000..84903fa
--- /dev/null
+++ b/internal/number/format.go
@@ -0,0 +1,321 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package number
+
+import (
+ "strconv"
+
+ "golang.org/x/text/language"
+)
+
+// TODO:
+// - public (but internal) API for creating formatters
+// - split out the logic that computes the visible digits from the rest of the
+// formatting code (needed for plural).
+// - grouping of fractions
+// - reuse percent pattern for permille
+// - padding
+
+// Formatter contains all the information needed to render a number.
+type Formatter struct {
+ *Pattern
+ Info
+ RoundingContext
+ f func(dst []byte, f *Formatter, d *Decimal) []byte
+}
+
+func lookupFormat(t language.Tag, tagToIndex []uint8) *Pattern {
+ for ; ; t = t.Parent() {
+ if ci, ok := language.CompactIndex(t); ok {
+ return &formats[tagToIndex[ci]]
+ }
+ }
+}
+
+func (f *Formatter) Format(dst []byte, d *Decimal) []byte {
+ return f.f(dst, f, d)
+}
+
+func appendDecimal(dst []byte, f *Formatter, d *Decimal) []byte {
+ if dst, ok := f.renderSpecial(dst, d); ok {
+ return dst
+ }
+ n := d.normalize()
+ if maxSig := int(f.MaxSignificantDigits); maxSig > 0 {
+ n.round(ToZero, maxSig)
+ }
+ digits := n.Digits
+ exp := n.Exp
+
+ // Split in integer and fraction part.
+ var intDigits, fracDigits []byte
+ var numInt, numFrac int
+ if exp > 0 {
+ numInt = int(exp)
+ if int(exp) >= len(digits) { // ddddd | ddddd00
+ intDigits = digits
+ } else { // ddd.dd
+ intDigits = digits[:exp]
+ fracDigits = digits[exp:]
+ numFrac = len(fracDigits)
+ }
+ } else {
+ fracDigits = digits
+ numFrac = -int(exp) + len(digits)
+ }
+ // Cap integer digits. Remove *most-significant* digits.
+ if f.MaxIntegerDigits > 0 && numInt > int(f.MaxIntegerDigits) {
+ offset := numInt - int(f.MaxIntegerDigits)
+ if offset > len(intDigits) {
+ numInt = 0
+ intDigits = nil
+ } else {
+ numInt = int(f.MaxIntegerDigits)
+ intDigits = intDigits[offset:]
+ // for keeping track of significant digits
+ digits = digits[offset:]
+ }
+ // Strip leading zeros. Resulting number of digits is significant digits.
+ for len(intDigits) > 0 && intDigits[0] == 0 {
+ intDigits = intDigits[1:]
+ digits = digits[1:]
+ numInt--
+ }
+ }
+ if f.MaxSignificantDigits == 0 && int(f.MaxFractionDigits) < numFrac {
+ if extra := numFrac - int(f.MaxFractionDigits); extra > len(fracDigits) {
+ numFrac = 0
+ fracDigits = nil
+ } else {
+ numFrac = int(f.MaxFractionDigits)
+ fracDigits = fracDigits[:len(fracDigits)-extra]
+ }
+ }
+
+ neg := d.Neg && numInt+numFrac > 0
+ affix, suffix := f.getAffixes(neg)
+ dst = appendAffix(dst, f, affix, neg)
+ savedLen := len(dst)
+
+ minInt := int(f.MinIntegerDigits)
+ if minInt == 0 && f.MinSignificantDigits > 0 {
+ minInt = 1
+ }
+ // add leading zeros
+ for i := numInt; i < minInt; i++ {
+ dst = f.AppendDigit(dst, 0)
+ if f.needsSep(minInt - i) {
+ dst = append(dst, f.Symbol(SymGroup)...)
+ }
+ }
+ i := 0
+ for ; i < len(intDigits); i++ {
+ dst = f.AppendDigit(dst, intDigits[i])
+ if f.needsSep(numInt - i) {
+ dst = append(dst, f.Symbol(SymGroup)...)
+ }
+ }
+ for ; i < numInt; i++ {
+ dst = f.AppendDigit(dst, 0)
+ if f.needsSep(numInt - i) {
+ dst = append(dst, f.Symbol(SymGroup)...)
+ }
+ }
+
+ trailZero := int(f.MinFractionDigits) - numFrac
+ if d := int(f.MinSignificantDigits) - len(digits); d > 0 && d > trailZero {
+ trailZero = d
+ }
+ if numFrac > 0 || trailZero > 0 || f.Flags&AlwaysDecimalSeparator != 0 {
+ dst = append(dst, f.Symbol(SymDecimal)...)
+ }
+ // Add leading zeros
+ for i := numFrac - len(fracDigits); i > 0; i-- {
+ dst = f.AppendDigit(dst, 0)
+ }
+ i = 0
+ for ; i < len(fracDigits); i++ {
+ dst = f.AppendDigit(dst, fracDigits[i])
+ }
+ for ; trailZero > 0; trailZero-- {
+ dst = f.AppendDigit(dst, 0)
+ }
+ // Ensure that at least one digit is written no matter what. This makes
+ // things more robust, even though a pattern should always require at least
+ // one fraction or integer digit.
+ if len(dst) == savedLen {
+ dst = f.AppendDigit(dst, 0)
+ }
+ return appendAffix(dst, f, suffix, neg)
+}
+
+func appendScientific(dst []byte, f *Formatter, d *Decimal) []byte {
+ if dst, ok := f.renderSpecial(dst, d); ok {
+ return dst
+ }
+ // Significant digits are transformed by parser for scientific notation and
+ // do not need to be handled here.
+ maxInt, numInt := int(f.MaxIntegerDigits), int(f.MinIntegerDigits)
+ if numInt == 0 {
+ numInt = 1
+ }
+ maxSig := int(f.MaxFractionDigits) + numInt
+ minSig := int(f.MinFractionDigits) + numInt
+ n := d.normalize()
+ if maxSig > 0 {
+ n.round(ToZero, maxSig)
+ }
+ digits := n.Digits
+ exp := n.Exp
+
+ // If a maximum number of integers is specified, the minimum must be 1
+ // and the exponent is grouped by this number (e.g. for engineering)
+ if len(digits) == 0 {
+ exp = 0
+ } else if maxInt > numInt {
+ // Correct the exponent to reflect a single integer digit.
+ exp--
+ numInt = 1
+ // engineering
+ // 0.01234 ([12345]e-1) -> 1.2345e-2 12.345e-3
+ // 12345 ([12345]e+5) -> 1.2345e4 12.345e3
+ d := int(exp) % maxInt
+ if d < 0 {
+ d += maxInt
+ }
+ exp -= int32(d)
+ numInt += d
+ } else {
+ exp -= int32(numInt)
+ }
+ var intDigits, fracDigits []byte
+ if numInt <= len(digits) {
+ intDigits = digits[:numInt]
+ fracDigits = digits[numInt:]
+ } else {
+ intDigits = digits
+ }
+ neg := d.Neg && len(digits) > 0
+ affix, suffix := f.getAffixes(neg)
+ dst = appendAffix(dst, f, affix, neg)
+ savedLen := len(dst)
+
+ i := 0
+ for ; i < len(intDigits); i++ {
+ dst = f.AppendDigit(dst, intDigits[i])
+ if f.needsSep(numInt - i) {
+ dst = append(dst, f.Symbol(SymGroup)...)
+ }
+ }
+ for ; i < numInt; i++ {
+ dst = f.AppendDigit(dst, 0)
+ if f.needsSep(numInt - i) {
+ dst = append(dst, f.Symbol(SymGroup)...)
+ }
+ }
+
+ trailZero := minSig - numInt - len(fracDigits)
+ if len(fracDigits) > 0 || trailZero > 0 || f.Flags&AlwaysDecimalSeparator != 0 {
+ dst = append(dst, f.Symbol(SymDecimal)...)
+ }
+ i = 0
+ for ; i < len(fracDigits); i++ {
+ dst = f.AppendDigit(dst, fracDigits[i])
+ }
+ for ; trailZero > 0; trailZero-- {
+ dst = f.AppendDigit(dst, 0)
+ }
+ // Ensure that at least one digit is written no matter what. This makes
+ // things more robust, even though a pattern should always require at least
+ // one fraction or integer digit.
+ if len(dst) == savedLen {
+ dst = f.AppendDigit(dst, 0)
+ }
+
+ // exp
+ dst = append(dst, f.Symbol(SymExponential)...)
+ switch {
+ case exp < 0:
+ dst = append(dst, f.Symbol(SymMinusSign)...)
+ exp = -exp
+ case f.Flags&AlwaysExpSign != 0:
+ dst = append(dst, f.Symbol(SymPlusSign)...)
+ }
+ buf := [12]byte{}
+ b := strconv.AppendUint(buf[:0], uint64(exp), 10)
+ for i := len(b); i < int(f.MinExponentDigits); i++ {
+ dst = f.AppendDigit(dst, 0)
+ }
+ for _, c := range b {
+ dst = f.AppendDigit(dst, c-'0')
+ }
+ return appendAffix(dst, f, suffix, neg)
+}
+
+func (f *Formatter) getAffixes(neg bool) (affix, suffix string) {
+ str := f.Affix
+ if str != "" {
+ if f.NegOffset > 0 {
+ if neg {
+ str = str[f.NegOffset:]
+ } else {
+ str = str[:f.NegOffset]
+ }
+ }
+ sufStart := 1 + str[0]
+ affix = str[1:sufStart]
+ suffix = str[sufStart+1:]
+ } else if neg {
+ affix = "-"
+ }
+ return affix, suffix
+}
+
+func (f *Formatter) renderSpecial(dst []byte, d *Decimal) (b []byte, ok bool) {
+ if d.NaN {
+ return fmtNaN(dst, f), true
+ }
+ if d.Inf {
+ return fmtInfinite(dst, f, d), true
+ }
+ return dst, false
+}
+
+func fmtNaN(dst []byte, f *Formatter) []byte {
+ return append(dst, f.Symbol(SymNan)...)
+}
+
+func fmtInfinite(dst []byte, f *Formatter, d *Decimal) []byte {
+ if d.Neg {
+ dst = append(dst, f.Symbol(SymMinusSign)...)
+ }
+ return append(dst, f.Symbol(SymInfinity)...)
+}
+
+func appendAffix(dst []byte, f *Formatter, affix string, neg bool) []byte {
+ quoting := false
+ escaping := false
+ for _, r := range affix {
+ switch {
+ case escaping:
+ // escaping occurs both inside and outside of quotes
+ dst = append(dst, string(r)...)
+ escaping = false
+ case r == '\\':
+ escaping = true
+ case r == '\'':
+ quoting = !quoting
+ case !quoting && (r == '-' || r == '+'):
+ if neg {
+ dst = append(dst, f.Symbol(SymMinusSign)...)
+ } else {
+ dst = append(dst, f.Symbol(SymPlusSign)...)
+ }
+ default:
+ dst = append(dst, string(r)...)
+ }
+ }
+ return dst
+}
diff --git a/internal/number/format_test.go b/internal/number/format_test.go
new file mode 100755
index 0000000..355a33a
--- /dev/null
+++ b/internal/number/format_test.go
@@ -0,0 +1,363 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package number
+
+import (
+ "fmt"
+ "log"
+ "strings"
+ "testing"
+
+ "golang.org/x/text/language"
+)
+
+func TestAppendDecimal(t *testing.T) {
+ type pairs map[string]string // alternates with decimal input and result
+
+ testCases := []struct {
+ pattern string
+ // We want to be able to test some forms of patterns that cannot be
+ // represented as a string.
+ pat *Pattern
+
+ test pairs
+ }{{
+ pattern: "0",
+ test: pairs{
+ "0": "0",
+ "1": "1",
+ "-1": "-1",
+ ".00": "0",
+ "10.": "10",
+ "12": "12",
+ "1.2": "1",
+ "NaN": "NaN",
+ "-Inf": "-∞",
+ },
+ }, {
+ pattern: "+0",
+ test: pairs{
+ "0": "+0",
+ "1": "+1",
+ "-1": "-1",
+ ".00": "+0",
+ "10.": "+10",
+ "12": "+12",
+ "1.2": "+1",
+ "NaN": "NaN",
+ "-Inf": "-∞",
+ },
+ }, {
+ pattern: "0 +",
+ test: pairs{
+ "0": "0 +",
+ "1": "1 +",
+ "-1": "1 -",
+ ".00": "0 +",
+ },
+ }, {
+ pattern: "0;0-",
+ test: pairs{
+ "-1": "1-",
+ },
+ }, {
+ pattern: "0000",
+ test: pairs{
+ "0": "0000",
+ "1": "0001",
+ "12": "0012",
+ "12345": "12345",
+ },
+ }, {
+ pattern: ".0",
+ test: pairs{
+ "0": ".0",
+ "1": "1.0",
+ "1.2": "1.2",
+ "1.2345": "1.2",
+ },
+ }, {
+ pattern: "#.0",
+ test: pairs{
+ "0": ".0",
+ },
+ }, {
+ pattern: "#.0#",
+ test: pairs{
+ "0": ".0",
+ "1": "1.0",
+ },
+ }, {
+ pattern: "0.0#",
+ test: pairs{
+ "0": "0.0",
+ },
+ }, {
+ pattern: "#0.###",
+ test: pairs{
+ "0": "0",
+ "1": "1",
+ "1.2": "1.2",
+ "1.2345": "1.234", // rounding should have been done earlier
+ "1234.5": "1234.5",
+ "1234.567": "1234.567",
+ },
+ }, {
+ pattern: "#0.######",
+ test: pairs{
+ "0": "0",
+ "1234.5678": "1234.5678",
+ "0.123456789": "0.123456",
+ "NaN": "NaN",
+ "Inf": "∞",
+ },
+
+ // Test separators.
+ }, {
+ pattern: "#,#.00",
+ test: pairs{
+ "100": "1,0,0.00",
+ },
+ }, {
+ pattern: "#,0.##",
+ test: pairs{
+ "10": "1,0",
+ },
+ }, {
+ pattern: "#,0",
+ test: pairs{
+ "10": "1,0",
+ },
+ }, {
+ pattern: "#,##,#.00",
+ test: pairs{
+ "1000": "1,00,0.00",
+ },
+ }, {
+ pattern: "#,##0.###",
+ test: pairs{
+ "0": "0",
+ "1234.5678": "1,234.567",
+ "0.123456789": "0.123",
+ },
+ }, {
+ pattern: "#,##,##0.###",
+ test: pairs{
+ "0": "0",
+ "123456789012": "1,23,45,67,89,012",
+ "0.123456789": "0.123",
+ },
+
+ // Support for ill-formed patterns.
+ }, {
+ pattern: "#",
+ test: pairs{
+ ".00": "0",
+ "0": "0",
+ "1": "1",
+ "10.": "10",
+ },
+ }, {
+ pattern: ".#",
+ test: pairs{
+ "0": "0",
+ "1": "1",
+ "1.2": "1.2",
+ "1.2345": "1.2",
+ },
+ }, {
+ pattern: "#,#.##",
+ test: pairs{
+ "10": "1,0",
+ },
+ }, {
+ pattern: "#,#",
+ test: pairs{
+ "10": "1,0",
+ },
+
+ // Special patterns
+ }, {
+ pattern: "#,max_int=2",
+ pat: &Pattern{
+ MaxIntegerDigits: 2,
+ },
+ test: pairs{
+ "2017": "17",
+ },
+ }, {
+ pattern: "0,max_int=2",
+ pat: &Pattern{
+ MaxIntegerDigits: 2,
+ MinIntegerDigits: 1,
+ },
+ test: pairs{
+ "2000": "0",
+ "2001": "1",
+ "2017": "17",
+ },
+ }, {
+ pattern: "00,max_int=2",
+ pat: &Pattern{
+ MaxIntegerDigits: 2,
+ MinIntegerDigits: 2,
+ },
+ test: pairs{
+ "2000": "00",
+ "2001": "01",
+ "2017": "17",
+ },
+ }, {
+ pattern: "@@@@,max_int=2",
+ pat: &Pattern{
+ MaxIntegerDigits: 2,
+ MinSignificantDigits: 4,
+ },
+ test: pairs{
+ "2017": "17.00",
+ "2000": "0.000",
+ "2001": "1.000",
+ },
+
+ // Significant digits
+ }, {
+ pattern: "@@##",
+ test: pairs{
+ "1": "1.0",
+ "0.1": "0.10",
+ "123": "123",
+ "1234": "1234",
+ "12345": "12340",
+ },
+ }, {
+ pattern: "@@@@",
+ test: pairs{
+ "1": "1.000",
+ ".1": "0.1000",
+ ".001": "0.001000",
+ "123": "123.0",
+ "1234": "1234",
+ "12345": "12340", // rounding down
+ "NaN": "NaN",
+ "-Inf": "-∞",
+ },
+
+ // TODO: rounding
+ // {"@@@@": "23456": "23460"}, // rounding up
+ // TODO: padding
+
+ // Scientific and Engineering notation
+ }, {
+ pattern: "#E0",
+ test: pairs{
+ "0": "0E0",
+ "1": "1E0",
+ "123.456": "1E2",
+ },
+ }, {
+ pattern: "#E+0",
+ test: pairs{
+ "0": "0E+0",
+ "1000": "1E+3",
+ "1E100": "1E+100",
+ "1E-100": "1E-100",
+ "NaN": "NaN",
+ "-Inf": "-∞",
+ },
+ }, {
+ pattern: "##0E00",
+ test: pairs{
+ "100": "100E00",
+ "12345": "10E03",
+ "123.456": "100E00",
+ },
+ }, {
+ pattern: "##0.###E00",
+ test: pairs{
+ "100": "100E00",
+ "12345": "12.34E03",
+ "123.456": "123.4E00",
+ },
+ }, {
+ pattern: "##0.000E00",
+ test: pairs{
+ "100": "100.0E00",
+ "12345": "12.34E03",
+ "123.456": "123.4E00",
+ },
+ }}
+
+ // TODO:
+ // "@@E0",
+ // "@###E00",
+ // "0.0%",
+ // "0.0‰",
+ // "#,##0.00¤",
+ // "#,##0.00 ¤;(#,##0.00 ¤)",
+ // // padding
+ // "*x#",
+ // "#*x",
+ // "*xpre#suf",
+ // "pre*x#suf",
+ // "pre#*xsuf",
+ // "pre#suf*x",
+ for _, tc := range testCases {
+ pat := tc.pat
+ if pat == nil {
+ var err error
+ if pat, err = ParsePattern(tc.pattern); err != nil {
+ log.Fatal(err)
+ }
+ }
+ f := &Formatter{
+ pat,
+ InfoFromTag(language.English),
+ RoundingContext{},
+ appendDecimal,
+ }
+ if strings.IndexByte(tc.pattern, 'E') != -1 {
+ f.f = appendScientific
+ }
+ for dec, want := range tc.test {
+ buf := make([]byte, 100)
+ t.Run(tc.pattern+"/"+dec, func(t *testing.T) {
+ dec := mkdec(dec)
+ buf = f.Format(buf[:0], &dec)
+ if got := string(buf); got != want {
+ t.Errorf("\n got %q\nwant %q", got, want)
+ }
+ })
+ }
+ }
+}
+
+func TestLocales(t *testing.T) {
+ testCases := []struct {
+ tag language.Tag
+ num string
+ want string
+ }{
+ {language.Make("en"), "123456.78", "123,456.78"},
+ {language.Make("de"), "123456.78", "123.456,78"},
+ {language.Make("de-CH"), "123456.78", "123'456.78"},
+ {language.Make("fr"), "123456.78", "123 456,78"},
+ {language.Make("bn"), "123456.78", "১,২৩,৪৫৬.৭৮"},
+ }
+ for _, tc := range testCases {
+ t.Run(fmt.Sprint(tc.tag, "/", tc.num), func(t *testing.T) {
+ f := &Formatter{
+ lookupFormat(tc.tag, tagToDecimal),
+ InfoFromTag(tc.tag),
+ RoundingContext{},
+ appendDecimal,
+ }
+ d := mkdec(tc.num)
+ b := f.Format(nil, &d)
+ if got := string(b); got != tc.want {
+ t.Errorf("got %q; want %q", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/internal/number/pattern.go b/internal/number/pattern.go
index ad8f729..5610c60 100644
--- a/internal/number/pattern.go
+++ b/internal/number/pattern.go
@@ -65,7 +65,27 @@
MinExponentDigits uint8
}
-// A PatternFlag is a bit mask for the flag field of a Format.
+func (f *Pattern) needsSep(pos int) bool {
+ p := pos - 1
+ size := int(f.GroupingSize[0])
+ if size == 0 || p == 0 {
+ return false
+ }
+ if p == size {
+ return true
+ }
+ if p -= size; p < 0 {
+ return false
+ }
+ // TODO: make second groupingsize the same as first if 0 so that we can
+ // avoid this check.
+ if x := int(f.GroupingSize[1]); x != 0 {
+ size = x
+ }
+ return p%size == 0
+}
+
+// A PatternFlag is a bit mask for the flag field of a Pattern.
type PatternFlag uint8
const (
diff --git a/internal/number/tables.go b/internal/number/tables.go
index 5c799b4..10baa0a 100644
--- a/internal/number/tables.go
+++ b/internal/number/tables.go
@@ -1037,7 +1037,7 @@
RoundIncrement: 0x0,
PadRune: 0,
FormatWidth: 0x7,
- GroupingSize: [2]uint8{0x0,
+ GroupingSize: [2]uint8{0x3,
0x0},
Flags: 0x0,
MinIntegerDigits: 0x1,
@@ -1105,7 +1105,7 @@
RoundIncrement: 0x0,
PadRune: 0,
FormatWidth: 0x6,
- GroupingSize: [2]uint8{0x0,
+ GroupingSize: [2]uint8{0x3,
0x0},
Flags: 0x0,
MinIntegerDigits: 0x1,