sourcecamAxonPlugin::Number.fan

//
// Copyright (c) 2010, SkyFoundry LLC
// All Rights Reserved
//
// History:
//   15 Sep 10  Brian Frank  Creation
//

**
** Number represents a numeric value and an optional Unit.
**
@Js
@Serializable { simple = true }
const class Number
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  static const Number negOne := Number(-1f)
  static const Number zero   := Number(0f)
  static const Number one    := Number(1f)
  static const Number ten    := Number(10f)
  static const Number nan    := Number(Float.nan)
  static const Number posInf := Number(Float.posInf)
  static const Number negInf := Number(Float.negInf)

  ** Parse from a string according to zinc syntax
  static new fromStr(Str s, Bool checked := true)
  {
    try
    {
      return ZincReader(s.in).readScalar
    }
    catch (Err e)
    {
      if (checked) throw ParseErr("Number $s.toCode ($e.msg)")
      return null
    }
  }

  ** Construct from scalar value and optional unit.
  new make(Float val, Unit? unit := null)
  {
    this.float   = val
    this.unitRef = unit
  }

  ** Construct from scalar integer and optional unit.
  new makeInt(Int val, Unit? unit := null)
  {
    this.float   = val.toFloat
    this.unitRef = unit
  }

  ** Construct from scalar Int, Float, or Decimal and optional unit.
  static Number makeNum(Num val, Unit? unit := null) { make(val.toFloat, unit) }

  ** Construct from a duration, standardize unit is hours
  ** If unit is null, then a best attempt is made based on magnitude.
  new makeDuration(Duration dur, Unit? unit := hr)
  {
    if (unit == null)
    {
      if (dur < 1sec) unit = ms
      else if (dur < 1min) unit = sec
      else if (dur < 1hr) unit = mins
      else if (dur < 1day) unit = hr
      else unit = day
    }
    this.float   = dur.ticks.toFloat / 1e9f / unit.scale
    this.unitRef = unit
  }

//////////////////////////////////////////////////////////////////////////
// Identity
//////////////////////////////////////////////////////////////////////////

  **
  ** Get the scalar value as an Float
  **
  Float toFloat() { float }

  **
  ** Is this number a whole integer without a fractional part
  **
  Bool isInt() { float == float.floor && -1e12f <= float && float <= 1e12f }

  **
  ** Get the scalar value as an Int
  **
  Int toInt() { float.toInt }

  **
  ** Get unit associated with this number or null.
  **
  Unit? unit() { unitRef }

  **
  ** Get this number as a Fantom Duration instance
  **
  Duration? toDuration(Bool checked := true)
  {
    if (unit === hr)   return toDurationMult(1hr)
    if (unit === mins) return toDurationMult(1min)
    if (unit === sec)  return toDurationMult(1sec)
    if (unit === day)  return toDurationMult(1day)
    if (unit === mo)   return toDurationMult(30day)
    if (unit === week) return toDurationMult(7day)
    if (unit === year) return toDurationMult(365day)
    if (unit === ms)   return toDurationMult(1ms)
    if (unit === ns)   return toDurationMult(1ns)
    if (checked) throw UnitErr("Not duration unit: $this")
    return null
  }
  private Duration toDurationMult(Duration mult)
  {
    Duration((float * mult.ticks.toFloat).toInt)
  }

  **
  ** Hash is based on val
  **
  override Int hash() { float.hash }

  **
  ** Equality is based on val and unit.  NaN is equal
  ** to itself (like Float.compare, but unlike Float.equals)
  **
  override Bool equals(Obj? that)
  {
    x := that as Number
    if (x == null) return false
    return float.compare(x.float) == 0 && unit === x.unit
  }

  **
  ** Compare is based on val.
  ** Throw `UnitErr` is this and b have incompatible units.
  **
  override Int compare(Obj that)
  {
    x := (Number)that
    if (unit !== x.unit && unit != null && x.unit != null)
      throw UnitErr("$unit <=> $x.unit")
    return float <=> x.float
  }

  **
  ** Return if this number is approximately equal to that - see `sys::Float.approx`
  **
  Bool approx(Number that, Float? tolerance := null)
  {
    if (unit !== that.unit) return false
    return float.approx(that.float, tolerance)
  }

  **
  ** Is the floating value NaN.
  **
  Bool isNaN() { float.isNaN }

  **
  ** Return if this number if pos/neg infinity or NaN
  **
  Bool isSpecial()
  {
    float == Float.posInf || float == Float.negInf || float.isNaN
  }

  **
  ** String representation
  **
  override Str toStr()
  {
    s := isInt ? toInt.toStr : float.toStr
    if (unit != null && float != Float.posInf && float != Float.negInf && !float.isNaN)
      s += unit.symbol
    return s
  }

  **
  ** Trio/zinc code representation, same as `toStr`
  **
  Str toCode() { toStr }

