diff --git a/lib.go b/lib.go index 8ac88dc39d..af4cb2605b 100644 --- a/lib.go +++ b/lib.go @@ -856,28 +856,53 @@ func bstrMarshal(s string) (result string) { return result } -// newRat converts decimals to rational fractions with the required precision. -func newRat(n float64, iterations int64, prec float64) *big.Rat { - x := int64(math.Floor(n)) - y := n - float64(x) - rat := continuedFraction(y, 1, iterations, prec) - return rat.Add(rat, new(big.Rat).SetInt64(x)) -} - -// continuedFraction returns rational from decimal with the continued fraction -// algorithm. -func continuedFraction(n float64, i int64, limit int64, prec float64) *big.Rat { - if i >= limit || n <= prec { - return big.NewRat(0, 1) - } - inverted := 1 / n - y := int64(math.Floor(inverted)) - x := inverted - float64(y) - ratY := new(big.Rat).SetInt64(y) - ratNext := continuedFraction(x, i+1, limit, prec) - res := ratY.Add(ratY, ratNext) - res = res.Inv(res) - return res +// floatToFraction converts a float number to a fraction string representation +// with specified placeholder widths for numerator and denominator. +func floatToFraction(x float64, numeratorPlaceHolder, denominatorPlaceHolder int) string { + if denominatorPlaceHolder <= 0 { + return "" + } + num, den := floatToFracUseContinuedFraction(x, int64(math.Pow10(denominatorPlaceHolder))) + if num == 0 { + return strings.Repeat(" ", numeratorPlaceHolder+denominatorPlaceHolder+1) + } + numStr := strconv.FormatInt(num, 10) + denStr := strconv.FormatInt(den, 10) + numeratorPlaceHolder = max(numeratorPlaceHolder-len(numStr), 0) + denominatorPlaceHolder = max(denominatorPlaceHolder-len(denStr), 0) + return fmt.Sprintf("%s%s/%s%s", strings.Repeat(" ", numeratorPlaceHolder), numStr, denStr, strings.Repeat(" ", denominatorPlaceHolder)) +} + +// floatToFracUseContinuedFraction implement convert a floating-point decimal +// to a fraction using continued fractions and recurrence relations. +func floatToFracUseContinuedFraction(r float64, denominatorLimit int64) (num, den int64) { + p_1 := int64(1) // LaTex: p_{-1} + q_1 := int64(0) // LaTex: q_{-1} + p_2 := int64(0) // LaTex: p_{-2} + q_2 := int64(1) // LaTex: q_{-2} + var lasta, lastb int64 + var curra, currb int64 + for k := 0; ; k++ { + // a_{k} = \lfloor r_{k} \rfloor + a := int64(math.Floor(r)) + // Fundamental recurrence formulas: p_{k} = a_{k} \cdot p_{k-1} + p_{k-2} + curra, currb = a*p_1+p_2, a*q_1+q_2 + p_2 = p_1 + q_2 = q_1 + p_1 = curra + q_1 = currb + frac := r - float64(a) + if q_1 >= denominatorLimit { + return lasta, lastb + } + if math.Abs(frac) < 1e-12 { + // use safe floating-point number comparison. If the input(r) is a real number, here is 0. + return curra, currb + } + lasta, lastb = curra, currb + // r_{k+1} = \frac{1}{r_{k} - a_{k}} + r = 1.0 / frac + } } // assignFieldValue assigns the value from an immutable reflect.Value to a diff --git a/lib_test.go b/lib_test.go index fe5eed0530..a30f36c18e 100644 --- a/lib_test.go +++ b/lib_test.go @@ -6,6 +6,7 @@ import ( "encoding/xml" "fmt" "io" + "math" "os" "strconv" "strings" @@ -412,3 +413,10 @@ func TestUnzipToTemp(t *testing.T) { _, err = f.unzipToTemp(z.File[0]) assert.EqualError(t, err, "EOF") } + +func TestFloat2Frac(t *testing.T) { + assert.Empty(t, floatToFraction(0.19, 0, 0)) + assert.Equal(t, "1/5", floatToFraction(0.19, 1, 1)) + assert.Equal(t, "9999/10000", strings.Trim(floatToFraction(0.9999, 10, 10), " ")) + assert.Equal(t, "954888175898973913/351283728530932463", floatToFraction(math.E, 1, 18)) +} diff --git a/numfmt.go b/numfmt.go index 479d3b5649..1dd5354c65 100644 --- a/numfmt.go +++ b/numfmt.go @@ -5439,27 +5439,8 @@ func (nf *numberFormat) printNumberLiteral(text string) string { // negative numeric. func (nf *numberFormat) fractionHandler(frac float64, token nfp.Token, numeratorPlaceHolder int) string { var rat, result string - var lastRat *big.Rat if token.TType == nfp.TokenTypeDigitalPlaceHolder { - denominatorPlaceHolder := len(token.TValue) - for i := range 5000 { - if r := newRat(frac, int64(i), 0); len(r.Denom().String()) <= denominatorPlaceHolder { - lastRat = r // record the last valid ratio, and delay conversion to string - continue - } - break - } - if lastRat != nil { - if lastRat.Num().Int64() == 0 { - rat = strings.Repeat(" ", numeratorPlaceHolder+denominatorPlaceHolder+1) - } else { - num := lastRat.Num().String() - den := lastRat.Denom().String() - numeratorPlaceHolder = max(numeratorPlaceHolder-len(num), 0) - denominatorPlaceHolder = max(denominatorPlaceHolder-len(den), 0) - rat = fmt.Sprintf("%s%s/%s%s", strings.Repeat(" ", numeratorPlaceHolder), num, den, strings.Repeat(" ", denominatorPlaceHolder)) - } - } + rat = floatToFraction(frac, numeratorPlaceHolder, len(token.TValue)) result += rat } if token.TType == nfp.TokenTypeDenominator { @@ -5539,8 +5520,9 @@ func (nf *numberFormat) numberHandler() string { intLen, fracLen = nf.getNumberPartLen() result string ) - if isNum, precision, decimal := isNumeric(nf.value); isNum { - if precision > 15 && intLen+fracLen > 15 && !nf.useScientificNotation { + if intLen+fracLen > 15 && !nf.useScientificNotation { + isNum, precision, decimal := isNumeric(nf.value) + if isNum && precision > 15 { return nf.printNumberLiteral(nf.printBigNumber(decimal, fracLen)) } } diff --git a/numfmt_test.go b/numfmt_test.go index 065fc58df6..0a3cd8a53b 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -4350,8 +4350,7 @@ func BenchmarkNumFmtPlaceHolder(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { for _, item := range items { - result := format(item[0], item[1], false, CellTypeNumber, nil) - _ = result + _ = format(item[0], item[1], false, CellTypeNumber, nil) } } }