math/big: support new octal prefixes 0o and 0O

This CL extends the various SetString and Parse methods for
Ints, Rats, and Floats to accept the new octal prefixes.

The main change is in natconv.go, all other changes are
documentation and test updates.

Finally, this CL also fixes TestRatSetString which silently
dropped certain failures.

Updates #12711.

Change-Id: I5ee5879e25013ba1e6eda93ff280915f25ab5d55
Reviewed-on: https://go-review.googlesource.com/c/go/+/165898
Reviewed-by: Emmanuel Odeke <emm.odeke@gmail.com>
This commit is contained in:
Robert Griesemer 2019-03-06 17:14:27 -08:00
parent 2dd066d4a7
commit 129c6e4496
7 changed files with 114 additions and 60 deletions

View file

@ -97,6 +97,8 @@ func (z *Float) scan(r io.ByteScanner, base int) (f *Float, b int, err error) {
fallthrough // 10**e == 5**e * 2**e
case 2:
exp2 += d
case 8:
exp2 += d * 3 // octal digits are 3 bits each
case 16:
exp2 += d * 4 // hexadecimal digits are 4 bits each
default:
@ -222,21 +224,21 @@ func (z *Float) pow5(n uint64) *Float {
//
// number = [ sign ] [ prefix ] mantissa [ exponent ] | infinity .
// sign = "+" | "-" .
// prefix = "0" ( "x" | "X" | "b" | "B" ) .
// prefix = "0" ( "b" | "B" | "o" | "O" | "x" | "X" ) .
// mantissa = digits | digits "." [ digits ] | "." digits .
// exponent = ( "e" | "E" | "p" | "P" ) [ sign ] digits .
// digits = digit { digit } .
// digit = "0" ... "9" | "a" ... "z" | "A" ... "Z" .
// infinity = [ sign ] ( "inf" | "Inf" ) .
//
// The base argument must be 0, 2, 10, or 16. Providing an invalid base
// The base argument must be 0, 2, 8, 10, or 16. Providing an invalid base
// argument will lead to a run-time panic.
//
// For base 0, the number prefix determines the actual base: A prefix of
// "0x" or "0X" selects base 16, and a "0b" or "0B" prefix selects
// base 2; otherwise, the actual base is 10 and no prefix is accepted.
// The octal prefix "0" is not supported (a leading "0" is simply
// considered a "0").
// ``0b'' or ``0B'' selects base 2, ``0o'' or ``0O'' selects base 8, and
// ``0x'' or ``0X'' selects base 16. Otherwise, the actual base is 10 and
// no prefix is accepted. The octal prefix "0" is not supported (a leading
// "0" is simply considered a "0").
//
// A "p" or "P" exponent indicates a binary (rather then decimal) exponent;
// for instance "0x1.fffffffffffffp1023" (using base 0) represents the

View file

@ -110,6 +110,27 @@ func TestFloatSetFloat64String(t *testing.T) {
{"0b0.01p2", 1},
{"0b0.01P+2", 1},
// octal mantissa, decimal exponent
{"0o0", 0},
{"-0o0", -zero_},
{"0o0e+10", 0},
{"-0o0e-10", -zero_},
{"0o12", 10},
{"0O12E2", 1000},
{"0o.4", 0.5},
{"0o.01", 0.015625},
{"0o.01e3", 15.625},
// octal mantissa, binary exponent
{"0o0p+10", 0},
{"-0o0p-10", -zero_},
{"0o.12p6", 10},
{"0o4p-3", 0.5},
{"0o0014p-6", 0.1875},
{"0o.001p9", 1},
{"0o0.01p7", 2},
{"0O0.01P+2", 0.0625},
// hexadecimal mantissa and exponent
{"0x0", 0},
{"-0x0", -zero_},

View file

@ -172,8 +172,9 @@ func (x *Int) Format(s fmt.State, ch rune) {
//
// The base argument must be 0 or a value from 2 through MaxBase. If the base
// is 0, the string prefix determines the actual conversion base. A prefix of
// ``0x'' or ``0X'' selects base 16; the ``0'' prefix selects base 8, and a
// ``0b'' or ``0B'' prefix selects base 2. Otherwise the selected base is 10.
// ``0b'' or ``0B'' selects base 2; a ``0'', ``0o'', or ``0O'' prefix selects
// base 8, and a ``0x'' or ``0X'' prefix selects base 16. Otherwise the selected
// base is 10.
//
func (z *Int) scan(r io.ByteScanner, base int) (*Int, int, error) {
// determine sign

View file

@ -17,19 +17,24 @@ var stringTests = []struct {
val int64
ok bool
}{
// invalid inputs
{in: ""},
{in: "a"},
{in: "z"},
{in: "+"},
{in: "-"},
{in: "0b"},
{in: "0o"},
{in: "0x"},
{in: "0y"},
{in: "2", base: 2},
{in: "0b2", base: 0},
{in: "08"},
{in: "8", base: 8},
{in: "0xg", base: 0},
{in: "g", base: 16},
// valid inputs
{"0", "0", 0, 0, true},
{"0", "0", 10, 0, true},
{"0", "0", 16, 0, true},
@ -40,6 +45,8 @@ var stringTests = []struct {
{"10", "10", 16, 16, true},
{"-10", "-10", 16, -16, true},
{"+10", "10", 16, 16, true},
{"0b10", "2", 0, 2, true},
{"0o10", "8", 0, 8, true},
{"0x10", "16", 0, 16, true},
{in: "0x10", base: 16},
{"-0x10", "-16", 0, -16, true},

View file

@ -61,25 +61,25 @@ func pow(x Word, n int) (p Word) {
// a digit count, and a read or syntax error err, if any.
//
// number = [ prefix ] mantissa .
// prefix = "0" [ "x" | "X" | "b" | "B" ] .
// prefix = "0" [ "b" | "B" | "o" | "O" | "x" | "X" ] .
// mantissa = digits | digits "." [ digits ] | "." digits .
// digits = digit { digit } .
// digit = "0" ... "9" | "a" ... "z" | "A" ... "Z" .
//
// Unless fracOk is set, the base argument must be 0 or a value between
// 2 and MaxBase. If fracOk is set, the base argument must be one of
// 0, 2, 10, or 16. Providing an invalid base argument leads to a run-
// 0, 2, 8, 10, or 16. Providing an invalid base argument leads to a run-
// time panic.
//
// For base 0, the number prefix determines the actual base: A prefix of
// ``0x'' or ``0X'' selects base 16; if fracOk is not set, the ``0'' prefix
// selects base 8, and a ``0b'' or ``0B'' prefix selects base 2. Otherwise
// ``0b'' or ``0B'' selects base 2, ``0o'' or ``0O'' selects base 8, and
// ``0x'' or ``0X'' selects base 16. If fracOk is false, a ``0'' prefix
// (immediately followed by digits) selects base 8 as well. Otherwise,
// the selected base is 10 and no prefix is accepted.
//
// If fracOk is set, an octal prefix is ignored (a leading ``0'' simply
// stands for a zero digit), and a period followed by a fractional part
// is permitted. The result value is computed as if there were no period
// present; and the count value is used to determine the fractional part.
// If fracOk is set, a period followed by a fractional part is permitted.
// The result value is computed as if there were no period present; and
// the count value is used to determine the fractional part.
//
// For bases <= 36, lower and upper case letters are considered the same:
// The letters 'a' to 'z' and 'A' to 'Z' represent digit values 10 to 35.
@ -95,7 +95,7 @@ func (z nat) scan(r io.ByteScanner, base int, fracOk bool) (res nat, b, count in
// reject illegal bases
baseOk := base == 0 ||
!fracOk && 2 <= base && base <= MaxBase ||
fracOk && (base == 2 || base == 10 || base == 16)
fracOk && (base == 2 || base == 8 || base == 10 || base == 16)
if !baseOk {
panic(fmt.Sprintf("illegal number base %d", base))
}
@ -103,46 +103,44 @@ func (z nat) scan(r io.ByteScanner, base int, fracOk bool) (res nat, b, count in
// one char look-ahead
ch, err := r.ReadByte()
if err != nil {
return
return // io.EOF is also an error in this case
}
// determine actual base
b = base
b, prefix := base, 0
if base == 0 {
// actual base is 10 unless there's a base prefix
b = 10
if ch == '0' {
count = 1
switch ch, err = r.ReadByte(); err {
case nil:
// possibly one of 0x, 0X, 0b, 0B
if !fracOk {
b = 8
ch, err = r.ReadByte()
if err != nil {
if err == io.EOF {
err = nil // not an error; input is "0"
res = z[:0]
}
switch ch {
case 'x', 'X':
b = 16
case 'b', 'B':
b = 2
}
switch b {
case 16, 2:
count = 0 // prefix is not counted
if ch, err = r.ReadByte(); err != nil {
// io.EOF is also an error in this case
return
}
case 8:
count = 0 // prefix is not counted
}
case io.EOF:
// input is "0"
res = z[:0]
err = nil
return
}
// possibly one of 0b, 0B, 0o, 0O, 0x, 0X
switch ch {
case 'b', 'B':
b, prefix = 2, 'b'
case 'o', 'O':
b, prefix = 8, 'o'
case 'x', 'X':
b, prefix = 16, 'x'
default:
// read error
return
if !fracOk {
b, prefix = 8, '0'
}
}
if prefix != 0 {
count = 0 // prefix is not counted
if prefix != '0' {
if ch, err = r.ReadByte(); err != nil {
return // io.EOF is also an error in this case
}
}
}
}
}
@ -216,14 +214,12 @@ func (z nat) scan(r io.ByteScanner, base int, fracOk bool) (res nat, b, count in
if count == 0 {
// no digits found
switch {
case base == 0 && b == 8:
if prefix == '0' {
// there was only the octal prefix 0 (possibly followed by digits > 7);
// count as one digit and return base 10, not 8
count = 1
b = 10
case base != 0 || b != 8:
// there was neither a mantissa digit nor the octal prefix 0
} else {
err = errors.New("syntax error scanning number")
}
return

View file

@ -112,23 +112,31 @@ var natScanTests = []struct {
ok bool // expected success
next rune // next character (or 0, if at EOF)
}{
// error: no mantissa
// invalid: no mantissa
{},
{s: "?"},
{base: 10},
{base: 36},
{base: 62},
{s: "?", base: 10},
{s: "0b"},
{s: "0o"},
{s: "0x"},
{s: "0b2"},
{s: "0B2"},
{s: "0o8"},
{s: "0O8"},
{s: "0xg"},
{s: "0Xg"},
{s: "345", base: 2},
// error: incorrect use of decimal point
// invalid: incorrect use of decimal point
{s: ".0"},
{s: ".0", base: 10},
{s: ".", base: 0},
{s: "0x.0"},
// no errors
// valid, no decimal point
{"0", 0, false, nil, 10, 1, true, 0},
{"0", 10, false, nil, 10, 1, true, 0},
{"0", 36, false, nil, 36, 1, true, 0},
@ -136,11 +144,17 @@ var natScanTests = []struct {
{"1", 0, false, nat{1}, 10, 1, true, 0},
{"1", 10, false, nat{1}, 10, 1, true, 0},
{"0 ", 0, false, nil, 10, 1, true, ' '},
{"00 ", 0, false, nil, 8, 1, true, ' '}, // octal 0
{"0b1", 0, false, nat{1}, 2, 1, true, 0},
{"0B11000101", 0, false, nat{0xc5}, 2, 8, true, 0},
{"0B110001012", 0, false, nat{0xc5}, 2, 8, true, '2'},
{"07", 0, false, nat{7}, 8, 1, true, 0},
{"08", 0, false, nil, 10, 1, true, '8'},
{"08", 10, false, nat{8}, 10, 2, true, 0},
{"018", 0, false, nat{1}, 8, 1, true, '8'},
{"0b1", 0, false, nat{1}, 2, 1, true, 0},
{"0b11000101", 0, false, nat{0xc5}, 2, 8, true, 0},
{"0o7", 0, false, nat{7}, 8, 1, true, 0},
{"0o18", 0, false, nat{1}, 8, 1, true, '8'},
{"0O17", 0, false, nat{017}, 8, 2, true, 0},
{"03271", 0, false, nat{03271}, 8, 4, true, 0},
{"10ab", 0, false, nat{10}, 10, 2, true, 'a'},
{"1234567890", 0, false, nat{1234567890}, 10, 10, true, 0},
@ -153,13 +167,20 @@ var natScanTests = []struct {
{"0xdeadbeef", 0, false, nat{0xdeadbeef}, 16, 8, true, 0},
{"0XDEADBEEF", 0, false, nat{0xdeadbeef}, 16, 8, true, 0},
// no errors, decimal point
// valid, with decimal point
{"0.", 0, false, nil, 10, 1, true, '.'},
{"0.", 10, true, nil, 10, 0, true, 0},
{"0.1.2", 10, true, nat{1}, 10, -1, true, '.'},
{".000", 10, true, nil, 10, -3, true, 0},
{"12.3", 10, true, nat{123}, 10, -1, true, 0},
{"012.345", 10, true, nat{12345}, 10, -3, true, 0},
{"0.1", 0, true, nat{1}, 10, -1, true, 0},
{"0.1", 2, true, nat{1}, 2, -1, true, 0},
{"0.12", 2, true, nat{1}, 2, -1, true, '2'},
{"0b0.1", 0, true, nat{1}, 2, -1, true, 0},
{"0B0.12", 0, true, nat{1}, 2, -1, true, '2'},
{"0o0.7", 0, true, nat{7}, 8, -1, true, 0},
{"0O0.78", 0, true, nat{7}, 8, -1, true, '8'},
}
func TestScanBase(t *testing.T) {

View file

@ -58,11 +58,13 @@ var setStringTests = []StringTest{
// These are not supported by fmt.Fscanf.
var setStringTests2 = []StringTest{
{"0x10", "16", true},
{"0b1000/3", "8/3", true},
{"0B1000/0x8", "1", true},
{"-010/1", "-8", true}, // TODO(gri) should we even permit octal here?
{"-010.", "-10", true},
{"-0o10/1", "-8", true},
{"0x10/1", "16", true},
{"0x10/0x20", "1/2", true},
{"0b1000/3", "8/3", true},
{in: "4/3x"},
// TODO(gri) add more tests
}
@ -81,8 +83,12 @@ func TestRatSetString(t *testing.T) {
} else if x.RatString() != test.out {
t.Errorf("#%d SetString(%q) got %s want %s", i, test.in, x.RatString(), test.out)
}
} else if x != nil {
t.Errorf("#%d SetString(%q) got %p want nil", i, test.in, x)
} else {
if test.ok {
t.Errorf("#%d SetString(%q) expected success", i, test.in)
} else if x != nil {
t.Errorf("#%d SetString(%q) got %p want nil", i, test.in, x)
}
}
}
}