diff --git a/.gitignore b/.gitignore index de4a392..c606205 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor /composer.lock +/doc/coverage diff --git a/phpunit.xml b/phpunit.xml index ad9b759..c48c5d8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,8 +2,8 @@ diff --git a/src/Ingredient/Base.php b/src/Ingredient/Base.php index 85f647d..475e686 100644 --- a/src/Ingredient/Base.php +++ b/src/Ingredient/Base.php @@ -35,6 +35,13 @@ class Base return $this->components; } + public function getComponentPercent(string $component): float + { + return array_key_exists($component, $this->components) + ? $this->components[$component] + : 0; + } + public static function parseComponents(string $base) { $found = []; diff --git a/src/Ingredient/Ingredient.php b/src/Ingredient/Ingredient.php index cc7f82b..29ed22d 100644 --- a/src/Ingredient/Ingredient.php +++ b/src/Ingredient/Ingredient.php @@ -12,6 +12,14 @@ class Ingredient implements IngredientInterface protected $base; + /** + * Ingredient constructor + * + * @param string The name of the ingredient + * @param string|null The brand of the ingredient + * @param float Percent + * @param string The base mix (Default PG100) + */ public function __construct(string $name, ?string $brand=null, float $percent=0.0, string $base="PG100") { $this->name = $name; @@ -52,4 +60,12 @@ class Ingredient implements IngredientInterface return $this->base; } + /** + * {@inheritDoc} + */ + public function getSpecificGravity(): ?float + { + return null; + } + } \ No newline at end of file diff --git a/src/Ingredient/IngredientInterface.php b/src/Ingredient/IngredientInterface.php index edf3460..b68b6a4 100644 --- a/src/Ingredient/IngredientInterface.php +++ b/src/Ingredient/IngredientInterface.php @@ -34,4 +34,11 @@ interface IngredientInterface */ public function getBase(): ?string; + /** + * Return the ASG in g/mL for the specific flavor, or null if not available. + * + * @return float|null + */ + public function getSpecificGravity(): ?float; + } \ No newline at end of file diff --git a/src/Ingredient/NicotineBase.php b/src/Ingredient/NicotineBase.php index acb6ea4..38b7937 100644 --- a/src/Ingredient/NicotineBase.php +++ b/src/Ingredient/NicotineBase.php @@ -5,19 +5,49 @@ namespace NoccyLabs\Juicer\Ingredient; class NicotineBase { + const MASS_NICOTINE = 1.01; + protected $base; protected $strength; + /** + * NicotineBase constructor + * + * @param Base The base mix of the nicotine base + * @param int The strength of the nic base (eg. 18) + */ public function __construct(Base $base, int $strength) { $this->base = $base; - $this->nicotineStrength = $strength; + $this->strength = $strength; } + /** + * Get the Apparent Specific Gravity of the nicotine mixture taking + * in account the base mix and nicotine strength. + * + * @return float + */ public function getSpecificGravity(): float { - return 0.0; + + // 18mg/ml is 1.8% nicotine + $nicotinePercent = $this->strength / 1000; + + // Figure out how much the remaining percent is + $baseRemain = 1.0 - $nicotinePercent; + + $base = $this->base->getComponents(); + $remainVg = $baseRemain * ($base['VG'] / 100); + $remainPg = $baseRemain * ($base['PG'] / 100); + + $sgNic = $nicotinePercent * self::MASS_NICOTINE; + $sgVg = $remainVg * Base::MASS_VG; + $sgPg = $remainPg * Base::MASS_PG; + + return $sgNic + $sgVg + $sgPg; + } } diff --git a/src/Recipe/Importer/JsonImporter.php b/src/Recipe/Importer/JsonImporter.php index 4435530..0a5a61c 100644 --- a/src/Recipe/Importer/JsonImporter.php +++ b/src/Recipe/Importer/JsonImporter.php @@ -3,18 +3,26 @@ namespace NoccyLabs\Juicer\Recipe\Importer; use NoccyLabs\Juicer\Recipe\RecipeInterface; -use NoccyLabs\Juicer\Ingredient\IngredientInterface; use NoccyLabs\Juicer\Recipe\Recipe; use NoccyLabs\Juicer\Ingredient\Ingredient; +/** + * Import recipes from Json + * + */ class JsonImporter { + /** + * Import a recipe from json + * + * @param string The json string to parse and import + * @return RecipeInterface + */ public function import(string $json): RecipeInterface { $data = json_decode($json); - $recipe = new Recipe(); $recipe->setRecipeName(@$data->recipe); $recipe->setRecipeAuthor(@$data->author); @@ -29,6 +37,12 @@ class JsonImporter return $recipe; } + /** + * Import a recipe from json contained in a file + * + * @param string The filename to read and import + * @return RecipeInterface + */ public function readFromFile(string $filename): RecipeInterface { $fd = fopen($filename, "r"); diff --git a/src/Recipe/Mixer/MeasuredIngredient.php b/src/Recipe/Mixer/MeasuredIngredient.php index f25d4e8..1da1e20 100644 --- a/src/Recipe/Mixer/MeasuredIngredient.php +++ b/src/Recipe/Mixer/MeasuredIngredient.php @@ -22,47 +22,96 @@ class MeasuredIngredient implements IngredientInterface /** @var float Weight in grams (g) */ protected $weight; - public function __construct(string $name, ?string $brand, string $base, float $asg, float $percent, float $volume) + /** + * MeasuredIngredient constructor + * + * @param IngredientInterface|string The ingredient + * @param float The percent of the ingredient to use + * @param float The volume of the ingredient to mix (in mL) + * @param string The base of the ingredient, override any set on ingredient + */ + public function __construct($ingredient, float $percent, float $volume, string $base=null) { - $this->name = $name; - $this->brand = $brand; - $this->base = $base; + if ($ingredient instanceof IngredientInterface) { + $asg = $ingredient->getSpecificGravity(); + $this->name = $ingredient->getFlavorName(); + $this->brand = $ingredient->getFlavorBrand(); + $this->base = $base??$ingredient->getBase(); + } else { + $asg = null; + $this->name = $ingredient; + $this->base = $base??"PG100"; + } + + if (!$asg) { + $base = new Base($this->base); + $asg = $base->getSpecificGravity(); + } + $this->asg = $asg; $this->percent = $percent; $this->volume = $volume; $this->weight = $volume * $asg; } + /** + * + * @return string + */ public function getFlavorName(): string { return $this->name; } + /** + * + * @return string|null + */ public function getFlavorBrand(): ?string { return $this->brand; } + /** + * + * @return string|null + */ public function getBase(): ?string { return $this->base; } + /** + * + * @return float + */ public function getPercent(): float { return $this->percent; } + /** + * + * @return float + */ public function getSpecificGravity(): float { return $this->asg; } + /** + * + * @return float + */ public function getVolume(): float { return $this->volume; } + /** + * + * @return float + */ public function getWeight(): float { return $this->weight; diff --git a/src/Recipe/Mixer/MeasuredRecipe.php b/src/Recipe/Mixer/MeasuredRecipe.php new file mode 100644 index 0000000..e74221c --- /dev/null +++ b/src/Recipe/Mixer/MeasuredRecipe.php @@ -0,0 +1,31 @@ +getComponents(); if (array_key_exists('VG', $components) && $components['VG'] > 0) { - $mixed[] = new MeasuredIngredient("VG", null, "VG100", Base::MASS_VG, 100, $volume); + $targetVg = $components['VG']; } if (array_key_exists('PG', $components) && $components['PG'] > 0) { - $mixed[] = new MeasuredIngredient("PG", null, "PG100", Base::MASS_PG, 100, $volume); + $targetPg = $components['PG']; + } + + $addedVg = 0; + $addedPg = 0; + + foreach ($recipe->getIngredients() as $ingredient) { + $ingredientBase = new Base($ingredient->getBase()); + $ingredientPercent = $ingredient->getPercent(); + $floatPercent = $ingredientPercent / 100; + $addedVg += $floatPercent * $ingredientBase->getComponentPercent('VG'); + $addedPg += $floatPercent * $ingredientBase->getComponentPercent('PG'); + } + + if ($targetVg > $addedVg) { + $remainingVg = max(0, $targetVg - $addedVg); + $mixed[] = new MeasuredIngredient("VG", $remainingVg, $volume * ($remainingVg/100), "VG100"); + } + if ($targetPg > $addedPg) { + $remainingPg = max(0, $targetPg - $addedPg); + $mixed[] = new MeasuredIngredient("PG", $remainingPg, $volume * ($remainingPg/100), "PG100"); + } + + foreach ($recipe->getIngredients() as $ingredient) { + $ingredientBase = new Base($ingredient->getBase()); + $ingredientPercent = $ingredient->getPercent(); + $floatPercent = $ingredientPercent / 100; + $mixed[] = new MeasuredIngredient($ingredient, $ingredientPercent, $volume * $floatPercent); } return $mixed; diff --git a/tests/Ingredient/NicotineBaseTest.php b/tests/Ingredient/NicotineBaseTest.php new file mode 100644 index 0000000..e5662bc --- /dev/null +++ b/tests/Ingredient/NicotineBaseTest.php @@ -0,0 +1,35 @@ +assertEquals(round($expected,3), round($nic->getSpecificGravity(),3)); + } + +} diff --git a/tests/Recipe/Importer/JsonImporterTest.php b/tests/Recipe/Importer/JsonImporterTest.php index b7fbab7..c15f9da 100644 --- a/tests/Recipe/Importer/JsonImporterTest.php +++ b/tests/Recipe/Importer/JsonImporterTest.php @@ -5,6 +5,7 @@ namespace NoccyLabs\Juicer\Recipe\Importer; use NoccyLabs\Juicer\Recipe\Recipe; use NoccyLabs\Juicer\Ingredient\Ingredient; use NoccyLabs\Juicer\Recipe\Exporter\JsonExporter; +use NoccyLabs\Juicer\Recipe\RecipeInterface; class JsonImporterTest extends \PhpUnit\Framework\TestCase { @@ -41,5 +42,14 @@ class JsonImporterTest extends \PhpUnit\Framework\TestCase $this->assertEquals($recipe, $importedRecipe); } + public function testThatRecipesCanBeImportedFromFile() + { + + $importer = new JsonImporter(); + $importedRecipe = $importer->readFromFile(__DIR__."/../../data/recipe1.json"); + + $this->assertInstanceOf(RecipeInterface::class, $importedRecipe); + $this->assertEquals("Recipe 1", $importedRecipe->getRecipeName()); + } } \ No newline at end of file diff --git a/tests/Recipe/Mixer/MixerTest.php b/tests/Recipe/Mixer/MixerTest.php index 4bded74..81678a9 100644 --- a/tests/Recipe/Mixer/MixerTest.php +++ b/tests/Recipe/Mixer/MixerTest.php @@ -5,55 +5,74 @@ namespace NoccyLabs\Juicer\Recipe\Mixer; use NoccyLabs\Juicer\Recipe\Recipe; use NoccyLabs\Juicer\Ingredient\Ingredient; use NoccyLabs\Juicer\Ingredient\Base; +use NoccyLabs\Juicer\Recipe\RecipeInterface; class MixerTest extends \PhpUnit\Framework\TestCase { - public function testMixingEmptyRecipesWithVg() + + public function getDataForEmptyRecipes() { - $recipe = new Recipe(); - $mixer = new Mixer(); - - $base = new Base("VG100"); - $mixed = $mixer->mixRecipe($recipe, 10, $base, 0); - - $this->assertCount(1, $mixed); - $mixedVg = reset($mixed); - - $this->assertEquals(10, $mixedVg->getVolume()); - $this->assertEquals("VG", $mixedVg->getFlavorName()); + return [ + [ 'VG100', 10, [ 'VG' => 10 ], 10 ], + [ 'PG100', 10, [ 'PG' => 10 ], 10 ], + [ 'VG70', 10, [ 'VG' => 7, 'PG' => 3 ], 10 ], + [ 'VG50', 10, [ 'VG' => 5, 'PG' => 5 ], 10 ] + ]; } - public function testMixingEmptyRecipesWithPg() + /** + * @dataProvider getDataForEmptyRecipes + */ + public function testMixingEmptyRecipes($base, $amount, array $assertBases, $assertAmount) { $recipe = new Recipe(); $mixer = new Mixer(); - $base = new Base("PG100"); - $mixed = $mixer->mixRecipe($recipe, 10, $base, 0); + $base = new Base($base); + $mixed = $mixer->mixRecipe($recipe, $amount, $base, 0); - $this->assertCount(1, $mixed); - $mixedPg = reset($mixed); - - $this->assertEquals(10, $mixedPg->getVolume()); - $this->assertEquals("PG", $mixedPg->getFlavorName()); + $this->assertCount(count($assertBases), $mixed); + $mixedAmount = 0; + foreach ($mixed as $measured) { + $name = $measured->getFlavorName(); + if (!array_key_exists($name, $assertBases)) { + $this->assertFail("Mixed contains unexpected ingredients"); + } + $mixedAmount += $measured->getVolume(); + } + $this->assertEquals($assertAmount, $mixedAmount); } - public function testMixingEmptyRecipesWith70Vg30Pg() + public function getDataForDummyRecipes() + { + $ingredientA1 = new Ingredient("Ingredient A1", null, 2); + $ingredientA2 = new Ingredient("Ingredient A2", null, 3); + $recipeA = new Recipe(); + $recipeA->setRecipeName("Recipe A"); + $recipeA->addIngredient($ingredientA1); + $recipeA->addIngredient($ingredientA2); + return [ + [ $recipeA, 'VG70', 10 ], + ]; + } + + /** + * @dataProvider getDataForDummyRecipes + */ + public function testMixingDummyRecipes(RecipeInterface $recipe, $base, $amount) { - $recipe = new Recipe(); $mixer = new Mixer(); + $base = new Base($base); + $mixed = $mixer->mixRecipe($recipe, $amount, $base, 0); - $base = new Base("VG70"); - $mixed = $mixer->mixRecipe($recipe, 10, $base, 0); + $expectedCount = count($base->getComponents()) + count($recipe->getIngredients()); - $this->assertCount(2, $mixed); - - $mixedVg = array_shift($mixed); - $this->assertEquals(10, $mixedVg->getVolume()); - $this->assertEquals("VG", $mixedVg->getFlavorName()); - $mixedPg = array_shift($mixed); - $this->assertEquals(10, $mixedPg->getVolume()); - $this->assertEquals("PG", $mixedPg->getFlavorName()); + $this->assertCount($expectedCount, $mixed); + $mixedAmount = 0; + foreach ($mixed as $measured) { + $mixedAmount += $measured->getVolume(); + } + $this->assertEquals($amount, $mixedAmount); } } \ No newline at end of file diff --git a/tests/data/recipe1.json b/tests/data/recipe1.json new file mode 100644 index 0000000..2e1130a --- /dev/null +++ b/tests/data/recipe1.json @@ -0,0 +1,9 @@ +{ + "recipe": "Recipe 1", + "author": "Noccy", + "tags": [ "foo", "bar" ], + "ingredients": [ + { "flavor":"Flavor A", "brand":"Brand A", "percent":3 }, + { "flavor":"Flavor B", "brand":"Brand B", "percent":2 } + ] +}