Created
February 23, 2020 23:27
-
-
Save rleibman/aae13e38ae0d3490c3b5e5ec3b47bc68 to your computer and use it in GitHub Desktop.
Cooking Squants: some squant based utilities to deal with cooking
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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