Cleanup, docs and tests.

This commit is contained in:
Qi Xiao 2021-04-05 01:50:22 +01:00
parent da67ba8a4a
commit 5c643181a4
12 changed files with 480 additions and 175 deletions

View File

@ -142,6 +142,8 @@ func makeMap(input Inputs) (vals.Map, error) {
// Output `$low`, `$low` + `$step`, ..., proceeding as long as smaller than
// `$high`. If not given, `$low` defaults to 0.
//
// This command is [exactness-preserving](#exactness-preserving).
//
// Examples:
//
// ```elvish-transcript
@ -156,19 +158,24 @@ func makeMap(input Inputs) (vals.Map, error) {
// ▶ 5
// ```
//
// Beware floating point oddities:
// When using floating-point numbers, beware that computation errors can result
// in an unexpected number of outputs:
//
// ```elvish-transcript
// ~> range 0 0.8 &step=.1
// ▶ 0
// ▶ 0.1
// ▶ 0.2
// ▶ 0.30000000000000004
// ▶ 0.4
// ▶ 0.5
// ▶ 0.6
// ▶ 0.7
// ▶ 0.7999999999999999
// ~> range 0.9 &step=0.3
// ▶ (num 0.0)
// ▶ (num 0.3)
// ▶ (num 0.6)
// ▶ (num 0.8999999999999999)
// ```
//
// Using exact rationals can avoid this problem:
//
// ```elvish-transcript
// ~> range 9/10 &step=3/10
// ▶ (num 0)
// ▶ (num 3/10)
// ▶ (num 3/5)
// ```
//
// Etymology:

View File

@ -2,6 +2,7 @@ package eval_test
import (
"math"
"math/big"
"testing"
. "src.elv.sh/pkg/eval"
@ -48,6 +49,22 @@ func TestRange(t *testing.T) {
That(`range 3`).Puts(0, 1, 2),
That(`range 1 3`).Puts(1, 2),
That(`range 0 10 &step=3`).Puts(0, 3, 6, 9),
That(`range 10_000_000_000_000_000_000 10_000_000_000_000_000_003`).
Puts(
vals.ParseNum("10_000_000_000_000_000_000"),
vals.ParseNum("10_000_000_000_000_000_001"),
vals.ParseNum("10_000_000_000_000_000_002")),
That(`range 10_000_000_000_000_000_000 10_000_000_000_000_000_003 &step=2`).
Puts(
vals.ParseNum("10_000_000_000_000_000_000"),
vals.ParseNum("10_000_000_000_000_000_002")),
That(`range 23/10`).Puts(0, 1, 2),
That(`range 1/10 23/10`).Puts(
big.NewRat(1, 10), big.NewRat(11, 10), big.NewRat(21, 10)),
That(`range 1/10 9/10 &step=3/10`).Puts(
big.NewRat(1, 10), big.NewRat(4, 10), big.NewRat(7, 10)),
)
}

View File

@ -129,9 +129,9 @@ func TestPrintf(t *testing.T) {
That(`printf '%t' $true`).Prints("true"),
That(`printf '%t' $nil`).Prints("false"),
That(`printf '%3d' (float64 5)`).Prints(" 5"),
That(`printf '%3d' (num 5)`).Prints(" 5"),
That(`printf '%3d' 5`).Prints(" 5"),
That(`printf '%08b' (float64 5)`).Prints("00000101"),
That(`printf '%08b' (num 5)`).Prints("00000101"),
That(`printf '%08b' 5`).Prints("00000101"),
That(`printf '%.1f' 3.1415`).Prints("3.1"),

View File

@ -148,110 +148,71 @@ func toFloat64(f float64) float64 {
// ```
func lt(nums ...vals.Num) bool {
return chainCompare(nums, func(pair vals.NumSlice) bool {
switch pair := pair.(type) {
case []int:
return pair[0] < pair[1]
case []*big.Int:
return pair[0].Cmp(pair[1]) < 0
case []*big.Rat:
return pair[0].Cmp(pair[1]) < 0
case []float64:
return pair[0] < pair[1]
default:
panic("unreachable")
}
})
return chainCompare(nums,
func(a, b int) bool { return a < b },
func(a, b *big.Int) bool { return a.Cmp(b) < 0 },
func(a, b *big.Rat) bool { return a.Cmp(b) < 0 },
func(a, b float64) bool { return a < b })
}
func le(nums ...vals.Num) bool {
return chainCompare(nums, func(pair vals.NumSlice) bool {
switch pair := pair.(type) {
case []int:
return pair[0] <= pair[1]
case []*big.Int:
return pair[0].Cmp(pair[1]) <= 0
case []*big.Rat:
return pair[0].Cmp(pair[1]) <= 0
case []float64:
return pair[0] <= pair[1]
default:
panic("unreachable")
}
})
return chainCompare(nums,
func(a, b int) bool { return a <= b },
func(a, b *big.Int) bool { return a.Cmp(b) <= 0 },
func(a, b *big.Rat) bool { return a.Cmp(b) <= 0 },
func(a, b float64) bool { return a <= b })
}
func eqNum(nums ...vals.Num) bool {
return chainCompare(nums, func(pair vals.NumSlice) bool {
switch pair := pair.(type) {
case []int:
return pair[0] == pair[1]
case []*big.Int:
return pair[0].Cmp(pair[1]) == 0
case []*big.Rat:
return pair[0].Cmp(pair[1]) == 0
case []float64:
return pair[0] == pair[1]
default:
panic("unreachable")
}
})
return chainCompare(nums,
func(a, b int) bool { return a == b },
func(a, b *big.Int) bool { return a.Cmp(b) == 0 },
func(a, b *big.Rat) bool { return a.Cmp(b) == 0 },
func(a, b float64) bool { return a == b })
}
func ne(nums ...vals.Num) bool {
return chainCompare(nums, func(pair vals.NumSlice) bool {
switch pair := pair.(type) {
case []int:
return pair[0] != pair[1]
case []*big.Int:
return pair[0].Cmp(pair[1]) != 0
case []*big.Rat:
return pair[0].Cmp(pair[1]) != 0
case []float64:
return pair[0] != pair[1]
default:
panic("unreachable")
}
})
return chainCompare(nums,
func(a, b int) bool { return a != b },
func(a, b *big.Int) bool { return a.Cmp(b) != 0 },
func(a, b *big.Rat) bool { return a.Cmp(b) != 0 },
func(a, b float64) bool { return a != b })
}
func gt(nums ...vals.Num) bool {
return chainCompare(nums, func(pair vals.NumSlice) bool {
switch pair := pair.(type) {
case []int:
return pair[0] > pair[1]
case []*big.Int:
return pair[0].Cmp(pair[1]) > 0
case []*big.Rat:
return pair[0].Cmp(pair[1]) > 0
case []float64:
return pair[0] > pair[1]
default:
panic("unreachable")
}
})
return chainCompare(nums,
func(a, b int) bool { return a > b },
func(a, b *big.Int) bool { return a.Cmp(b) > 0 },
func(a, b *big.Rat) bool { return a.Cmp(b) > 0 },
func(a, b float64) bool { return a > b })
}
func ge(nums ...vals.Num) bool {
return chainCompare(nums, func(pair vals.NumSlice) bool {
switch pair := pair.(type) {
case []int:
return pair[0] >= pair[1]
case []*big.Int:
return pair[0].Cmp(pair[1]) >= 0
case []*big.Rat:
return pair[0].Cmp(pair[1]) >= 0
case []float64:
return pair[0] >= pair[1]
default:
panic("unreachable")
}
})
return chainCompare(nums,
func(a, b int) bool { return a >= b },
func(a, b *big.Int) bool { return a.Cmp(b) >= 0 },
func(a, b *big.Rat) bool { return a.Cmp(b) >= 0 },
func(a, b float64) bool { return a >= b })
}
func chainCompare(nums []vals.Num, p func(pair vals.NumSlice) bool) bool {
func chainCompare(nums []vals.Num,
p1 func(a, b int) bool, p2 func(a, b *big.Int) bool,
p3 func(a, b *big.Rat) bool, p4 func(a, b float64) bool) bool {
for i := 0; i < len(nums)-1; i++ {
if !p(vals.UnifyNums(nums[i:i+2], 0)) {
var r bool
switch pair := vals.UnifyNums(nums[i:i+2], 0).(type) {
case []int:
r = p1(pair[0], pair[1])
case []*big.Int:
r = p2(pair[0], pair[1])
case []*big.Rat:
r = p3(pair[0], pair[1])
case []float64:
r = p4(pair[0], pair[1])
}
if !r {
return false
}
}
@ -287,13 +248,13 @@ func add(rawNums ...vals.Num) vals.Num {
for _, num := range nums {
acc.Add(acc, num)
}
return vals.NormalizeNum(acc)
return vals.NormalizeBigInt(acc)
case []*big.Rat:
acc := big.NewRat(0, 1)
for _, num := range nums {
acc.Add(acc, num)
}
return vals.NormalizeNum(acc)
return vals.NormalizeBigRat(acc)
case []float64:
acc := float64(0)
for _, num := range nums {
@ -426,13 +387,13 @@ func mul(rawNums ...vals.Num) vals.Num {
for _, num := range nums {
acc.Mul(acc, num)
}
return vals.NormalizeNum(acc)
return vals.NormalizeBigInt(acc)
case []*big.Rat:
acc := big.NewRat(1, 1)
for _, num := range nums {
acc.Mul(acc, num)
}
return vals.NormalizeNum(acc)
return vals.NormalizeBigRat(acc)
case []float64:
acc := float64(1)
for _, num := range nums {

View File

@ -1,6 +1,9 @@
package eval_test
import (
"math"
"math/big"
"strings"
"testing"
. "src.elv.sh/pkg/eval"
@ -9,6 +12,28 @@ import (
. "src.elv.sh/pkg/eval/evaltest"
)
const (
zeros = "0000000000000000000"
// Values that exceed the range of int64, used for testing BigInt.
z = "1" + zeros + "0"
z1 = "1" + zeros + "1" // z+1
z2 = "1" + zeros + "2" // z+2
z3 = "1" + zeros + "3" // z+3
zz = "2" + zeros + "0" // 2z
zz1 = "2" + zeros + "1" // 2z+1
zz2 = "2" + zeros + "2" // 2z+2
zz3 = "2" + zeros + "3" // 2z+3
)
func TestNum(t *testing.T) {
Test(t,
That("num 1").Puts(1),
That("num "+z).Puts(bigInt(z)),
That("num 1/2").Puts(big.NewRat(1, 2)),
That("num (num 1)").Puts(1),
)
}
func TestFloat64(t *testing.T) {
Test(t,
That("float64 1").Puts(1.0),
@ -16,32 +41,166 @@ func TestFloat64(t *testing.T) {
)
}
func TestNumberComparisonCommands(t *testing.T) {
func TestNumCmp(t *testing.T) {
Test(t,
// FixInt
That("< 1 2 3").Puts(true),
That("< 1 3 2").Puts(false),
// BigInt
That("< "+args(z1, z2, z3)).Puts(true),
That("< "+args(z1, z3, z2)).Puts(false),
// BigInt and FixInt
That("< "+args("1", z1)).Puts(true),
// BigRat
That("< 1/4 1/3 1/2").Puts(true),
That("< 1/4 1/2 1/3").Puts(false),
// BigRat, BigInt and FixInt
That("< "+args("1/2", "1", z1)).Puts(true),
That("< "+args("1/2", z1, "1")).Puts(false),
// Float
That("< 1.0 2.0 3.0").Puts(true),
That("< 1.0 3.0 2.0").Puts(false),
// Float, BigRat and FixInt
That("< 1.0 3/2 2").Puts(true),
That("< 1.0 2 3/2").Puts(false),
// Mixing of types not tested for commands below; they share the same
// code path as <.
// FixInt
That("<= 1 1 2").Puts(true),
That("<= 1 2 1").Puts(false),
// BigInt
That("<= "+args(z1, z1, z2)).Puts(true),
That("<= "+args(z1, z2, z1)).Puts(false),
// BigRat
That("<= 1/3 1/3 1/2").Puts(true),
That("<= 1/3 1/2 1/1").Puts(true),
// Float
That("<= 1.0 1.0 2.0").Puts(true),
That("<= 1.0 2.0 1.0").Puts(false),
// FixInt
That("== 1 1 1").Puts(true),
That("== 1 2 1").Puts(false),
// BigInt
That("== "+args(z1, z1, z1)).Puts(true),
That("== "+args(z1, z2, z1)).Puts(false),
// BigRat
That("== 1/2 1/2 1/2").Puts(true),
That("== 1/2 1/3 1/2").Puts(false),
// Float
That("== 1.0 1.0 1.0").Puts(true),
That("== 1.0 2.0 1.0").Puts(false),
// FixInt
That("!= 1 2 1").Puts(true),
That("!= 1 1 2").Puts(false),
// BigInt
That("!= "+args(z1, z2, z1)).Puts(true),
That("!= "+args(z1, z1, z2)).Puts(false),
// BigRat
That("!= 1/2 1/3 1/2").Puts(true),
That("!= 1/2 1/2 1/3").Puts(false),
// Float
That("!= 1.0 2.0 1.0").Puts(true),
That("!= 1.0 1.0 2.0").Puts(false),
// FixInt
That("> 3 2 1").Puts(true),
That("> 3 1 2").Puts(false),
// BigInt
That("> "+args(z3, z2, z1)).Puts(true),
That("> "+args(z3, z1, z2)).Puts(false),
// BigRat
That("> 1/2 1/3 1/4").Puts(true),
That("> 1/2 1/4 1/3").Puts(false),
// Float
That("> 3.0 2.0 1.0").Puts(true),
That("> 3.0 1.0 2.0").Puts(false),
// FixInt
That(">= 3 3 2").Puts(true),
That(">= 3 2 3").Puts(false),
// BigInt
That(">= "+args(z3, z3, z2)).Puts(true),
That(">= "+args(z3, z2, z3)).Puts(false),
// BigRat
That(">= 1/2 1/2 1/3").Puts(true),
That(">= 1/2 1/3 1/2").Puts(false),
// Float
That(">= 3.0 3.0 2.0").Puts(true),
That(">= 3.0 2.0 3.0").Puts(false),
)
}
func TestArithmeticCommands(t *testing.T) {
Test(t,
// TODO test more edge cases
// No argument
That("+").Puts(0),
// FixInt
That("+ 233100 233").Puts(233333),
That("- 233333 233100").Puts(233),
// BigInt
That("+ "+args(z, z1)).Puts(bigInt(zz1)),
// BigInt and FixInt
That("+ 1 2 "+z).Puts(bigInt(z3)),
// BigRat
That("+ 1/2 1/3 1/4").Puts(big.NewRat(13, 12)),
// BigRat, BigInt and FixInt
That("+ 1/2 1/2 1 "+z).Puts(bigInt(z2)),
// Float
That("+ 0.5 0.25 1.0").Puts(1.75),
// Float and other types
That("+ 0.5 1/4 1").Puts(1.75),
// Mixing of types not tested for commands below; they share the same
// code path as +.
// One argument - negation
That("- 233").Puts(-233),
That("* 353 661").Puts(233333),
That("- "+z).Puts(bigInt("-"+z)),
That("- 1/2").Puts(big.NewRat(-1, 2)),
That("- 1.0").Puts(-1.0),
// FixInt
That("- 20 10 2").Puts(8),
// BigInt
That("- "+args(zz3, z1)).Puts(bigInt(z2)),
// Float
That("- 2.0 1.0 0.5").Puts(0.5),
// No argument
That("*").Puts(1),
// FixInt
That("* 2 7 4").Puts(56),
// BigInt
That("* 2 "+z1).Puts(bigInt(zz2)),
// Float
That("* 2.0 0.5 1.75").Puts(1.75),
// 0 * non-infinity
That("* 0 1/2 1.0").Puts(0),
// 0 * infinity
That("* 0 +Inf").Puts(math.NaN()),
// One argument - inversion
That("/ 2").Puts(big.NewRat(1, 2)),
That("/ "+z).Puts(bigRat("1/"+z)),
That("/ 2.0").Puts(0.5),
// FixInt
That("/ 233333 353").Puts(661),
That("/ 3 4 2").Puts(big.NewRat(3, 8)),
// BigInt
That("/ "+args(zz, z)).Puts(2),
That("/ "+args(zz, "2")).Puts(bigInt(z)),
That("/ "+args(z1, z)).Puts(bigRat(z1+"/"+z)),
// Float
That("/ 1.0 2.0 4.0").Puts(0.125),
// 0 / non-zero
That("/ 0 1/2 0.1").Puts(0),
// anything / 0
That("/ 0 0").Throws(ErrDivideByZero, "/ 0 0"),
That("/ 1 0").Throws(ErrDivideByZero, "/ 1 0"),
That("/ 1.0 0").Throws(ErrDivideByZero, "/ 1.0 0"),
That("% 23 7").Puts(2),
That("% 1 0").Throws(ErrDivideByZero, "% 1 0"),
)
@ -57,3 +216,23 @@ func TestRandint(t *testing.T) {
That("randint 1 2 3").Throws(ErrorWithType(errs.ArityMismatch{}), "randint 1 2 3"),
)
}
func bigInt(s string) *big.Int {
z, ok := new(big.Int).SetString(s, 0)
if !ok {
panic("cannot parse as big int: " + s)
}
return z
}
func bigRat(s string) *big.Rat {
z, ok := new(big.Rat).SetString(s)
if !ok {
panic("cannot parse as big rat: " + s)
}
return z
}
func args(s ...string) string {
return strings.Join(s, " ")
}

View File

@ -2,6 +2,7 @@ package vals
import (
"errors"
"math/big"
"testing"
. "src.elv.sh/pkg/tt"
@ -33,8 +34,16 @@ func (rconcatter) RConcat(lhs interface{}) (interface{}, error) {
func TestConcat(t *testing.T) {
Test(t, Fn("Concat", Concat), Table{
Args("foo", "bar").Rets("foobar", nil),
// string+number
Args("foo", 2).Rets("foo2", nil),
Args("foo", bigInt(z)).Rets("foo"+z, nil),
Args("foo", big.NewRat(1, 2)).Rets("foo1/2", nil),
Args("foo", 2.0).Rets("foo2.0", nil),
// number+string
Args(2, "foo").Rets("2foo", nil),
Args(bigInt(z), "foo").Rets(z+"foo", nil),
Args(big.NewRat(1, 2), "foo").Rets("1/2foo", nil),
Args(2.0, "foo").Rets("2.0foo", nil),
// LHS implements Concatter and succeeds
Args(concatter{}, "bar").Rets("concatter bar", nil),

View File

@ -86,8 +86,6 @@ func ScanToGo(src interface{}, ptr interface{}) error {
*ptr = r
}
return err
case Scanner:
return ptr.ScanElvish(src)
default:
// Do a generic `*ptr = src` via reflection
ptrType := TypeOf(ptr)
@ -103,11 +101,6 @@ func ScanToGo(src interface{}, ptr interface{}) error {
}
}
// Scanner is implemented by types that can scan an Elvish value into itself.
type Scanner interface {
ScanElvish(interface{}) error
}
// FromGo converts a Go value to an Elvish value. Most types are returned as
// is, but exact numerical types are normalized to one of int, *big.Int and
// *big.Rat, using the small representation that can hold the value, and runes
@ -155,12 +148,8 @@ func elvToFloat(arg interface{}) (float64, error) {
func elvToInt(arg interface{}) (int, error) {
switch arg := arg.(type) {
case float64:
i := int(arg)
if float64(i) != arg {
return 0, errMustBeInteger
}
return i, nil
case int:
return arg, nil
case string:
num, err := strconv.ParseInt(arg, 0, 0)
if err == nil {
@ -172,6 +161,21 @@ func elvToInt(arg interface{}) (int, error) {
}
}
func elvToNum(arg interface{}) (Num, error) {
switch arg := arg.(type) {
case int, *big.Int, *big.Rat, float64:
return arg, nil
case string:
n := ParseNum(arg)
if n == nil {
return 0, cannotParseAs{"number", Repr(arg, -1)}
}
return n, nil
default:
return 0, errMustBeNumber
}
}
func elvToRune(arg interface{}) (rune, error) {
ss, ok := arg.(string)
if !ok {
@ -187,18 +191,3 @@ func elvToRune(arg interface{}) (rune, error) {
}
return r, nil
}
func elvToNum(arg interface{}) (Num, error) {
switch arg := arg.(type) {
case int, *big.Int, *big.Rat, float64:
return arg, nil
case string:
n := ParseNum(arg)
if n == nil {
return 0, cannotParseAs{"number", Repr(arg, -1)}
}
return n, nil
default:
return 0, errMustBeNumber
}
}

View File

@ -1,6 +1,7 @@
package vals
import (
"math/big"
"reflect"
"testing"
@ -22,33 +23,70 @@ func scanToGo2(src interface{}, dstInit interface{}) (interface{}, error) {
func TestScanToGo(t *testing.T) {
Test(t, Fn("ScanToGo", scanToGo2), Table{
// int
Args("12", 0).Rets(12),
Args("0x12", 0).Rets(0x12),
Args(12.0, 0).Rets(12),
Args("23", 0.0).Rets(23.0),
Args("0x23", 0.0).Rets(float64(0x23)),
Args("x", ' ').Rets('x'),
Args("foo", "").Rets("foo"),
Args(someType{"foo"}, someType{}).Rets(someType{"foo"}),
Args(nil, nil).Rets(nil),
Args(12.0, 0).Rets(0, errMustBeInteger),
Args(0.5, 0).Rets(0, errMustBeInteger),
Args("x", someType{}).Rets(Any, wrongType{"!!vals.someType", "string"}),
Args(someType{}, 0).Rets(Any, errMustBeInteger),
Args("x", 0).Rets(Any, cannotParseAs{"integer", "x"}),
// float64
Args("23", 0.0).Rets(23.0),
Args("0x23", 0.0).Rets(float64(0x23)),
Args(someType{}, 0.0).Rets(Any, errMustBeNumber),
Args("x", 0.0).Rets(Any, cannotParseAs{"number", "x"}),
// Num is tested below
// rune
Args("x", ' ').Rets('x'),
Args(someType{}, ' ').Rets(Any, errMustBeString),
Args("\xc3\x28", ' ').Rets(Any, errMustBeValidUTF8), // Invalid UTF8
Args("ab", ' ').Rets(Any, errMustHaveSingleRune),
// Other types don't undergo any conversion, as long as the types match
Args("foo", "").Rets("foo"),
Args(someType{"foo"}, someType{}).Rets(someType{"foo"}),
Args(nil, nil).Rets(nil),
Args("x", someType{}).Rets(Any, wrongType{"!!vals.someType", "string"}),
})
}
func scanToGoNum(src interface{}) (Num, error) {
var n Num
err := ScanToGo(src, &n)
return n, err
}
func TestScanToGoNum(t *testing.T) {
Test(t, Fn("ScanToGo", scanToGoNum), Table{
// Strings are automatically converted
Args("12").Rets(12),
Args(z).Rets(bigInt(z)),
Args("1/2").Rets(big.NewRat(1, 2)),
Args("12.0").Rets(12.0),
// Already numbers
Args(12).Rets(12),
Args(bigInt(z)).Rets(bigInt(z)),
Args(big.NewRat(1, 2)).Rets(big.NewRat(1, 2)),
Args(12.0).Rets(12.0),
})
}
func TestFromGo(t *testing.T) {
Test(t, Fn("FromGo", FromGo), Table{
Args(12).Rets(12),
Args(1.5).Rets(1.5),
// BigInt -> int, when in range
Args(bigInt(z)).Rets(bigInt(z)),
Args(big.NewInt(100)).Rets(100),
// BigRat -> BigInt or int, when denominator is 1
Args(bigRat(z1 + "/" + z)).Rets(bigRat(z1 + "/" + z)),
Args(bigRat(z + "/1")).Rets(bigInt(z)),
Args(bigRat("2/1")).Rets(2),
// rune -> string
Args('x').Rets("x"),
// Other types don't undergo any conversion
Args(nil).Rets(nil),
Args(someType{"foo"}).Rets(someType{"foo"}),
})

View File

@ -8,27 +8,27 @@ import (
"strings"
)
// Num is a stand-in type for int, *big.Int, *big.Rat or float64. This type
// doesn't offer type safety, but is useful as a marker.
type Num interface{}
// NumSlice is a stand-in type for []int, []*big.Int, []*big.Rat or []float64.
// This type doesn't offer type safety, but is useful as a marker.
type NumSlice interface{}
// ParseNum parses a string into a suitable number type. If the string does not
// represent a valid number, it returns nil.
func ParseNum(s string) Num {
b := []byte(s)
if strings.ContainsRune(s, '/') {
// Parse as big.Rat
var r big.Rat
if r.UnmarshalText(b) == nil {
return &r
if z, ok := new(big.Rat).SetString(s); ok {
return NormalizeBigRat(z)
}
return nil
}
// Try parsing as big.Int
z := &big.Int{}
if z.UnmarshalText(b) == nil {
if i, ok := getFixInt(z); ok {
return i
}
return z
if z, ok := new(big.Int).SetString(s, 0); ok {
return NormalizeBigInt(z)
}
// Try parsing as float64
if f, err := strconv.ParseFloat(s, 64); err == nil {
@ -37,9 +37,11 @@ func ParseNum(s string) Num {
return nil
}
// NumType represents a number type.
type NumType uint8
// Precedence used for unifying number types.
// Possible values for NumType, sorted in the order of implicit conversion
// (lower types can be implicitly converted to higher types).
const (
FixInt NumType = iota
BigInt
@ -47,6 +49,9 @@ const (
Float64
)
// UnifyNums unifies the given slice of numbers into the same type, converting
// those with lower NumType to the higest NumType present in the slice. The typ
// argument can be used to force the minimum NumType.
func UnifyNums(nums []Num, typ NumType) NumSlice {
for _, num := range nums {
if t := getNumType(num); t > typ {
@ -133,28 +138,26 @@ func getNumType(n Num) NumType {
}
}
func NormalizeNum(n Num) Num {
switch n := n.(type) {
case int:
return n
case *big.Int:
// NormalizeBigInt converts a big.Int to an int if it is within the range of
// int. Otherwise it returns n as is.
func NormalizeBigInt(z *big.Int) Num {
if i, ok := getFixInt(z); ok {
return i
}
return z
}
// NormalizeBigRat converts a big.Rat to a big.Int (or an int if within the
// range) if its denominator is 1.
func NormalizeBigRat(z *big.Rat) Num {
if z.IsInt() {
n := z.Num()
if i, ok := getFixInt(n); ok {
return i
}
return n
case *big.Rat:
if n.IsInt() {
if i, ok := getFixInt(n.Num()); ok {
return i
}
return n
}
return n
case float64:
return n
default:
panic("invalid num type" + fmt.Sprintf("%T", n))
}
return z
}
func getFixInt(z *big.Int) (int, bool) {

89
pkg/eval/vals/num_test.go Normal file
View File

@ -0,0 +1,89 @@
package vals
import (
"math"
"math/big"
"testing"
. "src.elv.sh/pkg/tt"
)
// Test utilities.
const (
zeros = "0000000000000000000"
// Values that exceed the range of int64, used for testing BigInt.
z = "1" + zeros + "0"
z1 = "1" + zeros + "1" // z+1
z2 = "1" + zeros + "2" // z+2
z3 = "1" + zeros + "3" // z+3
zz = "2" + zeros + "0" // 2z
zz1 = "2" + zeros + "1" // 2z+1
zz2 = "2" + zeros + "2" // 2z+2
zz3 = "2" + zeros + "3" // 2z+3
)
func TestParseNum(t *testing.T) {
Test(t, Fn("ParseNum", ParseNum), Table{
Args("1").Rets(1),
Args(z).Rets(bigInt(z)),
Args("1/2").Rets(big.NewRat(1, 2)),
Args("2/1").Rets(2),
Args(z + "/1").Rets(bigInt(z)),
Args("1.0").Rets(1.0),
Args("1e-5").Rets(1e-5),
Args("x").Rets(nil),
Args("x/y").Rets(nil),
})
}
func TestUnifyNums(t *testing.T) {
Test(t, Fn("UnifyNums", UnifyNums), Table{
Args([]Num{1, 2, 3, 4}, FixInt).
Rets([]int{1, 2, 3, 4}),
Args([]Num{1, 2, 3, bigInt(z)}, FixInt).
Rets([]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3), bigInt(z)}),
Args([]Num{1, 2, 3, big.NewRat(1, 2)}, FixInt).
Rets([]*big.Rat{
big.NewRat(1, 1), big.NewRat(2, 1),
big.NewRat(3, 1), big.NewRat(1, 2)}),
Args([]Num{1, 2, bigInt(z), big.NewRat(1, 2)}, FixInt).
Rets([]*big.Rat{
big.NewRat(1, 1), big.NewRat(2, 1), bigRat(z), big.NewRat(1, 2)}),
Args([]Num{1, 2, 3, 4.0}, FixInt).
Rets([]float64{1, 2, 3, 4}),
Args([]Num{1, 2, big.NewRat(1, 2), 4.0}, FixInt).
Rets([]float64{1, 2, 0.5, 4}),
Args([]Num{1, 2, big.NewInt(3), 4.0}, FixInt).
Rets([]float64{1, 2, 3, 4}),
Args([]Num{1, 2, bigInt(z), 4.0}, FixInt).
Rets([]float64{1, 2, math.Inf(1), 4}),
Args([]Num{1, 2, 3, 4}, BigInt).
Rets([]*big.Int{
big.NewInt(1), big.NewInt(2), big.NewInt(3), big.NewInt(4)}),
})
}
func bigInt(s string) *big.Int {
z, ok := new(big.Int).SetString(s, 0)
if !ok {
panic("cannot parse as big int: " + s)
}
return z
}
func bigRat(s string) *big.Rat {
z, ok := new(big.Rat).SetString(s)
if !ok {
panic("cannot parse as big rat: " + s)
}
return z
}

View File

@ -2,6 +2,7 @@ package vals
import (
"fmt"
"math/big"
"os"
"testing"
@ -19,17 +20,27 @@ func repr(a interface{}) string { return Repr(a, NoPretty) }
func TestRepr(t *testing.T) {
Test(t, Fn("repr", repr), Table{
Args(nil).Rets("$nil"),
Args(false).Rets("$false"),
Args(true).Rets("$true"),
Args("foo").Rets("foo"),
Args(1).Rets("(num 1)"),
Args(bigInt(z)).Rets("(num " + z + ")"),
Args(big.NewRat(1, 2)).Rets("(num 1/2)"),
Args(1.0).Rets("(num 1.0)"),
Args(1e10).Rets("(num 10000000000.0)"),
Args(os.Stdin).Rets(
fmt.Sprintf("<file{%s %d}>", os.Stdin.Name(), os.Stdin.Fd())),
Args(EmptyList).Rets("[]"),
Args(MakeList("foo", "bar")).Rets("[foo bar]"),
Args(EmptyMap).Rets("[&]"),
Args(MakeMap("foo", "bar")).Rets("[&foo=bar]"),
Args(reprer{}).Rets("<reprer>"),
Args(nonReprer{}).Rets("<unknown {}>"),
})

View File

@ -12,9 +12,11 @@ func TestToString(t *testing.T) {
// string
Args("a").Rets("a"),
Args(1).Rets("1"),
// float64
Args(42.0).Rets("42.0"),
Args(0.1).Rets("0.1"),
Args(42.0).Rets("42.0"),
// Whole numbers with more than 14 digits and trailing 0 are printed in
// scientific notation.
Args(1e13).Rets("10000000000000.0"),