number: added number package
provides more styles and more control over
formatting than fmt.
Change-Id: Ib9d2c27db14804af911556e3caa8b35fddd32fce
Reviewed-on: https://go-review.googlesource.com/59812
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/number/doc.go b/number/doc.go
new file mode 100644
index 0000000..97088d1
--- /dev/null
+++ b/number/doc.go
@@ -0,0 +1,29 @@
+// 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 formats numbers according to the customs of different locales.
+//
+// The number formats of this package allow for greater formatting flexibility
+// than passing values to message.Printf calls as is. It currently supports the
+// builtin Go types and anything that implements the Convert interface
+// (currently internal).
+//
+// p := message.NewPrinter(language.English)
+//
+// p.Printf("%v bottles of beer on the wall.", number.Decimal(1234))
+// // Prints: 1,234 bottles of beer on the wall.
+//
+// p.Printf("%v of gophers lose too much fur", number.Percent(0.12))
+// // Prints: 12% of gophers lose too much fur.
+//
+// p := message.NewPrinter(language.Dutch)
+//
+// p.Printf("There are %v bikes per household.", number.Decimal(1.2))
+// // Prints: Er zijn 1,2 fietsen per huishouden.
+//
+// Provided that the printed translation is available.
+//
+// The width and scale specified in the formatting directives override the
+// configuration of the formatter.
+package number
diff --git a/number/examples_test.go b/number/examples_test.go
new file mode 100644
index 0000000..fb9bcc9
--- /dev/null
+++ b/number/examples_test.go
@@ -0,0 +1,28 @@
+// 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_test
+
+import (
+ "golang.org/x/text/language"
+ "golang.org/x/text/message"
+ "golang.org/x/text/number"
+)
+
+func ExampleMaxIntegerDigits() {
+ const year = 1999
+ p := message.NewPrinter(language.English)
+ p.Println("Year:", number.Decimal(year, number.MaxIntegerDigits(2)))
+
+ // Output:
+ // Year: 99
+}
+
+func ExampleIncrementString() {
+ p := message.NewPrinter(language.English)
+
+ p.Println(number.Decimal(1.33, number.IncrementString("0.50")))
+
+ // Output: 1.50
+}
diff --git a/number/format.go b/number/format.go
new file mode 100755
index 0000000..62af651
--- /dev/null
+++ b/number/format.go
@@ -0,0 +1,106 @@
+// 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"
+ "strings"
+
+ "golang.org/x/text/feature/plural"
+ "golang.org/x/text/internal/format"
+ "golang.org/x/text/internal/number"
+ "golang.org/x/text/language"
+)
+
+// A FormatFunc formates a number.
+type FormatFunc func(x interface{}, opts ...Option) Formatter
+
+// NewFormat creates a FormatFunc based on another FormatFunc and new options.
+// Use NewFormat to cash the creation of formatters.
+func NewFormat(format FormatFunc, opts ...Option) FormatFunc {
+ o := *format(nil).options
+ n := len(o.options)
+ o.options = append(o.options[:n:n], opts...)
+ return func(x interface{}, opts ...Option) Formatter {
+ return newFormatter(&o, opts, x)
+ }
+}
+
+type options struct {
+ verbs string
+ initFunc initFunc
+ options []Option
+ pluralFunc func(t language.Tag, scale int) (f plural.Form, n int)
+}
+
+type optionFlag uint16
+
+const (
+ hasScale optionFlag = 1 << iota
+ hasPrecision
+ noSeparator
+ exact
+)
+
+type initFunc func(f *number.Formatter, t language.Tag)
+
+func newFormatter(o *options, opts []Option, value interface{}) Formatter {
+ if len(opts) > 0 {
+ n := *o
+ n.options = opts
+ o = &n
+ }
+ return Formatter{o, value}
+}
+
+func newOptions(verbs string, f initFunc) *options {
+ return &options{verbs: verbs, initFunc: f}
+}
+
+type Formatter struct {
+ *options
+ value interface{}
+}
+
+// Format implements format.Formatter. It is for internal use only for now.
+func (f Formatter) Format(state format.State, verb rune) {
+ // TODO: consider implementing fmt.Formatter instead and using the following
+ // piece of code. This allows numbers to be rendered mostly as expected
+ // when using fmt. But it may get weird with the spellout options and we
+ // may need more of format.State over time.
+ // lang := language.Und
+ // if s, ok := state.(format.State); ok {
+ // lang = s.Language()
+ // }
+
+ lang := state.Language()
+ if !strings.Contains(f.verbs, string(verb)) {
+ fmt.Fprintf(state, "%%!%s(%T=%v)", string(verb), f.value, f.value)
+ return
+ }
+ var p number.Formatter
+ f.initFunc(&p, lang)
+ for _, o := range f.options.options {
+ o(lang, &p)
+ }
+ if w, ok := state.Width(); ok {
+ p.FormatWidth = uint16(w)
+ }
+ if prec, ok := state.Precision(); ok {
+ switch verb {
+ case 'd':
+ p.SetScale(0)
+ case 'f':
+ p.SetScale(prec)
+ case 'e':
+ p.SetPrecision(prec + 1)
+ case 'g':
+ p.SetPrecision(prec)
+ }
+ }
+ var d number.Decimal
+ d.Convert(p.RoundingContext, f.value)
+ state.Write(p.Format(nil, &d))
+}
diff --git a/number/format_test.go b/number/format_test.go
new file mode 100644
index 0000000..30be72d
--- /dev/null
+++ b/number/format_test.go
@@ -0,0 +1,45 @@
+// 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 (
+ "testing"
+
+ "golang.org/x/text/language"
+ "golang.org/x/text/message"
+)
+
+func TestWrongVerb(t *testing.T) {
+ testCases := []struct {
+ f Formatter
+ fmt string
+ want string
+ }{{
+ f: Decimal(12),
+ fmt: "%e",
+ want: "%!e(int=12)",
+ }, {
+ f: Scientific(12),
+ fmt: "%f",
+ want: "%!f(int=12)",
+ }, {
+ f: Engineering(12),
+ fmt: "%f",
+ want: "%!f(int=12)",
+ }, {
+ f: Percent(12),
+ fmt: "%e",
+ want: "%!e(int=12)",
+ }}
+ for _, tc := range testCases {
+ t.Run("", func(t *testing.T) {
+ tag := language.Und
+ got := message.NewPrinter(tag).Sprintf(tc.fmt, tc.f)
+ if got != tc.want {
+ t.Errorf("got %q; want %q", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/number/number.go b/number/number.go
new file mode 100755
index 0000000..0a6e62f
--- /dev/null
+++ b/number/number.go
@@ -0,0 +1,77 @@
+// 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
+
+// TODO:
+// p.Printf("The gauge was at %v.", number.Spell(number.Percent(23)))
+// // Prints: The gauge was at twenty-three percent.
+//
+// p.Printf("From here to %v!", number.Spell(math.Inf()))
+// // Prints: From here to infinity!
+//
+
+import (
+ "golang.org/x/text/internal/number"
+)
+
+const (
+ decimalVerbs = "vfgd"
+ scientificVerbs = "veg"
+)
+
+// Decimal represents a number as a floating point decimal.
+func Decimal(x interface{}, opts ...Option) Formatter {
+ return newFormatter(decimalOptions, opts, x)
+}
+
+var decimalOptions = newOptions(decimalVerbs, (*number.Formatter).InitDecimal)
+
+// Scientific prints a values in scientific format.
+func Scientific(x interface{}, opts ...Option) Formatter {
+ return newFormatter(scientificOptions, opts, x)
+}
+
+var scientificOptions = newOptions(scientificVerbs, (*number.Formatter).InitScientific)
+
+// Engineering formats a number using engineering notation, which is like
+// scientific notation, but with the exponent normalized to multiples of 3.
+func Engineering(x interface{}, opts ...Option) Formatter {
+ return newFormatter(engineeringOptions, opts, x)
+}
+
+var engineeringOptions = newOptions(scientificVerbs, (*number.Formatter).InitEngineering)
+
+// Percent formats a number as a percentage. A value of 1.0 means 100%.
+func Percent(x interface{}, opts ...Option) Formatter {
+ return newFormatter(percentOptions, opts, x)
+}
+
+var percentOptions = newOptions(decimalVerbs, (*number.Formatter).InitPercent)
+
+// PerMille formats a number as a per mille indication. A value of 1.0 means
+// 1000‰.
+func PerMille(x interface{}, opts ...Option) Formatter {
+ return newFormatter(perMilleOptions, opts, x)
+}
+
+var perMilleOptions = newOptions(decimalVerbs, (*number.Formatter).InitPerMille)
+
+// TODO:
+// - Shortest: akin to verb 'g' of 'G'
+//
+// TODO: RBNF forms:
+// - Compact: 1M 3.5T
+// - CompactBinary: 1Mi 3.5Ti
+// - Long: 1 million
+// - Ordinal:
+// - Roman: MCMIIXX
+// - RomanSmall: mcmiixx
+// - Text: numbers as it typically appears in running text, allowing
+// language-specific choices for when to use numbers and when to use words.
+// - Spell?: spelled-out number. Maybe just allow as an option?
+
+// NOTE: both spelled-out numbers and ordinals, to render correctly, need
+// detailed linguistic information from the translated string into which they
+// are substituted. We will need to implement that first.
diff --git a/number/number_test.go b/number/number_test.go
new file mode 100644
index 0000000..f380b7e
--- /dev/null
+++ b/number/number_test.go
@@ -0,0 +1,190 @@
+// 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 (
+ "strings"
+ "testing"
+
+ "golang.org/x/text/language"
+ "golang.org/x/text/message"
+)
+
+func TestFormatter(t *testing.T) {
+ overrides := map[string]string{
+ "en": "*e#######0",
+ "nl": "*n#######0",
+ }
+ testCases := []struct {
+ desc string
+ tag string
+ f Formatter
+ want string
+ }{{
+ desc: "decimal",
+ f: Decimal(3),
+ want: "3",
+ }, {
+ desc: "decimal fraction",
+ f: Decimal(0.123),
+ want: "0.123",
+ }, {
+ desc: "separators",
+ f: Decimal(1234.567),
+ want: "1,234.567",
+ }, {
+ desc: "no separators",
+ f: Decimal(1234.567, NoSeparator()),
+ want: "1234.567",
+ }, {
+ desc: "max integer",
+ f: Decimal(1973, MaxIntegerDigits(2)),
+ want: "73",
+ }, {
+ desc: "max integer overflow",
+ f: Decimal(1973, MaxIntegerDigits(1000)),
+ want: "1,973",
+ }, {
+ desc: "min integer",
+ f: Decimal(12, MinIntegerDigits(5)),
+ want: "00,012",
+ }, {
+ desc: "max fraction zero",
+ f: Decimal(0.12345, MaxFractionDigits(0)),
+ want: "0",
+ }, {
+ desc: "max fraction 2",
+ f: Decimal(0.12, MaxFractionDigits(2)),
+ want: "0.12",
+ }, {
+ desc: "min fraction 2",
+ f: Decimal(0.12, MaxFractionDigits(2)),
+ want: "0.12",
+ }, {
+ desc: "max fraction overflow",
+ f: Decimal(0.123, MaxFractionDigits(1e6)),
+ want: "0.123",
+ }, {
+ desc: "min integer overflow",
+ f: Decimal(0, MinIntegerDigits(1e6)),
+ want: strings.Repeat("000,", 255/3-1) + "000",
+ }, {
+ desc: "min fraction overflow",
+ f: Decimal(0, MinFractionDigits(1e6)),
+ want: "0." + strings.Repeat("0", 255), // TODO: fraction separators
+ }, {
+ desc: "format width",
+ f: Decimal(123, FormatWidth(10)),
+ want: " 123",
+ }, {
+ desc: "format width pad option before",
+ f: Decimal(123, PadRune('*'), FormatWidth(10)),
+ want: "*******123",
+ }, {
+ desc: "format width pad option after",
+ f: Decimal(123, FormatWidth(10), PadRune('*')),
+ want: "*******123",
+ }, {
+ desc: "format width illegal",
+ f: Decimal(123, FormatWidth(-1)),
+ want: "123",
+ }, {
+ desc: "increment",
+ f: Decimal(10.33, IncrementString("0.5")),
+ want: "10.5",
+ }, {
+ desc: "increment",
+ f: Decimal(10, IncrementString("ppp")),
+ want: "10",
+ }, {
+ desc: "increment and scale",
+ f: Decimal(10.33, IncrementString("0.5"), Scale(2)),
+ want: "10.50",
+ }, {
+ desc: "pattern overrides en",
+ tag: "en",
+ f: Decimal(101, PatternOverrides(overrides)),
+ want: "eeeee101",
+ }, {
+ desc: "pattern overrides nl",
+ tag: "nl",
+ f: Decimal(101, PatternOverrides(overrides)),
+ want: "nnnnn101",
+ }, {
+ desc: "pattern overrides de",
+ tag: "de",
+ f: Decimal(101, PatternOverrides(overrides)),
+ want: "101",
+ }, {
+ desc: "language selection",
+ tag: "bn",
+ f: Decimal(123456.78, Scale(2)),
+ want: "১,২৩,৪৫৬.৭৮",
+ }, {
+ desc: "scale",
+ f: Decimal(1234.567, Scale(2)),
+ want: "1,234.57",
+ }, {
+ desc: "scientific",
+ f: Scientific(3.00),
+ want: "3\u202f×\u202f10⁰",
+ }, {
+ desc: "scientific",
+ f: Scientific(1234),
+ want: "1.234\u202f×\u202f10³",
+ }, {
+ desc: "scientific",
+ f: Scientific(1234, Scale(2)),
+ want: "1.23\u202f×\u202f10³",
+ }, {
+ desc: "engineering",
+ f: Engineering(12345),
+ want: "12.345\u202f×\u202f10³",
+ }, {
+ desc: "engineering scale",
+ f: Engineering(12345, Scale(2)),
+ want: "12.34\u202f×\u202f10³",
+ }, {
+ desc: "engineering precision(4)",
+ f: Engineering(12345, Precision(4)),
+ want: "12.34\u202f×\u202f10³",
+ }, {
+ desc: "engineering precision(2)",
+ f: Engineering(1234.5, Precision(2)),
+ want: "1.2\u202f×\u202f10³",
+ }, {
+ desc: "percent",
+ f: Percent(0.12),
+ want: "12%",
+ }, {
+ desc: "permille",
+ f: PerMille(0.123),
+ want: "123‰",
+ }, {
+ desc: "percent rounding",
+ f: PerMille(0.12345),
+ want: "123‰",
+ }, {
+ desc: "percent fraction",
+ f: PerMille(0.12345, Scale(2)),
+ want: "123.45‰",
+ }, {
+ desc: "percent fraction",
+ f: PerMille(0.12345, Scale(1)),
+ want: "123.4‰",
+ }}
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ tag := language.Und
+ if tc.tag != "" {
+ tag = language.MustParse(tc.tag)
+ }
+ got := message.NewPrinter(tag).Sprint(tc.f)
+ if got != tc.want {
+ t.Errorf("got %q; want %q", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/number/option.go b/number/option.go
new file mode 100644
index 0000000..2e73525
--- /dev/null
+++ b/number/option.go
@@ -0,0 +1,177 @@
+// 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"
+
+ "golang.org/x/text/internal/number"
+ "golang.org/x/text/language"
+)
+
+// An Option configures a Formatter.
+type Option option
+
+type option func(tag language.Tag, f *number.Formatter)
+
+// TODO: SpellOut requires support of the ICU RBNF format.
+// func SpellOut() Option
+
+// NoSeparator causes a number to be displayed without grouping separators.
+func NoSeparator() Option {
+ return func(t language.Tag, f *number.Formatter) {
+ f.GroupingSize = [2]uint8{}
+ }
+}
+
+// MaxIntegerDigits limits the number of integer digits, eliminating the
+// most significant digits.
+func MaxIntegerDigits(max int) Option {
+ return func(t language.Tag, f *number.Formatter) {
+ if max >= 1<<8 {
+ max = (1 << 8) - 1
+ }
+ f.MaxIntegerDigits = uint8(max)
+ }
+}
+
+// MinIntegerDigits specifies the minimum number of integer digits, adding
+// leading zeros when needed.
+func MinIntegerDigits(min int) Option {
+ return func(t language.Tag, f *number.Formatter) {
+ if min >= 1<<8 {
+ min = (1 << 8) - 1
+ }
+ f.MinIntegerDigits = uint8(min)
+ }
+}
+
+// MaxFractionDigits specifies the maximum number of digits after the comma.
+func MaxFractionDigits(max int) Option {
+ return func(t language.Tag, f *number.Formatter) {
+ if max >= 1<<15 {
+ max = (1 << 15) - 1
+ }
+ f.MaxFractionDigits = int16(max)
+ }
+}
+
+// MinFractionDigits specifies the minimum number of digits after the comma.
+func MinFractionDigits(min int) Option {
+ return func(t language.Tag, f *number.Formatter) {
+ if min >= 1<<8 {
+ min = (1 << 8) - 1
+ }
+ f.MinFractionDigits = uint8(min)
+ }
+}
+
+// Precision sets the maximum number of significant digits. A negative value
+// means exact.
+func Precision(prec int) Option {
+ return func(t language.Tag, f *number.Formatter) {
+ f.SetPrecision(prec)
+ }
+}
+
+// Scale simultaneously sets MinFractionDigits and MaxFractionDigits to the
+// given value.
+func Scale(decimals int) Option {
+ return func(t language.Tag, f *number.Formatter) {
+ f.SetScale(decimals)
+ }
+}
+
+// IncrementString sets the incremental value to which numbers should be
+// rounded. For instance: Increment("0.05") will cause 1.44 to round to 1.45.
+// IncrementString also sets scale to the scale of the increment.
+func IncrementString(decimal string) Option {
+ increment := 0
+ scale := 0
+ d := decimal
+ p := 0
+ for ; p < len(d) && '0' <= d[p] && d[p] <= '9'; p++ {
+ increment *= 10
+ increment += int(d[p]) - '0'
+ }
+ if p < len(d) && d[p] == '.' {
+ for p++; p < len(d) && '0' <= d[p] && d[p] <= '9'; p++ {
+ increment *= 10
+ increment += int(d[p]) - '0'
+ scale++
+ }
+ }
+ if p < len(d) {
+ increment = 0
+ scale = 0
+ }
+ return func(t language.Tag, f *number.Formatter) {
+ f.Increment = uint32(increment)
+ f.IncrementScale = uint8(scale)
+ f.SetScale(scale)
+ }
+}
+
+func noop(language.Tag, *number.Formatter) {}
+
+// PatternOverrides allows users to specify alternative patterns for specific
+// languages. The Pattern will be overridden for all languages in a subgroup as
+// well. The function will panic for invalid input. It is best to create this
+// option at startup time.
+// PatternOverrides must be the first Option passed to a formatter.
+func PatternOverrides(patterns map[string]string) Option {
+ // TODO: make it so that it does not have to be the first option.
+ // TODO: use -x-nochild to indicate it does not override child tags.
+ m := map[language.Tag]*number.Pattern{}
+ for k, v := range patterns {
+ tag := language.MustParse(k)
+ p, err := number.ParsePattern(v)
+ if err != nil {
+ panic(fmt.Errorf("number: PatternOverrides: %v", err))
+ }
+ m[tag] = p
+ }
+ return func(t language.Tag, f *number.Formatter) {
+ // TODO: Use language grouping relation instead of parent relation.
+ // TODO: Should parent implement the grouping relation?
+ for lang := t; ; lang = t.Parent() {
+ if p, ok := m[lang]; ok {
+ f.Pattern = *p
+ break
+ }
+ if lang == language.Und {
+ break
+ }
+ }
+ }
+}
+
+// FormatWidth sets the total format width.
+func FormatWidth(n int) Option {
+ if n <= 0 {
+ return noop
+ }
+ return func(t language.Tag, f *number.Formatter) {
+ f.FormatWidth = uint16(n)
+ if f.PadRune == 0 {
+ f.PadRune = ' '
+ }
+ }
+}
+
+// PadRune sets the rune to be used for filling up to the format width.
+func PadRune(r rune) Option {
+ return func(t language.Tag, f *number.Formatter) {
+ f.PadRune = r
+ }
+}
+
+// TODO:
+// - FormatPosition (using type aliasing?)
+// - Multiplier: find a better way to represent and figure out what to do
+// with clashes with percent/permille.
+// - NumberingSystem(nu string): not accessable in number.Info now. Also, should
+// this be keyed by language or generic?
+// - SymbolOverrides(symbols map[string]map[number.SymbolType]string) Option