* math.decimal: support exponent notation in from_string (#31104) * math.decimal: validate exponent grammar and exact supported range --------- Co-authored-by: Alexander Medvednikov <alexander@medvednikov.com>
This commit is contained in:
1 parent
d48a41a247
commit
c2eda29242
3 files changed
+96
-3
No files matched your search
@@ -10,6 +10,11 @@ rounding), `half_down`, `up`, `down`, `ceiling` or `floor`. `div_prec` is
|
||||
`div` with `half_up`. The `/` operator uses `decimal.default_division_precision`
|
||||
and panics on a zero divisor; use `div` where the divisor can be zero.
|
||||
|
||||
`from_string` accepts an optional base-10 exponent, for example `1.5e-2` is `0.015`.
|
||||
An exponent can have one leading `+` or `-` sign and any number of leading zeros.
|
||||
Exponents must be between -100 and 100 inclusive. Larger magnitudes return an
|
||||
`exponent out of range` error rather than changing the value or allocating large powers of ten.
|
||||
|
||||
## Example
|
||||
|
||||
```v
|
||||
|
||||
@@ -75,7 +75,8 @@ pub fn from_minor_units(units i64, precision int) !Decimal {
|
||||
return new(big.integer_from_i64(units), precision)
|
||||
}
|
||||
|
||||
// from_string parses a decimal string without exponent notation.
|
||||
// from_string parses a decimal string with an optional base-10 exponent.
|
||||
// Exponents from -100 to 100 are supported; larger magnitudes return an error.
|
||||
pub fn from_string(input string) !Decimal {
|
||||
value := input.trim_space()
|
||||
if value.len == 0 {
|
||||
@@ -86,11 +87,44 @@ pub fn from_string(input string) !Decimal {
|
||||
mut digit_count := 0
|
||||
mut scale := 0
|
||||
mut digits := strings.new_builder(value.len)
|
||||
mut exponent := 0
|
||||
mut exponent_negative := false
|
||||
mut exponent_sign_seen := false
|
||||
mut exponent_digits := 0
|
||||
mut in_exponent := false
|
||||
for index, ch in value {
|
||||
if index == 0 && (ch == `+` || ch == `-`) {
|
||||
negative = ch == `-`
|
||||
continue
|
||||
}
|
||||
if in_exponent {
|
||||
if ch == `+` || ch == `-` {
|
||||
if exponent_digits > 0 || exponent_sign_seen {
|
||||
return error('math.decimal: invalid decimal value `${input}`')
|
||||
}
|
||||
exponent_sign_seen = true
|
||||
exponent_negative = ch == `-`
|
||||
continue
|
||||
}
|
||||
if ch >= `0` && ch <= `9` {
|
||||
exponent_digits++
|
||||
digit := int(ch - `0`)
|
||||
// Check before multiplying to reject large exponents without integer overflow.
|
||||
if exponent > 10 || (exponent == 10 && digit > 0) {
|
||||
return error('math.decimal: exponent out of range `${input}`')
|
||||
}
|
||||
exponent = exponent * 10 + digit
|
||||
continue
|
||||
}
|
||||
return error('math.decimal: invalid decimal value `${input}`')
|
||||
}
|
||||
if ch == `e` || ch == `E` {
|
||||
if digit_count == 0 {
|
||||
return error('math.decimal: invalid decimal value `${input}`')
|
||||
}
|
||||
in_exponent = true
|
||||
continue
|
||||
}
|
||||
if ch >= `0` && ch <= `9` {
|
||||
digits.write_u8(ch)
|
||||
digit_count++
|
||||
@@ -108,12 +142,25 @@ pub fn from_string(input string) !Decimal {
|
||||
if digit_count == 0 {
|
||||
return error('math.decimal: invalid decimal value `${input}`')
|
||||
}
|
||||
if in_exponent && exponent_digits == 0 {
|
||||
return error('math.decimal: invalid decimal value `${input}`')
|
||||
}
|
||||
if exponent_negative {
|
||||
exponent = -exponent
|
||||
}
|
||||
mut coefficient_string := digits.str()
|
||||
if negative {
|
||||
coefficient_string = '-' + coefficient_string
|
||||
}
|
||||
coefficient := big.integer_from_string(coefficient_string)!
|
||||
return new(coefficient, scale)
|
||||
mut coefficient := big.integer_from_string(coefficient_string)!
|
||||
// Apply exponent to scale: value = coefficient * 10^(exponent - scale)
|
||||
mut final_scale := scale - exponent
|
||||
if final_scale < 0 {
|
||||
// multiply coefficient by 10^(-final_scale)
|
||||
coefficient = coefficient * pow10(-final_scale)
|
||||
final_scale = 0
|
||||
}
|
||||
return new(coefficient, final_scale)
|
||||
}
|
||||
|
||||
// coefficient returns the integer coefficient stored by `d`.
|
||||
|
||||
@@ -53,6 +53,47 @@ fn test_from_string_rejects_invalid_input() {
|
||||
assert_parse_error('.', 'math.decimal: invalid decimal value `.`')
|
||||
}
|
||||
|
||||
fn test_from_string_exponent_notation() {
|
||||
cases := {
|
||||
'1e3': '1000'
|
||||
'1.5E-2': '0.015'
|
||||
'-12e+2': '-1200'
|
||||
'+12E-1': '1.2'
|
||||
'.25e1': '2.5'
|
||||
'1.2e-2': '0.012'
|
||||
'1.e2': '100'
|
||||
' 1e2 ': '100'
|
||||
'001.200e0': '1.2'
|
||||
'1e000001': '10'
|
||||
'1e-00001': '0.1'
|
||||
'-0e+100': '0'
|
||||
'+0.00e-100': '0'
|
||||
}
|
||||
for input, expected in cases {
|
||||
assert must_decimal(input).str() == expected, input
|
||||
}
|
||||
zeros := '0'.repeat(128)
|
||||
assert must_decimal('1e${zeros}').str() == '1'
|
||||
assert must_decimal('1e-${zeros}').str() == '1'
|
||||
assert must_decimal('1e+${zeros}100').str() == '1' + '0'.repeat(100)
|
||||
assert must_decimal('1e-${zeros}100').str() == '0.' + '0'.repeat(99) + '1'
|
||||
assert must_decimal('-0e-${zeros}').is_zero()
|
||||
}
|
||||
|
||||
fn test_from_string_rejects_invalid_exponents() {
|
||||
for input in ['1e', '1e+', '1e-', 'e1', '.e1', '-e1', '1e--2', '1e+-2', '1e-+2', '1e++2', '1e2-',
|
||||
'1e2+', '1e2e3', '1E2E3', '1e2.0', '1 e2', '1e 2'] {
|
||||
assert_parse_error(input, 'math.decimal: invalid decimal value `${input}`')
|
||||
}
|
||||
}
|
||||
|
||||
fn test_from_string_rejects_exponents_out_of_range() {
|
||||
for input in ['1e101', '1e-101', '1e+101', '1e2147483648', '1e-2147483648', '1e99999999999999999999',
|
||||
'1e-99999999999999999999', '1e00000000000000101', '1e-00000000000000101'] {
|
||||
assert_parse_error(input, 'math.decimal: exponent out of range `${input}`')
|
||||
}
|
||||
}
|
||||
|
||||
fn test_add_sub_and_mul() {
|
||||
assert (must_decimal('1.23') + must_decimal('4.5')).str() == '5.73'
|
||||
assert (must_decimal('4.5') - must_decimal('1.23')).str() == '3.27'
|
||||
|
||||
Reference in new issue
Block a user