//////////////////////////////////////////////////////////////////////////
// Operators
//////////////////////////////////////////////////////////////////////////

  **
  ** Negate this number.  Shortcut is -a.
  **
  @Operator Number negate() { make(-float, unit) }

  **
  ** Increment this number.  Shortcut is ++a.
  **
  @Operator Number increment() { make(float+1f, unit) }

  **
  ** Decrement this number.  Shortcut is --a.
  **
  @Operator Number decrement() { make(float-1f, unit) }

  **
  ** Add this with b.  Shortcut is a+b.
  ** Throw `UnitErr` is this and b have incompatible units.
  **
  @Operator Number plus(Number b) { make(float + b.float, plusUnit(unit, b.unit)) }

  private static Unit? plusUnit(Unit? a, Unit? b)
  {
    if (b == null) return a
    if (a == null) return b
    if (a === b)   return a
    if ((a === F && b === Fdeg) || (a === Fdeg && b === F)) return F
    if ((a === C && b === Cdeg) || (a === Cdeg && b === C)) return C
    throw UnitErr("$a + $b")
  }

  **
  ** Subtract b from this.  Shortcut is a-b.
  ** The b.unit must match this.unit.
  **
  @Operator Number minus(Number b) { make(float - b.float, minusUnit(unit, b.unit)) }

  private static Unit? minusUnit(Unit? a, Unit? b)
  {
    if (b == null) return a
    if (a == null) return b
    if (a === F && b === F) return Fdeg
    if (a === C && b === C) return Cdeg
    if (a === F && b === Fdeg) return F
    if (a === C && b === Cdeg) return C
    if (a === b)   return a
    throw UnitErr("$a - $b")
  }

  **
  ** Multiple this and b.  Shortcut is a*b.
  ** The resulting unit is derived from the product of this and b.
  ** Throw `UnitErr` if a*b does not match a unit in the unit database.
  **
  @Operator Number mult(Number b) { make(float * b.float, multUnit(unit, b.unit)) }

  private static Unit? multUnit(Unit? a, Unit? b)
  {
    if (b == null) return a
    if (a == null) return b
    try
      return  a * b
    catch
      return defineUnit(a, '_', b)
  }

  **
  ** Divide this by b.  Shortcut is a/b.
  ** The resulting unit is derived from the quotient of this and b.
  ** Throw `UnitErr` if a/b does not match a unit in the unit database.
  **
  @Operator Number div(Number b) { make(float / b.float, divUnit(unit, b.unit)) }

  private static Unit? divUnit(Unit? a, Unit? b)
  {
    if (b == null) return a
    try
      return a / b
    catch
      return defineUnit(a, '/', b)
  }

  **
  ** Return remainder of this divided by b.  Shortcut is a%b.
  ** The unit of b must be null.
  **
  @Operator Number mod(Number b)
  {
    if (b.unit != null) throw UnitErr("$unit % $b")
    return make(float % b.float, unit)
  }

  private static Unit defineUnit(Unit a, Int symbol, Unit b)
  {
    // build up new string _a/b or _a_b
    s := StrBuf()
    aStr := a.toStr
    if (aStr.startsWith("_")) s.add(aStr)
    else s.addChar('_').add(aStr)

    s.addChar(symbol)

    bStr := b.toStr
    if (bStr.startsWith("_")) bStr = bStr[1..-1]
    s.add(bStr)

    // define if not created yet
    str := s.toStr
    unit := Unit.fromStr(str, false)
    if (unit == null) unit = Unit.define(str)
    return unit
  }

  @NoDoc static Unit? loadUnit(Str str, Bool checked := false)
  {
    unit := Unit.fromStr(str, false)
    if (unit != null) return unit

    if (!str.isEmpty && str[0] == '_') return Unit.define(str)

    if (checked) throw Err("Unit not defined: $str.toCode")
    return null
  }

