From c3c91577a80541f7cfb3f9a98863c024117927c2 Mon Sep 17 00:00:00 2001 From: shcabin Date: Tue, 7 Oct 2025 23:02:01 +0800 Subject: [PATCH 1/4] convert using continued fractions fundamental recurrence formulas --- lib.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ numfmt.go | 21 +-------------------- numfmt_test.go | 3 +-- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/lib.go b/lib.go index e12965d0f1..f15efa108c 100644 --- a/lib.go +++ b/lib.go @@ -880,6 +880,54 @@ func continuedFraction(n float64, i int64, limit int64, prec float64) *big.Rat { return res } +func floatToFraction(x float64, numeratorPlaceHolder, denominatorPlaceHolder int) string { + if denominatorPlaceHolder <= 0 { + return "" + } + var rat string + num, den := floatToFracUseContinuedFraction(x, int64(math.Pow10(denominatorPlaceHolder))) + if num == 0 { + rat = strings.Repeat(" ", numeratorPlaceHolder+denominatorPlaceHolder+1) + } else { + numStr := strconv.FormatInt(num, 10) + denStr := strconv.FormatInt(den, 10) + numeratorPlaceHolder = max(numeratorPlaceHolder-len(numStr), 0) + denominatorPlaceHolder = max(denominatorPlaceHolder-len(denStr), 0) + rat = fmt.Sprintf("%s%s/%s%s", strings.Repeat(" ", numeratorPlaceHolder), numStr, denStr, strings.Repeat(" ", denominatorPlaceHolder)) + } + return rat +} + +// floatToFracUseContinuedFraction implement convert a floating-point decimal +// to a fraction using continued fractions and recurrence relations. +func floatToFracUseContinuedFraction(x float64, denominatorLimit int64) (num, den int64) { + p_1 := int64(1) + q_1 := int64(0) + p_2 := int64(0) + q_2 := int64(1) + var lasta, lastb int64 + var curra, currb int64 + for i := 0; i < 100; i++ { + a := int64(math.Floor(x)) + 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 := x - float64(a) + if q_1 >= denominatorLimit { + return lasta, lastb //big.NewRat(lasta, lastb) + } + if math.Abs(frac) < 1e-12 { + return curra, currb //big.NewRat(curra, currb) + } + + lasta, lastb = curra, currb + x = 1.0 / frac + } + return lasta, lastb //big.NewRat(lasta, lastb) +} + // assignFieldValue assigns the value from an immutable reflect.Value to a // mutable reflect.Value based on the type of the immutable value. func assignFieldValue(field string, immutable, mutable reflect.Value) { diff --git a/numfmt.go b/numfmt.go index d65942924a..905db1c64d 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 { 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) } } } From 232d35665084cb1696c8d554958beafd60bf6731 Mon Sep 17 00:00:00 2001 From: shcabin Date: Thu, 9 Oct 2025 22:27:59 +0800 Subject: [PATCH 2/4] convert using continued fractions fundamental recurrence formulas --- lib_test.go | 37 +++++++++++++++++++++++++++++++++++++ numfmt.go | 5 +++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib_test.go b/lib_test.go index fe5eed0530..b16c083b20 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,39 @@ func TestUnzipToTemp(t *testing.T) { _, err = f.unzipToTemp(z.File[0]) assert.EqualError(t, err, "EOF") } + +func oldFractionHandler(frac float64, fracPlaceHolder int) string { + var rat string + for i := 0; i < 5000; i++ { + if r := newRat(frac, int64(i), 0); len(r.Denom().String()) <= fracPlaceHolder { + if rat = r.String(); strings.HasPrefix(rat, "0/") { + rat = strings.Repeat(" ", 3) + } + continue + } + break + } + return rat +} + +func TestFloat2Frac(t *testing.T) { + valueList := []float64{0.19, 0.54, 0.9, 0.99, 0.999, 0.999} + for _, v := range valueList { + for idx := 0; idx <= 10; idx++ { + res1 := strings.Trim(oldFractionHandler(v, idx), " ") + res2 := strings.Trim(floatToFraction(v, idx, idx), " ") + assert.Equal(t, res1, res2, "value %f, fracPlaceHolder %d", v, idx) + } + } + for k := range 10 { + val := math.Pi / math.Pow(10, float64(k)) + for idx := 1; idx <= 5; idx++ { + res1 := strings.Trim(oldFractionHandler(val, idx), " ") + res2 := strings.Trim(floatToFraction(val, idx, idx), " ") + assert.Equal(t, res1, res2, "value %f, fracPlaceHolder %d", val, idx) + if res1 != "" { + t.Log(idx, val, res1, res2) + } + } + } +} diff --git a/numfmt.go b/numfmt.go index 905db1c64d..c72bbabcc6 100644 --- a/numfmt.go +++ b/numfmt.go @@ -5520,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)) } } From 3279c0fb8dd8c29246d30d1ca290c577fc4fd4b8 Mon Sep 17 00:00:00 2001 From: shcabin <5463832+shcabin@users.noreply.github.co> Date: Mon, 10 Nov 2025 00:15:07 +0800 Subject: [PATCH 3/4] add reference --- lib.go | 30 +++++++++++++++++------------- lib_test.go | 3 ++- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib.go b/lib.go index 97541b5de2..f9d9bf1018 100644 --- a/lib.go +++ b/lib.go @@ -900,32 +900,36 @@ func floatToFraction(x float64, numeratorPlaceHolder, denominatorPlaceHolder int // floatToFracUseContinuedFraction implement convert a floating-point decimal // to a fraction using continued fractions and recurrence relations. -func floatToFracUseContinuedFraction(x float64, denominatorLimit int64) (num, den int64) { - p_1 := int64(1) - q_1 := int64(0) - p_2 := int64(0) - q_2 := int64(1) +// Formula reference: https://en.wikipedia.org/wiki/Continued_fraction#Fundamental_recurrence_formulas +// or https://oi-wiki.org/math/number-theory/continued-fraction/#%E9%80%92%E6%8E%A8%E5%85%B3%E7%B3%BB +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 i := 0; i < 100; i++ { - a := int64(math.Floor(x)) + 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 := x - float64(a) + frac := r - float64(a) if q_1 >= denominatorLimit { - return lasta, lastb //big.NewRat(lasta, lastb) + return lasta, lastb } if math.Abs(frac) < 1e-12 { - return curra, currb //big.NewRat(curra, currb) + // use safe floating-point number comparison. If the input(r) is a real number, here is 0. + return curra, currb } - lasta, lastb = curra, currb - x = 1.0 / frac + // r_{k+1} = \frac{1}{r_{k} - a_{k}} + r = 1.0 / frac } - return lasta, lastb //big.NewRat(lasta, lastb) } // assignFieldValue assigns the value from an immutable reflect.Value to a diff --git a/lib_test.go b/lib_test.go index b16c083b20..90e6ceb6ac 100644 --- a/lib_test.go +++ b/lib_test.go @@ -429,7 +429,7 @@ func oldFractionHandler(frac float64, fracPlaceHolder int) string { } func TestFloat2Frac(t *testing.T) { - valueList := []float64{0.19, 0.54, 0.9, 0.99, 0.999, 0.999} + valueList := []float64{0.19, 0.54, 0.9, 0.99, 0.999, 0.9999} for _, v := range valueList { for idx := 0; idx <= 10; idx++ { res1 := strings.Trim(oldFractionHandler(v, idx), " ") @@ -448,4 +448,5 @@ func TestFloat2Frac(t *testing.T) { } } } + assert.Equal(t, "954888175898973913/351283728530932463", floatToFraction(math.E, 1, 18)) } From 1896979645d97a86c97de0cf58730eef2f53c41a Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 10 Nov 2025 10:15:57 +0800 Subject: [PATCH 4/4] Remove unused functions and simplify code --- lib.go | 45 +++++++++------------------------------------ lib_test.go | 36 +++--------------------------------- 2 files changed, 12 insertions(+), 69 deletions(-) diff --git a/lib.go b/lib.go index f9d9bf1018..af4cb2605b 100644 --- a/lib.go +++ b/lib.go @@ -856,52 +856,25 @@ 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 "" } - var rat string num, den := floatToFracUseContinuedFraction(x, int64(math.Pow10(denominatorPlaceHolder))) if num == 0 { - rat = strings.Repeat(" ", numeratorPlaceHolder+denominatorPlaceHolder+1) - } else { - numStr := strconv.FormatInt(num, 10) - denStr := strconv.FormatInt(den, 10) - numeratorPlaceHolder = max(numeratorPlaceHolder-len(numStr), 0) - denominatorPlaceHolder = max(denominatorPlaceHolder-len(denStr), 0) - rat = fmt.Sprintf("%s%s/%s%s", strings.Repeat(" ", numeratorPlaceHolder), numStr, denStr, strings.Repeat(" ", denominatorPlaceHolder)) - } - return rat + 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. -// Formula reference: https://en.wikipedia.org/wiki/Continued_fraction#Fundamental_recurrence_formulas -// or https://oi-wiki.org/math/number-theory/continued-fraction/#%E9%80%92%E6%8E%A8%E5%85%B3%E7%B3%BB func floatToFracUseContinuedFraction(r float64, denominatorLimit int64) (num, den int64) { p_1 := int64(1) // LaTex: p_{-1} q_1 := int64(0) // LaTex: q_{-1} diff --git a/lib_test.go b/lib_test.go index 90e6ceb6ac..a30f36c18e 100644 --- a/lib_test.go +++ b/lib_test.go @@ -414,39 +414,9 @@ func TestUnzipToTemp(t *testing.T) { assert.EqualError(t, err, "EOF") } -func oldFractionHandler(frac float64, fracPlaceHolder int) string { - var rat string - for i := 0; i < 5000; i++ { - if r := newRat(frac, int64(i), 0); len(r.Denom().String()) <= fracPlaceHolder { - if rat = r.String(); strings.HasPrefix(rat, "0/") { - rat = strings.Repeat(" ", 3) - } - continue - } - break - } - return rat -} - func TestFloat2Frac(t *testing.T) { - valueList := []float64{0.19, 0.54, 0.9, 0.99, 0.999, 0.9999} - for _, v := range valueList { - for idx := 0; idx <= 10; idx++ { - res1 := strings.Trim(oldFractionHandler(v, idx), " ") - res2 := strings.Trim(floatToFraction(v, idx, idx), " ") - assert.Equal(t, res1, res2, "value %f, fracPlaceHolder %d", v, idx) - } - } - for k := range 10 { - val := math.Pi / math.Pow(10, float64(k)) - for idx := 1; idx <= 5; idx++ { - res1 := strings.Trim(oldFractionHandler(val, idx), " ") - res2 := strings.Trim(floatToFraction(val, idx, idx), " ") - assert.Equal(t, res1, res2, "value %f, fracPlaceHolder %d", val, idx) - if res1 != "" { - t.Log(idx, val, res1, res2) - } - } - } + 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)) }