blob: 27dff3480f084af52c2814dd2d0216d7b34d79dc [file] [log] [blame]
Joe Tsai879b18d2018-08-03 17:22:24 -07001// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package json
6
7import (
8 "bytes"
Herbie Ongd3f8f2d2019-03-06 00:28:23 -08009 "fmt"
Joe Tsai879b18d2018-08-03 17:22:24 -070010 "io"
11 "regexp"
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080012 "strconv"
Joe Tsai879b18d2018-08-03 17:22:24 -070013 "unicode/utf8"
14
Damien Neile89e6242019-05-13 23:55:40 -070015 "google.golang.org/protobuf/internal/errors"
Joe Tsai879b18d2018-08-03 17:22:24 -070016)
17
Herbie Ongc96a79d2019-03-08 10:49:17 -080018// call specifies which Decoder method was invoked.
19type call uint8
20
21const (
22 readCall call = iota
23 peekCall
24)
25
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080026// Decoder is a token-based JSON decoder.
27type Decoder struct {
Herbie Ong8ac9dd22019-03-27 12:20:50 -070028 // lastCall is last method called, either readCall or peekCall.
29 // Initial value is readCall.
Herbie Ongc96a79d2019-03-08 10:49:17 -080030 lastCall call
31
32 // value contains the last read value.
33 value Value
34
35 // err contains the last read error.
36 err error
Joe Tsai879b18d2018-08-03 17:22:24 -070037
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080038 // startStack is a stack containing StartObject and StartArray types. The
39 // top of stack represents the object or the array the current value is
40 // directly located in.
41 startStack []Type
42
43 // orig is used in reporting line and column.
44 orig []byte
45 // in contains the unconsumed input.
46 in []byte
Joe Tsai879b18d2018-08-03 17:22:24 -070047}
48
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080049// NewDecoder returns a Decoder to read the given []byte.
50func NewDecoder(b []byte) *Decoder {
51 return &Decoder{orig: b, in: b}
52}
53
Herbie Ongc96a79d2019-03-08 10:49:17 -080054// Peek looks ahead and returns the next JSON type without advancing a read.
55func (d *Decoder) Peek() Type {
56 defer func() { d.lastCall = peekCall }()
57 if d.lastCall == readCall {
58 d.value, d.err = d.Read()
59 }
60 return d.value.typ
61}
62
63// Read returns the next JSON value. It will return an error if there is no
Herbie Ongdecef412019-04-17 15:47:43 -070064// valid value. For String types containing invalid UTF8 characters, a non-fatal
65// error is returned and caller can call Read for the next value.
Herbie Ongc96a79d2019-03-08 10:49:17 -080066func (d *Decoder) Read() (Value, error) {
67 defer func() { d.lastCall = readCall }()
68 if d.lastCall == peekCall {
69 return d.value, d.err
70 }
71
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080072 var nerr errors.NonFatal
Herbie Ongdecef412019-04-17 15:47:43 -070073 value, err := d.parseNext()
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080074 if !nerr.Merge(err) {
Joe Tsai879b18d2018-08-03 17:22:24 -070075 return Value{}, err
76 }
Herbie Ongdecef412019-04-17 15:47:43 -070077 n := value.size
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080078
79 switch value.typ {
80 case EOF:
81 if len(d.startStack) != 0 ||
Herbie Ongc96a79d2019-03-08 10:49:17 -080082 d.value.typ&Null|Bool|Number|String|EndObject|EndArray == 0 {
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080083 return Value{}, io.ErrUnexpectedEOF
84 }
85
86 case Null:
87 if !d.isValueNext() {
88 return Value{}, d.newSyntaxError("unexpected value null")
89 }
90
91 case Bool, Number:
92 if !d.isValueNext() {
Herbie Ong8ac9dd22019-03-27 12:20:50 -070093 return Value{}, d.newSyntaxError("unexpected value %v", value.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -080094 }
95
96 case String:
97 if d.isValueNext() {
98 break
99 }
100 // Check if this is for an object name.
Herbie Ongc96a79d2019-03-08 10:49:17 -0800101 if d.value.typ&(StartObject|comma) == 0 {
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700102 return Value{}, d.newSyntaxError("unexpected value %v", value.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800103 }
104 d.in = d.in[n:]
105 d.consume(0)
106 if c := d.in[0]; c != ':' {
107 return Value{}, d.newSyntaxError(`unexpected character %v, missing ":" after object name`, string(c))
108 }
109 n = 1
110 value.typ = Name
111
112 case StartObject, StartArray:
113 if !d.isValueNext() {
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700114 return Value{}, d.newSyntaxError("unexpected character %v", value.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800115 }
116 d.startStack = append(d.startStack, value.typ)
117
118 case EndObject:
119 if len(d.startStack) == 0 ||
Herbie Ongc96a79d2019-03-08 10:49:17 -0800120 d.value.typ == comma ||
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800121 d.startStack[len(d.startStack)-1] != StartObject {
122 return Value{}, d.newSyntaxError("unexpected character }")
123 }
124 d.startStack = d.startStack[:len(d.startStack)-1]
125
126 case EndArray:
127 if len(d.startStack) == 0 ||
Herbie Ongc96a79d2019-03-08 10:49:17 -0800128 d.value.typ == comma ||
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800129 d.startStack[len(d.startStack)-1] != StartArray {
130 return Value{}, d.newSyntaxError("unexpected character ]")
131 }
132 d.startStack = d.startStack[:len(d.startStack)-1]
133
134 case comma:
135 if len(d.startStack) == 0 ||
Herbie Ongc96a79d2019-03-08 10:49:17 -0800136 d.value.typ&(Null|Bool|Number|String|EndObject|EndArray) == 0 {
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800137 return Value{}, d.newSyntaxError("unexpected character ,")
138 }
Joe Tsai879b18d2018-08-03 17:22:24 -0700139 }
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800140
Herbie Ongdecef412019-04-17 15:47:43 -0700141 // Update d.value only after validating value to be in the right sequence.
142 d.value = value
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800143 d.in = d.in[n:]
144
Herbie Ongc96a79d2019-03-08 10:49:17 -0800145 if d.value.typ == comma {
146 return d.Read()
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800147 }
148 return value, nerr.E
Joe Tsai879b18d2018-08-03 17:22:24 -0700149}
150
Herbie Ongdecef412019-04-17 15:47:43 -0700151// Any sequence that looks like a non-delimiter (for error reporting).
152var errRegexp = regexp.MustCompile(`^([-+._a-zA-Z0-9]{1,32}|.)`)
Joe Tsai879b18d2018-08-03 17:22:24 -0700153
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800154// parseNext parses for the next JSON value. It returns a Value object for
Herbie Ongdecef412019-04-17 15:47:43 -0700155// different types, except for Name. It does not handle whether the next value
156// is in a valid sequence or not.
157func (d *Decoder) parseNext() (value Value, err error) {
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800158 // Trim leading spaces.
159 d.consume(0)
Joe Tsai879b18d2018-08-03 17:22:24 -0700160
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800161 in := d.in
162 if len(in) == 0 {
Herbie Ongdecef412019-04-17 15:47:43 -0700163 return d.newValue(EOF, nil, 0), nil
Joe Tsai879b18d2018-08-03 17:22:24 -0700164 }
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800165
166 switch in[0] {
Herbie Ongdecef412019-04-17 15:47:43 -0700167 case 'n':
168 n := matchWithDelim("null", in)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800169 if n == 0 {
Herbie Ongdecef412019-04-17 15:47:43 -0700170 return Value{}, d.newSyntaxError("invalid value %s", errRegexp.Find(in))
Joe Tsai879b18d2018-08-03 17:22:24 -0700171 }
Herbie Ongdecef412019-04-17 15:47:43 -0700172 return d.newValue(Null, in, n), nil
173
174 case 't':
175 n := matchWithDelim("true", in)
176 if n == 0 {
177 return Value{}, d.newSyntaxError("invalid value %s", errRegexp.Find(in))
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800178 }
Herbie Ongdecef412019-04-17 15:47:43 -0700179 return d.newBoolValue(in, n, true), nil
180
181 case 'f':
182 n := matchWithDelim("false", in)
183 if n == 0 {
184 return Value{}, d.newSyntaxError("invalid value %s", errRegexp.Find(in))
185 }
186 return d.newBoolValue(in, n, false), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800187
Joe Tsai879b18d2018-08-03 17:22:24 -0700188 case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
Herbie Onga3421952019-03-21 18:12:26 -0700189 n, ok := consumeNumber(in)
190 if !ok {
Herbie Ongdecef412019-04-17 15:47:43 -0700191 return Value{}, d.newSyntaxError("invalid number %s", errRegexp.Find(in))
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800192 }
Herbie Ongdecef412019-04-17 15:47:43 -0700193 return d.newValue(Number, in, n), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800194
Joe Tsai879b18d2018-08-03 17:22:24 -0700195 case '"':
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800196 var nerr errors.NonFatal
197 s, n, err := d.parseString(in)
198 if !nerr.Merge(err) {
Herbie Ongdecef412019-04-17 15:47:43 -0700199 return Value{}, err
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800200 }
Herbie Ongdecef412019-04-17 15:47:43 -0700201 return d.newStringValue(in, n, s), nerr.E
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800202
Joe Tsai879b18d2018-08-03 17:22:24 -0700203 case '{':
Herbie Ongdecef412019-04-17 15:47:43 -0700204 return d.newValue(StartObject, in, 1), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800205
206 case '}':
Herbie Ongdecef412019-04-17 15:47:43 -0700207 return d.newValue(EndObject, in, 1), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800208
209 case '[':
Herbie Ongdecef412019-04-17 15:47:43 -0700210 return d.newValue(StartArray, in, 1), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800211
212 case ']':
Herbie Ongdecef412019-04-17 15:47:43 -0700213 return d.newValue(EndArray, in, 1), nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800214
215 case ',':
Herbie Ongdecef412019-04-17 15:47:43 -0700216 return d.newValue(comma, in, 1), nil
Joe Tsai879b18d2018-08-03 17:22:24 -0700217 }
Herbie Ongdecef412019-04-17 15:47:43 -0700218 return Value{}, d.newSyntaxError("invalid value %s", errRegexp.Find(in))
Joe Tsai879b18d2018-08-03 17:22:24 -0700219}
220
Herbie Ongdecef412019-04-17 15:47:43 -0700221// position returns line and column number of index in given orig slice.
222func position(orig []byte, idx int) (int, int) {
223 b := orig[:idx]
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800224 line := bytes.Count(b, []byte("\n")) + 1
225 if i := bytes.LastIndexByte(b, '\n'); i >= 0 {
226 b = b[i+1:]
Joe Tsai879b18d2018-08-03 17:22:24 -0700227 }
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800228 column := utf8.RuneCount(b) + 1 // ignore multi-rune characters
229 return line, column
Joe Tsai879b18d2018-08-03 17:22:24 -0700230}
231
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800232// newSyntaxError returns an error with line and column information useful for
233// syntax errors.
234func (d *Decoder) newSyntaxError(f string, x ...interface{}) error {
235 e := errors.New(f, x...)
Herbie Ongdecef412019-04-17 15:47:43 -0700236 line, column := position(d.orig, len(d.orig)-len(d.in))
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800237 return errors.New("syntax error (line %d:%d): %v", line, column, e)
Joe Tsai879b18d2018-08-03 17:22:24 -0700238}
239
Herbie Ongdecef412019-04-17 15:47:43 -0700240// matchWithDelim matches s with the input b and verifies that the match
Joe Tsai879b18d2018-08-03 17:22:24 -0700241// terminates with a delimiter of some form (e.g., r"[^-+_.a-zA-Z0-9]").
Herbie Ongdecef412019-04-17 15:47:43 -0700242// As a special case, EOF is considered a delimiter. It returns the length of s
243// if there is a match, else 0.
244func matchWithDelim(s string, b []byte) int {
245 if !bytes.HasPrefix(b, []byte(s)) {
246 return 0
247 }
248
249 n := len(s)
250 if n < len(b) && isNotDelim(b[n]) {
251 return 0
Joe Tsai879b18d2018-08-03 17:22:24 -0700252 }
253 return n
254}
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800255
256// isNotDelim returns true if given byte is a not delimiter character.
257func isNotDelim(c byte) bool {
258 return (c == '-' || c == '+' || c == '.' || c == '_' ||
259 ('a' <= c && c <= 'z') ||
260 ('A' <= c && c <= 'Z') ||
261 ('0' <= c && c <= '9'))
262}
263
264// consume consumes n bytes of input and any subsequent whitespace.
265func (d *Decoder) consume(n int) {
266 d.in = d.in[n:]
267 for len(d.in) > 0 {
268 switch d.in[0] {
269 case ' ', '\n', '\r', '\t':
270 d.in = d.in[1:]
271 default:
272 return
273 }
274 }
275}
276
277// isValueNext returns true if next type should be a JSON value: Null,
278// Number, String or Bool.
279func (d *Decoder) isValueNext() bool {
280 if len(d.startStack) == 0 {
Herbie Ongc96a79d2019-03-08 10:49:17 -0800281 return d.value.typ == 0
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800282 }
283
284 start := d.startStack[len(d.startStack)-1]
285 switch start {
286 case StartObject:
Herbie Ongc96a79d2019-03-08 10:49:17 -0800287 return d.value.typ&Name != 0
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800288 case StartArray:
Herbie Ongc96a79d2019-03-08 10:49:17 -0800289 return d.value.typ&(StartArray|comma) != 0
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800290 }
291 panic(fmt.Sprintf(
292 "unreachable logic in Decoder.isValueNext, lastType: %v, startStack: %v",
Herbie Ongc96a79d2019-03-08 10:49:17 -0800293 d.value.typ, start))
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800294}
295
Herbie Ong670d8082019-03-31 19:10:33 -0700296// newValue constructs a Value for given Type.
Herbie Ongdecef412019-04-17 15:47:43 -0700297func (d *Decoder) newValue(typ Type, input []byte, size int) Value {
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800298 return Value{
Herbie Ongdecef412019-04-17 15:47:43 -0700299 typ: typ,
300 input: d.orig,
301 start: len(d.orig) - len(input),
302 size: size,
Herbie Ong670d8082019-03-31 19:10:33 -0700303 }
304}
305
306// newBoolValue constructs a Value for a JSON boolean.
Herbie Ongdecef412019-04-17 15:47:43 -0700307func (d *Decoder) newBoolValue(input []byte, size int, b bool) Value {
Herbie Ong670d8082019-03-31 19:10:33 -0700308 return Value{
Herbie Ongdecef412019-04-17 15:47:43 -0700309 typ: Bool,
310 input: d.orig,
311 start: len(d.orig) - len(input),
312 size: size,
313 boo: b,
Herbie Ong670d8082019-03-31 19:10:33 -0700314 }
315}
316
317// newStringValue constructs a Value for a JSON string.
Herbie Ongdecef412019-04-17 15:47:43 -0700318func (d *Decoder) newStringValue(input []byte, size int, s string) Value {
Herbie Ong670d8082019-03-31 19:10:33 -0700319 return Value{
Herbie Ongdecef412019-04-17 15:47:43 -0700320 typ: String,
321 input: d.orig,
322 start: len(d.orig) - len(input),
323 size: size,
324 str: s,
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800325 }
326}
327
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700328// Clone returns a copy of the Decoder for use in reading ahead the next JSON
329// object, array or other values without affecting current Decoder.
330func (d *Decoder) Clone() *Decoder {
331 ret := *d
332 ret.startStack = append([]Type(nil), ret.startStack...)
333 return &ret
334}
335
Herbie Ongdecef412019-04-17 15:47:43 -0700336// Value provides a parsed JSON type and value.
337//
338// The original input slice is stored in this struct in order to compute for
339// position as needed. The raw JSON value is derived from the original input
340// slice given start and size.
341//
Herbie Ong670d8082019-03-31 19:10:33 -0700342// For JSON boolean and string, it holds the converted value in boo and str
Herbie Ongdecef412019-04-17 15:47:43 -0700343// fields respectively. For JSON number, the raw JSON value holds a valid number
344// which is converted only in Int or Float. Other JSON types do not require any
Herbie Ong670d8082019-03-31 19:10:33 -0700345// additional data.
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800346type Value struct {
Herbie Ongdecef412019-04-17 15:47:43 -0700347 typ Type
348 input []byte
349 start int
350 size int
351 boo bool
352 str string
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800353}
354
355func (v Value) newError(f string, x ...interface{}) error {
356 e := errors.New(f, x...)
Herbie Ongdecef412019-04-17 15:47:43 -0700357 line, col := v.Position()
358 return errors.New("error (line %d:%d): %v", line, col, e)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800359}
360
361// Type returns the JSON type.
362func (v Value) Type() Type {
363 return v.typ
364}
365
366// Position returns the line and column of the value.
367func (v Value) Position() (int, int) {
Herbie Ongdecef412019-04-17 15:47:43 -0700368 return position(v.input, v.start)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800369}
370
371// Bool returns the bool value if token is Bool, else it will return an error.
372func (v Value) Bool() (bool, error) {
373 if v.typ != Bool {
Herbie Ongdecef412019-04-17 15:47:43 -0700374 return false, v.newError("%s is not a bool", v.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800375 }
Herbie Ong670d8082019-03-31 19:10:33 -0700376 return v.boo, nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800377}
378
379// String returns the string value for a JSON string token or the read value in
380// string if token is not a string.
381func (v Value) String() string {
382 if v.typ != String {
Herbie Ongdecef412019-04-17 15:47:43 -0700383 return v.Raw()
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800384 }
Herbie Ong670d8082019-03-31 19:10:33 -0700385 return v.str
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800386}
387
388// Name returns the object name if token is Name, else it will return an error.
389func (v Value) Name() (string, error) {
390 if v.typ != Name {
Herbie Ongdecef412019-04-17 15:47:43 -0700391 return "", v.newError("%s is not an object name", v.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800392 }
Herbie Ong670d8082019-03-31 19:10:33 -0700393 return v.str, nil
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800394}
395
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700396// Raw returns the read value in string.
397func (v Value) Raw() string {
Herbie Ongdecef412019-04-17 15:47:43 -0700398 return string(v.input[v.start : v.start+v.size])
Herbie Ong8ac9dd22019-03-27 12:20:50 -0700399}
400
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800401// Float returns the floating-point number if token is Number, else it will
402// return an error.
403//
404// The floating-point precision is specified by the bitSize parameter: 32 for
405// float32 or 64 for float64. If bitSize=32, the result still has type float64,
406// but it will be convertible to float32 without changing its value. It will
407// return an error if the number exceeds the floating point limits for given
408// bitSize.
409func (v Value) Float(bitSize int) (float64, error) {
410 if v.typ != Number {
Herbie Ongdecef412019-04-17 15:47:43 -0700411 return 0, v.newError("%s is not a number", v.Raw())
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800412 }
Herbie Ongdecef412019-04-17 15:47:43 -0700413 f, err := strconv.ParseFloat(v.Raw(), bitSize)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800414 if err != nil {
415 return 0, v.newError("%v", err)
416 }
417 return f, nil
418}
419
420// Int returns the signed integer number if token is Number, else it will
421// return an error.
422//
423// The given bitSize specifies the integer type that the result must fit into.
424// It returns an error if the number is not an integer value or if the result
425// exceeds the limits for given bitSize.
426func (v Value) Int(bitSize int) (int64, error) {
427 s, err := v.getIntStr()
428 if err != nil {
429 return 0, err
430 }
431 n, err := strconv.ParseInt(s, 10, bitSize)
432 if err != nil {
433 return 0, v.newError("%v", err)
434 }
435 return n, nil
436}
437
438// Uint returns the signed integer number if token is Number, else it will
439// return an error.
440//
441// The given bitSize specifies the unsigned integer type that the result must
442// fit into. It returns an error if the number is not an unsigned integer value
443// or if the result exceeds the limits for given bitSize.
444func (v Value) Uint(bitSize int) (uint64, error) {
445 s, err := v.getIntStr()
446 if err != nil {
447 return 0, err
448 }
449 n, err := strconv.ParseUint(s, 10, bitSize)
450 if err != nil {
451 return 0, v.newError("%v", err)
452 }
453 return n, nil
454}
455
456func (v Value) getIntStr() (string, error) {
457 if v.typ != Number {
458 return "", v.newError("%s is not a number", v.input)
459 }
Herbie Ongdecef412019-04-17 15:47:43 -0700460 parts, ok := parseNumber(v.input[v.start : v.start+v.size])
Herbie Onga3421952019-03-21 18:12:26 -0700461 if !ok {
462 return "", v.newError("%s is not a number", v.input)
463 }
464 num, ok := normalizeToIntString(parts)
Herbie Ongd3f8f2d2019-03-06 00:28:23 -0800465 if !ok {
466 return "", v.newError("cannot convert %s to integer", v.input)
467 }
468 return num, nil
469}