From 19b3e23fd09082e676b91b4e0392bae6b1ae0f68 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 12 Apr 2024 16:20:59 -0700 Subject: [PATCH] scalar factors --- core/src/main/scala/coulomb/quantity.scala | 158 ++++++++++-------- core/src/main/scala/coulomb/syntax.scala | 44 +++-- core/src/test/scala/coulomb/quantity.scala | 21 ++- .../test/scala/coulomb/spirequantity.scala | 9 +- 4 files changed, 122 insertions(+), 110 deletions(-) diff --git a/core/src/main/scala/coulomb/quantity.scala b/core/src/main/scala/coulomb/quantity.scala index a88d8a46f..bf2995c42 100644 --- a/core/src/main/scala/coulomb/quantity.scala +++ b/core/src/main/scala/coulomb/quantity.scala @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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` @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/core/src/main/scala/coulomb/syntax.scala b/core/src/main/scala/coulomb/syntax.scala index 3d895fdf5..41b479ee2 100644 --- a/core/src/main/scala/coulomb/syntax.scala +++ b/core/src/main/scala/coulomb/syntax.scala @@ -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] diff --git a/core/src/test/scala/coulomb/quantity.scala b/core/src/test/scala/coulomb/quantity.scala index 9edd6b9e9..16b725641 100644 --- a/core/src/test/scala/coulomb/quantity.scala +++ b/core/src/test/scala/coulomb/quantity.scala @@ -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 @@ -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 diff --git a/core/src/test/scala/coulomb/spirequantity.scala b/core/src/test/scala/coulomb/spirequantity.scala index d57a5d954..6c217921f 100644 --- a/core/src/test/scala/coulomb/spirequantity.scala +++ b/core/src/test/scala/coulomb/spirequantity.scala @@ -153,12 +153,8 @@ class SpireQuantitySuite extends CoulombSuite: assertCE("BigInt(2).withUnit[Meter].pow[-1 / 2]") } - /* - test("mul and div by lifted unitless values") { - import coulomb.policy.spire.standard.given - import scala.language.implicitConversions - - val q = 2.withUnit[Meter] + test("scalar factors") { + val q = Rational(2).withUnit[Meter] (Rational(2) * q).assertQ[Rational, Meter](4) (Rational(2) / q).assertQ[Rational, 1 / Meter](1) @@ -166,7 +162,6 @@ class SpireQuantitySuite extends CoulombSuite: (q * Rational(2)).assertQ[Rational, Meter](4) (q / Rational(2)).assertQ[Rational, Meter](1) } - */ test("< strict") { import coulomb.policy.strict.given