Repository: go
Issue: proposal: x/text/number: convert directly from string to decimal
Labels: Proposal

### Proposal Details

Currently only integer and float types are supported for converting to Decimal.

The problem with this is that converting to a float can cause issues for very small and very large numbers.

The code for this is here: https://github.com/golang/text/blob/3043346206dbc748052ef6e130f428f895cd3760/internal/number/decimal.go#L308-L351

Allowing a `string` to be converted directly would avoid float related issues and would also improve the usability as it is no longer needed to first convert to a `float64` etc.

With a small diff like this it works for the most common cases:
```diff
diff --git a/internal/number/decimal.go b/internal/number/decimal.go
index e128cf3..06b711a 100644
--- a/internal/number/decimal.go
+++ b/internal/number/decimal.go
@@ -312,6 +312,8 @@ func (d *Decimal) Convert(r RoundingContext, number interface{}) {
        case Converter:
                d.clear()
                f.Convert(d, r)
+       case string:
+               d.ConvertString(r, f)
        case float32:
                d.ConvertFloat(r, float64(f), 32)
        case float64:
@@ -471,6 +473,28 @@ func (d *Decimal) ConvertFloat(r RoundingContext, x float64, size int) {
        }
 }
 
+func (d *Decimal) ConvertString(r RoundingContext, x string) {
+       for i, n := range x {
+               if n > 47 && n < 58 { // 0-9
+                       d.Digits = append(d.Digits, byte(n-48))
+               } else if n == 46 { // '.'
+                       d.Exp = int32(i)
+                       if d.Neg {
+                               d.Exp-- // Adjust for '-' character
+                       }
+               } else if n == 45 { // '-'
+                       if i > 0 { // Only allowed as first character
+                               d.NaN = true
+                               return
+                       }
+                       d.Neg = true
+               } else {
+                       d.NaN = true
+                       return
+               }
+       }
+}
+
 func (d *Decimal) fillIntDigits(x uint64) {
        if cap(d.Digits) < maxIntDigits {
                d.Digits = d.buf[:]
```

```go
package main

import (
	"fmt"
	"strconv"

	"golang.org/x/text/language"
	"golang.org/x/text/message"
	"golang.org/x/text/number"
)

func main() {
	p := message.NewPrinter(language.English)

	nstrs := []string{"12332.1234567890123456789012345678901", "1.1", "1.06", "-2.4", "abc", "0-1"}
	for i, nstr := range nstrs {

		fmt.Printf("input:      %v\n", nstr)
		nr := number.Decimal(nstr, number.Scale(30))
		p.Printf("as string:  %v\n", nr)

		if nrf, err := strconv.ParseFloat(nstr, 64); err == nil {
			nr = number.Decimal(nrf, number.Scale(30))
			p.Printf("as float64: %v\n", nr)
		}

		if i+1 < len(nstrs) {
			fmt.Printf("-----------------------------\n")
		}
	}
}
```

```
input:      12332.1234567890123456789012345678901
as string:  12,332.123456789012345678901234567890
as float64: 12,332.123456789011470391415059566498
-----------------------------
input:      1.1
as string:  1.100000000000000000000000000000
as float64: 1.100000000000000088817841970013
-----------------------------
input:      1.06
as string:  1.060000000000000000000000000000
as float64: 1.060000000000000053290705182008
-----------------------------
input:      -2.4
as string:  -2.400000000000000000000000000000
as float64: -2.399999999999999911182158029987
-----------------------------
input:      abc
as string:  NaN
-----------------------------
input:      0-1
as string:  NaN
```

Open issues/questions/etc:
- This isn't following the `RoundingContext`
- This doesn't support things like `1e10`
- This doesn't support `Inf`
- This probably needs some range checking to make sure `d.Digits` etc are in an acceptable range (`maxIntDigits`?)
- Another option is to use `big.Rat` or other `math/big` types. But converting from `a/b` to `Digits/Exp` doesn't look simple to me and this wouldn't make this more userfriendly. It also looks like overkill for many cases and it might be slower as well.
- This would return `NaN` for something like `1.23 `, triggered by the trailing space.
- This doesn't handle different minus signs that are in unicode. (https://pyatl.dev/2024/09/01/bitten-by-unicode/)
- Maybe something else could be used to normalize the string first?
- Maybe this can be made easier somehow where the type can supply a `Convert(...)` function?