commit 6e662f0263e200b8254a306bac53e19bfa3b1c52 Author: Christopher Vagnetoft Date: Mon Jul 8 01:13:30 2019 +0200 Initial checkin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de4a392 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor +/composer.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..4450bd7 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# NoccyLabs Juicer + +This is a library to help write juice-mixing stuff in PHP. + +## Features + +* Mix by weight or by volume. +* Calculate weights for mixing based on the VG/PG ratio or a specific values. +* Calculate weights for nicotine based on the nicotine strength and base. +* Interfaces allow for custom entity-backed implementations. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8566e18 --- /dev/null +++ b/composer.json @@ -0,0 +1,17 @@ +{ + "name": "noccylabs/juicer", + "description": "E-liquid mixing library", + "type": "library", + "license": "GPL-3.0", + "authors": [ + { + "name": "Christopher Vagnetoft", + "email": "cvagnetoft@gmail.com" + } + ], + "autoload": { + "psr-4": { + "NoccyLabs\\Juicer\\": "src/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..ad9b759 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,21 @@ + + + + + tests + + + + + + src + + + diff --git a/src/Ingredient/Base.php b/src/Ingredient/Base.php new file mode 100644 index 0000000..85f647d --- /dev/null +++ b/src/Ingredient/Base.php @@ -0,0 +1,73 @@ + self::MASS_PG, + 'VG' => self::MASS_VG, + ]; + + protected $base; + + protected $components = []; + + protected $specificGravity; + + public function __construct(string $base) + { + $this->components = self::parseComponents($base); + $this->base = $base; + $this->specificGravity = self::calculateSpecificGravity($this->components); + } + + public function getSpecificGravity(): float + { + return $this->specificGravity; + } + + public function getComponents(): array + { + return $this->components; + } + + public static function parseComponents(string $base) + { + $found = []; + if (!preg_match('/^([A-Z]+)([0-9]{1,3}+)$/i', $base, $match)) { + throw new \Exception(); + } + + if ($match[1] == "PG") { + return [ + 'PG' => $match[2], + 'VG' => 100 - $match[2] + ]; + } elseif ($match[1] == "VG") { + return [ + 'PG' => 100 - $match[2], + 'VG' => $match[2] + ]; + } + } + + public static function calculateSpecificGravity(array $components): float + { + $specificGravity = 0.0; + foreach ($components as $component=>$percent) { + $percentFloat = $percent / 100; + $specificGravity += (self::$MASS[$component] * $percentFloat); + } + return $specificGravity; + } + + public function __toString() + { + return $this->base; + } +} + diff --git a/src/Ingredient/Ingredient.php b/src/Ingredient/Ingredient.php new file mode 100644 index 0000000..cc7f82b --- /dev/null +++ b/src/Ingredient/Ingredient.php @@ -0,0 +1,55 @@ +name = $name; + $this->brand = $brand; + $this->percent = $percent; + $this->base = $base; + } + + /** + * {@inheritDoc} + */ + public function getFlavorName(): string + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getFlavorBrand(): ?string + { + return $this->brand; + } + + /** + * {@inheritDoc} + */ + public function getPercent(): float + { + return $this->percent; + } + + /** + * {@inheritDoc} + */ + public function getBase(): ?string + { + return $this->base; + } + +} \ No newline at end of file diff --git a/src/Ingredient/IngredientInterface.php b/src/Ingredient/IngredientInterface.php new file mode 100644 index 0000000..edf3460 --- /dev/null +++ b/src/Ingredient/IngredientInterface.php @@ -0,0 +1,37 @@ +base = $base; + $this->nicotineStrength = $strength; + } + + public function getSpecificGravity(): float + { + return 0.0; + } + +} + diff --git a/src/Recipe/Exporter/JsonExporter.php b/src/Recipe/Exporter/JsonExporter.php new file mode 100644 index 0000000..b79eef6 --- /dev/null +++ b/src/Recipe/Exporter/JsonExporter.php @@ -0,0 +1,49 @@ +getIngredients() as $ingredient) { + $ingredients[] = [ + 'brand' => $ingredient->getFlavorBrand(), + 'flavor' => $ingredient->getFlavorName(), + 'percent' => $ingredient->getPercent() + ]; + } + + $document = [ + 'recipe' => $recipe->getRecipeName(), + 'author' => $recipe->getRecipeAuthor(), + 'tags' => $recipe->getTags(), + 'description' => $recipe->getDescription(), + 'extra' => $recipe->getExtra(), + 'ingredients' => $ingredients + ]; + + return json_encode($document,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + + } + + public function writeToFile(RecipeInterface $recipe, string $filename) + { + $json = $this->export($recipe); + + $fd = fopen($filename, "w"); + if (!$fd) { + throw new \InvalidArgumentException(); + } + fwrite($fd, $json, strlen($json)); + fclose($fd); + } + +} \ No newline at end of file diff --git a/src/Recipe/Importer/JsonImporter.php b/src/Recipe/Importer/JsonImporter.php new file mode 100644 index 0000000..4435530 --- /dev/null +++ b/src/Recipe/Importer/JsonImporter.php @@ -0,0 +1,44 @@ +setRecipeName(@$data->recipe); + $recipe->setRecipeAuthor(@$data->author); + $recipe->setDescription(@$data->description); + $recipe->setExtra((array)@$data->extra); + + foreach ((array)@$data->ingredients as $ingredientData) { + $ingredient = new Ingredient($ingredientData->flavor, $ingredientData->brand, $ingredientData->percent); + $recipe->addIngredient($ingredient); + } + + return $recipe; + } + + public function readFromFile(string $filename): RecipeInterface + { + $fd = fopen($filename, "r"); + if (!$fd) { + throw new \InvalidArgumentException(); + } + $json = fread($fd, filesize($filename)); + fclose($fd); + + return $this->import($json); + } + +} \ No newline at end of file diff --git a/src/Recipe/Mixer/MeasuredIngredient.php b/src/Recipe/Mixer/MeasuredIngredient.php new file mode 100644 index 0000000..f25d4e8 --- /dev/null +++ b/src/Recipe/Mixer/MeasuredIngredient.php @@ -0,0 +1,70 @@ +name = $name; + $this->brand = $brand; + $this->base = $base; + $this->asg = $asg; + $this->percent = $percent; + $this->volume = $volume; + $this->weight = $volume * $asg; + } + + public function getFlavorName(): string + { + return $this->name; + } + + public function getFlavorBrand(): ?string + { + return $this->brand; + } + + public function getBase(): ?string + { + return $this->base; + } + + public function getPercent(): float + { + return $this->percent; + } + + public function getSpecificGravity(): float + { + return $this->asg; + } + + public function getVolume(): float + { + return $this->volume; + } + + public function getWeight(): float + { + return $this->weight; + } +} \ No newline at end of file diff --git a/src/Recipe/Mixer/Mixer.php b/src/Recipe/Mixer/Mixer.php new file mode 100644 index 0000000..8c656d9 --- /dev/null +++ b/src/Recipe/Mixer/Mixer.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); + } + + if (array_key_exists('PG', $components) && $components['PG'] > 0) { + $mixed[] = new MeasuredIngredient("PG", null, "PG100", Base::MASS_PG, 100, $volume); + } + + return $mixed; + } + +} \ No newline at end of file diff --git a/src/Recipe/Recipe.php b/src/Recipe/Recipe.php new file mode 100644 index 0000000..e7189dc --- /dev/null +++ b/src/Recipe/Recipe.php @@ -0,0 +1,110 @@ +name = $name; + } + + public function setRecipeAuthor(?string $author) + { + $this->author = $author; + } + + public function setTags(array $tags) + { + $this->tags = $tags; + } + + public function addTag(string $tag) + { + $this->tags[] = $tag; + } + + public function setDescription(?string $description) + { + $this->description = $description; + } + + public function addIngredient(IngredientInterface $ingredient) + { + $this->ingredients[] = $ingredient; + } + + public function setExtra(array $extra) + { + $this->extra = $extra; + } + + /** + * {@inheritDoc} + */ + public function getRecipeName(): ?string + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getRecipeAuthor(): ?string + { + return $this->author; + } + + /** + * {@inheritDoc} + */ + public function getTags(): array + { + return $this->tags; + } + + /** + * {@inheritDoc} + */ + public function getExtra(): array + { + return $this->extra; + } + + /** + * {@inheritDoc} + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * {@inheritDoc} + */ + public function getIngredients(): array + { + return $this->ingredients; + } + +} \ No newline at end of file diff --git a/src/Recipe/RecipeInterface.php b/src/Recipe/RecipeInterface.php new file mode 100644 index 0000000..72acaf6 --- /dev/null +++ b/src/Recipe/RecipeInterface.php @@ -0,0 +1,27 @@ +assertEquals($expected, $base->getSpecificGravity()); + } + +} \ No newline at end of file diff --git a/tests/Recipe/Exporter/JsonExporterTest.php b/tests/Recipe/Exporter/JsonExporterTest.php new file mode 100644 index 0000000..6652d9d --- /dev/null +++ b/tests/Recipe/Exporter/JsonExporterTest.php @@ -0,0 +1,65 @@ +setRecipeName("foo"); + $recipe->setRecipeAuthor("bar"); + + $this->assertInstanceOf(Recipe::class, $recipe); + $this->assertEquals("foo", $recipe->getRecipeName()); + $this->assertEquals("bar", $recipe->getRecipeAuthor()); + + $exporter = new JsonExporter(); + + $exported = $exporter->export($recipe); + $expected = json_encode([ + 'recipe' => 'foo', + 'author' => 'bar', + 'tags' => [], + 'description' => null, + 'extra' => [], + 'ingredients' => [] + ], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + + $this->assertEquals($expected, $exported); + } + + public function testThatRecipesWithIngredientsCanBeExportedToJson() + { + $recipe = new Recipe(); + $recipe->setRecipeName("foo"); + $recipe->setRecipeAuthor("bar"); + $recipe->addIngredient(new Ingredient("Cherry", "FA", 1)); + $recipe->addIngredient(new Ingredient("Vanilla Swirl", "TFA", 2)); + + $this->assertInstanceOf(Recipe::class, $recipe); + $this->assertEquals("foo", $recipe->getRecipeName()); + $this->assertEquals("bar", $recipe->getRecipeAuthor()); + + $exporter = new JsonExporter(); + + $exported = $exporter->export($recipe); + $expected = json_encode([ + 'recipe' => 'foo', + 'author' => 'bar', + 'tags' => [], + 'description' => null, + 'extra' => [], + 'ingredients' => [ + [ 'brand' => 'FA', 'flavor' => 'Cherry', 'percent' => 1 ], + [ 'brand' => 'TFA', 'flavor' => 'Vanilla Swirl', 'percent' => 2 ] + ] + ], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + + $this->assertEquals($expected, $exported); + } + +} \ No newline at end of file diff --git a/tests/Recipe/Importer/JsonImporterTest.php b/tests/Recipe/Importer/JsonImporterTest.php new file mode 100644 index 0000000..b7fbab7 --- /dev/null +++ b/tests/Recipe/Importer/JsonImporterTest.php @@ -0,0 +1,45 @@ +import($json); + + $this->assertEquals("foo", $recipe->getRecipeName()); + $this->assertEquals("bar", $recipe->getRecipeAuthor()); + + $this->assertCount(1, $recipe->getIngredients()); + + } + + public function testThatRecipesCanBeImportedFromExportedJson() + { + + $recipe = new Recipe(); + $recipe->setRecipeName("foo"); + $recipe->setRecipeAuthor("bar"); + $recipe->addIngredient(new Ingredient("Cherry", "FA", 1)); + $recipe->addIngredient(new Ingredient("Vanilla Swirl", "TFA", 2)); + + $exporter = new JsonExporter(); + $exported = $exporter->export($recipe); + + $importer = new JsonImporter(); + $importedRecipe = $importer->import($exported); + + $this->assertEquals($recipe, $importedRecipe); + } + + +} \ No newline at end of file diff --git a/tests/Recipe/Mixer/MixerTest.php b/tests/Recipe/Mixer/MixerTest.php new file mode 100644 index 0000000..4bded74 --- /dev/null +++ b/tests/Recipe/Mixer/MixerTest.php @@ -0,0 +1,59 @@ +mixRecipe($recipe, 10, $base, 0); + + $this->assertCount(1, $mixed); + $mixedVg = reset($mixed); + + $this->assertEquals(10, $mixedVg->getVolume()); + $this->assertEquals("VG", $mixedVg->getFlavorName()); + } + + public function testMixingEmptyRecipesWithPg() + { + $recipe = new Recipe(); + $mixer = new Mixer(); + + $base = new Base("PG100"); + $mixed = $mixer->mixRecipe($recipe, 10, $base, 0); + + $this->assertCount(1, $mixed); + $mixedPg = reset($mixed); + + $this->assertEquals(10, $mixedPg->getVolume()); + $this->assertEquals("PG", $mixedPg->getFlavorName()); + } + + public function testMixingEmptyRecipesWith70Vg30Pg() + { + $recipe = new Recipe(); + $mixer = new Mixer(); + + $base = new Base("VG70"); + $mixed = $mixer->mixRecipe($recipe, 10, $base, 0); + + $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()); + } + +} \ No newline at end of file diff --git a/tests/Recipe/RecipeTest.php b/tests/Recipe/RecipeTest.php new file mode 100644 index 0000000..09eb0d9 --- /dev/null +++ b/tests/Recipe/RecipeTest.php @@ -0,0 +1,28 @@ +setRecipeName("foo"); + $recipe->setRecipeAuthor("bar"); + + $ingredient1 = new Ingredient("Ingredient 1"); + $ingredient2 = new Ingredient("Ingredient 2"); + + $recipe->addIngredient($ingredient1); + $recipe->addIngredient($ingredient2); + + $this->assertInstanceOf(Recipe::class, $recipe); + $this->assertEquals("foo", $recipe->getRecipeName()); + $this->assertEquals("bar", $recipe->getRecipeAuthor()); + $this->assertEquals([$ingredient1, $ingredient2], $recipe->getIngredients()); + } + +} \ No newline at end of file