diff --git a/number/doc.go b/number/doc.go new file mode 100644 index 000000000..97088d137 --- /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 000000000..fb9bcc960 --- /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 000000000..62af65154 --- /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 000000000..30be72d2f --- /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 000000000..0a6e62ff9 --- /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 000000000..f380b7ebf --- /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 000000000..2e7352585 --- /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