Skip to content

Commit

Permalink
scalar factors
Browse files Browse the repository at this point in the history
  • Loading branch information
erikerlandson committed Apr 12, 2024
1 parent 05af7a1 commit 19b3e23
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 110 deletions.
158 changes: 84 additions & 74 deletions core/src/main/scala/coulomb/quantity.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ object Quantity:
object Applier:
given g_Applier[U]: Applier[U] = new Applier[U]

extension [V](v: V)
extension[V](v: V)
/**
* Lift a raw value into a unit quantity
* @tparam U
Expand All @@ -111,7 +111,7 @@ object Quantity:
*/
inline def withUnit[U]: Quantity[V, U] = v

extension [VL, UL](ql: Quantity[VL, UL])
extension[V, U, Q[V, U] <: Quantity[V, U]](q: Q[V, U])
/**
* extract the raw value of a unit quantity
* @return
Expand All @@ -121,7 +121,7 @@ object Quantity:
* q.value // => 1.5
* }}}
*/
inline def value: VL = ql
inline def value: V = q

/**
* returns a string representing this Quantity, using unit abbreviations
Expand All @@ -131,7 +131,7 @@ object Quantity:
* q.show // => "1.5 m/s"
* }}}
*/
inline def show: String = s"${ql.value.toString} ${ShowUnit[UL]}"
inline def show: String = s"${q.value.toString} ${ShowUnit[U]}"

/**
* returns a string representing this Quantity, using full unit names
Expand All @@ -142,7 +142,7 @@ object Quantity:
* }}}
*/
inline def showFull: String =
s"${ql.value.toString} ${ShowUnit.full[UL]}"
s"${q.value.toString} ${ShowUnit.full[U]}"

/**
* convert a quantity to a new value type
Expand All @@ -156,8 +156,8 @@ object Quantity:
* q.toValue[Float] // => Quantity[Meter](1.0f)
* }}}
*/
inline def toValue[V]: Quantity[V, UL] =
ValueConversion[VL, V](ql)
inline def toValue[VT]: Quantity[VT, U] =
ValueConversion[V, VT](q)

/**
* convert a quantity to a new unit type
Expand All @@ -175,8 +175,8 @@ object Quantity:
* q.toUnit[Hectare] // => compile error
* }}}
*/
inline def toUnit[U]: Quantity[VL, U] =
UnitConversion[VL, UL, U](ql)
inline def toUnit[UT]: Quantity[V, UT] =
UnitConversion[V, U, UT](q)

/**
* negate the value of a `Quantity`
Expand All @@ -189,9 +189,9 @@ object Quantity:
* }}}
*/
inline def unary_-(using
alg: AdditiveGroup[VL]
): Quantity[VL, UL] =
alg.negate(ql)
alg: AdditiveGroup[V]
): Quantity[V, U] =
alg.negate(q)

