diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4197df8bf..2bab40fe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,11 +80,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 runtime/.js/target spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: mkdir -p collections/.jvm/target runtime/.js/target spire/.js/target spire/.jvm/target collections/.js/target collections/.native/target testkit/.native/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.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 runtime/.js/target spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: tar cf targets.tar collections/.jvm/target runtime/.js/target spire/.js/target spire/.jvm/target collections/.js/target collections/.native/target testkit/.native/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.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 e1ae53893..d88c70324 100644 --- a/build.sbt +++ b/build.sbt @@ -3,7 +3,7 @@ // and check in the updates to github workflow yamls // base version for assessing MIMA -ThisBuild / tlBaseVersion := "0.8" +ThisBuild / tlBaseVersion := "0.9" // publish settings // artifacts now publish to s01.oss.sonatype.org, per: @@ -45,6 +45,7 @@ lazy val root = tlCrossRootProject .aggregate( core, units, + collections, runtime, parser, pureconfig, @@ -74,6 +75,20 @@ lazy val units = crossProject(JVMPlatform, JSPlatform, NativePlatform) libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.5.0" % Test ) +lazy val collections = crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .in(file("collections")) + .settings(name := "coulomb-collections") + .dependsOn( + core % "compile->compile;test->test", + units % Test + ) + .settings( + tlVersionIntroduced := Map("3" -> "0.8.1") + ) + .settings(commonSettings: _*) + .settings(libraryDependencies += "org.typelevel" %%% "algebra" % "2.10.0") + // see also: https://github.com/lampepfl/dotty/issues/7647 lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) @@ -179,6 +194,7 @@ lazy val all = project .dependsOn( core.jvm, units.jvm, + collections.jvm, runtime.jvm, parser.jvm, pureconfig.jvm, @@ -186,6 +202,12 @@ lazy val all = project refined.jvm ) // scala repl only needs JVMPlatform subproj builds .settings(name := "coulomb-all") + .settings( + // when you import in the REPL, this warning is ridiculous + Compile / console.key / scalacOptions ~= (_.filterNot { + _ == "-Wunused:imports" + }) + ) .enablePlugins(NoPublishPlugin) // don't publish // a published artifact aggregating API docs for viewing at javadoc.io @@ -209,6 +231,7 @@ lazy val docs = project .dependsOn( core.jvm, units.jvm, + collections.jvm, runtime.jvm, parser.jvm, pureconfig.jvm, diff --git a/collections/src/main/scala/coulomb/collection/immutable/vector.scala b/collections/src/main/scala/coulomb/collection/immutable/vector.scala new file mode 100644 index 000000000..6266419e0 --- /dev/null +++ b/collections/src/main/scala/coulomb/collection/immutable/vector.scala @@ -0,0 +1,188 @@ +/* + * 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.collection.immutable + +import scala.collection.{AbstractIterator, StrictOptimizedSeqOps, View, mutable} +import scala.collection.immutable.{IndexedSeq, IndexedSeqOps} +import scala.reflect.ClassTag + +import coulomb.* +import coulomb.syntax.* +import coulomb.conversion.* + +final class QuantityVector[V, U] private ( + val values: Vector[V] +) extends IndexedSeq[Quantity[V, U]], + IndexedSeqOps[Quantity[V, U], IndexedSeq, QuantityVector[V, U]], + StrictOptimizedSeqOps[Quantity[V, U], IndexedSeq, QuantityVector[V, U]]: + + override def className = "QuantityVector" + + def length: Int = values.length + + def apply(idx: Int): Quantity[V, U] = + values(idx).withUnit[U] + + override def iterator: Iterator[Quantity[V, U]] = + values.iterator.map(_.withUnit[U]) + + inline def ++(suffix: IterableOnce[Quantity[V, U]]): QuantityVector[V, U] = + concat(suffix) + + inline def ++[VS, US](suffix: IterableOnce[Quantity[VS, US]])(using + qc: scala.Conversion[Quantity[VS, US], Quantity[V, U]] + ): QuantityVector[V, U] = + concat(suffix) + + def concat(suffix: IterableOnce[Quantity[V, U]]): QuantityVector[V, U] = + val svec: Vector[V] = suffix match + case qve: QuantityVector[?, ?] => + qve.asInstanceOf[QuantityVector[V, U]].values + case seqe: scala.collection.Seq[?] => + val seq = + seqe.asInstanceOf[scala.collection.Seq[Quantity[V, U]]] + Vector.from(seq.map(_.value)) + case _ => + Vector.from(suffix.iterator.map(_.value)) + new QuantityVector[V, U](values ++ svec) + + def concat[VS, US](suffix: IterableOnce[Quantity[VS, US]])(using + cnv: scala.Conversion[Quantity[VS, US], Quantity[V, U]] + ): QuantityVector[V, U] = + val svec: Vector[V] = cnv match + // if we have a QuantityConversion we can optimize + // by applying directly to raw values + case qce: QuantityConversion[?, ?, ?, ?] => + val qc = qce.asInstanceOf[QuantityConversion[VS, US, V, U]] + suffix match + case qve: QuantityVector[?, ?] => + qve.asInstanceOf[QuantityVector[VS, US]] + .values + .map(qc.raw) + case seqe: scala.collection.Seq[?] => + val seq = seqe.asInstanceOf[ + scala.collection.Seq[Quantity[VS, US]] + ] + Vector.from(seq.map { e => qc.raw(e.value) }) + case _ => + Vector.from(suffix.iterator.map { e => + qc.raw(e.value) + }) + case _ => + // if it isn't a QuantityConversion, we can only assume basic + // scala.Conversion function + suffix match + case qve: QuantityVector[?, ?] => + qve.asInstanceOf[QuantityVector[VS, US]].map(cnv).values + case seqe: scala.collection.Seq[?] => + val seq = seqe.asInstanceOf[ + scala.collection.Seq[Quantity[VS, US]] + ] + Vector.from(seq.map(cnv(_).value)) + case _ => + Vector.from(suffix.iterator.map(cnv(_).value)) + new QuantityVector[V, U](values ++ svec) + + def map[VF, UF]( + f: Quantity[V, U] => Quantity[VF, UF] + ): QuantityVector[VF, UF] = + strictOptimizedMap(newSpecificBuilderQV[VF, UF], f) + + def flatMap[VF, UF]( + f: Quantity[V, U] => IterableOnce[Quantity[VF, UF]] + ): QuantityVector[VF, UF] = + strictOptimizedFlatMap(newSpecificBuilderQV[VF, UF], f) + + override def empty: QuantityVector[V, U] = QuantityVector.empty[V, U] + + override protected def fromSpecific( + it: IterableOnce[Quantity[V, U]] + ): QuantityVector[V, U] = + QuantityVector.from(it) + + override protected def newSpecificBuilder + : mutable.Builder[Quantity[V, U], QuantityVector[V, U]] = + newSpecificBuilderQV[V, U] + + protected def newSpecificBuilderQV[VB, UB] + : mutable.Builder[Quantity[VB, UB], QuantityVector[VB, UB]] = + mutable.ArrayBuffer + .newBuilder[Quantity[VB, UB]] + .mapResult(QuantityVector.from) + + def toValue[VO](using + vc: ValueConversion[V, VO] + ): QuantityVector[VO, U] = + QuantityVector[U](values.map { v => vc(v) }) + + def toUnit[UO](using + uc: UnitConversion[V, U, UO] + ): QuantityVector[V, UO] = + QuantityVector[UO](values.map { v => uc(v) }) + + def toVU[VO, UO](using + vc: ValueConversion[V, VO], + uc: UnitConversion[VO, U, UO] + ): QuantityVector[VO, UO] = + QuantityVector[UO](values.map { v => uc(vc(v)) }) + +object QuantityVector: + def apply[U](using a: Applier[U]) = a + + class Applier[U]: + def apply[V](vs: IterableOnce[V]): QuantityVector[V, U] = + new QuantityVector[V, U](Vector.from(vs)) + def apply[V](vs: V*): QuantityVector[V, U] = + new QuantityVector[V, U](Vector.from(vs)) + + object Applier: + given ctx_Applier[U]: Applier[U] = new Applier[U] + + def apply[V, U](args: Quantity[V, U]*): QuantityVector[V, U] = + from(args) + + def empty[V, U]: QuantityVector[V, U] = + new QuantityVector[V, U](Vector.empty[V]) + + def from[V, U](it: IterableOnce[Quantity[V, U]]): QuantityVector[V, U] = + it match + case qv: QuantityVector[?, ?] => + // this trick works because we already know that the + // element type is Quantity[V, U] + qv.asInstanceOf[QuantityVector[V, U]] + case seq: scala.collection.Seq[?] => + val qs = seq.asInstanceOf[scala.collection.Seq[Quantity[V, U]]] + new QuantityVector[V, U](Vector.from(qs.map(_.value))) + case _ => + new QuantityVector[V, U](Vector.from(it.iterator.map(_.value))) + +object benchmark: + import java.time.* + + def time[X](x: => X, n: Int = 11): Double = { + var t0 = Instant.now.toEpochMilli + val times = for { + _ <- 0 until n + } yield { + val _ = x + val t = Instant.now.toEpochMilli + val tt = (t - t0).toDouble / 1000.0 + t0 = t + tt + } + times(times.length / 2) + } diff --git a/core/src/main/scala/coulomb/conversion/conversion.scala b/core/src/main/scala/coulomb/conversion/conversion.scala index fda538058..bee20fdc1 100644 --- a/core/src/main/scala/coulomb/conversion/conversion.scala +++ b/core/src/main/scala/coulomb/conversion/conversion.scala @@ -18,6 +18,9 @@ package coulomb.conversion import scala.annotation.implicitNotFound +import coulomb.* +import coulomb.syntax.* + /** conversion of value types, assuming some constant unit type */ @implicitNotFound("No value conversion in scope for value types ${VF} => ${VT}") abstract class ValueConversion[VF, VT] extends (VF => VT) @@ -49,3 +52,12 @@ abstract class DeltaUnitConversion[V, B, UF, UT] extends (V => V) "No truncating unit conversion in scope for value type ${V}, unit types ${UF} => ${UT}" ) abstract class TruncatingDeltaUnitConversion[V, B, UF, UT] extends (V => V) + +/** scala.Conversion for Quantity[VF, UF] => Quantity[VT, UT] */ +@implicitNotFound( + "No Conversion in context for Quantity[${VF}, ${UF}] => Quantity[${VT}, ${UT}]" +) +class QuantityConversion[VF, UF, VT, UT](val raw: VF => VT) + extends _root_.scala.Conversion[Quantity[VF, UF], Quantity[VT, UT]]: + inline def apply(q: Quantity[VF, UF]): Quantity[VT, UT] = + raw(q.value).withUnit[UT] diff --git a/core/src/main/scala/coulomb/conversion/standard/scala.scala b/core/src/main/scala/coulomb/conversion/standard/scala.scala index bf170676e..99f414f9e 100644 --- a/core/src/main/scala/coulomb/conversion/standard/scala.scala +++ b/core/src/main/scala/coulomb/conversion/standard/scala.scala @@ -26,25 +26,24 @@ object scala: // whenever a valid conversion exists: // https://docs.scala-lang.org/scala3/reference/contextual/conversions.html - given ctx_Quantity_Conversion_1V1U[V, U] - : Conversion[Quantity[V, U], Quantity[V, U]] = - (q: Quantity[V, U]) => q + given ctx_Quantity_Conversion_1V1U[V, U]: QuantityConversion[V, U, V, U] = + new QuantityConversion[V, U, V, U]((v: V) => v) given ctx_Quantity_Conversion_1V2U[V, UF, UT](using uc: UnitConversion[V, UF, UT] - ): Conversion[Quantity[V, UF], Quantity[V, UT]] = - (q: Quantity[V, UF]) => uc(q.value).withUnit[UT] + ): QuantityConversion[V, UF, V, UT] = + new QuantityConversion[V, UF, V, UT](uc) given ctx_Quantity_Conversion_2V1U[U, VF, VT](using vc: ValueConversion[VF, VT] - ): Conversion[Quantity[VF, U], Quantity[VT, U]] = - (q: Quantity[VF, U]) => vc(q.value).withUnit[U] + ): QuantityConversion[VF, U, VT, U] = + new QuantityConversion[VF, U, VT, U](vc) given ctx_Quantity_Conversion_2V2U[VF, UF, VT, UT](using vc: ValueConversion[VF, VT], uc: UnitConversion[VT, UF, UT] - ): Conversion[Quantity[VF, UF], Quantity[VT, UT]] = - (q: Quantity[VF, UF]) => uc(vc(q.value)).withUnit[UT] + ): QuantityConversion[VF, UF, VT, UT] = + new QuantityConversion[VF, UF, VT, UT]((v: VF) => uc(vc(v))) given ctx_DeltaQuantity_conversion_2V2U[B, VF, UF, VT, UT](using vc: ValueConversion[VF, VT],