From 68e7cb2b3ac10b29b65dc0ecfeaa7b1d1abac60f Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 12 Mar 2023 16:35:08 -0700 Subject: [PATCH] ConfigWriter[Quantity[V, U]] --- parser/src/main/scala/coulomb/parser.scala | 26 +++++++------- .../scala/coulomb/parser/infra/parsing.scala | 30 ++++++++++++---- .../src/main/scala/coulomb/pureconfig.scala | 35 +++++++++++++++++-- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 9a4ec8adb..238a9bd16 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -23,23 +23,23 @@ import coulomb.rational.Rational abstract class RuntimeUnitParser: def parse(expr: String): Either[String, RuntimeUnit] - def render(u: RuntimeUnit): Either[String, String] + def render(u: RuntimeUnit): String object standard: abstract class RuntimeUnitExprParser extends RuntimeUnitParser: def unames: Map[String, String] def pnames: Set[String] - private lazy val parser: (String => Either[String, RuntimeUnit]) = - infra.parsing.parser(unames, pnames) - - private lazy val unamesinv: Map[String, String] = + lazy val unamesinv: Map[String, String] = unames.map { (k, v) => (v, k) } - def parse(expr: String): Either[String, RuntimeUnit] = + private lazy val parser: (String => Either[String, RuntimeUnit]) = + infra.parsing.parser(unames, pnames, unamesinv) + + final def parse(expr: String): Either[String, RuntimeUnit] = parser(expr) - def render(u: RuntimeUnit): Either[String, String] = + final def render(u: RuntimeUnit): String = def paren(s: String, tl: Boolean): String = if (tl) s else s"($s)" def rparen(r: Rational, tl: Boolean): String = @@ -52,17 +52,19 @@ object standard: case RuntimeUnit.UnitConst(value) => rparen(value, tl) case RuntimeUnit.UnitType(path) => - // this can error out if map isn't defined - unamesinv(path) + if (unamesinv.contains(path)) + // if it is in the inverse mapping write the name + unamesinv(path) + else + // otherwise write the fully qualified type name + s"@$path" case RuntimeUnit.Mul(l, r) => paren(s"${work(l)}*${work(r)}", tl) case RuntimeUnit.Div(n, d) => paren(s"${work(n)}/${work(d)}", tl) case RuntimeUnit.Pow(b, e) => paren(s"${work(b)}^${rparen(e, false)}", tl) - Try { work(u, tl = true) } match - case Success(s) => Right(s) - case Failure(e) => Left(s"$e") + work(u, tl = true) object RuntimeUnitExprParser: inline def of[UTL <: Tuple]: RuntimeUnitExprParser = diff --git a/parser/src/main/scala/coulomb/parser/infra/parsing.scala b/parser/src/main/scala/coulomb/parser/infra/parsing.scala index 1aa4fcf2b..205c7f865 100644 --- a/parser/src/main/scala/coulomb/parser/infra/parsing.scala +++ b/parser/src/main/scala/coulomb/parser/infra/parsing.scala @@ -22,8 +22,8 @@ import coulomb.RuntimeUnit import coulomb.rational.Rational object parsing: - def parser(unames: Map[String, String], pnames: Set[String]): (String => Either[String, RuntimeUnit]) = - val p = catsparse.unit(catsparse.named(unames, pnames)) + def parser(unames: Map[String, String], pnames: Set[String], unamesinv: Map[String, String]): (String => Either[String, RuntimeUnit]) = + val p = catsparse.unit(catsparse.named(unames, pnames), catsparse.typed(unamesinv)) (expr: String) => p.parse(expr) match case Right((_, u)) => Right(u) case Left(e) => Left(s"$e") @@ -33,7 +33,7 @@ object parsing: import _root_.cats.parse.* // for consuming whitespace - val ws: Parser[Unit] = Parser.charIn(" \t\r\n").void + val ws: Parser[Unit] = Parser.charIn(" \t").void val ws0: Parser0[Unit] = ws.rep0.void // numeric literals parse into UnitConst objects @@ -66,7 +66,16 @@ object parsing: // one possible extension would be "any printable char not in { '(', ')', '*', etc }" // however I'm not sure if there is an efficient way to express that // (starting char can also not be digit, + or -) - Parser.charIn('a' to 'z').rep.string + Rfc5234.alpha.rep.string + + // scala identifier + val idlit: Parser[String] = + (Rfc5234.alpha ~ (Rfc5234.alpha | Rfc5234.digit | Parser.char('$')).rep0).string + + // fully qualified scala module path for a UnitType + val typelit: Parser[String] = + // I expect at least one '.' in the type path + Parser.char('@') *> (idlit ~ (Parser.char('.') ~ idlit).rep).string // used for left-factoring the parsing for sequences of mul and div val muldivop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = @@ -81,7 +90,7 @@ object parsing: RuntimeUnit.Pow(b, e.toRational.toSeq.head) } - def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = + def unit(named: Parser[RuntimeUnit], typed: Parser[RuntimeUnit]): Parser[RuntimeUnit] = lazy val unitexpr: Parser[RuntimeUnit] = Parser.defer { // sequence of mul and div operators // these have lowest precedence and form the top of the parse tree @@ -97,7 +106,7 @@ object parsing: // numeric literal, named unit, or sub-expr in parens lazy val atom: Parser[RuntimeUnit] = - paren | (numlit <* ws0) | (named <* ws0) + paren | (numlit <* ws0) | (typed <* ws0) | (named <* ws0) // any unit subexpression inside of parens: () lazy val paren: Parser[RuntimeUnit] = @@ -125,6 +134,15 @@ object parsing: // (trailing whitespace is consumed inside unitexpr) ws0.with1 *> unitexpr <* Parser.end + def typed(unamesinv: Map[String, String]): Parser[RuntimeUnit] = + typelit.flatMap { path => + if (unamesinv.contains(path)) + // type paths are ok if they are in the map + Parser.pure(RuntimeUnit.UnitType(path)) + else + Parser.failWith[RuntimeUnit](s"unrecognized unit type '$path'") + } + // parses "raw" unit literals - only succeeds if the literal is // in the list of defined units (or unit prefixes) // these lists are intended to be constructed at compile-time via scala metaprogramming diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index 8c62df398..b5e9e7bd7 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -31,6 +31,16 @@ object pureconfig: import _root_.pureconfig.error.CannotConvert import coulomb.parser.RuntimeUnitParser + + import com.typesafe.config.ConfigValue + + // probably useful for unit testing, will keep them here for now + extension [V, U](q: Quantity[V, U]) + inline def toCV(using ConfigWriter[Quantity[V, U]]): ConfigValue = + ConfigWriter[Quantity[V, U]].to(q) + extension (conf: ConfigValue) + inline def toQuantity[V, U](using ConfigReader[Quantity[V, U]]): Quantity[V, U] = + ConfigReader[Quantity[V, U]].from(conf).toSeq.head object intlit: def unapply(lit: String): Option[BigInt] = @@ -70,6 +80,13 @@ object pureconfig: } } + given ctx_RuntimeUnit_Writer(using + parser: RuntimeUnitParser + ): ConfigWriter[RuntimeUnit] = + ConfigWriter[String].contramap[RuntimeUnit] { u => + parser.render(u) + } + given ctx_RuntimeQuantity_Reader[V](using ConfigReader[V], ConfigReader[RuntimeUnit] @@ -78,9 +95,16 @@ object pureconfig: RuntimeQuantity(v, u) } + given ctx_RuntimeQuantity_Writer[V](using + ConfigWriter[V], + ConfigWriter[RuntimeUnit] + ): ConfigWriter[RuntimeQuantity[V]] = + ConfigWriter.forProduct2("value", "unit") { (q: RuntimeQuantity[V]) => + (q.value, q.unit) + } + inline given ctx_Quantity_Reader[V, U](using - crv: ConfigReader[V], - cru: ConfigReader[RuntimeUnit], + crq: ConfigReader[RuntimeQuantity[V]], crt: CoefficientRuntime, vcr: ValueConversion[Rational, V], mul: MultiplicativeSemigroup[V] @@ -90,3 +114,10 @@ object pureconfig: case Right(coef) => Right(mul.times(coef, rq.value).withUnit[U]) case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) } + + inline given ctx_Quantity_Writer[V, U](using + ConfigWriter[RuntimeQuantity[V]] + ): ConfigWriter[Quantity[V, U]] = + ConfigWriter[RuntimeQuantity[V]].contramap[Quantity[V, U]] { q => + RuntimeQuantity(q.value, RuntimeUnit.of[U]) + }