Change math:pow to produce exact results in certain cases; deprecate math:pow10.

This commit is contained in:
Qi Xiao 2021-06-10 01:46:59 +01:00
parent 59e4a7302a
commit af3292418f
5 changed files with 128 additions and 37 deletions

View File

@ -36,6 +36,8 @@ or compiled, even if it is not executed:
- The `prclose` and `pwclose` commands are deprecated. Use `file:close`
instead.
- The `math:pow10` command is deprecated. Use `math:pow 10 $exponent` instead.
# Notable bugfixes
- Iterating over certain list slices no longer crash Elvish

View File

@ -37,8 +37,8 @@ var fns = map[string]interface{}{
"log2": math.Log2,
"max": max,
"min": min,
"pow": math.Pow, // TODO: Make exactness-preserving for integer exponents
"pow10": math.Pow10, // TODO: Make exactness-preserving for integer exponents
"pow": pow,
"pow10": pow10,
"round": round,
"round-to-even": roundToEven,
"sin": math.Sin,
@ -565,16 +565,77 @@ func min(rawNums ...vals.Num) (vals.Num, error) {
// math:pow $base $exponent
// ```
//
// Output the result of raising `$base` to the power of `$exponent`. Examples:
// Outputs the result of raising `$base` to the power of `$exponent`.
//
// This function produces an exact result when `$base` is exact and `$exponent`
// is an exact integer. Otherwise it produces an inexact result.
//
// Examples:
//
// ```elvish-transcript
// ~> math:pow 3 2
// ▶ (float64 9)
// ▶ (num 9)
// ~> math:pow -2 2
// ▶ (float64 4)
// ▶ (num 4)
// ~> math:pow 1/2 3
// ▶ (num 1/8)
// ~> math:pow 1/2 -3
// ▶ (num 8)
// ~> math:pow 9 1/2
// ▶ (num 3.0)
// ~> math:pow 12 1.1
// ▶ (num 15.38506624784179)
// ```
//
// @cf math:pow10
func pow(base, exp vals.Num) vals.Num {
if isExact(base) && isExactInt(exp) {
// Produce exact result
switch exp {
case 0:
return 1
case 1:
return base
case -1:
return new(big.Rat).Inv(vals.PromoteToBigRat(base))
}
exp := vals.PromoteToBigInt(exp)
if isExactInt(base) && exp.Sign() > 0 {
base := vals.PromoteToBigInt(base)
return new(big.Int).Exp(base, exp, nil)
}
base := vals.PromoteToBigRat(base)
if exp.Sign() < 0 {
base = new(big.Rat).Inv(base)
exp = new(big.Int).Neg(exp)
}
return new(big.Rat).SetFrac(
new(big.Int).Exp(base.Num(), exp, nil),
new(big.Int).Exp(base.Denom(), exp, nil))
}
// Produce inexact result
basef := vals.ConvertToFloat64(base)
expf := vals.ConvertToFloat64(exp)
return math.Pow(basef, expf)
}
func isExact(n vals.Num) bool {
switch n.(type) {
case int, *big.Int, *big.Rat:
return true
default:
return false
}
}
func isExactInt(n vals.Num) bool {
switch n.(type) {
case int, *big.Int:
return true
default:
return false
}
}
//elvdoc:fn pow10
//
@ -582,19 +643,17 @@ func min(rawNums ...vals.Num) (vals.Num, error) {
// math:pow10 $exponent
// ```
//
// Output the result of raising ten to the power of `$exponent` which must be
// an integer. Note that `$exponent > 308` results in +Inf and `$exponent <
// -323` results in zero. Examples:
// Equivalent to `math:pow 10 $exponent`.
//
// ```elvish-transcript
// ~> math:pow10 2
// ▶ (float64 100)
// ~> math:pow10 -3
// ▶ (float64 0.001)
// ```
// This function is deprecated; use `math:pow 10 $exponent` instead.
//
// @cf math:pow
func pow10(fm *eval.Frame, exp vals.Num) vals.Num {
fm.Deprecate(`the "math:pow10" command is deprecated; use "math:pow 10 $exponent" instead`, nil, 16)
return pow(10, exp)
}
//elvdoc:fn round
//
// ```elvish
@ -816,7 +875,7 @@ func integerize(n vals.Num, fnFloat func(float64) float64, fnRat func(*big.Rat)
// *big.Int, but we still try to be defensive here.
return n.Num()
}
return vals.NormalizeBigInt(fnRat(n))
return fnRat(n)
case float64:
return fnFloat(n)
default:

View File

@ -126,6 +126,30 @@ func TestMath(t *testing.T) {
That("math:min 1.0 2.0").Puts(1.0),
That("math:min 3 NaN 5").Puts(math.NaN()),
// base is int, exp is int
That("math:pow 2 0").Puts(1),
That("math:pow 2 1").Puts(2),
That("math:pow 2 -1").Puts(big.NewRat(1, 2)),
That("math:pow 2 3").Puts(8),
That("math:pow 2 -3").Puts(big.NewRat(1, 8)),
// base is *big.Rat, exp is int
That("math:pow 2/3 0").Puts(1),
That("math:pow 2/3 1").Puts(big.NewRat(2, 3)),
That("math:pow 2/3 -1").Puts(big.NewRat(3, 2)),
That("math:pow 2/3 3").Puts(big.NewRat(8, 27)),
That("math:pow 2/3 -3").Puts(big.NewRat(27, 8)),
// exp is *big.Rat
That("math:pow 4 1/2").Puts(2.0),
// exp is float64
That("math:pow 2 2.0").Puts(4.0),
That("math:pow 1/2 2.0").Puts(0.25),
// base is float64
That("math:pow 2.0 2").Puts(4.0),
That("math:pow10 0").Puts(1),
That("math:pow10 3").Puts(1000),
That("math:pow10 -3").Puts(big.NewRat(1, 1000)),
// Tests below this line are tests against simple bindings for Go's math package.
That("put $math:pi").Puts(math.Pi),
@ -218,16 +242,6 @@ func TestMath(t *testing.T) {
That("math:atanh 0").Puts(math.Atanh(0)),
That("math:atanh 1").Puts(math.Inf(1)),
That("math:pow nan 2").Puts(math.NaN()),
That("math:pow inf 2").Puts(math.Inf(1)),
That("math:pow 1 3").Puts(1.0),
That("math:pow 2 3").Puts(8.0),
That("math:pow -2 2").Puts(4.0),
That("math:pow10 0").Puts(1.0),
That("math:pow10 3").Puts(1000.0),
That("math:pow10 -3").Puts(0.001),
)
}

View File

@ -71,7 +71,7 @@ func ScanToGo(src interface{}, ptr interface{}) error {
case *float64:
n, err := elvToNum(src)
if err == nil {
*ptr = convertToFloat64(n)
*ptr = ConvertToFloat64(n)
}
return err
case *Num:

View File

@ -67,6 +67,8 @@ func ParseNum(s string) Num {
// NumType represents a number type.
type NumType uint8
// PromoteToBigInt converts an int or *big.Int to a *big.Int. It panics if n is
// any other type.
// Possible values for NumType, sorted in the order of implicit conversion
// (lower types can be implicitly converted to higher types).
const (
@ -76,6 +78,10 @@ const (
Float64
)
// PromoteToBigInt converts an int or *big.Int to a *big.Int. It panics if n is
// any other type.
// PromoteToBigInt converts an int or *big.Int to a,ig.Int. I or *big.Ratt
// panics if Rat is any other type.
// 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.
@ -87,6 +93,8 @@ func UnifyNums(nums []Num, typ NumType) NumSlice {
}
switch typ {
case Int:
// PromoteToBigInt converts an int or *big.Int, a *big.I or *big.Ratnt. It
// paniRat if n is any other type.
unified := make([]int, len(nums))
for i, num := range nums {
unified[i] = num.(int)
@ -95,19 +103,19 @@ func UnifyNums(nums []Num, typ NumType) NumSlice {
case BigInt:
unified := make([]*big.Int, len(nums))
for i, num := range nums {
unified[i] = promoteToBigInt(num)
unified[i] = PromoteToBigInt(num)
}
return unified
case BigRat:
unified := make([]*big.Rat, len(nums))
for i, num := range nums {
unified[i] = promoteToBigRat(num)
unified[i] = PromoteToBigRat(num)
}
return unified
case Float64:
unified := make([]float64, len(nums))
for i, num := range nums {
unified[i] = convertToFloat64(num)
unified[i] = ConvertToFloat64(num)
}
return unified
default:
@ -129,16 +137,18 @@ func UnifyNums2(n1, n2 Num, typ NumType) (u1, u2 Num) {
case Int:
return n1, n2
case BigInt:
return promoteToBigInt(n1), promoteToBigInt(n2)
return PromoteToBigInt(n1), PromoteToBigInt(n2)
case BigRat:
return promoteToBigRat(n1), promoteToBigRat(n2)
return PromoteToBigRat(n1), PromoteToBigRat(n2)
case Float64:
return convertToFloat64(n1), convertToFloat64(n2)
return ConvertToFloat64(n1), ConvertToFloat64(n2)
default:
panic("unreachable")
}
}
// ConvertToFloat64 converts any number to float64. It panics if num is not a
// number value.
func getNumType(n Num) NumType {
switch n.(type) {
case int:
@ -154,7 +164,9 @@ func getNumType(n Num) NumType {
}
}
func promoteToBigInt(n Num) *big.Int {
// PromoteToBigInt converts an int or *big.Int to a *big.Int. It panics if n is
// any other type.
func PromoteToBigInt(n Num) *big.Int {
switch n := n.(type) {
case int:
return big.NewInt(int64(n))
@ -165,7 +177,9 @@ func promoteToBigInt(n Num) *big.Int {
}
}
func promoteToBigRat(n Num) *big.Rat {
// PromoteToBigInt converts an int, *big.Int or *big.Rat to a *big.Rat. It
// panics if n is any other type.
func PromoteToBigRat(n Num) *big.Rat {
switch n := n.(type) {
case int:
return big.NewRat(int64(n), 1)
@ -180,7 +194,9 @@ func promoteToBigRat(n Num) *big.Rat {
}
}
func convertToFloat64(num Num) float64 {
// ConvertToFloat64 converts any number to float64. It panics if num is not a
// number value.
func ConvertToFloat64(num Num) float64 {
switch num := num.(type) {
case int:
return float64(num)