/**
* add this quantity to another
Expand All @@ -215,11 +215,11 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
inline def +[UR](qr: Quantity[VL, UR])(using
alg: AdditiveSemigroup[VL]
): Quantity[VL, UL] =
val qrv: VL = ops.alignU[VL, UR, UL](qr)
alg.plus(ql, qrv)
inline def +[UR](qr: Quantity[V, UR])(using
alg: AdditiveSemigroup[V]
): Quantity[V, U] =
val qrv: V = ops.alignU[V, UR, U](qr)
alg.plus(q, qrv)

/**
* subtract another quantity from this one
Expand All @@ -243,11 +243,11 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
inline def -[UR](qr: Quantity[VL, UR])(using
alg: AdditiveGroup[VL]
): Quantity[VL, UL] =
val qrv: VL = ops.alignU[VL, UR, UL](qr)
alg.minus(ql, qrv)
inline def -[UR](qr: Quantity[V, UR])(using
alg: AdditiveGroup[V]
): Quantity[V, U] =
val qrv: V = ops.alignU[V, UR, U](qr)
alg.minus(q, qrv)

/**
* multiply this quantity by another
Expand All @@ -269,11 +269,16 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
transparent inline def *[UR](qr: Quantity[VL, UR])(using
alg: MultiplicativeSemigroup[VL],
su: SimplifiedUnit[UL * UR]
): Quantity[VL, su.UO] =
Quantity[VL, su.UO](alg.times(ql, qr))
transparent inline def *[UR](qr: Quantity[V, UR])(using
alg: MultiplicativeSemigroup[V],
su: SimplifiedUnit[U * UR]
): Quantity[V, su.UO] =
alg.times(q, qr).withUnit[su.UO]

inline def *(v: V)(using
alg: MultiplicativeSemigroup[V]
): Quantity[V, U] =
alg.times(q, v).withUnit[U]

/**
* divide this quantity by another
Expand All @@ -295,11 +300,16 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
transparent inline def /[UR](qr: Quantity[VL, UR])(using
alg: MultiplicativeGroup[VL],
su: SimplifiedUnit[UL / UR]
): Quantity[VL, su.UO] =
Quantity[VL, su.UO](alg.div(ql, qr))
transparent inline def /[UR](qr: Quantity[V, UR])(using
alg: MultiplicativeGroup[V],
su: SimplifiedUnit[U / UR]
): Quantity[V, su.UO] =
alg.div(q, qr).withUnit[su.UO]

inline def /(v: V)(using
alg: MultiplicativeGroup[V]
): Quantity[V, U] =
alg.div(q, v).withUnit[U]

/**
* divide this quantity by another, using truncating (integer) division
Expand All @@ -321,11 +331,11 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
transparent inline def tquot[UR](qr: Quantity[VL, UR])(using
alg: TruncatedDivision[VL],
su: SimplifiedUnit[UL / UR]
): Quantity[VL, su.UO] =
Quantity[VL, su.UO](alg.tquot(ql, qr))
transparent inline def tquot[UR](qr: Quantity[V, UR])(using
alg: TruncatedDivision[V],
su: SimplifiedUnit[U / UR]
): Quantity[V, su.UO] =
Quantity[V, su.UO](alg.tquot(q, qr))

/**
* raise this quantity to a rational or integer power
Expand All @@ -342,27 +352,27 @@ object Quantity:
* }}}
*/
transparent inline def pow[E](using
su: SimplifiedUnit[UL ^ E]
): Quantity[VL, su.UO] =
val v: VL = compiletime.summonFrom {
case alg: Fractional[VL] =>
su: SimplifiedUnit[U ^ E]
): Quantity[V, su.UO] =
val v: V = compiletime.summonFrom {
case alg: Fractional[V] =>
val e = typeexpr.asRational[E]
if ((e.denominator == 1) && (e.numerator.isValidInt))
alg.pow(ql, e.numerator.toInt)
alg.pow(q, e.numerator.toInt)
else
alg.fpow(ql, alg.fromRational(e))
case alg: MultiplicativeGroup[VL] =>
alg.pow(ql, typeexpr.asInt[E])
case alg: MultiplicativeMonoid[VL] =>
alg.pow(ql, typeexpr.asNonNegInt[E])
case alg: MultiplicativeSemigroup[VL] =>
alg.pow(ql, typeexpr.asPosInt[E])
alg.fpow(q, alg.fromRational(e))
case alg: MultiplicativeGroup[V] =>
alg.pow(q, typeexpr.asInt[E])
case alg: MultiplicativeMonoid[V] =>
alg.pow(q, typeexpr.asNonNegInt[E])
case alg: MultiplicativeSemigroup[V] =>
alg.pow(q, typeexpr.asPosInt[E])
case _ =>
compiletime.error(
"no algebra in context that supports power"
)
}
Quantity[VL, su.UO](v)
Quantity[V, su.UO](v)

