From 8cf0a7de2ff010e242a62f9daf50cec935491e2c Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 7 Jan 2023 11:24:44 -0700 Subject: [PATCH] refined integration for scala3 (#392) * positive additive semigroup * refined value res * trailing spaces * refined value conversions * refined unit conversions * refined algebraic policy * unit testing class * test refined add strict * more lift * toValue, toUnit, value, show * support type dealiasing in typestring * standard addition test * pay format troll * test refined standard addition * test refined multiply strict * make refined algs concrete classes * test divide over positives * test power over positive refined * stub docs for coulomb-refined * refined policy concepts * start mdoc for coulomb-refined * refined positive example * finish draft of coulomb-refined doc * either rule test * coulomb.refined.syntax * test either refined lifting * test either refined lifting * explicit conversions refined either * refined either addition * refined either multiply * test overflow and underflow * refined either divide * refined either power * add docs for refineVU and Either * truncating val conv for either * truncating/delta unit conversions for either --- .github/workflows/ci.yml | 4 +- build.sbt | 13 +- core/src/main/scala/coulomb/infra/meta.scala | 41 ++- .../test/scala/coulomb/testing/testing.scala | 2 +- docs/coulomb-refined.md | 163 +++++++++ docs/directory.conf | 1 + .../coulomb/conversion/standard/unit.scala | 114 +++++++ .../coulomb/conversion/standard/value.scala | 71 ++++ .../coulomb/ops/algebra/refined/all.scala | 154 +++++++++ .../coulomb/ops/resolution/refined.scala | 41 +++ .../policy/overlay/refined/policy.scala | 23 ++ .../main/scala/coulomb/syntax/refined.scala | 53 +++ refined/src/test/scala/coulomb/quantity.scala | 315 ++++++++++++++++++ .../src/test/scala/coulomb/testutils.scala | 27 ++ 14 files changed, 1000 insertions(+), 22 deletions(-) create mode 100644 docs/coulomb-refined.md create mode 100644 refined/src/main/scala/coulomb/conversion/standard/unit.scala create mode 100644 refined/src/main/scala/coulomb/conversion/standard/value.scala create mode 100644 refined/src/main/scala/coulomb/ops/algebra/refined/all.scala create mode 100644 refined/src/main/scala/coulomb/ops/resolution/refined.scala create mode 100644 refined/src/main/scala/coulomb/policy/overlay/refined/policy.scala create mode 100644 refined/src/main/scala/coulomb/syntax/refined.scala create mode 100644 refined/src/test/scala/coulomb/quantity.scala create mode 100644 refined/src/test/scala/coulomb/testutils.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b2fe0fe1..daf801d35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,11 +94,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: mkdir -p spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target testkit/.js/target unidocs/target .js/target core/.native/target site/target spire/.native/target core/.js/target units/.native/target core/.jvm/target .jvm/target .native/target units/.js/target testkit/.jvm/target project/target + run: mkdir -p spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target testkit/.js/target unidocs/target .js/target core/.native/target site/target spire/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: tar cf targets.tar spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target testkit/.js/target unidocs/target .js/target core/.native/target site/target spire/.native/target core/.js/target units/.native/target core/.jvm/target .jvm/target .native/target units/.js/target testkit/.jvm/target project/target + run: tar cf targets.tar spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target testkit/.js/target unidocs/target .js/target core/.native/target site/target spire/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') diff --git a/build.sbt b/build.sbt index 12828162c..a3564e82d 100644 --- a/build.sbt +++ b/build.sbt @@ -59,6 +59,14 @@ lazy val spire = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings(commonSettings: _*) .settings(libraryDependencies += "org.typelevel" %%% "spire" % "0.18.0") +lazy val refined = crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .in(file("refined")) + .settings(name := "coulomb-refined") + .dependsOn(core % "compile->compile;test->test", units % Test) + .settings(commonSettings: _*) + .settings(libraryDependencies += "eu.timepit" %%% "refined" % "0.10.1") + lazy val testkit = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("testkit")) @@ -82,7 +90,8 @@ lazy val all = project .dependsOn( core.jvm, units.jvm, - spire.jvm + spire.jvm, + refined.jvm ) // scala repl only needs JVMPlatform subproj builds .settings(name := "coulomb-all") .enablePlugins(NoPublishPlugin) // don't publish @@ -103,7 +112,7 @@ lazy val unidocs = project // http://localhost:4242 lazy val docs = project .in(file("site")) - .dependsOn(core.jvm, units.jvm, spire.jvm) + .dependsOn(core.jvm, units.jvm, spire.jvm, refined.jvm) .enablePlugins(TypelevelSitePlugin) // https://github.com/sbt/sbt-jmh diff --git a/core/src/main/scala/coulomb/infra/meta.scala b/core/src/main/scala/coulomb/infra/meta.scala index 3270c23e2..3b4ec6f99 100644 --- a/core/src/main/scala/coulomb/infra/meta.scala +++ b/core/src/main/scala/coulomb/infra/meta.scala @@ -371,24 +371,31 @@ object meta: case (u, e0) :: tail => (u, e0 * e) :: unifyPow(e, tail) def typestr(using Quotes)(t: quotes.reflect.TypeRepr): String = + // The policy goal here is that type aliases are never expanded. + typestring(t, false) + + def typestring(using + Quotes + )(t: quotes.reflect.TypeRepr, dealias: Boolean): String = import quotes.reflect.* - def work(tr: TypeRepr): String = tr match - // The policy goal here is that type aliases are never expanded. - case typealias(_) => tr.typeSymbol.name - case unitconst(v) => s"$v" - case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[*] => - s"(${work(lhs)} * ${work(rhs)})" - case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[/] => - s"(${work(lhs)} / ${work(rhs)})" - case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[^] => - s"(${work(lhs)} ^ ${work(rhs)})" - case AppliedType(tc, ta) => - val tcn = tc.typeSymbol.name - val as = ta.map(work) - if (as.length == 0) tcn - else - tcn + "[" + as.mkString(",") + "]" - case t => t.typeSymbol.name + def work(trp: TypeRepr): String = + val tr = if (dealias) trp.dealias else trp + tr match + case typealias(_) => tr.typeSymbol.name + case unitconst(v) => s"$v" + case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[*] => + s"(${work(lhs)} * ${work(rhs)})" + case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[/] => + s"(${work(lhs)} / ${work(rhs)})" + case AppliedType(op, List(lhs, rhs)) if op =:= TypeRepr.of[^] => + s"(${work(lhs)} ^ ${work(rhs)})" + case AppliedType(tc, ta) => + val tcn = tc.typeSymbol.name + val as = ta.map(work) + if (as.length == 0) tcn + else + tcn + "[" + as.mkString(",") + "]" + case t => t.typeSymbol.name work(t) object typealias: diff --git a/core/src/test/scala/coulomb/testing/testing.scala b/core/src/test/scala/coulomb/testing/testing.scala index 9173b7571..ce37bd660 100644 --- a/core/src/test/scala/coulomb/testing/testing.scala +++ b/core/src/test/scala/coulomb/testing/testing.scala @@ -83,7 +83,7 @@ object types: private def tsmeta[T](using Type[T], Quotes): Expr[String] = import quotes.reflect.* - Expr(coulomb.infra.meta.typestr(TypeRepr.of[T])) + Expr(coulomb.infra.meta.typestring(TypeRepr.of[T], true)) private def temeta[T1, T2](using Type[T1], diff --git a/docs/coulomb-refined.md b/docs/coulomb-refined.md new file mode 100644 index 000000000..dcb6e59be --- /dev/null +++ b/docs/coulomb-refined.md @@ -0,0 +1,163 @@ +# coulomb-refined + +The `coulomb-refined` package defines policies and utilities for integrating the +[refined](https://github.com/fthomas/refined#refined-simple-refinement-types-for-scala) +typelevel libraries with `coulomb`. + +## Quick Start + +### documentation + +You can browse the `coulomb-refined` policies +[here](https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb/policy/overlay/refined.html). + +### packages + +Include `coulomb-refined` with your Scala project: + +```scala +libraryDependencies += "com.manyangled" %% "coulomb-core" % "@VERSION@" +libraryDependencies += "com.manyangled" %% "coulomb-refined" % "@VERSION@" +``` + +### import + +To import the standard coulomb policy with the refined overlay: + +```scala mdoc +// fundamental coulomb types and methods +import coulomb.* +import coulomb.syntax.* + +// common refined definitions +import eu.timepit.refined.* +import eu.timepit.refined.api.* +import eu.timepit.refined.numeric.* + +// algebraic definitions +import algebra.instances.all.given +import coulomb.ops.algebra.all.{*, given} + +// standard policy for spire and scala types +import coulomb.policy.standard.given +import scala.language.implicitConversions + +// overlay policy for refined integrations +import coulomb.policy.overlay.refined.algebraic.given + +// coulomb syntax for refined integrations +import coulomb.syntax.refined.* +``` + +### examples + +Examples in this section will use the following workaround as a replacement for +[refineMV](https://github.com/fthomas/refined/issues/932) +until it is ported forward to Scala 3. + +```scala mdoc +// a workaround for refineMV not being available in scala3 +// https://github.com/fthomas/refined/issues/932 +object workaround: + extension [V](v: V) + def withRP[P](using Validate[V, P]): Refined[V, P] = + refineV[P].unsafeFrom(v) + +import workaround.* +``` + +The `coulomb-refined` package supports `refined` predicates that are algebraically well-behaved for applicable operations. +Primarily this means the predicates `Positive` and `NonNegative`. +For example, the positive doubles are an additive semigroup and multiplicative group, +as the following code demonstrates. + +@:callout(info) +The +[table][algebraic-policy-table] +below summarizes the full list of supported `refined` predicates and associated algebras. +@:@ + +```scala mdoc +import coulomb.units.si.{*, given} +import coulomb.units.us.{*, given} + +val pos1 = 1d.withRP[Positive].withUnit[Meter] +val pos2 = 2d.withRP[Positive].withUnit[Meter] +val pos3 = 3d.withRP[Positive].withUnit[Second] + +// positive doubles are an additive semigroup +pos1 + pos2 + +// also a multiplicative semigroup +pos1 * pos2 +pos2.pow[2] + +// also a multiplicative group +pos2 / pos3 +pos2.pow[0] +``` + +The standard `refined` function for refining values with run-time checking is `refineV`, +which returns an `Either`. +The `coulomb-refined` package supplies a similar variation `refinedVU`. +These objects are also supported by algebras. + +```scala mdoc +// This refinement succeeds, and returns a Right value +val pe1 = refineVU[Positive, Meter](1) + +// This refinement fails, and returns a Left value +val pe2 = refineVU[Positive, Meter](0) + +// positives are an additive semigroup +pe1 + pe1 + +// algebras operating on Left values result in a Left +pe1 + pe2 +``` + +## Policies + +### policy overlays + +The `coulomb-refined` package currently provides a single "overlay" policy. +An overlay policy is designed to work with any other policies currently in scope, +and lift them into another abstraction; +in this case, lifting policies for value type(s) `V` into `Refined[V, P]`. +The `Refined` abstraction guarantees that a value of type `V` satisfies some predicate `P`, +and the semantics of `V` remain otherwise unchanged. + +For example, given any algebra in scope for a type `V` that defines addition, +the `coulomb-refined` overlay defines the corresponding `Refined[V, P]` addition +like so: +```scala +plus(x: Refined[V, P], y: Refined[V, P]): Refined[V, P] = +// (x.value + y.value) refined by P +``` + +@:callout(info) +Because the refined algebraic policy is an overlay, +you can use it with your choice of base policies, +for example with +[core policies](concepts.md#coulomb-policies) +or +[spire policies](coulomb-spire.md#policies). +@:@ + +### algebraic policy table + +The following table summarizes the "algebraic" overlay policy. +Examples of Fractional value types include Double, Float, BigDecimal, spire Rational, etc. +Integral value types include Int, Long, BigInt, etc. + +| Value Type | Predicate | Add Alg | Mult Alg | `+` | `*` | `/` | `pow` (exponent) | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Fractional | Positive | semigroup | group | Y | Y | Y | Y (rational) | +| Fractional | NonNegative | semigroup | semigroup | Y | Y | N | Y (pos int) | +| Integral | Positive | semigroup | semigroup | Y | Y | N | Y (pos int) | +| Integral | NonNegative | semigroup | semigroup | Y | Y | N | Y (pos int) | + +@:callout(info) +The table above also applies to `Either` objects returned by `refineVU` as discussed +in the examples section above. +@:@ diff --git a/docs/directory.conf b/docs/directory.conf index 9d03ff9f8..0768a21ad 100644 --- a/docs/directory.conf +++ b/docs/directory.conf @@ -4,4 +4,5 @@ laika.navigationOrder = [ develop.md coulomb-units.md coulomb-spire.md + coulomb-refined.md ] diff --git a/refined/src/main/scala/coulomb/conversion/standard/unit.scala b/refined/src/main/scala/coulomb/conversion/standard/unit.scala new file mode 100644 index 000000000..f804974e4 --- /dev/null +++ b/refined/src/main/scala/coulomb/conversion/standard/unit.scala @@ -0,0 +1,114 @@ +/* + * Copyright 2022 Erik Erlandson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coulomb.conversion.refined + +import scala.util.{Try, Success, Failure} + +import coulomb.conversion.* + +import eu.timepit.refined.* +import eu.timepit.refined.api.* +import eu.timepit.refined.numeric.* + +object unit: + given ctx_UC_Refined_Positive[V, UF, UT](using + uc: UnitConversion[V, UF, UT], + vld: Validate[V, Positive] + ): UnitConversion[Refined[V, Positive], UF, UT] = + (v: Refined[V, Positive]) => + refineV[Positive].unsafeFrom(uc(v.value)) + + given ctx_UC_Refined_NonNegative[V, UF, UT](using + uc: UnitConversion[V, UF, UT], + vld: Validate[V, NonNegative] + ): UnitConversion[Refined[V, NonNegative], UF, UT] = + (v: Refined[V, NonNegative]) => + refineV[NonNegative].unsafeFrom(uc(v.value)) + + given ctx_TUC_Refined_Positive[V, UF, UT](using + uc: TruncatingUnitConversion[V, UF, UT], + vld: Validate[V, Positive] + ): TruncatingUnitConversion[Refined[V, Positive], UF, UT] = + (v: Refined[V, Positive]) => + refineV[Positive].unsafeFrom(uc(v.value)) + + given ctx_TUC_Refined_NonNegative[V, UF, UT](using + uc: TruncatingUnitConversion[V, UF, UT], + vld: Validate[V, NonNegative] + ): TruncatingUnitConversion[Refined[V, NonNegative], UF, UT] = + (v: Refined[V, NonNegative]) => + refineV[NonNegative].unsafeFrom(uc(v.value)) + + given ctx_DUC_Refined_Positive[V, B, UF, UT](using + uc: DeltaUnitConversion[V, B, UF, UT], + vld: Validate[V, Positive] + ): DeltaUnitConversion[Refined[V, Positive], B, UF, UT] = + (v: Refined[V, Positive]) => + refineV[Positive].unsafeFrom(uc(v.value)) + + given ctx_DUC_Refined_NonNegative[V, B, UF, UT](using + uc: DeltaUnitConversion[V, B, UF, UT], + vld: Validate[V, NonNegative] + ): DeltaUnitConversion[Refined[V, NonNegative], B, UF, UT] = + (v: Refined[V, NonNegative]) => + refineV[NonNegative].unsafeFrom(uc(v.value)) + + given ctx_TDUC_Refined_Positive[V, B, UF, UT](using + uc: TruncatingDeltaUnitConversion[V, B, UF, UT], + vld: Validate[V, Positive] + ): TruncatingDeltaUnitConversion[Refined[V, Positive], B, UF, UT] = + (v: Refined[V, Positive]) => + refineV[Positive].unsafeFrom(uc(v.value)) + + given ctx_TDUC_Refined_NonNegative[V, B, UF, UT](using + uc: TruncatingDeltaUnitConversion[V, B, UF, UT], + vld: Validate[V, NonNegative] + ): TruncatingDeltaUnitConversion[Refined[V, NonNegative], B, UF, UT] = + (v: Refined[V, NonNegative]) => + refineV[NonNegative].unsafeFrom(uc(v.value)) + + given ctx_UC_Refined_Either[V, P, UF, UT](using + uc: UnitConversion[Refined[V, P], UF, UT] + ): UnitConversion[Either[String, Refined[V, P]], UF, UT] = + (v: Either[String, Refined[V, P]]) => + Try(v.map(uc)) match + case Success(x) => x + case Failure(e) => Left(e.getMessage) + + given ctx_TUC_Refined_Either[V, P, UF, UT](using + uc: TruncatingUnitConversion[Refined[V, P], UF, UT] + ): TruncatingUnitConversion[Either[String, Refined[V, P]], UF, UT] = + (v: Either[String, Refined[V, P]]) => + Try(v.map(uc)) match + case Success(x) => x + case Failure(e) => Left(e.getMessage) + + given ctx_DUC_Refined_Either[V, B, P, UF, UT](using + uc: DeltaUnitConversion[Refined[V, P], B, UF, UT] + ): DeltaUnitConversion[Either[String, Refined[V, P]], B, UF, UT] = + (v: Either[String, Refined[V, P]]) => + Try(v.map(uc)) match + case Success(x) => x + case Failure(e) => Left(e.getMessage) + + given ctx_TDUC_Refined_Either[V, B, P, UF, UT](using + uc: TruncatingDeltaUnitConversion[Refined[V, P], B, UF, UT] + ): TruncatingDeltaUnitConversion[Either[String, Refined[V, P]], B, UF, UT] = + (v: Either[String, Refined[V, P]]) => + Try(v.map(uc)) match + case Success(x) => x + case Failure(e) => Left(e.getMessage) diff --git a/refined/src/main/scala/coulomb/conversion/standard/value.scala b/refined/src/main/scala/coulomb/conversion/standard/value.scala new file mode 100644 index 000000000..20f67ebd1 --- /dev/null +++ b/refined/src/main/scala/coulomb/conversion/standard/value.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2022 Erik Erlandson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coulomb.conversion.refined + +import scala.util.{Try, Success, Failure} + +import coulomb.conversion.* + +import eu.timepit.refined.* +import eu.timepit.refined.api.* +import eu.timepit.refined.numeric.* + +object value: + given ctx_VC_Refined_Positive[VF, VT](using + vc: ValueConversion[VF, VT], + vld: Validate[VT, Positive] + ): ValueConversion[Refined[VF, Positive], Refined[VT, Positive]] = + (v: Refined[VF, Positive]) => + refineV[Positive].unsafeFrom(vc(v.value)) + + given ctx_VC_Refined_NonNegative[VF, VT](using + vc: ValueConversion[VF, VT], + vld: Validate[VT, NonNegative] + ): ValueConversion[Refined[VF, NonNegative], Refined[VT, NonNegative]] = + (v: Refined[VF, NonNegative]) => + refineV[NonNegative].unsafeFrom(vc(v.value)) + + given ctx_TVC_Refined_Positive[VF, VT](using + vc: TruncatingValueConversion[VF, VT], + vld: Validate[VT, Positive] + ): TruncatingValueConversion[Refined[VF, Positive], Refined[VT, Positive]] = + (v: Refined[VF, Positive]) => + refineV[Positive].unsafeFrom(vc(v.value)) + + given ctx_TVC_Refined_NonNegative[VF, VT](using + vc: TruncatingValueConversion[VF, VT], + vld: Validate[VT, NonNegative] + ): TruncatingValueConversion[Refined[VF, NonNegative], Refined[VT, NonNegative]] = + (v: Refined[VF, NonNegative]) => + refineV[NonNegative].unsafeFrom(vc(v.value)) + + given ctx_VC_Refined_Either[VF, VT, P](using + vc: ValueConversion[Refined[VF, P], Refined[VT, P]] + ): ValueConversion[Either[String, Refined[VF, P]], Either[String, Refined[VT, P]]] = + (v: Either[String, Refined[VF, P]]) => + Try(v.map(vc)) match + case Success(x) => x + case Failure(e) => Left(e.getMessage) + + given ctx_TVC_Refined_Either[VF, VT, P](using + vc: TruncatingValueConversion[Refined[VF, P], Refined[VT, P]] + ): TruncatingValueConversion[Either[String, Refined[VF, P]], Either[String, Refined[VT, P]]] = + (v: Either[String, Refined[VF, P]]) => + Try(v.map(vc)) match + case Success(x) => x + case Failure(e) => Left(e.getMessage) + diff --git a/refined/src/main/scala/coulomb/ops/algebra/refined/all.scala b/refined/src/main/scala/coulomb/ops/algebra/refined/all.scala new file mode 100644 index 000000000..f390abcb1 --- /dev/null +++ b/refined/src/main/scala/coulomb/ops/algebra/refined/all.scala @@ -0,0 +1,154 @@ +/* + * Copyright 2022 Erik Erlandson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coulomb.ops.algebra.refined + +import scala.util.{Try, Success, Failure} + +import algebra.ring.* + +import eu.timepit.refined.* +import eu.timepit.refined.api.* +import eu.timepit.refined.numeric.* + +import coulomb.ops.algebra.FractionalPower + +object all: + given ctx_AdditiveSemigroup_Refined_Positive[V](using + alg: AdditiveSemigroup[V], + vld: Validate[V, Positive] + ): AdditiveSemigroup[Refined[V, Positive]] = + new infra.ASGR[V, Positive] + + given ctx_AdditiveSemigroup_Refined_NonNegative[V](using + alg: AdditiveSemigroup[V], + vld: Validate[V, NonNegative] + ): AdditiveSemigroup[Refined[V, NonNegative]] = + new infra.ASGR[V, NonNegative] + + given ctx_MultiplicativeGroup_Refined_Positive[V](using + alg: MultiplicativeGroup[V], + vld: Validate[V, Positive] + ): MultiplicativeGroup[Refined[V, Positive]] = + new infra.MGR[V, Positive] + + given ctx_MultiplicativeMonoid_Refined_Positive[V](using + alg: MultiplicativeMonoid[V], + vld: Validate[V, Positive] + ): MultiplicativeMonoid[Refined[V, Positive]] = + new infra.MMR[V, Positive] + + given ctx_MultiplicativeMonoid_Refined_NonNegative[V](using + alg: MultiplicativeMonoid[V], + vld: Validate[V, NonNegative] + ): MultiplicativeMonoid[Refined[V, NonNegative]] = + new infra.MMR[V, NonNegative] + + given ctx_FractionalPower_Refined_Positive[V](using + alg: FractionalPower[V], + vld: Validate[V, Positive] + ): FractionalPower[Refined[V, Positive]] = + new infra.FPR[V, Positive] + + given ctx_AdditiveSemigroup_Refined_Either[V, P](using + alg: AdditiveSemigroup[Refined[V, P]] + ): AdditiveSemigroup[Either[String, Refined[V, P]]] = + new infra.ASGRE[V, P] + + given ctx_MultiplicativeGroup_Refined_Either[V, P](using + alg: MultiplicativeGroup[Refined[V, P]] + ): MultiplicativeGroup[Either[String, Refined[V, P]]] = + new infra.MGRE[V, P] + + given ctx_MultiplicativeMonoid_Refined_Either[V, P](using + alg: MultiplicativeMonoid[Refined[V, P]] + ): MultiplicativeMonoid[Either[String, Refined[V, P]]] = + new infra.MMRE[V, P] + + object infra: + class ASGR[V, P](using alg: AdditiveSemigroup[V], vld: Validate[V, P]) extends + AdditiveSemigroup[Refined[V, P]]: + def plus(x: Refined[V, P], y: Refined[V, P]): Refined[V, P] = + refineV[P].unsafeFrom(alg.plus(x.value, y.value)) + + class MSGR[V, P](using alg: MultiplicativeSemigroup[V], vld: Validate[V, P]) extends + MultiplicativeSemigroup[Refined[V, P]]: + def times(x: Refined[V, P], y: Refined[V, P]): Refined[V, P] = + refineV[P].unsafeFrom(alg.times(x.value, y.value)) + + class MMR[V, P](using alg: MultiplicativeMonoid[V], vld: Validate[V, P]) extends + MSGR[V, P] with MultiplicativeMonoid[Refined[V, P]]: + def one: Refined[V, P] = refineV[P].unsafeFrom(alg.one) + + class MGR[V, P](using alg: MultiplicativeGroup[V], vld: Validate[V, P]) extends + MMR[V, P] with MultiplicativeGroup[Refined[V, P]]: + def div(x: Refined[V, P], y: Refined[V, P]): Refined[V, P] = + refineV[P].unsafeFrom(alg.div(x.value, y.value)) + + class FPR[V, P](using alg: FractionalPower[V], vld: Validate[V, P]) extends + FractionalPower[Refined[V, P]]: + def pow(v: Refined[V, P], e: Double): Refined[V, P] = + refineV[P].unsafeFrom(alg.pow(v.value, e)) + + class ASGRE[V, P](using alg: AdditiveSemigroup[Refined[V, P]]) extends + AdditiveSemigroup[Either[String, Refined[V, P]]]: + def plus(x: Either[String, Refined[V, P]], y: Either[String, Refined[V, P]]) = + (x, y) match + case (Left(xe), Left(ye)) => Left(s"($xe)($ye)") + case (Left(xe), Right(_)) => Left(xe) + case (Right(_), Left(ye)) => Left(ye) + case (Right(xv), Right(yv)) => + Try(alg.plus(xv, yv)) match + case Success(z) => Right(z) + case Failure(e) => Left(e.getMessage) + + class MSGRE[V, P](using alg: MultiplicativeSemigroup[Refined[V, P]]) extends + MultiplicativeSemigroup[Either[String, Refined[V, P]]]: + def times(x: Either[String, Refined[V, P]], y: Either[String, Refined[V, P]]) = + (x, y) match + case (Left(xe), Left(ye)) => Left(s"($xe)($ye)") + case (Left(xe), Right(_)) => Left(xe) + case (Right(_), Left(ye)) => Left(ye) + case (Right(xv), Right(yv)) => + Try(alg.times(xv, yv)) match + case Success(z) => Right(z) + case Failure(e) => Left(e.getMessage) + + class MMRE[V, P](using alg: MultiplicativeMonoid[Refined[V, P]]) extends + MSGRE[V, P] with MultiplicativeMonoid[Either[String, Refined[V, P]]]: + def one: Either[String, Refined[V, P]] = + Try(alg.one) match + case Success(z) => Right(z) + case Failure(e) => Left(e.getMessage) + + class MGRE[V, P](using alg: MultiplicativeGroup[Refined[V, P]]) extends + MMRE[V, P] with MultiplicativeGroup[Either[String, Refined[V, P]]]: + def div(x: Either[String, Refined[V, P]], y: Either[String, Refined[V, P]]) = + (x, y) match + case (Left(xe), Left(ye)) => Left(s"($xe)($ye)") + case (Left(xe), Right(_)) => Left(xe) + case (Right(_), Left(ye)) => Left(ye) + case (Right(xv), Right(yv)) => + Try(alg.div(xv, yv)) match + case Success(z) => Right(z) + case Failure(e) => Left(e.getMessage) + + class FPRE[V, P](using alg: FractionalPower[Refined[V, P]]) extends + FractionalPower[Either[String, Refined[V, P]]]: + def pow(v: Either[String, Refined[V, P]], e: Double): Either[String, Refined[V, P]] = + Try(v.map(alg.pow(_, e))) match + case Success(z) => z + case Failure(e) => Left(e.getMessage) diff --git a/refined/src/main/scala/coulomb/ops/resolution/refined.scala b/refined/src/main/scala/coulomb/ops/resolution/refined.scala new file mode 100644 index 000000000..fb96ca221 --- /dev/null +++ b/refined/src/main/scala/coulomb/ops/resolution/refined.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2022 Erik Erlandson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coulomb.ops.resolution + +import eu.timepit.refined.* +import eu.timepit.refined.api.* +import eu.timepit.refined.numeric.* + +import coulomb.ops.ValueResolution + +import coulomb.policy.priority.* + +object refined: + transparent inline given ctx_VR_Refined_Positive[VL, VR, Positive](using Prio0)(using + vres: ValueResolution[VL, VR] + ): ValueResolution[Refined[VL, Positive], Refined[VR, Positive]] = + new ValueResolution.NC[Refined[VL, Positive], Refined[VR, Positive], Refined[vres.VO, Positive]] + + transparent inline given ctx_VR_Refined_NonNegative[VL, VR, NonNegative](using Prio1)(using + vres: ValueResolution[VL, VR] + ): ValueResolution[Refined[VL, NonNegative], Refined[VR, NonNegative]] = + new ValueResolution.NC[Refined[VL, NonNegative], Refined[VR, NonNegative], Refined[vres.VO, NonNegative]] + + transparent inline given ctx_VR_Refined_Either[VL, VR, P](using + vres: ValueResolution[Refined[VL, P], Refined[VR, P]] + ): ValueResolution[Either[String, Refined[VL, P]], Either[String, Refined[VR, P]]] = + new ValueResolution.NC[Either[String, Refined[VL, P]], Either[String, Refined[VR, P]], Either[String, vres.VO]] diff --git a/refined/src/main/scala/coulomb/policy/overlay/refined/policy.scala b/refined/src/main/scala/coulomb/policy/overlay/refined/policy.scala new file mode 100644 index 000000000..038d938db --- /dev/null +++ b/refined/src/main/scala/coulomb/policy/overlay/refined/policy.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2022 Erik Erlandson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coulomb.policy.overlay.refined + +object algebraic: + export coulomb.ops.algebra.refined.all.given + export coulomb.ops.resolution.refined.given + export coulomb.conversion.refined.value.given + export coulomb.conversion.refined.unit.given diff --git a/refined/src/main/scala/coulomb/syntax/refined.scala b/refined/src/main/scala/coulomb/syntax/refined.scala new file mode 100644 index 000000000..c67aab126 --- /dev/null +++ b/refined/src/main/scala/coulomb/syntax/refined.scala @@ -0,0 +1,53 @@ +/* + * Copyright 2022 Erik Erlandson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coulomb.syntax.refined + +import coulomb.* +import coulomb.syntax.* + +import eu.timepit.refined.* +import eu.timepit.refined.api.* + +/** + * Lift a raw value into a unit quantity with a Refined value type + * @tparam P + * the Refined predicate type (e.g. Positive, NonNegative) + * @tparam U + * the desired unit type + * @param v + * the raw value to lift + * @return + * a unit quantity whose value is refined by P + * {{{ + * val dist = refineVU[NonNegative, Meter](1.0) + * }}} + */ +def refineVU[P, U]: infra.ApplyRefineVU[P, U] = + new infra.ApplyRefineVU[P, U] + +extension [V](v: V) + def withRefinedUnit[P, U](using + Validate[V, P] + ): Quantity[Either[String, Refined[V, P]], U] = + refineV[P](v).withUnit[U] + +object infra: + class ApplyRefineVU[P, U]: + def apply[V](v: V)(using + Validate[V, P] + ): Quantity[Either[String, Refined[V, P]], U] = + refineV[P](v).withUnit[U] diff --git a/refined/src/test/scala/coulomb/quantity.scala b/refined/src/test/scala/coulomb/quantity.scala new file mode 100644 index 000000000..402a1e3ea --- /dev/null +++ b/refined/src/test/scala/coulomb/quantity.scala @@ -0,0 +1,315 @@ +/* + * Copyright 2022 Erik Erlandson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import coulomb.testing.CoulombSuite + +class RefinedQuantityAlgebraicSuite extends CoulombSuite: + import eu.timepit.refined.* + import eu.timepit.refined.api.* + import eu.timepit.refined.numeric.* + + import coulomb.* + import coulomb.syntax.* + import coulomb.syntax.refined.* + + import algebra.instances.all.given + import coulomb.ops.algebra.all.{*, given} + + import coulomb.units.si.{*, given} + import coulomb.units.si.prefixes.{*, given} + import coulomb.units.mksa.{*, given} + import coulomb.units.us.{*, given} + + import coulomb.testing.refined.* + + type RefinedE[V, P] = Either[String, Refined[V, P]] + + test("lift") { + 1d.withRP[Positive].withUnit[Meter].assertQ[Refined[Double, Positive], Meter](1d.withRP[Positive]) + 1f.withRP[Positive].withUnit[Meter].assertQ[Refined[Float, Positive], Meter](1f.withRP[Positive]) + 1L.withRP[Positive].withUnit[Meter].assertQ[Refined[Long, Positive], Meter](1L.withRP[Positive]) + 1.withRP[Positive].withUnit[Meter].assertQ[Refined[Int, Positive], Meter](1.withRP[Positive]) + + 1d.withRP[NonNegative].withUnit[Meter].assertQ[Refined[Double, NonNegative], Meter](1d.withRP[NonNegative]) + 1f.withRP[NonNegative].withUnit[Meter].assertQ[Refined[Float, NonNegative], Meter](1f.withRP[NonNegative]) + 1L.withRP[NonNegative].withUnit[Meter].assertQ[Refined[Long, NonNegative], Meter](1L.withRP[NonNegative]) + 1.withRP[NonNegative].withUnit[Meter].assertQ[Refined[Int, NonNegative], Meter](1.withRP[NonNegative]) + + refineV[Positive](1d).withUnit[Meter] + .assertQ[RefinedE[Double, Positive], Meter](refineV[Positive](1d)) + refineVU[Positive, Meter](1d) + .assertQ[RefinedE[Double, Positive], Meter](refineV[Positive](1d)) + 1d.withRefinedUnit[Positive, Meter] + .assertQ[RefinedE[Double, Positive], Meter](refineV[Positive](1d)) + } + + test("value") { + 1.withRP[Positive].withUnit[Meter].value.assertVT[Refined[Int, Positive]](1.withRP[Positive]) + + refineVU[Positive, Meter](1).value.assertVT[RefinedE[Int, Positive]](refineV[Positive](1)) + } + + test("show") { + assertEquals(1.withRP[Positive].withUnit[Meter].show, "1 m") + assertEquals(refineVU[Positive, Meter](1).show, "Right(1) m") + } + + test("showFull") { + assertEquals(1.withRP[NonNegative].withUnit[Second].showFull, "1 second") + assertEquals(refineVU[Positive, Meter](1).showFull, "Right(1) meter") + } + + test("toValue") { + import coulomb.policy.strict.given + import coulomb.policy.overlay.refined.algebraic.given + + 1.withRP[Positive].withUnit[Meter].toValue[Refined[Double, Positive]] + .assertQ[Refined[Double, Positive], Meter](1d.withRP[Positive]) + 1L.withRP[NonNegative].withUnit[Meter].toValue[Refined[Float, NonNegative]] + .assertQ[Refined[Float, NonNegative], Meter](1f.withRP[NonNegative]) + + assertCE("1d.withRP[Positive].withUnit[Meter].toValue[Refined[Int, Positive]]") + + 1.5.withRP[Positive].withUnit[Meter].tToValue[Refined[Int, Positive]] + .assertQ[Refined[Int, Positive], Meter](1.withRP[Positive]) + 1.5f.withRP[NonNegative].withUnit[Meter].tToValue[Refined[Int, NonNegative]] + .assertQ[Refined[Int, NonNegative], Meter](1.withRP[NonNegative]) + + refineVU[Positive, Meter](1).toValue[RefinedE[Double, Positive]] + .assertQ[RefinedE[Double, Positive], Meter](refineV[Positive](1d)) + assert(refineVU[Positive, Meter](Double.MinPositiveValue) + .toValue[RefinedE[Float, Positive]].value.isLeft) + } + + test("toUnit") { + import coulomb.policy.strict.given + import coulomb.policy.overlay.refined.algebraic.given + + 1d.withRP[Positive].withUnit[Kilo * Meter].toUnit[Meter] + .assertQ[Refined[Double, Positive], Meter](1000d.withRP[Positive]) + 1f.withRP[NonNegative].withUnit[Kilo * Meter].toUnit[Meter] + .assertQ[Refined[Float, NonNegative], Meter](1000f.withRP[NonNegative]) + + assertCE("1.withRP[Positive].withUnit[Kilo * Meter].toUnit[Meter]") + + 1.withRP[Positive].withUnit[Meter].tToUnit[Yard] + .assertQ[Refined[Int, Positive], Yard](1.withRP[Positive]) + 1.withRP[NonNegative].withUnit[Meter].tToUnit[Yard] + .assertQ[Refined[Int, NonNegative], Yard](1.withRP[NonNegative]) + + refineVU[Positive, Kilo * Meter](1d).toUnit[Meter] + .assertQ[RefinedE[Double, Positive], Meter](refineV[Positive](1000d)) + assert(refineVU[Positive, Meter](Double.MinPositiveValue) + .toUnit[Kilo * Meter].value.isLeft) + } + + test("add strict") { + import coulomb.policy.strict.given + import coulomb.policy.overlay.refined.algebraic.given + + (1d.withRP[Positive].withUnit[Meter] + 2d.withRP[Positive].withUnit[Meter]) + .assertQ[Refined[Double, Positive], Meter](3d.withRP[Positive]) + (1.withRP[Positive].withUnit[Meter] + 2.withRP[Positive].withUnit[Meter]) + .assertQ[Refined[Int, Positive], Meter](3.withRP[Positive]) + + (1f.withRP[NonNegative].withUnit[Meter] + 2f.withRP[NonNegative].withUnit[Meter]) + .assertQ[Refined[Float, NonNegative], Meter](3f.withRP[NonNegative]) + (1L.withRP[NonNegative].withUnit[Meter] + 2L.withRP[NonNegative].withUnit[Meter]) + .assertQ[Refined[Long, NonNegative], Meter](3L.withRP[NonNegative]) + + // differing value or unit types are not supported in strict policy + assertCE("1.withRP[Positive].withUnit[Meter] + 2d.withRP[Positive].withUnit[Meter]") + assertCE("1d.withRP[NonNegative].withUnit[Meter] + 2d.withRP[Positive].withUnit[Meter]") + assertCE("1d.withRP[Positive].withUnit[Meter] + 2d.withRP[Positive].withUnit[Yard]") + + val x = refineVU[Positive, Meter](1d) + val z = refineVU[Positive, Meter](0d) + (x + x).assertQ[RefinedE[Double, Positive], Meter](refineV[Positive](2d)) + assert((x + z).value.isLeft) + assert((z + x).value.isLeft) + assert((z + z).value.isLeft) + + // args valid but final operation violates predicate + val v = refineVU[Positive, Meter](2000000000) + assert(v.value.isRight) + assert((v + v).value.isLeft) + } + + test("add standard") { + import coulomb.policy.standard.given + import coulomb.policy.overlay.refined.algebraic.given + + // same unit and value type + (1d.withRP[Positive].withUnit[Meter] + 2d.withRP[Positive].withUnit[Meter]) + .assertQ[Refined[Double, Positive], Meter](3d.withRP[Positive]) + + // same unit, differing value types + (1L.withRP[NonNegative].withUnit[Meter] + 2f.withRP[NonNegative].withUnit[Meter]) + .assertQ[Refined[Float, NonNegative], Meter](3f.withRP[NonNegative]) + + // same value, differing units + (1f.withRP[Positive].withUnit[Meter] + 1f.withRP[Positive].withUnit[Kilo * Meter]) + .assertQ[Refined[Float, Positive], Meter](1001f.withRP[Positive]) + + // value and unit type are different + (1.withRP[Positive].withUnit[Meter] + 1d.withRP[Positive].withUnit[Kilo * Meter]) + .assertQ[Refined[Double, Positive], Meter](1001d.withRP[Positive]) + (1f.withRP[NonNegative].withUnit[Meter] + 1L.withRP[NonNegative].withUnit[Kilo * Meter]) + .assertQ[Refined[Float, NonNegative], Meter](1001f.withRP[NonNegative]) + + val x = refineVU[Positive, Meter](1d) + val y = refineVU[Positive, Kilo * Meter](1) + val z = refineVU[Positive, Kilo * Meter](0) + (x + y).assertQ[RefinedE[Double, Positive], Meter](refineV[Positive](1001d)) + assert((x + z).value.isLeft) + assert((z + x).value.isLeft) + assert((z + z).value.isLeft) + } + + test("multiply strict") { + import coulomb.policy.strict.given + import coulomb.policy.overlay.refined.algebraic.given + + (2d.withRP[Positive].withUnit[Meter] * 3d.withRP[Positive].withUnit[Meter]) + .assertQ[Refined[Double, Positive], Meter ^ 2](6d.withRP[Positive]) + (2.withRP[NonNegative].withUnit[Meter] * 3.withRP[NonNegative].withUnit[Meter]) + .assertQ[Refined[Int, NonNegative], Meter ^ 2](6.withRP[NonNegative]) + + // differing value types are not supported in strict policy + assertCE("2.withRP[Positive].withUnit[Meter] * 3d.withRP[Positive].withUnit[Meter]") + assertCE("2d.withRP[NonNegative].withUnit[Meter] * 3d.withRP[Positive].withUnit[Meter]") + + val x = refineVU[Positive, Meter](2d) + val z = refineVU[Positive, Meter](0d) + (x * x).assertQ[RefinedE[Double, Positive], Meter ^ 2](refineV[Positive](4d)) + assert((x * z).value.isLeft) + assert((z * x).value.isLeft) + assert((z * z).value.isLeft) + + // args valid but final operation violates predicate + val v = refineVU[Positive, Meter](Double.MinPositiveValue) + assert(v.value.isRight) + assert((v * v).value.isLeft) + } + + test("multiply standard") { + import coulomb.policy.standard.given + import coulomb.policy.overlay.refined.algebraic.given + + (2f.withRP[Positive].withUnit[Meter] * 3f.withRP[Positive].withUnit[Meter]) + .assertQ[Refined[Float, Positive], Meter ^ 2](6f.withRP[Positive]) + (2L.withRP[NonNegative].withUnit[Meter] * 3L.withRP[NonNegative].withUnit[Meter]) + .assertQ[Refined[Long, NonNegative], Meter ^ 2](6L.withRP[NonNegative]) + + (2.withRP[Positive].withUnit[Meter] * 3d.withRP[Positive].withUnit[Meter]) + .assertQ[Refined[Double, Positive], Meter ^ 2](6d.withRP[Positive]) + (2f.withRP[NonNegative].withUnit[Meter] * 3L.withRP[NonNegative].withUnit[Meter]) + .assertQ[Refined[Float, NonNegative], Meter ^ 2](6f.withRP[NonNegative]) + + val x = refineVU[Positive, Meter](2d) + val y = refineVU[Positive, Meter](2) + val z = refineVU[Positive, Meter](0) + (x * y).assertQ[RefinedE[Double, Positive], Meter ^ 2](refineV[Positive](4d)) + assert((x * z).value.isLeft) + assert((z * x).value.isLeft) + assert((z * z).value.isLeft) + } + + test("divide strict") { + import coulomb.policy.strict.given + import coulomb.policy.overlay.refined.algebraic.given + + (12d.withRP[Positive].withUnit[Meter] / 3d.withRP[Positive].withUnit[Second]) + .assertQ[Refined[Double, Positive], Meter / Second](4d.withRP[Positive]) + (12f.withRP[Positive].withUnit[Meter] / 3f.withRP[Positive].withUnit[Second]) + .assertQ[Refined[Float, Positive], Meter / Second](4f.withRP[Positive]) + + // differing values types not supported + assertCE("12d.withRP[Positive].withUnit[Meter] / 3f.withRP[Positive].withUnit[Second]") + // NonNegative is not multiplicative group + assertCE("12d.withRP[NonNegative].withUnit[Meter] / 3d.withRP[NonNegative].withUnit[Second]") + // Integrals are not a multiplicative group + assertCE("12.withRP[Positive].withUnit[Meter] / 3.withRP[Positive].withUnit[Second]") + + val x = refineVU[Positive, Meter](2d) + val z = refineVU[Positive, Meter](0d) + (x / x).assertQ[RefinedE[Double, Positive], 1](refineV[Positive](1d)) + assert((x / z).value.isLeft) + assert((z / x).value.isLeft) + assert((z / z).value.isLeft) + + val v = refineVU[Positive, Meter](Double.MinPositiveValue) + assert(v.value.isRight) + assert((v / x).value.isLeft) + } + + test("divide standard") { + import coulomb.policy.standard.given + import coulomb.policy.overlay.refined.algebraic.given + + (12d.withRP[Positive].withUnit[Meter] / 3d.withRP[Positive].withUnit[Second]) + .assertQ[Refined[Double, Positive], Meter / Second](4d.withRP[Positive]) + + (12d.withRP[Positive].withUnit[Meter] / 3.withRP[Positive].withUnit[Second]) + .assertQ[Refined[Double, Positive], Meter / Second](4d.withRP[Positive]) + (12.withRP[Positive].withUnit[Meter] / 3f.withRP[Positive].withUnit[Second]) + .assertQ[Refined[Float, Positive], Meter / Second](4f.withRP[Positive]) + + // NonNegative is not multiplicative group + assertCE("12d.withRP[NonNegative].withUnit[Meter] / 3d.withRP[NonNegative].withUnit[Second]") + // Integrals are not a multiplicative group + assertCE("12.withRP[Positive].withUnit[Meter] / 3.withRP[Positive].withUnit[Second]") + + val x = refineVU[Positive, Meter](6d) + val y = refineVU[Positive, Meter](2) + val z = refineVU[Positive, Meter](0d) + (x / y).assertQ[RefinedE[Double, Positive], 1](refineV[Positive](3d)) + assert((x / z).value.isLeft) + assert((z / x).value.isLeft) + assert((z / z).value.isLeft) + } + + test("power") { + import coulomb.policy.strict.given + import coulomb.policy.overlay.refined.algebraic.given + + // FractionalPower (algebras supporting rational exponents) + 2d.withRP[Positive].withUnit[Meter].pow[0] + .assertQ[Refined[Double, Positive], 1](1d.withRP[Positive]) + 2d.withRP[Positive].withUnit[Meter].pow[2] + .assertQ[Refined[Double, Positive], Meter ^ 2](4d.withRP[Positive]) + 2d.withRP[Positive].withUnit[Meter].pow[-1] + .assertQ[Refined[Double, Positive], 1 / Meter](0.5d.withRP[Positive]) + 4d.withRP[Positive].withUnit[Meter].pow[1 / 2] + .assertQ[Refined[Double, Positive], Meter ^ (1 / 2)](2d.withRP[Positive]) + + // non-negative integer exponents allowed by multiplicative monoid + 2.withRP[Positive].withUnit[Meter].pow[0] + .assertQ[Refined[Int, Positive], 1](1.withRP[Positive]) + 2.withRP[Positive].withUnit[Meter].pow[2] + .assertQ[Refined[Int, Positive], Meter ^ 2](4.withRP[Positive]) + assertCE("2.withRP[Positive].withUnit[Meter].pow[-1]") + + val x = refineVU[Positive, Meter](2d) + val z = refineVU[Positive, Meter](0d) + x.pow[0].assertQ[RefinedE[Double, Positive], 1](refineV[Positive](1d)) + x.pow[2].assertQ[RefinedE[Double, Positive], Meter ^ 2](refineV[Positive](4d)) + assert(z.pow[2].value.isLeft) + + val v = refineVU[Positive, Meter](Double.MinPositiveValue) + assert(v.value.isRight) + assert(v.pow[2].value.isLeft) + } diff --git a/refined/src/test/scala/coulomb/testutils.scala b/refined/src/test/scala/coulomb/testutils.scala new file mode 100644 index 000000000..6328a3144 --- /dev/null +++ b/refined/src/test/scala/coulomb/testutils.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Erik Erlandson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package coulomb.testing.refined + +import eu.timepit.refined.* +import eu.timepit.refined.api.* +import eu.timepit.refined.numeric.* + +extension [V](v: V) + // a workaround for refineMV not being available in scala3 + // https://github.com/fthomas/refined/issues/932 + def withRP[P](using Validate[V, P]): Refined[V, P] = + refineV[P].unsafeFrom(v)