kode4food/ale

View on GitHub
pkg/data/rational.go

Summary

Maintainability
B
5 hrs
Test Coverage
A
98%
package data

import (
    "cmp"
    "fmt"
    "math"
    "math/big"
    "math/rand"
    "strconv"
    "strings"

    "github.com/kode4food/ale/internal/types"
)

type (
    // Float represents a 64-bit floating point number
    Float float64

    // Ratio represents a number having a numerator and denominator
    Ratio big.Rat
)

const (
    // ErrExpectedFloat is raised when a call to ParseFloat can't properly
    // interpret its input as a floating point number
    ErrExpectedFloat = "value is not a float: %s"

    // ErrExpectedRatio is raised when a call to ParseRatio can't properly
    // interpret its input as a ratio
    ErrExpectedRatio = "value is not a ratio: %s"
)

var (
    rationalHash = rand.Uint64()
    one          = big.NewInt(1)
)

// ParseFloat attempts to parse a string representing a float
func ParseFloat(s string) (Number, error) {
    res, err := strconv.ParseFloat(s, 64)
    if err != nil {
        return nil, fmt.Errorf(ErrExpectedFloat, s)
    }
    return Float(res), nil
}

// MustParseFloat forcefully parses a string representing a float
func MustParseFloat(s string) Number {
    res, err := ParseFloat(s)
    if err != nil {
        panic(err)
    }
    return res
}

// Cmp compares this Float to another Number
func (l Float) Cmp(r Number) Comparison {
    if math.IsNaN(float64(l)) {
        return Incomparable
    }
    rf, ok := r.(Float)
    if !ok {
        pl, pr := purify(l, r)
        return pl.Cmp(pr)
    }
    if math.IsNaN(float64(rf)) {
        return Incomparable
    }
    return Comparison(cmp.Compare(l, rf))
}

// Add adds this Float to another Number
func (l Float) Add(r Number) Number {
    if rf, ok := r.(Float); ok {
        return l + rf
    }
    pl, pr := purify(l, r)
    return pl.Add(pr)
}

// Sub subtracts another Number from this Float
func (l Float) Sub(r Number) Number {
    if rf, ok := r.(Float); ok {
        return l - rf
    }
    pl, pr := purify(l, r)
    return pl.Sub(pr)
}

// Mul multiplies this Float by another Number
func (l Float) Mul(r Number) Number {
    if rf, ok := r.(Float); ok {
        return l * rf
    }
    pl, pr := purify(l, r)
    return pl.Mul(pr)
}

// Div divides this Float by another Number
func (l Float) Div(r Number) Number {
    if rf, ok := r.(Float); ok {
        return l / rf
    }
    pl, pr := purify(l, r)
    return pl.Div(pr)
}

// Mod calculates the remainder of dividing this Float by another Number
func (l Float) Mod(r Number) Number {
    if rf, ok := r.(Float); ok {
        res := Float(math.Mod(float64(l), float64(rf)))
        if (res < 0 && rf > 0) || (res > 0 && rf < 0) {
            return res + rf
        }
        return res
    }
    pl, pr := purify(l, r)
    return pl.Mod(pr)
}

// IsNaN returns whether this Float is not a number
func (l Float) IsNaN() bool {
    return math.IsNaN(float64(l))
}

// IsPosInf returns whether this Float represents positive infinity
func (l Float) IsPosInf() bool {
    return math.IsInf(float64(l), 1)
}

// IsNegInf returns whether this Float represents negative infinity
func (l Float) IsNegInf() bool {
    return math.IsInf(float64(l), -1)
}

// Equal compares this Float to another for equality
func (l Float) Equal(r Value) bool {
    if r, ok := r.(Float); ok {
        return l == r
    }
    return false
}

// String converts this Float to a string
func (l Float) String() string {
    i := int64(l)
    if Float(i) == l {
        return fmt.Sprintf("%d.0", i)
    }
    return strings.ToLower(fmt.Sprintf("%g", l))
}

// Type returns the Type for this Float Value
func (Float) Type() types.Type {
    return types.BasicNumber
}

// HashCode returns a hash code for this Float
func (l Float) HashCode() uint64 {
    return rationalHash ^ uint64(l)
}

// ParseRatio attempts to parse a string representing a ratio
func ParseRatio(s string) (Number, error) {
    if res, ok := new(big.Rat).SetString(s); ok {
        return maybeWhole(res), nil
    }
    return nil, fmt.Errorf(ErrExpectedRatio, s)
}

