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

integrate with typelevel refined package #73

Merged
merged 37 commits into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
167df93
refined integrations
erikerlandson Apr 4, 2020
3f5e96b
toRefined and withRefinedUnit
erikerlandson Apr 18, 2020
eb0eb84
rename internal toRefined -> applyPred
erikerlandson Apr 18, 2020
d66373c
basic operator testing
erikerlandson Apr 18, 2020
608dab8
add some unsound behaviors testing for addition
erikerlandson Apr 18, 2020
c406d60
better behavior coverage for addition
erikerlandson Apr 19, 2020
2d35323
fill out subtraction tests
erikerlandson Apr 19, 2020
12fcab0
tests for multiplication
erikerlandson Apr 19, 2020
f6f472c
test refined division
erikerlandson Apr 19, 2020
8ec048d
refined power tests
erikerlandson Apr 19, 2020
04fffe2
unary negative test for refined
erikerlandson Apr 20, 2020
dff4a36
refined ordering comparisons
erikerlandson Apr 20, 2020
8b80c86
refined quantity conversions testing
erikerlandson Apr 20, 2020
78fde17
applyUnitExpr method for constructing quantities from i/o
erikerlandson Apr 22, 2020
bb9d5f2
improvements to pureconfig quantity i/o
erikerlandson Apr 22, 2020
15af84d
clean up imports
erikerlandson Apr 22, 2020
2b9687c
coulomb-pureconfig-refined
erikerlandson Apr 22, 2020
f520ba3
readme for coulomb-refined
erikerlandson Apr 22, 2020
5e4e7d3
fix quote
erikerlandson Apr 22, 2020
becd9ad
fix bug handling empty prefix units
erikerlandson Apr 23, 2020
0fd3a7d
new withImports constructor for QuantityParser
erikerlandson Apr 23, 2020
6e28095
test for withImports and applyUnitExpr
erikerlandson Apr 23, 2020
ff8f930
add comment about empty pfunit list
erikerlandson Apr 23, 2020
5968556
clean up imports
erikerlandson Apr 23, 2020
c6dc8c3
scala 2.13.2
erikerlandson Apr 23, 2020
b2d6a4f
scaladoc for QuantityParser methods
erikerlandson Apr 23, 2020
f87c6d4
decouple ConfigReader from ConfigWriter
erikerlandson Apr 23, 2020
1311e57
update pureconfig deps
erikerlandson Apr 23, 2020
561e7b3
fix pureconfig doc
erikerlandson Apr 23, 2020
8dedb92
coulomb-pureconfig-refined/README.md
erikerlandson Apr 23, 2020
6c93fc3
links for coulomb-refined and coulomb-pureconfig-refined
erikerlandson Apr 23, 2020
53088fb
clean up sub-package tutorial examples
erikerlandson Apr 24, 2020
3c95b4e
fix example output
erikerlandson Apr 24, 2020
7fdb6c0
fix more example output
erikerlandson Apr 24, 2020
4b386e1
refined 0.9.14
erikerlandson Apr 24, 2020
5e1a54c
release 0.4.5
erikerlandson Apr 24, 2020
f99bc53
add coulomb-pureconfig-refined to doc deps
erikerlandson Apr 24, 2020
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
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: scala

scala:
- 2.13.1
- 2.13.2

jdk:
- oraclejdk8
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ In addition to core functionality and fundamental units, coulomb provides the fo
* [coulomb-avro](coulomb-avro/) - an integration package with Apache Avro schema and i/o
* [coulomb-pureconfig](coulomb-pureconfig/) - extends the pureconfig with awareness of unit Quantity
* [coulomb-typesafe-config](coulomb-typesafe-config/) - unit awareness for the typesafe config library
* [coulomb-refined](coulomb-refined/) - integrates coulomb Quantity with Refined values
* [coulomb-pureconfig-refined](coulomb-pureconfig-refined/) - integrate coulomb + pureconfig + refined

