math.decimal: support exponent notation in from_string (#31104) (#31190)

* 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:
Mahdi ramezaan zaadeandAlexander Medvednikov authored and GitHub committed 2026-10-26 15:28:23 +03:00
1 parent d48a41a247
commit c2eda29242
3 files changed
+96 -3

No files matched your search

+5
View File
@@ -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
+50 -3
View File
@@ -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`.
+41
View File
@@ -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'