Skip to content

Commit

Permalink
use grisu2 for formatting (#347)
Browse files Browse the repository at this point in the history
port grisu2 formatter from redis src
  • Loading branch information
alicebob committed Oct 4, 2023
1 parent aa376e7 commit 85eaaad
Show file tree
Hide file tree
Showing 11 changed files with 629 additions and 20 deletions.
2 changes: 1 addition & 1 deletion cmd_sorted_set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func TestSortedSetAdd(t *testing.T) {

mustDo(t, c,
"ZADD", "z", "INCR", "XX", "1.2", "one",
proto.String("3.6"),
proto.String("3.5999999999999996"),
)

mustNil(t, c,
Expand Down
26 changes: 26 additions & 0 deletions fpconv/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
This code is derived from the C code in redis-7.2.0/deps/fpconv/*, which has
this license:

Boost Software License - Version 1.0 - August 17th, 2003

Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:

The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
6 changes: 6 additions & 0 deletions fpconv/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.PHONY: test fuzz
test:
go test

fuzz:
go test -fuzz=Fuzz
3 changes: 3 additions & 0 deletions fpconv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This is a translation of the actual C code in Redis (7.2) which does the float
-> string conversion.
Strconv does a close enough job, but we can use the exact same logic, so why not.
286 changes: 286 additions & 0 deletions fpconv/dtoa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package fpconv

import (
"math"
)

var (
fracmask uint64 = 0x000FFFFFFFFFFFFF
expmask uint64 = 0x7FF0000000000000
hiddenbit uint64 = 0x0010000000000000
signmask uint64 = 0x8000000000000000
expbias int64 = 1023 + 52
zeros = []rune("0000000000000000000000")

tens = []uint64{
10000000000000000000,
1000000000000000000,
100000000000000000,
10000000000000000,
1000000000000000,
100000000000000,
10000000000000,
1000000000000,
100000000000,
10000000000,
1000000000,
100000000,
10000000,
1000000,
100000,
10000,
1000,
100,
10,
1}
)

func absv(n int) int {
if n < 0 {
return -n
}
return n
}

func minv(a, b int) int {
if a < b {
return a
}
return b
}

func Dtoa(d float64) string {
var (
dest [25]rune // Note C has 24, which is broken
digits [18]rune

str_len int = 0
neg = false
)

if get_dbits(d)&signmask != 0 {
dest[0] = '-'
str_len++
neg = true
}

if spec := filter_special(d, dest[str_len:]); spec != 0 {
return string(dest[:str_len+spec])
}

var (
k int = 0
ndigits int = grisu2(d, &digits, &k)
)

str_len += emit_digits(&digits, ndigits, dest[str_len:], k, neg)
return string(dest[:str_len])
}

func filter_special(fp float64, dest []rune) int {
if fp == 0.0 {
dest[0] = '0'
return 1
}

if math.IsNaN(fp) {
dest[0] = 'n'
dest[1] = 'a'
dest[2] = 'n'
return 3
}
if math.IsInf(fp, 0) {
dest[0] = 'i'
dest[1] = 'n'
dest[2] = 'f'
return 3
}
return 0
}

func grisu2(d float64, digits *[18]rune, K *int) int {
w := build_fp(d)

lower, upper := get_normalized_boundaries(w)

w = normalize(w)

var k int64
cp := find_cachedpow10(upper.exp, &k)

w = multiply(w, cp)
upper = multiply(upper, cp)
lower = multiply(lower, cp)

lower.frac++
upper.frac--

*K = int(-k)

return generate_digits(w, upper, lower, digits[:], K)
}

func emit_digits(digits *[18]rune, ndigits int, dest []rune, K int, neg bool) int {
exp := int(absv(K + ndigits - 1))

/* write plain integer */
if K >= 0 && (exp < (ndigits + 7)) {
copy(dest, digits[:ndigits])
copy(dest[ndigits:], zeros[:K])

return ndigits + K
}

/* write decimal w/o scientific notation */
if K < 0 && (K > -7 || exp < 4) {
offset := int(ndigits - absv(K))
/* fp < 1.0 -> write leading zero */
if offset <= 0 {
offset = -offset
dest[0] = '0'
dest[1] = '.'
copy(dest[2:], zeros[:offset])
copy(dest[offset+2:], digits[:ndigits])

return ndigits + 2 + offset

/* fp > 1.0 */
} else {
copy(dest, digits[:offset])
dest[offset] = '.'
copy(dest[offset+1:], digits[offset:offset+ndigits-offset])

return ndigits + 1
}
}
/* write decimal w/ scientific notation */
l := 18 // was: 18-neg
if neg {
l--
}
ndigits = minv(ndigits, l)

var idx int = 0
dest[idx] = digits[0]
idx++

if ndigits > 1 {
dest[idx] = '.'
idx++
copy(dest[idx:], digits[+1:ndigits-1+1])
idx += ndigits - 1
}

dest[idx] = 'e'
idx++

sign := '+'
if K+ndigits-1 < 0 {
sign = '-'
}
dest[idx] = sign
idx++

var cent rune = 0

if exp > 99 {
cent = rune(exp / 100)
dest[idx] = cent + '0'
idx++
exp -= int(cent) * 100
}
if exp > 9 {
dec := rune(exp / 10)
dest[idx] = dec + '0'
idx++
exp -= int(dec) * 10
} else if cent != 0 {
dest[idx] = '0'
idx++
}

dest[idx] = rune(exp%10) + '0'
idx++

return idx
}

func generate_digits(fp, upper, lower Fp, digits []rune, K *int) int {
var (
wfrac = uint64(upper.frac - fp.frac)
delta = uint64(upper.frac - lower.frac)
)

one := Fp{
frac: 1 << -upper.exp,
exp: upper.exp,
}

part1 := uint64(upper.frac >> -one.exp)
part2 := uint64(upper.frac & (one.frac - 1))

var (
idx = 0
kappa = 10
index = 10
)
/* 1000000000 */
for ; kappa > 0; index++ {
div := tens[index]
digit := part1 / div

if digit != 0 || idx != 0 {
digits[idx] = rune(digit) + '0'
idx++
}

part1 -= digit * div
kappa--

tmp := (part1 << -one.exp) + part2
if tmp <= delta {
*K += kappa
round_digit(digits, idx, delta, tmp, div<<-one.exp, wfrac)

return idx
}
}

/* 10 */
index = 18
for {
var unit uint64 = tens[index]
part2 *= 10
delta *= 10
kappa--

digit := part2 >> -one.exp
if digit != 0 || idx != 0 {
digits[idx] = rune(digit) + '0'
idx++
}

part2 &= uint64(one.frac) - 1
if part2 < delta {
*K += kappa
round_digit(digits, idx, delta, part2, uint64(one.frac), wfrac*unit)

return idx
}

index--
}
}

func round_digit(digits []rune,
ndigits int,
delta uint64,
rem uint64,
kappa uint64,
frac uint64) {
for rem < frac && delta-rem >= kappa &&
(rem+kappa < frac || frac-rem > rem+kappa-frac) {
digits[ndigits-1]--
rem += kappa
}
}

0 comments on commit 85eaaad

Please sign in to comment.