/**
* test this quantity for equality with another
Expand All @@ -385,11 +395,11 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
inline def ===[UR](qr: Quantity[VL, UR])(using
ord: Order[VL]
inline def ===[UR](qr: Quantity[V, UR])(using
ord: Order[V]
): Boolean =
val qrv: VL = ops.alignU[VL, UR, UL](qr)
ord.compare(ql.value, qrv) == 0
val qrv: V = ops.alignU[V, UR, U](qr)
ord.compare(q, qrv) == 0

/**
* test this quantity for inequality with another
Expand All @@ -412,11 +422,11 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
inline def =!=[UR](qr: Quantity[VL, UR])(using
ord: Order[VL]
inline def =!=[UR](qr: Quantity[V, UR])(using
ord: Order[V]
): Boolean =
val qrv: VL = ops.alignU[VL, UR, UL](qr)
ord.compare(ql, qrv) != 0
val qrv: V = ops.alignU[V, UR, U](qr)
ord.compare(q, qrv) != 0

/**
* test if this quantity is less than another
Expand All @@ -439,11 +449,11 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
inline def <[UR](qr: Quantity[VL, UR])(using
ord: Order[VL]
inline def <[UR](qr: Quantity[V, UR])(using
ord: Order[V]
): Boolean =
val qrv: VL = ops.alignU[VL, UR, UL](qr)
ord.compare(ql, qrv) < 0
val qrv: V = ops.alignU[V, UR, U](qr)
ord.compare(q, qrv) < 0

/**
* test if this quantity is less than or equal to another
Expand All @@ -466,11 +476,11 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
inline def <=[UR](qr: Quantity[VL, UR])(using
ord: Order[VL]
inline def <=[UR](qr: Quantity[V, UR])(using
ord: Order[V]
): Boolean =
val qrv: VL = ops.alignU[VL, UR, UL](qr)
ord.compare(ql, qrv) <= 0
val qrv: V = ops.alignU[V, UR, U](qr)
ord.compare(q, qrv) <= 0

/**
* test if this quantity is greater than another
Expand All @@ -493,11 +503,11 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
inline def >[UR](qr: Quantity[VL, UR])(using
ord: Order[VL]
inline def >[UR](qr: Quantity[V, UR])(using
ord: Order[V]
): Boolean =
val qrv: VL = ops.alignU[VL, UR, UL](qr)
ord.compare(ql, qrv) > 0
val qrv: V = ops.alignU[V, UR, U](qr)
ord.compare(q, qrv) > 0

/**
* test if this quantity is greater than or equal to another
Expand All @@ -520,8 +530,8 @@ object Quantity:
* result may depend on what algebras, policies, and other typeclasses
* are in scope
*/
inline def >=[UR](qr: Quantity[VL, UR])(using
ord: Order[VL]
inline def >=[UR](qr: Quantity[V, UR])(using
ord: Order[V]
): Boolean =
val qrv: VL = ops.alignU[VL, UR, UL](qr)
ord.compare(ql, qrv) >= 0
val qrv: V = ops.alignU[V, UR, U](qr)
ord.compare(q, qrv) >= 0
44 changes: 26 additions & 18 deletions core/src/main/scala/coulomb/syntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,34 @@

package coulomb.syntax

import _root_.algebra.ring.*
import spire.math.*

import coulomb.*
import coulomb.infra.SimplifiedUnit

export coulomb.Quantity.withUnit
export coulomb.DeltaQuantity.withDeltaUnit

object factors:
import _root_.algebra.ring.*

import coulomb.*
import coulomb.conversion.*
import coulomb.infra.SimplifiedUnit
// there are three tricks I applied to get scalar factors to work.
// 1. I use the signature:
// extension[V, U, Q[V, U] <: Quantity[V, U]](q: Q[V, U])
// for the Quantity methods, which helps the type system to
// distinguish from the left-factor overloadings defined in this file.
// 2. I define the right-factor overloadings in the Quantity extension,
// because defining them separately here is confusing the compiler
// 3. I curry `using alg: ...` first below, which allows the compiler to
// pick the correct typeclass.

extension (v: Double)
def *[VR, UR](q: Quantity[VR, UR])(using
alg: MultiplicativeSemigroup[VR],
vc: ValueConversion[Double, VR]
): Quantity[VR, UR] =
Quantity[VR, UR](alg.times(vc(v), q.value))
extension[V](v: V)
inline def *[U](using
alg: MultiplicativeSemigroup[V]
)(q: Quantity[V, U]): Quantity[V, U] =
alg.times(v, q.value).withUnit[U]

def /[VR, UR](q: Quantity[VR, UR])(using
alg: MultiplicativeGroup[VR],
vc: ValueConversion[Double, VR],
su: SimplifiedUnit[1 / UR]
): Quantity[VR, su.UO] =
Quantity[VR, su.UO](alg.div(vc(v), q.value))
inline def /[U](using
alg: MultiplicativeGroup[V]
)(q: Quantity[V, U])(using
su: SimplifiedUnit[1 / U]
): Quantity[V, su.UO] =
alg.div(v, q.value).withUnit[su.UO]
21 changes: 10 additions & 11 deletions core/src/test/scala/coulomb/quantity.scala
Original file line number Diff line number Diff line change
Expand Up @@ -336,17 +336,6 @@ class QuantitySuite extends CoulombSuite:
(-(7.withUnit[Liter])).assertQ[Int, Liter](-7)
}

test("mul and div by lifted unitless values") {
import coulomb.policy.standard.given
import coulomb.syntax.factors.*

val q = 2d.withUnit[Meter]

// left-hand arguments have to be defined on per type basis
(2 * q).assertQ[Double, Meter](4.0)
(2 / q).assertQ[Double, 1 / Meter](1.0)
}

test("equality strict") {
import coulomb.policy.strict.given

Expand Down Expand Up @@ -451,6 +440,16 @@ class QuantitySuite extends CoulombSuite:
assertEquals(summon[ShowUnit[KiloMeter]].full, "kilometer")
}

test("scalar factors") {
val q = 2d.withUnit[Meter]

(2d * q).assertQ[Double, Meter](4)
(2d / q).assertQ[Double, 1 / Meter](1)

(q * 2d).assertQ[Double, Meter](4)
(q / 2d).assertQ[Double, Meter](1)
}

test("cats Eq, Ord, Hash") {
import _root_.cats.kernel.{Eq, Hash, Order}
import coulomb.policy.strict.given
Expand Down
Loading

0 comments on commit 19b3e23

Please sign in to comment.