// MustParseRatio forcefully parses a string representing a ratio
func MustParseRatio(s string) Number {
    res, err := ParseRatio(s)
    if err != nil {
        panic(err)
    }
    return res
}

// Cmp compares this Ratio to another Number
func (l *Ratio) Cmp(r Number) Comparison {
    if rr, ok := r.(*Ratio); ok {
        lb := (*big.Rat)(l)
        rb := (*big.Rat)(rr)
        return Comparison(lb.Cmp(rb))
    }
    pl, pr := purify(l, r)
    return pl.Cmp(pr)
}

// Add adds this Ratio to another Number
func (l *Ratio) Add(r Number) Number {
    if rr, ok := r.(*Ratio); ok {
        lb := (*big.Rat)(l)
        rb := (*big.Rat)(rr)
        res := new(big.Rat).Add(lb, rb)
        return maybeWhole(res)
    }
    pl, pr := purify(l, r)
    return pl.Add(pr)
}

// Sub subtracts another Number from this Ratio
func (l *Ratio) Sub(r Number) Number {
    if rr, ok := r.(*Ratio); ok {
        lb := (*big.Rat)(l)
        rb := (*big.Rat)(rr)
        res := new(big.Rat).Sub(lb, rb)
        return maybeWhole(res)
    }
    pl, pr := purify(l, r)
    return pl.Sub(pr)
}

// Mul multiplies this Ratio by another Number
func (l *Ratio) Mul(r Number) Number {
    if rr, ok := r.(*Ratio); ok {
        lb := (*big.Rat)(l)
        rb := (*big.Rat)(rr)
        res := new(big.Rat).Mul(lb, rb)
        return maybeWhole(res)
    }
    pl, pr := purify(l, r)
    return pl.Mul(pr)
}

// Div divides this Ratio by another Number
func (l *Ratio) Div(r Number) Number {
    if rr, ok := r.(*Ratio); ok {
        lb := (*big.Rat)(l)
        rb := (*big.Rat)(rr)
        res := new(big.Rat).Quo(lb, rb)
        return maybeWhole(res)
    }
    pl, pr := purify(l, r)
    return pl.Div(pr)
}

// Mod calculates the remainder of dividing this Ratio by another Number
func (l *Ratio) Mod(r Number) Number {
    if rr, ok := r.(*Ratio); ok {
        lb := (*big.Rat)(l)
        rb := (*big.Rat)(rr)
        n := new(big.Int).Mul(lb.Num(), rb.Denom())
        d := new(big.Int).Mul(lb.Denom(), rb.Num())
        res := new(big.Rat).SetFrac(new(big.Int).Div(n, d), one)
        res = res.Mul(res, rb)
        res = res.Sub(lb, res)
        return maybeWhole(res)
    }
    pl, pr := purify(l, r)
    return pl.Mod(pr)
}

// IsNaN returns whether this Ratio is not a number
func (*Ratio) IsNaN() bool {
    return false
}

// IsPosInf returns whether this Ratio represents positive infinity
func (*Ratio) IsPosInf() bool {
    return false
}

// IsNegInf returns whether this Ratio represents negative infinity
func (*Ratio) IsNegInf() bool {
    return false
}

// Equal compares this Ratio to another for equality
func (l *Ratio) Equal(r Value) bool {
    if l == r {
        return true
    }
    if r, ok := r.(*Ratio); ok {
        lb := (*big.Rat)(l)
        rb := (*big.Rat)(r)
        return lb.Cmp(rb) == 0
    }
    return false
}

// String converts this Ratio to a string
func (l *Ratio) String() string {
    return (*big.Rat)(l).String()
}

// Type returns the Type for this Ratio Value
func (*Ratio) Type() types.Type {
    return types.BasicNumber
}

// HashCode returns a hash code for this Ratio
func (l *Ratio) HashCode() uint64 {
    br := (*big.Rat)(l)
    return rationalHash ^ br.Num().Uint64() ^ br.Denom().Uint64()
}

func (l *Ratio) float() Float {
    f, _ := (*big.Rat)(l).Float64()
    return Float(f)
}

func maybeWhole(r *big.Rat) Number {
    if r.IsInt() {
        return maybeInteger(r.Num())
    }
    return (*Ratio)(r)
}