diff --git a/fracdex/fracdex.go b/fracdex/fracdex.go new file mode 100644 index 0000000..9119ef8 --- /dev/null +++ b/fracdex/fracdex.go @@ -0,0 +1,413 @@ +// Licensed under CC0-1.0 Universial by https://github.com/rocicorp/fracdex +package fracdex + +import ( + "errors" + "fmt" + "math" + "strings" +) + +const ( + base62Digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + smallestInt = "A00000000000000000000000000" + zero = "a0" +) + +// KeyBetween returns a key that sorts lexicographically between a and b. +// Either a or b can be empty strings. If a is empty it indicates smallest key, +// If b is empty it indicates largest key. +// b must be empty string or > a. +func KeyBetween(a, b string) (string, error) { + if a != "" { + err := validateOrderKey(a) + if err != nil { + return "", err + } + } + if b != "" { + err := validateOrderKey(b) + if err != nil { + return "", err + } + } + if a != "" && b != "" && a >= b { + return "", fmt.Errorf("%s >= %s", a, b) + } + if a == "" { + if b == "" { + return zero, nil + } + + ib, err := getIntPart(b) + if err != nil { + return "", err + } + fb := b[len(ib):] + if ib == smallestInt { + return ib + midpoint("", fb), nil + } + if ib < b { + return ib, nil + } + res, err := decrementInt(ib) + if err != nil { + return "", err + } + if res == "" { + return "", errors.New("range underflow") + } + return res, nil + } + + if b == "" { + ia, err := getIntPart(a) + if err != nil { + return "", err + } + fa := a[len(ia):] + i, err := incrementInt(ia) + if err != nil { + return "", err + } + if i == "" { + return ia + midpoint(fa, ""), nil + } + return i, nil + } + + ia, err := getIntPart(a) + if err != nil { + return "", err + } + fa := a[len(ia):] + ib, err := getIntPart(b) + if err != nil { + return "", err + } + fb := b[len(ib):] + if ia == ib { + return ia + midpoint(fa, fb), nil + } + i, err := incrementInt(ia) + if err != nil { + return "", err + } + if i == "" { + return "", errors.New("range overflow") + } + if i < b { + return i, nil + } + return ia + midpoint(fa, ""), nil +} + +// `a < b` lexicographically if `b` is non-empty. +// a == "" means first possible string. +// b == "" means last possible string. +func midpoint(a, b string) string { + if b != "" { + // remove longest common prefix. pad `a` with 0s as we + // go. note that we don't need to pad `b`, because it can't + // end before `a` while traversing the common prefix. + i := 0 + for ; i < len(b); i++ { + c := byte('0') + if len(a) > i { + c = a[i] + } + if c != b[i] { + break + } + } + if i > 0 { + if i > len(a) { + return b[0:i] + midpoint("", b[i:]) + } + return b[0:i] + midpoint(a[i:], b[i:]) + } + } + + // first digits (or lack of digit) are different + digitA := 0 + if a != "" { + digitA = strings.Index(base62Digits, string(a[0])) + } + digitB := len(base62Digits) + if b != "" { + digitB = strings.Index(base62Digits, string(b[0])) + } + if digitB-digitA > 1 { + midDigit := int(math.Round(0.5 * float64(digitA+digitB))) + return string(base62Digits[midDigit]) + } + + // first digits are consecutive + if len(b) > 1 { + return b[0:1] + } + + // `b` is empty or has length 1 (a single digit). + // the first digit of `a` is the previous digit to `b`, + // or 9 if `b` is null. + // given, for example, midpoint('49', '5'), return + // '4' + midpoint('9', null), which will become + // '4' + '9' + midpoint('', null), which is '495' + sa := "" + if len(a) > 0 { + sa = a[1:] + } + return string(base62Digits[digitA]) + midpoint(sa, "") +} + +func validateInt(i string) error { + exp, err := getIntLen(i[0]) + if err != nil { + return err + } + if len(i) != exp { + return fmt.Errorf("invalid integer part of order key: %s", i) + } + return nil +} + +func getIntLen(head byte) (int, error) { + if head >= 'a' && head <= 'z' { + return int(head - 'a' + 2), nil + } else if head >= 'A' && head <= 'Z' { + return int('Z' - head + 2), nil + } else { + return 0, fmt.Errorf("invalid order key head: %s", string(head)) + } +} + +func getIntPart(key string) (string, error) { + intPartLen, err := getIntLen(key[0]) + if err != nil { + return "", err + } + if intPartLen > len(key) { + return "", fmt.Errorf("invalid order key: %s", key) + } + return key[0:intPartLen], nil +} + +func validateOrderKey(key string) error { + if key == smallestInt { + return fmt.Errorf("invalid order key: %s", key) + } + // getIntPart will return error if the first character is bad, + // or the key is too short. we'd call it to check these things + // even if we didn't need the result + i, err := getIntPart(key) + if err != nil { + return err + } + f := key[len(i):] + if strings.HasSuffix(f, "0") { + return fmt.Errorf("invalid order key: %s", key) + } + return nil +} + +// returns error if x is invalid, or if range is exceeded. +func incrementInt(x string) (string, error) { + err := validateInt(x) + if err != nil { + return "", err + } + digs := strings.Split(x, "") + head := digs[0] + digs = digs[1:] + carry := true + for i := len(digs) - 1; carry && i >= 0; i-- { + d := strings.Index(base62Digits, digs[i]) + 1 + if d == len(base62Digits) { + digs[i] = "0" + } else { + digs[i] = string(base62Digits[d]) + carry = false + } + } + if carry { + if head == "Z" { + return "a0", nil + } + if head == "z" { + return "", nil + } + h := string(head[0] + 1) + if h > "a" { + digs = append(digs, "0") + } else { + digs = digs[1:] + } + return h + strings.Join(digs, ""), nil + } + return head + strings.Join(digs, ""), nil +} + +func decrementInt(x string) (string, error) { + err := validateInt(x) + if err != nil { + return "", err + } + digs := strings.Split(x, "") + head := digs[0] + digs = digs[1:] + borrow := true + for i := len(digs) - 1; borrow && i >= 0; i-- { + d := strings.Index(base62Digits, digs[i]) - 1 + if d == -1 { + digs[i] = string(base62Digits[len(base62Digits)-1]) + } else { + digs[i] = string(base62Digits[d]) + borrow = false + } + } + + if borrow { + if head == "a" { + return "Z" + string(base62Digits[len(base62Digits)-1]), nil + } + if head == "A" { + return "", nil + } + h := head[0] - 1 + if h < 'Z' { + digs = append(digs, string(base62Digits[len(base62Digits)-1])) + } else { + digs = digs[1:] + } + return string(h) + strings.Join(digs, ""), nil + } + + return head + strings.Join(digs, ""), nil +} + +// Float64Approx converts a key as generated by KeyBetween() to a float64. +// Because the range of keys is far larger than float64 can represent +// accurately, this is necessarily approximate. But for many use cases it should +// be, as they say, close enough for jazz. +func Float64Approx(key string) (float64, error) { + if key == "" { + return 0.0, errors.New("invalid order key") + } + + err := validateOrderKey(key) + if err != nil { + return 0.0, err + } + + ip, err := getIntPart(key) + if err != nil { + return 0.0, err + } + + digs := strings.Split(ip, "") + head := digs[0] + digs = digs[1:] + rv := float64(0) + for i := 0; i < len(digs); i++ { + d := digs[len(digs)-i-1] + p := strings.Index(base62Digits, d) + if p == -1 { + return 0.0, fmt.Errorf("invalid order key: %s", key) + } + rv += math.Pow(float64(len(base62Digits)), float64(i)) * float64(p) + } + + fp := key[len(ip):] + for i, d := range fp { + p := strings.Index(base62Digits, string(d)) + if p == -1 { + return 0.0, fmt.Errorf("invalid key: %s", key) + } + rv += (float64(p) / math.Pow(float64(len(base62Digits)), float64(i+1))) + } + + if head < "a" { + rv *= -1 + } + + return rv, nil +} + +// NKeysBetween returns n keys between a and b that sorts lexicographically. +// Either a or b can be empty strings. If a is empty it indicates smallest key, +// If b is empty it indicates largest key. +// b must be empty string or > a. +func NKeysBetween(a, b string, n uint) ([]string, error) { + if n == 0 { + return []string{}, nil + } + if n == 1 { + c, err := KeyBetween(a, b) + if err != nil { + return nil, err + } + return []string{c}, nil + } + if b == "" { + c, err := KeyBetween(a, b) + if err != nil { + return nil, err + } + result := make([]string, 0, n) + result = append(result, c) + for i := 0; i < int(n)-1; i++ { + c, err = KeyBetween(c, b) + if err != nil { + return nil, err + } + result = append(result, c) + } + return result, nil + } + if a == "" { + c, err := KeyBetween(a, b) + if err != nil { + return nil, err + } + result := make([]string, 0, n) + result = append(result, c) + for i := 0; i < int(n)-1; i++ { + c, err = KeyBetween(a, c) + if err != nil { + return nil, err + } + result = append(result, c) + } + reverse(result) + return result, nil + } + mid := n / 2 + c, err := KeyBetween(a, b) + if err != nil { + return nil, err + } + result := make([]string, 0, n) + { + r, err := NKeysBetween(a, c, mid) + if err != nil { + return nil, err + } + result = append(result, r...) + } + result = append(result, c) + { + r, err := NKeysBetween(c, b, n-mid-1) + if err != nil { + return nil, err + } + result = append(result, r...) + } + return result, nil +} + +func reverse(values []string) { + for i := 0; i < len(values)/2; i++ { + j := len(values) - i - 1 + values[i], values[j] = values[j], values[i] + } +} diff --git a/fracdex/fracdex_test.go b/fracdex/fracdex_test.go new file mode 100644 index 0000000..8417c44 --- /dev/null +++ b/fracdex/fracdex_test.go @@ -0,0 +1,118 @@ +// Licensed under CC0-1.0 Universial by https://github.com/rocicorp/fracdex +package fracdex + +import ( + "math" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeys(t *testing.T) { + assert := assert.New(t) + + test := func(a, b, exp string) { + act, err := KeyBetween(a, b) + if err != nil { + assert.Equal("", act) + assert.Equal(exp, err.Error()) + } else { + assert.Nil(err) + assert.Equal(exp, act) + } + } + + test("", "", "a0") + test("", "a0", "Zz") + test("", "Zz", "Zy") + test("a0", "", "a1") + test("a1", "", "a2") + test("a0", "a1", "a0V") + test("a1", "a2", "a1V") + test("a0V", "a1", "a0l") + test("Zz", "a0", "ZzV") + test("Zz", "a1", "a0") + test("", "Y00", "Xzzz") + test("bzz", "", "c000") + test("a0", "a0V", "a0G") + test("a0", "a0G", "a08") + test("b125", "b129", "b127") + test("a0", "a1V", "a1") + test("Zz", "a01", "a0") + test("", "a0V", "a0") + test("", "b999", "b99") + test("aV", "aV0V", "aV0G") + test( + "", + "A00000000000000000000000000", + "invalid order key: A00000000000000000000000000", + ) + test("", "A000000000000000000000000001", "A000000000000000000000000000V") + test("zzzzzzzzzzzzzzzzzzzzzzzzzzy", "", "zzzzzzzzzzzzzzzzzzzzzzzzzzz") + test("zzzzzzzzzzzzzzzzzzzzzzzzzzz", "", "zzzzzzzzzzzzzzzzzzzzzzzzzzzV") + test("a00", "", "invalid order key: a00") + test("a00", "a1", "invalid order key: a00") + test("0", "1", "invalid order key head: 0") + test("a1", "a0", "a1 >= a0") +} + +func TestNKeys(t *testing.T) { + assert := assert.New(t) + + test := func(a, b string, n uint, exp string) { + actSlice, err := NKeysBetween(a, b, n) + act := strings.Join(actSlice, " ") + if err != nil { + assert.Equal("", act) + assert.Equal(exp, err.Error()) + } else { + assert.Nil(err) + assert.Equal(exp, act) + } + } + test("", "", 5, "a0 a1 a2 a3 a4") + test("a4", "", 10, "a5 a6 a7 a8 a9 aA aB aC aD aE") + test("", "a0", 5, "Zv Zw Zx Zy Zz") + test( + "a0", + "a2", + 20, + "a04 a08 a0G a0K a0O a0V a0Z a0d a0l a0t a1 a14 a18 a1G a1O a1V a1Z a1d a1l a1t", + ) +} + +func TestToFloat64Approx(t *testing.T) { + assert := assert.New(t) + + test := func(key string, exp float64, expErr string) { + act, err := Float64Approx(key) + if expErr != "" { + assert.Equal(0.0, act) + assert.Equal(expErr, err.Error()) + } else { + assert.Equal(exp, act) + assert.NoError(err) + } + } + + test("a0", 0.0, "") + test("a1", 1.0, "") + test("az", 61.0, "") + test("b10", 62.0, "") + test("z20000000000000000000000000", math.Pow(62.0, 25.0)*2.0, "") + test("Z1", -1.0, "") + test("Zz", -61.0, "") + test("Y10", -62.0, "") + test("A20000000000000000000000000", math.Pow(62.0, 25.0)*-2.0, "") + + test("a0V", 0.5, "") + test("a00V", 31.0/math.Pow(62.0, 2.0), "") + test("aVV", 31.5, "") + test("ZVV", -31.5, "") + + test("", 0.0, "invalid order key") + test("!", 0.0, "invalid order key head: !") + test("a400", 0.0, "invalid order key: a400") + test("a!", 0.0, "invalid order key: a!") +}