//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////

  **
  ** Return absolute value of this number.
  **
  Number abs()
  {
    float >= 0f ? this : make(-float, unit)
  }

  **
  ** Return min value
  **
  Number min(Number that)
  {
    float <= that.float ? this : that
  }

  **
  ** Return max value
  **
  Number max(Number that)
  {
    float >= that.float ? this : that
  }

  **
  ** Get the ASCII upper case version of this number as a Unicode point.
  **
  Number upper()
  {
    int := toInt
    up := int.upper
    if (int == up) return this
    return makeInt(up, unit)
  }

  **
  ** Get the ASCII lower case version of this number as a Unicode point.
  **
  Number lower()
  {
    int := toInt
    lo := int.lower
    if (int == lo) return this
    return makeInt(lo, unit)
  }

  **
  ** Format the number according to `sys::Float.toLocale` pattern
  ** language.  Unit symbol is always added as suffix if available.
  **
  Str toLocale(Str? pattern := null)
  {
    if (unit === dollar)
      return StrBuf().addChar('$').add(toFloat.toLocale(pattern ?: "#,##0.00")).toStr

    if (unit === hr)   return toFloat.toLocale("0.##") + "$<sys::hourAbbr>"
    if (unit === mins) return toFloat.toLocale("0.##") + "$<sys::minAbbr>"
    if (unit === sec)  return toFloat.toLocale("0.##") + "$<sys::secAbbr>"

    Str? s
    if (pattern == null)
    {
      fabs := float.abs
      if (isInt) s = toInt.toLocale(null)
      else if (fabs >= 1000f) s = float.toLocale("#,##0")
      else if (fabs < 0.001f) s = toFloat.toStr
      else s = float.toLocale(null)
    }
    else if (pattern == "B")
    {
      s = toInt.toLocale(pattern)
    }
    else
    {
      s = float.toLocale(pattern)
    }

    if (unit != null)
    {
      symbol := unit.symbol
      if (symbol[0] == '_') symbol = symbol[1..-1]
      s += " " + symbol
    }

    return s
  }

//////////////////////////////////////////////////////////////////////////
// Private
//////////////////////////////////////////////////////////////////////////

  @NoDoc const static Unit F       := Unit("fahrenheit")
  @NoDoc const static Unit C       := Unit("celsius")
  @NoDoc const static Unit Fdeg    := Unit("fahrenheit_degrees")
  @NoDoc const static Unit Cdeg    := Unit("celsius_degrees")
  @NoDoc const static Unit ns      := Unit("ns")
  @NoDoc const static Unit ms      := Unit("ms")
  @NoDoc const static Unit sec     := Unit("s")
  @NoDoc const static Unit mins    := Unit("min")
  @NoDoc const static Unit hr      := Unit("h")
  @NoDoc const static Unit day     := Unit("day")
  @NoDoc const static Unit week    := Unit("wk")
  @NoDoc const static Unit mo      := Unit("mo")
  @NoDoc const static Unit year    := Unit("year")
  @NoDoc const static Unit percent := Unit("%")
  @NoDoc const static Unit dollar  := Unit("\$")

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  ** Numeric value
  private const Float float

  ** Optional number
  private const Unit? unitRef

}