feature/plural: add Select

Selects message based on plural form of
argument.

Change-Id: I8730e12b7b3c08317cdca3f899eebb8f23b2d83a
Reviewed-on: https://go-review.googlesource.com/58551
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/feature/plural/example_test.go b/feature/plural/example_test.go
new file mode 100644
index 0000000..c75408c
--- /dev/null
+++ b/feature/plural/example_test.go
@@ -0,0 +1,46 @@
+// 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 plural_test
+
+import (
+	"golang.org/x/text/feature/plural"
+	"golang.org/x/text/language"
+	"golang.org/x/text/message"
+)
+
+func ExampleSelect() {
+	// Manually set some translations. This is typically done programmatically.
+	message.Set(language.English, "%d files remaining",
+		plural.Selectf(1, "%d",
+			"=0", "done!",
+			plural.One, "one file remaining",
+			plural.Other, "%[1]d files remaining",
+		))
+	message.Set(language.Dutch, "%d files remaining",
+		plural.Selectf(1, "%d",
+			"=0", "klaar!",
+			// One can also use a string instead of a Kind
+			"one", "nog één bestand te gaan",
+			"other", "nog %[1]d bestanden te gaan",
+		))
+
+	p := message.NewPrinter(language.English)
+	p.Printf("%d files remaining", 5)
+	p.Println()
+	p.Printf("%d files remaining", 1)
+	p.Println()
+
+	p = message.NewPrinter(language.Dutch)
+	p.Printf("%d files remaining", 1)
+	p.Println()
+	p.Printf("%d files remaining", 0)
+	p.Println()
+
+	// Output:
+	// 5 files remaining
+	// one file remaining
+	// nog één bestand te gaan
+	// klaar!
+}
diff --git a/feature/plural/message.go b/feature/plural/message.go
new file mode 100755
index 0000000..22f5a26
--- /dev/null
+++ b/feature/plural/message.go
@@ -0,0 +1,238 @@
+// 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 plural
+
+import (
+	"fmt"
+	"io/ioutil"
+	"reflect"
+	"strconv"
+
+	"golang.org/x/text/internal/catmsg"
+	"golang.org/x/text/internal/number"
+	"golang.org/x/text/language"
+	"golang.org/x/text/message/catalog"
+)
+
+// Interface is used for types that can determine their own plural form.
+type Interface interface {
+	// PluralForm reports the plural form for the given language of the
+	// underlying value. It also returns the integer value. If the integer value
+	// is larger than fits in n, PluralForm may return a value modulo
+	// 10,000,000.
+	PluralForm(t language.Tag, scale int) (f Form, n int)
+}
+
+// Selectf returns the first case for which its selector is a match for the
+// arg-th substitution argument to a formatting call, formatting it as indicated
+// by format.
+//
+// The cases argument are pairs of selectors and messages. Selectors are of type
+// string or Form. Messages are of type string or catalog.Message. A selector
+// matches an argument if:
+//    - it is "other" or Other
+//    - it matches the plural form of the argument: "zero", "one", "two", "few",
+//      or "many", or the equivalent Form
+//    - it is of the form "=x" where x is an integer that matches the value of
+//      the argument.
+//    - it is of the form "<x" where x is an integer that is larger than the
+//      argument.
+//
+// The format argument determines the formatting parameters for which to
+// determine the plural form. This is especially relevant for non-integer
+// values.
+//
+// The format string may be "", in which case a best-effort attempt is made to
+// find a reasonable representation on which to base the plural form. Examples
+// of format strings are:
+//   - %.2f   decimal with scale 2
+//   - %.2e   scientific notation with precision 3 (scale + 1)
+//   - %d     integer
+func Selectf(arg int, format string, cases ...interface{}) catalog.Message {
+	var p parser
+	// Intercept the formatting parameters of format by doing a dummy print.
+	fmt.Fprintf(ioutil.Discard, format, &p)
+	m := &message{arg, kindDefault, 0, cases}
+	switch p.verb {
+	case 'g':
+		m.kind = kindPrecision
+		m.scale = p.scale
+	case 'f':
+		m.kind = kindScale
+		m.scale = p.scale
+	case 'e':
+		m.kind = kindScientific
+		m.scale = p.scale
+	case 'd':
+		m.kind = kindScale
+		m.scale = 0
+	default:
+		// TODO: do we need to handle errors?
+	}
+	return m
+}
+
+type parser struct {
+	verb  rune
+	scale int
+}
+
+func (p *parser) Format(s fmt.State, verb rune) {
+	p.verb = verb
+	p.scale = -1
+	if prec, ok := s.Precision(); ok {
+		p.scale = prec
+	}
+}
+
+type message struct {
+	arg   int
+	kind  int
+	scale int
+	cases []interface{}
+}
+
+const (
+	// Start with non-ASCII to allow skipping values.
+	kindDefault    = 0x80 + iota
+	kindScale      // verb f, number of fraction digits follows
+	kindScientific // verb e, number of fraction digits follows
+	kindPrecision  // verb g, number of significant digits follows
+)
+
+var handle = catmsg.Register("golang.org/x/text/feature/plural:plural", execute)
+
+func (m *message) Compile(e *catmsg.Encoder) error {
+	e.EncodeMessageType(handle)
+
+	e.EncodeUint(uint64(m.arg))
+
+	e.EncodeUint(uint64(m.kind))
+	if m.kind > kindDefault {
+		e.EncodeUint(uint64(m.scale))
+	}
+
+	forms := validForms(cardinal, e.Language())
+
+	for i := 0; i < len(m.cases); {
+		if err := compileSelector(e, forms, m.cases[i]); err != nil {
+			return err
+		}
+		if i++; i >= len(m.cases) {
+			return fmt.Errorf("plural: no message defined for selector %v", m.cases[i-1])
+		}
+		var msg catalog.Message
+		switch x := m.cases[i].(type) {
+		case string:
+			msg = catalog.String(x)
+		case catalog.Message:
+			msg = x
+		default:
+			return fmt.Errorf("plural: message of type %T; must be string or catalog.Message", x)
+		}
+		if err := e.EncodeMessage(msg); err != nil {
+			return err
+		}
+		i++
+	}
+	return nil
+}
+
+func compileSelector(e *catmsg.Encoder, valid []Form, selector interface{}) error {
+	form := Other
+	switch x := selector.(type) {
+	case string:
+		if x == "" {
+			return fmt.Errorf("plural: empty selector")
+		}
+		if c := x[0]; c == '=' || c == '<' {
+			val, err := strconv.ParseUint(x[1:], 10, 16)
+			if err != nil {
+				return fmt.Errorf("plural: invalid number in selector %q: %v", selector, err)
+			}
+			e.EncodeUint(uint64(c))
+			e.EncodeUint(val)
+			return nil
+		}
+		var ok bool
+		form, ok = countMap[x]
+		if !ok {
+			return fmt.Errorf("plural: invalid plural form %q", selector)
+		}
+	case Form:
+		form = x
+	default:
+		return fmt.Errorf("plural: selector of type %T; want string or Form", selector)
+	}
+
+	ok := false
+	for _, f := range valid {
+		if f == form {
+			ok = true
+			break
+		}
+	}
+	if !ok {
+		return fmt.Errorf("plural: form %q not supported for language %q", selector, e.Language())
+	}
+	e.EncodeUint(uint64(form))
+	return nil
+}
+
+func execute(d *catmsg.Decoder) bool {
+	lang := d.Language()
+	argN := int(d.DecodeUint())
+	kind := int(d.DecodeUint())
+	scale := -1 // default
+	if kind > kindDefault {
+		scale = int(d.DecodeUint())
+	}
+	form := Other
+	n := -1
+	if arg := d.Arg(argN); arg == nil {
+		// Default to Other.
+	} else if x, ok := arg.(Interface); ok {
+		// This covers lists and formatters from the number package.
+		form, n = x.PluralForm(lang, scale)
+	} else {
+		var f number.Formatter
+		switch kind {
+		case kindScale:
+			f.InitDecimal(lang)
+			f.SetScale(scale)
+		case kindScientific:
+			f.InitScientific(lang)
+			f.SetScale(scale)
+		case kindPrecision:
+			f.InitDecimal(lang)
+			f.SetPrecision(scale)
+		case kindDefault:
+			// sensible default
+			f.InitDecimal(lang)
+			if k := reflect.TypeOf(arg).Kind(); reflect.Int <= k && k <= reflect.Uintptr {
+				f.SetScale(0)
+			} else {
+				f.SetScale(2)
+			}
+		}
+		var dec number.Decimal // TODO: buffer in Printer
+		dec.Convert(f.RoundingContext, arg)
+		v := number.FormatDigits(&dec, f.RoundingContext)
+		if !v.NaN && !v.Inf {
+			form, n = cardinal.matchDisplayDigits(d.Language(), &v)
+		}
+	}
+	for !d.Done() {
+		f := d.DecodeUint()
+		if (f == '=' && n == int(d.DecodeUint())) ||
+			(f == '<' && 0 <= n && n < int(d.DecodeUint())) ||
+			form == Form(f) ||
+			Other == Form(f) {
+			return d.ExecuteMessage()
+		}
+		d.SkipMessage()
+	}
+	return false
+}
diff --git a/feature/plural/message_test.go b/feature/plural/message_test.go
new file mode 100644
index 0000000..1a89bd5
--- /dev/null
+++ b/feature/plural/message_test.go
@@ -0,0 +1,197 @@
+// 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 plural
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"golang.org/x/text/internal/catmsg"
+	"golang.org/x/text/language"
+	"golang.org/x/text/message/catalog"
+)
+
+func TestSelect(t *testing.T) {
+	lang := language.English
+	type test struct {
+		arg    interface{}
+		result string
+		err    string
+	}
+	testCases := []struct {
+		desc  string
+		msg   catalog.Message
+		err   string
+		tests []test
+	}{{
+		desc: "basic",
+		msg:  Selectf(1, "%d", "one", "foo", "other", "bar"),
+		tests: []test{
+			{arg: 0, result: "bar"},
+			{arg: 1, result: "foo"},
+			{arg: 2, result: "bar"},
+			{arg: opposite(1), result: "bar"},
+			{arg: opposite(2), result: "foo"},
+			{arg: "unknown", result: "bar"}, // other
+		},
+	}, {
+		desc: "comparisons",
+		msg: Selectf(1, "%d",
+			"=0", "zero",
+			"=1", "one",
+			"one", "cannot match", // never matches
+			"<5", "<5", // never matches
+			"=5", "=5",
+			Other, "other"),
+		tests: []test{
+			{arg: 0, result: "zero"},
+			{arg: 1, result: "one"},
+			{arg: 2, result: "<5"},
+			{arg: 4, result: "<5"},
+			{arg: 5, result: "=5"},
+			{arg: 6, result: "other"},
+			{arg: "unknown", result: "other"},
+		},
+	}, {
+		desc: "fractions",
+		msg:  Selectf(1, "%.2f", "one", "foo", "other", "bar"),
+		tests: []test{
+			// fractions are always plural in english
+			{arg: 0, result: "bar"},
+			{arg: 1, result: "bar"},
+		},
+	}, {
+		desc: "decimal without fractions",
+		msg:  Selectf(1, "%.of", "one", "foo", "other", "bar"),
+		tests: []test{
+			// fractions are always plural in english
+			{arg: 0, result: "bar"},
+			{arg: 1, result: "foo"},
+		},
+	}, {
+		desc: "scientific",
+		msg:  Selectf(1, "%.0e", "one", "foo", "other", "bar"),
+		tests: []test{
+			{arg: 0, result: "bar"},
+			{arg: 1, result: "foo"},
+		},
+	}, {
+		desc: "variable",
+		msg:  Selectf(1, "%.1g", "one", "foo", "other", "bar"),
+		tests: []test{
+			// fractions are always plural in english
+			{arg: 0, result: "bar"},
+			{arg: 1, result: "foo"},
+			{arg: 2, result: "bar"},
+		},
+	}, {
+		desc: "default",
+		msg:  Selectf(1, "", "one", "foo", "other", "bar"),
+		tests: []test{
+			{arg: 0, result: "bar"},
+			{arg: 1, result: "foo"},
+			{arg: 2, result: "bar"},
+			{arg: 1.0, result: "bar"},
+		},
+	}, {
+		desc: "nested",
+		msg:  Selectf(1, "", "other", Selectf(2, "", "one", "foo", "other", "bar")),
+		tests: []test{
+			{arg: 0, result: "bar"},
+			{arg: 1, result: "foo"},
+			{arg: 2, result: "bar"},
+		},
+	}, {
+		desc:  "arg unavailable",
+		msg:   Selectf(100, "%.2f", "one", "foo", "other", "bar"),
+		tests: []test{{arg: 1, result: "bar"}},
+	}, {
+		desc:  "no match",
+		msg:   Selectf(1, "%.2f", "one", "foo"),
+		tests: []test{{arg: 0, result: "bar", err: catmsg.ErrNoMatch.Error()}},
+	}, {
+		desc: "error invalid form",
+		err:  `invalid plural form "excessive"`,
+		msg:  Selectf(1, "%d", "excessive", "foo"),
+	}, {
+		desc: "error form not used by language",
+		err:  `form "many" not supported for language "en"`,
+		msg:  Selectf(1, "%d", "many", "foo"),
+	}, {
+		desc: "error invalid selector",
+		err:  `selector of type int; want string or Form`,
+		msg:  Selectf(1, "%d", 1, "foo"),
+	}, {
+		desc: "error missing message",
+		err:  `no message defined for selector one`,
+		msg:  Selectf(1, "%d", "one"),
+	}, {
+		desc: "error invalid number",
+		err:  `invalid number in selector "<1.00"`,
+		msg:  Selectf(1, "%d", "<1.00"),
+	}, {
+		desc: "error empty selector",
+		err:  `empty selector`,
+		msg:  Selectf(1, "%d", "", "foo"),
+	}, {
+		desc: "error invalid message",
+		err:  `message of type int; must be string or catalog.Message`,
+		msg:  Selectf(1, "%d", "one", 3),
+	}, {
+		desc: "nested error",
+		err:  `empty selector`,
+		msg:  Selectf(1, "", "other", Selectf(2, "", "")),
+	}}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			data, err := catmsg.Compile(lang, nil, tc.msg)
+			chkError(t, err, tc.err)
+			for _, tx := range tc.tests {
+				t.Run(fmt.Sprint(tx.arg), func(t *testing.T) {
+					r := renderer{arg: tx.arg}
+					d := catmsg.NewDecoder(lang, &r, nil)
+					err := d.Execute(data)
+					chkError(t, err, tx.err)
+					if r.result != tx.result {
+						t.Errorf("got %q; want %q", r.result, tx.result)
+					}
+				})
+			}
+		})
+	}
+}
+
+func chkError(t *testing.T, got error, want string) {
+	if (got == nil && want != "") ||
+		(got != nil && (want == "" || !strings.Contains(got.Error(), want))) {
+		t.Fatalf("got %v; want %v", got, want)
+	}
+	if got != nil {
+		t.SkipNow()
+	}
+}
+
+type renderer struct {
+	arg    interface{}
+	result string
+}
+
+func (r *renderer) Render(s string) { r.result += s }
+func (r *renderer) Arg(i int) interface{} {
+	if i > 10 { // Allow testing "arg unavailable" path
+		return nil
+	}
+	return r.arg
+}
+
+type opposite int
+
+func (o opposite) PluralForm(lang language.Tag, scale int) (Form, int) {
+	if o == 1 {
+		return Other, 1
+	}
+	return One, int(o)
+}
diff --git a/feature/plural/plural.go b/feature/plural/plural.go
index dc34b47..61faf18 100644
--- a/feature/plural/plural.go
+++ b/feature/plural/plural.go
@@ -13,6 +13,7 @@
 package plural
 
 import (
+	"golang.org/x/text/internal/number"
 	"golang.org/x/text/language"
 )
 
@@ -109,8 +110,8 @@
 //      123        []byte{1, 2, 3}     3      0
 //      123.4      []byte{1, 2, 3, 4}  3      1
 //      123.40     []byte{1, 2, 3, 4}  3      2
-//      100000     []byte{1}           6......0
-//      100000.00  []byte{1}           6......3
+//      100000     []byte{1}           6      0
+//      100000.00  []byte{1}           6      3
 func (p *Rules) MatchDigits(t language.Tag, digits []byte, exp, scale int) Form {
 	index, _ := language.CompactIndex(t)
 
@@ -123,6 +124,11 @@
 	return matchPlural(p, index, n, f, scale)
 }
 
+func (p *Rules) matchDisplayDigits(t language.Tag, d *number.Digits) (Form, int) {
+	n := getIntApprox(d.Digits, 0, int(d.Exp), 6, 1000000)
+	return p.MatchDigits(t, d.Digits, int(d.Exp), d.NumFracDigits()), n
+}
+
 func validForms(p *Rules, t language.Tag) (forms []Form) {
 	index, _ := language.CompactIndex(t)
 	offset := p.langToIndex[index]