Skip to content

Commit

Permalink
ConfigWriter[Quantity[V, U]]
Browse files Browse the repository at this point in the history
  • Loading branch information
erikerlandson committed Jun 3, 2023
1 parent 13cb7f7 commit dc0b2dd
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 20 deletions.
26 changes: 14 additions & 12 deletions parser/src/main/scala/coulomb/parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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 =
Expand Down
30 changes: 24 additions & 6 deletions parser/src/main/scala/coulomb/parser/infra/parsing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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] =
Expand All @@ -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
Expand All @@ -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: (<expr>)
lazy val paren: Parser[RuntimeUnit] =
Expand Down Expand Up @@ -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
Expand Down
35 changes: 33 additions & 2 deletions pureconfig/src/main/scala/coulomb/pureconfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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])
}

0 comments on commit dc0b2dd

Please sign in to comment.