Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coulomb collections #530

Open
wants to merge 9 commits into
base: scala3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
25 changes: 24 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -45,6 +45,7 @@ lazy val root = tlCrossRootProject
.aggregate(
core,
units,
collections,
runtime,
parser,
pureconfig,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -179,13 +194,20 @@ lazy val all = project
.dependsOn(
core.jvm,
units.jvm,
collections.jvm,
runtime.jvm,
parser.jvm,
pureconfig.jvm,
spire.jvm,
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
Expand All @@ -209,6 +231,7 @@ lazy val docs = project
.dependsOn(
core.jvm,
units.jvm,
collections.jvm,
runtime.jvm,
parser.jvm,
pureconfig.jvm,
Expand Down
188 changes: 188 additions & 0 deletions collections/src/main/scala/coulomb/collection/immutable/vector.scala
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 12 additions & 0 deletions core/src/main/scala/coulomb/conversion/conversion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
17 changes: 8 additions & 9 deletions core/src/main/scala/coulomb/conversion/standard/scala.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down