### Code of Conduct
The `coulomb` project supports the [Scala Code of Conduct](https://typelevel.org/code-of-conduct.html);
Expand Down
43 changes: 33 additions & 10 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

def commonSettings = Seq(
organization := "com.manyangled",
version := "0.4.1-SNAPSHOT",
scalaVersion := "2.13.1",
crossScalaVersions := Seq("2.13.1"),
version := "0.4.5",
scalaVersion := "2.13.2",
crossScalaVersions := Seq("2.13.2"),
licenses += ("Apache-2.0", url("http://opensource.org/licenses/Apache-2.0")),
resolvers ++= Seq(
Resolver.sonatypeRepo("releases"),
Expand All @@ -14,6 +14,7 @@ def commonSettings = Seq(
libraryDependencies ++= Seq(
"org.typelevel" %% "spire" % "0.17.0-M1" % Provided,
"eu.timepit" %% "singleton-ops" % "0.5.0" % Provided,
"eu.timepit" %% "refined" % "0.9.14" % Provided,
"com.lihaoyi" %% "utest" % "0.7.4" % Test
),
testFrameworks += new TestFramework("utest.runner.Framework"),
Expand Down Expand Up @@ -116,7 +117,7 @@ lazy val coulomb_avro = (project in file("coulomb-avro"))

def coulombPureConfigDeps = Seq(
"com.github.pureconfig" %% "pureconfig-core" % "0.12.3" % Provided,
"com.github.pureconfig" %% "pureconfig-generic" % "0.12.3" % Provided
"com.github.pureconfig" %% "pureconfig-generic" % "0.12.3" % Provided,
)

lazy val coulomb_pureconfig = (project in file("coulomb-pureconfig"))
Expand All @@ -127,19 +128,41 @@ lazy val coulomb_pureconfig = (project in file("coulomb-pureconfig"))
.settings(docDepSettings :_*)
.settings(libraryDependencies ++= coulombPureConfigDeps)

def coulombRefinedDeps = Seq(
"eu.timepit" %% "refined" % "0.9.14" % Provided
)

lazy val coulomb_refined = (project in file("coulomb-refined"))
.aggregate(coulomb)
.dependsOn(coulomb)
.settings(name := "coulomb-refined")
.settings(commonSettings :_*)
.settings(docDepSettings :_*)
.settings(libraryDependencies ++= coulombRefinedDeps)

lazy val coulomb_pureconfig_refined = (project in file("coulomb-pureconfig-refined"))
.aggregate(coulomb, coulomb_parser, coulomb_pureconfig, coulomb_refined)
.dependsOn(coulomb, coulomb_parser, coulomb_pureconfig, coulomb_refined)
.settings(name := "coulomb-pureconfig-refined")
.settings(commonSettings :_*)
.settings(docDepSettings :_*)
.settings(libraryDependencies ++= coulombPureConfigDeps)
.settings(libraryDependencies ++= coulombRefinedDeps)

lazy val coulomb_tests = (project in file("coulomb-tests"))
.aggregate(coulomb, coulomb_si_units, coulomb_mks_units, coulomb_accepted_units, coulomb_time_units, coulomb_info_units, coulomb_customary_units, coulomb_temp_units, coulomb_parser, coulomb_typesafe_config, coulomb_avro, coulomb_pureconfig)
.dependsOn(coulomb, coulomb_si_units, coulomb_mks_units, coulomb_accepted_units, coulomb_time_units, coulomb_info_units, coulomb_customary_units, coulomb_temp_units, coulomb_parser, coulomb_typesafe_config, coulomb_avro, coulomb_pureconfig)
.aggregate(coulomb, coulomb_si_units, coulomb_mks_units, coulomb_accepted_units, coulomb_time_units, coulomb_info_units, coulomb_customary_units, coulomb_temp_units, coulomb_parser, coulomb_typesafe_config, coulomb_avro, coulomb_pureconfig, coulomb_refined, coulomb_pureconfig_refined)
.dependsOn(coulomb, coulomb_si_units, coulomb_mks_units, coulomb_accepted_units, coulomb_time_units, coulomb_info_units, coulomb_customary_units, coulomb_temp_units, coulomb_parser, coulomb_typesafe_config, coulomb_avro, coulomb_pureconfig, coulomb_refined, coulomb_pureconfig_refined)
.settings(name := "coulomb-tests")
.settings(commonSettings :_*)
.settings(libraryDependencies ++= coulombAvroDeps)
.settings(libraryDependencies ++= coulombTypesafeConfigDeps)
.settings(libraryDependencies ++= coulombParserDeps)
.settings(libraryDependencies ++= coulombTypesafeConfigDeps)
.settings(libraryDependencies ++= coulombAvroDeps)
.settings(libraryDependencies ++= coulombRefinedDeps)
.settings(libraryDependencies ++= coulombPureConfigDeps)

lazy val coulomb_docs = (project in file("."))
.aggregate(coulomb, coulomb_si_units, coulomb_mks_units, coulomb_accepted_units, coulomb_time_units, coulomb_info_units, coulomb_customary_units, coulomb_temp_units, coulomb_parser, coulomb_typesafe_config, coulomb_avro, coulomb_pureconfig)
.dependsOn(coulomb, coulomb_si_units, coulomb_mks_units, coulomb_accepted_units, coulomb_time_units, coulomb_info_units, coulomb_customary_units, coulomb_temp_units, coulomb_parser, coulomb_typesafe_config, coulomb_avro, coulomb_pureconfig)
.aggregate(coulomb, coulomb_si_units, coulomb_mks_units, coulomb_accepted_units, coulomb_time_units, coulomb_info_units, coulomb_customary_units, coulomb_temp_units, coulomb_parser, coulomb_typesafe_config, coulomb_avro, coulomb_pureconfig, coulomb_refined, coulomb_pureconfig_refined)
.dependsOn(coulomb, coulomb_si_units, coulomb_mks_units, coulomb_accepted_units, coulomb_time_units, coulomb_info_units, coulomb_customary_units, coulomb_temp_units, coulomb_parser, coulomb_typesafe_config, coulomb_avro, coulomb_pureconfig, coulomb_refined, coulomb_pureconfig_refined)
.settings(name := "coulomb-docs")
.settings(commonSettings :_*)

Expand Down
53 changes: 52 additions & 1 deletion coulomb-parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ libraryDependencies ++= Seq(
)
```

The `coulomb` package `coulomb-parser` provides a utility for parsing a quantity expression DSL into
The `coulomb-parser` package provides a utility for parsing a quantity expression DSL into
correctly typed `Quantity` values, called `QuantityParser`.
The following coulomb packages make use of QuantityParser:

* [coulomb-avro](../coulomb-avro/)
* [coulomb-pureconfig](../coulomb-pureconfig/)
* [coulomb-pureconfig-refined](../coulomb-pureconfig-refined/)
* [coulomb-typesafe-config](../coulomb-typesafe-config/)

### Examples

A `QuantityParser` is instantiated with a list of types that it will recognize.
This example shows a quantity parser that can recognize values in bytes, seconds,
and the two prefixes mega and giga:
Expand Down Expand Up @@ -69,3 +71,52 @@ scala> qp[Double, Minute]("60 byte")
res4: scala.util.Try[coulomb.Quantity[Double,coulomb.time.Minute]] =
Failure(scala.tools.reflect.ToolBoxError: reflective compilation has failed ...
```

A quantity parser can be used to generate coefficients of conversion between
unit expression strings and a unit type, using the `coefficient` method.
Coulomb uses spire `Rational` as its internal representation of coefficients.
Incompatible units will result in a compile failure:

```scala

scala> val coef = qp.coefficient[Nat]("byte")
val coef: scala.util.Try[spire.math.Rational] = Success(18014398509481984/3248660424303251)

scala> coef.get
val res14: spire.math.Rational = 18014398509481984/3248660424303251

scala> val coef = qp.coefficient[Nat]("second")
val coef: scala.util.Try[spire.math.Rational] =
Failure(scala.tools.reflect.ToolBoxError: reflective compilation has failed:
```

Quantity parsers can be used to apply units to a raw value, using the `applyUnitExpr` method.
This method can be useful when writing i/o routines.

```scala
scala> val qv = qp.applyUnitExpr[Float, Minute](1f, "second")
val qv: scala.util.Try[coulomb.Quantity[Float,coulomb.time.Minute]] = Success(Quantity(0.016666668))

scala> qv.get.show
val res13: String = 0.016666668 min
```

#### include files

A QuantityParser is typically aware of necessary implicit definitions at global scope,
however in some scenarios it may not see needed definitions.
For example, this can happen with
[customized definitions](../README.md#unit-conversions-for-custom-value-types).
You can create a QuantityParser with additional imports using the `withImports` method:

```scala
val qp = QuantityParser.withImports[Foot :: Meter :: HNil]("my.custom1._", "my.custom2._", ...)
```

#### serialization

`QuantityParser` is `<: Serializable`.
Once created, these objects can be serialized and deserialized for use in
multiple contexts.
An example of "ser/de" can be seen in the quantity parser
[unit tests](../coulomb-tests/src/test/scala/coulomb/QuantityParser.scala)
92 changes: 76 additions & 16 deletions coulomb-parser/src/main/scala/coulomb/parser/QuantityParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,18 @@ import coulomb.parser.unitops.UnitTypeString
* val speed = qp[Double, Mile %/ Hour]("10.0 kilometer / second") // prefix units are parsed
* }}}
*/
class QuantityParser private (private val qpp: coulomb.parser.infra.QPP[_]) extends Serializable {
class QuantityParser private (
private val qpp: coulomb.parser.infra.QPP[_],
private val iseq: Seq[String]) extends Serializable {
import scala.reflect.runtime.universe.{ Try => _, _ }
import scala.tools.reflect.ToolBox

@transient private lazy val lex = new coulomb.parser.lexer.UnitDSLLexer(qpp.unames, qpp.pfnames)
@transient private lazy val parse = new coulomb.parser.parser.UnitDSLParser(qpp.nameToType)

@transient lazy val toolbox = runtimeMirror(getClass.getClassLoader).mkToolBox()
@transient private lazy val imports = iseq.map(i => s"import $i\n").mkString("")

@transient private lazy val toolbox = runtimeMirror(getClass.getClassLoader).mkToolBox()

/**
* Parse an expression into a unit typed Quantity
Expand All @@ -58,27 +62,61 @@ class QuantityParser private (private val qpp: coulomb.parser.infra.QPP[_]) exte
ntt: WeakTypeTag[N],
uts: UnitTypeString[U]): Try[Quantity[N, U]] = {
val tpeN = weakTypeOf[N]
val cast = s".toUnit[${uts.expr}].toValue[$tpeN]"
val cast = s".to[$tpeN, ${uts.expr}]"
for {
tok <- lex(quantityExpr)
ast <- parse(tok).toTry
code <- Try { s"($ast)${cast}" }
qeTree <- Try { toolbox.parse(code) }
qeEval <- Try { toolbox.eval(qeTree) }
qret <- Try { qeEval.asInstanceOf[Quantity[N, U]] }
qret <- Try {
val code = s"${imports}($ast)${cast}"
toolbox.eval(toolbox.parse(code)).asInstanceOf[Quantity[N, U]]
}
} yield {
qret
}
}

def coefficient[U2](u1: String)(implicit ut2: UnitTypeString[U2]): Try[Rational] = {
/**
* Parse a unit expression and apply it to a value
* @tparam V the value type
* @tparam U2 the output unit type
* @param v the raw value
* @param u1 the unit expression string, encodes a unit type `U1`
* e.g. "meter / (second &#94; 2)"
* @return a Try value wrapping `Quantity[V, U2]`.
* Effectively, generates `v.withUnit[U1].toUnit[U2]`
*/
def applyUnitExpr[V, U2](v: V, u1: String)(implicit
vtt: WeakTypeTag[V],
ut2: UnitTypeString[U2]
): Try[Quantity[V, U2]] = {
val tpeV = weakTypeOf[V]
for {
tok1 <- lex(u1)
ast1 <- parse.parseUnit(tok1).toTry
code <- Try { s"coulomb.Quantity.coefficient[${ast1},${ut2.expr}]" }
qeTree <- Try { toolbox.parse(code) }
qeEval <- Try { toolbox.eval(qeTree) }
qret <- Try { qeEval.asInstanceOf[Rational] }
tok <- lex(u1)
ut1 <- parse.parseUnit(tok).toTry
v2q <- Try {
val code = s"${imports}(v: $tpeV) => (coulomb.Quantity[$tpeV, $ut1](v)).toUnit[${ut2.expr}]"
toolbox.eval(toolbox.parse(code)).asInstanceOf[V => Quantity[V, U2]]
}
} yield {
v2q(v)
}
}

/**
* Parse a unit expression and obtain the conversion coefficient to another unit
* @tparam U2 the unit being converted to
* @param u1 the unit expression string, encodes a unit type `U1`
* @return a Try value wrapping the coefficient from U1 -> U2
*/
def coefficient[U2](u1: String)(implicit
ut2: UnitTypeString[U2]): Try[Rational] = {
for {
tok <- lex(u1)
ut1 <- parse.parseUnit(tok).toTry
qret <- Try {
val code = s"${imports}coulomb.Quantity.coefficient[$ut1, ${ut2.expr}]"
toolbox.eval(toolbox.parse(code)).asInstanceOf[Rational]
}
} yield {
qret
}
Expand All @@ -89,13 +127,35 @@ object QuantityParser {
/**
* Construct a QuantityParser instance that recognizes a given list of units.
* @tparam UL a list of unit types to expect, as a shapeless HList
* @return a QuantityParser object that is aware of the units in `UL`
* {{{
* // declare a quantity parser that will recognize "meter", "second" and prefix unit "kilo"
* // prefix units are automatically detected and parsed as prefixes, e.g. "kilometer"
* val qp = QuantityParser[Meter :: Second :: Kilo :: HNil]
* }}}
*/
def apply[UL <: shapeless.HList](implicit qpp: coulomb.parser.infra.QPP[UL]): QuantityParser = {
new QuantityParser(qpp)
def apply[UL <: shapeless.HList](implicit
qpp: coulomb.parser.infra.QPP[UL]): QuantityParser = {
new QuantityParser(qpp, List.empty[String])
}

/**
* Similar to `apply[UL]` method but also accepts a list of imports for
* expression compiling.
* Used for importing implicits that may not be picked up
* by default global scope.
* @tparam UL a list of unit types to expect, as a shapeless HList
* @param ilist a variable arg list of strings representing imports.
* Example `"org.custom.algebras._"`
* @return a QuantityParser object that is aware of the units in `UL`
* and will `import` the expressions given on `ilist` before
* parsing unit expressions
* {{{
* val qp = QuantityParser[Meter :: Second :: Kilo :: HNil]("org.custom.algebras._", ...)
* }}}
*/
def withImports[UL <: shapeless.HList](ilist: String*)(implicit
qpp: coulomb.parser.infra.QPP[UL]): QuantityParser = {
new QuantityParser(qpp, ilist)
}
}
9 changes: 6 additions & 3 deletions coulomb-parser/src/main/scala/coulomb/parser/parsing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ object lexer {
override val whiteSpace = "[ \t]+".r

val unitRE = {
val t = (pfunits ++ units).mkString("|")
// use space if length is zero because it cannot match
val upfu = pfunits ++ units
val t = if (upfu.length > 0) upfu.mkString("|") else " "
(s"($t)").r
}

val pfunitRE = {
val t1 = pfunits.mkString("|")
val t2 = units.mkString("|")
// use space if length is zero because it cannot match
val t1 = if (pfunits.length > 0) pfunits.mkString("|") else " "
val t2 = if (units.length > 0) units.mkString("|") else " "
(s"($t1)($t2)").r
}

Expand Down
Loading