Skip to content

Instantly share code, notes, and snippets.

@rleibman
Created February 23, 2020 23:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rleibman/aae13e38ae0d3490c3b5e5ec3b47bc68 to your computer and use it in GitHub Desktop.
Save rleibman/aae13e38ae0d3490c3b5e5ec3b47bc68 to your computer and use it in GitHub Desktop.
Cooking Squants: some squant based utilities to deal with cooking
import scala.util.matching.Regex
import squants.experimental.formatter.Formatter
import squants.experimental.unitgroups.UnitGroup
import squants.mass.{ Grams, Kilograms, Mass, Ounces, Pounds }
import squants.{ Dimensionless, DimensionlessUnit, Dozen, Each, Quantity, UnitOfMeasure }
package object cookingSquants {
private val mathContext: java.math.MathContext = new java.math.MathContext(3)
private val rangeRegex = "^([0-9\\s/./ ]+)(to|-)([0-9\\s/./ ]+)(.*)".r
private val singleNumRegex = "([0-9\\s/./]+)(.*)".r
private val fractionRegex = "(?:([0-9]*)\\.([0-9]+))|([0-9]*)(?:(?:[\\s]*([0-9]+)/([0-9]+)))|([0-9]*)".r
lazy val unitRegex: Regex =
s"^(${allCookingUnitsWithSynonyms.flatMap(u => u._1.symbol +: u._2).mkString("|")})[\\s]+".r
import squants.space._
lazy val allCookingUnits = volumeUnitGroup.units ++ massUnitGroup.units ++ lengthUnitGroup.units ++ dimensionlessUnitGroup.units
lazy val allCookingUnitsWithSynonyms = volumeUnitGroup.unitsWithSynonyms ++ massUnitGroup.unitsWithSynonyms ++ lengthUnitGroup.unitsWithSynonyms ++ dimensionlessUnitGroup.unitsWithSynonyms
def byUnitSymbol[A <: Quantity[A]](symbol: String): Option[UnitOfMeasure[A]] =
allCookingUnits.find(_.symbol == symbol).asInstanceOf[Option[UnitOfMeasure[A]]]
def bySynonym[A <: Quantity[A]](str: String): Option[UnitOfMeasure[A]] =
if (str == "t") {
Option(Teaspoons).asInstanceOf[Option[UnitOfMeasure[A]]]
} else if (str == "T") {
Option(Tablespoons).asInstanceOf[Option[UnitOfMeasure[A]]]
} else {
val lower = str.toLowerCase
allCookingUnitsWithSynonyms
.find(t => t._1.symbol == lower || t._2.contains(lower)).map(_._1)
.asInstanceOf[Option[UnitOfMeasure[A]]]
}
case class Container[A <: Quantity[A]](value: A)
trait CookingUnitGroup[A <: Quantity[A]] extends UnitGroup[A] with Formatter[A] {
//declare these in the order in which you consider them more useful, generally smaller to larger
def containers: Seq[Container[A]]
val unitsWithSynonyms: Set[(UnitOfMeasure[A], Seq[String])]
override lazy val units: Set[UnitOfMeasure[A]] = unitsWithSynonyms.map(_._1)
override def inBestUnit(quantity: Quantity[A]): A =
containers.map { container =>
(quantity.in(container.value.unit) / container.value.value, container)
}.filter(_._1.value > 0.999)
.lastOption
.map(t => quantity.in(t._2.value.unit))
.get
}
object Jigger extends VolumeUnit {
val symbol = "jigger"
val conversionFactor: Double = FluidOunces.conversionFactor * 1.5
}
object Dash extends VolumeUnit {
val symbol = "dash"
val conversionFactor: Double = FluidOunces.conversionFactor / 32d
}
object Splash extends VolumeUnit {
val symbol = "splash"
val conversionFactor: Double = FluidOunces.conversionFactor / 5d
}
object Fifth extends VolumeUnit {
val symbol = "fifth"
val conversionFactor: Double = UsGallons.conversionFactor / 5d
}
object Cloves extends DimensionlessUnit {
val symbol = "clove"
val conversionFactor: Double = Each.conversionFactor / 11d
}
val dimensionlessUnitGroup: CookingUnitGroup[Dimensionless] = new CookingUnitGroup[Dimensionless] {
override def containers: Seq[Container[Dimensionless]] = Seq(Container(Each(1)))
override val unitsWithSynonyms: Set[(UnitOfMeasure[Dimensionless], Seq[String])] = Set(
(Each, Seq("ea", "each", "bunch", "bunches", "slice", "slices", "package", "packages", "head", "heads")),
(Dozen, Seq("dozen", "dozens")),
(Cloves, Seq("clove", "cloves"))
)
}
val volumeUnitGroup: CookingUnitGroup[Volume] = new CookingUnitGroup[Volume] {
// units don't have to be specified in-order.
override val unitsWithSynonyms: Set[(UnitOfMeasure[Volume], Seq[String])] =
Set(
(Litres, Seq("liter", "liters", "litre", "litres")),
(UsPints, Seq("pint", "pints")),
(UsGallons, Seq("gallon", "gallons")),
(Teaspoons, Seq("teaspoon", "teaspoons", "ts", "t")),
(Tablespoons, Seq("tablespoon", "tablespoons", "tb", "T")),
(UsQuarts, Seq("quart", "quarts")),
(UsCups, Seq("cup", "cups")),
(FluidOunces, Seq("fluid ounces", "fl oz", "pony", "ponies")),
(Jigger, Seq("jiggers")),
(Dash, Seq("dashes")),
(Splash, Seq("splashes")),
(Fifth, Seq("fifths"))
)
val containers: Seq[Container[Volume]] = Seq(
Container(Teaspoons(.125)),
Container(Teaspoons(.25)),
Container(Teaspoons(.5)),
Container(Teaspoons(1.0)),
Container(Tablespoons(.5)),
Container(Tablespoons(1.0)),
Container(UsCups(.25)),
Container(UsCups(1.0 / 3.0)),
Container(UsCups(.5)),
Container(UsCups(1.0)),
Container(UsGallons(0.5)),
Container(UsGallons(1.0))
)
}
val lengthUnitGroup: CookingUnitGroup[Length] = new CookingUnitGroup[Length] {
override val unitsWithSynonyms: Set[(UnitOfMeasure[Length], Seq[String])] =
Set(
(Meters, Seq("meter", "meters", "metre", "metres")),
(Inches, Seq("inch", "inches"))
)
override def containers: Seq[Container[Length]] = Seq(Container(Inches(1)))
}
val massUnitGroup: CookingUnitGroup[Mass] = new CookingUnitGroup[Mass] {
override val unitsWithSynonyms: Set[(UnitOfMeasure[Mass], Seq[String])] =
Set(
(Grams, Seq("gram", "grams")),
(Kilograms, Seq("kilogram", "kilograms", "k")),
(Ounces, Seq("ounce", "ounces")),
(Pounds, Seq("lbs", "pound", "pounds"))
)
override def containers: Seq[Container[Mass]] = Seq(Container(Ounces(1)), Container(Pounds(1)))
}
def parseFraction(str: String): Option[BigDecimal] =
(str.trim match {
case "½" => Some(BigDecimal(.5))
case "⅓" => Some(BigDecimal(.333))
case "⅔" => Some(BigDecimal(.667))
case "¼" => Some(BigDecimal(.25))
case "¾" => Some(BigDecimal(.75))
case "⅛" => Some(BigDecimal(.125))
case "one" => Some(BigDecimal(1))
case "two" => Some(BigDecimal(2))
case "three" => Some(BigDecimal(3))
case "four" => Some(BigDecimal(4))
case fractionRegex("", decimal, null, null, null, null) => Some(BigDecimal("." + decimal))
case fractionRegex(whole, decimal, null, null, null, null) =>
Some(BigDecimal(whole.toDouble + ("." + decimal).toDouble))
case fractionRegex(null, null, "", numerator, denominator, null) =>
Some(BigDecimal(numerator.toDouble / denominator.toDouble))
case fractionRegex(null, null, whole, numerator, denominator, null) =>
Some(BigDecimal(whole.toDouble + numerator.toDouble / denominator.toDouble))
case fractionRegex(null, null, null, null, null, "") => None
case fractionRegex(null, null, null, null, null, whole) => Some(BigDecimal(whole))
case _ => None
}).map(_.round(mathContext))
def parseQty[A <: Quantity[A]](str: String): Option[Quantity[A]] =
str match {
case rangeRegex(from, _, to, unit2) =>
val mean = (for {
num1 <- parseFraction(from)
num2 <- parseFraction(to)
} yield (num1 + num2) / 2.0)
mean.map(n => bySynonym(unit2.trim).getOrElse(Each.asInstanceOf[UnitOfMeasure[A]])(n.toDouble)).asInstanceOf[Option[Quantity[A]]]
case singleNumRegex(numStr, unit2) =>
val numOpt = parseFraction(numStr)
numOpt.map{n =>
val res = bySynonym(unit2.trim).getOrElse(Each.asInstanceOf[UnitOfMeasure[A]])(n.toDouble)
res
}.asInstanceOf[Option[Quantity[A]]]
case other =>
parseFraction(other).map(n => Each(n)).asInstanceOf[Option[Quantity[A]]]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment