Add support for fractional data entry

This commit is contained in:
Christopher C. Wells 2021-01-15 05:47:29 -08:00
parent 3628ab1f51
commit 81f590515d
12 changed files with 157 additions and 19 deletions

View File

@ -3,6 +3,8 @@
namespace App\Http\Controllers;
use App\Models\Food;
use App\Rules\StringIsDecimalOrFraction;
use App\Support\Number;
use App\Support\Nutrients;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
@ -70,7 +72,7 @@ class FoodController extends Controller
'name' => 'required|string',
'detail' => 'nullable|string',
'brand' => 'nullable|string',
'serving_size' => 'required|numeric',
'serving_size' => ['required', new StringIsDecimalOrFraction],
'serving_unit' => 'nullable|string',
'serving_weight' => 'required|numeric',
'calories' => 'nullable|numeric',
@ -80,6 +82,7 @@ class FoodController extends Controller
'carbohydrates' => 'nullable|numeric',
'protein' => 'nullable|numeric',
]);
$attributes['serving_size'] = Number::floatFromString($attributes['serving_size']);
$food->fill(array_filter($attributes))->save();
return redirect(route('foods.show', $food))
->with('message', 'Changes saved!');

View File

@ -9,6 +9,8 @@ use App\Models\Food;
use App\Models\JournalEntry;
use App\Models\Recipe;
use App\Rules\ArrayNotEmpty;
use App\Rules\StringIsDecimalOrFraction;
use App\Support\Number;
use App\Support\Nutrients;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
@ -82,7 +84,7 @@ class JournalEntryController extends Controller
'date' => 'required|date',
'meal' => 'required|string',
'amounts' => ['required', 'array', new ArrayNotEmpty],
'amounts.*' => 'required_with:foods.*,recipes.*|nullable|numeric|min:0',
'amounts.*' => ['required_with:foods.*,recipes.*', 'nullable', new StringIsDecimalOrFraction],
'units' => ['required', 'array', new ArrayNotEmpty],
'units.*' => 'nullable|string',
'foods' => 'required|array',
@ -119,7 +121,7 @@ class JournalEntryController extends Controller
$food = $foods->get($id);
$nutrient_multiplier = Nutrients::calculateFoodNutrientMultiplier(
$food,
$input['amounts'][$key],
Number::floatFromString($input['amounts'][$key]),
$input['units'][$key],
);
foreach ($nutrients as $nutrient => $amount) {
@ -134,7 +136,7 @@ class JournalEntryController extends Controller
foreach ($recipes_selected as $key => $id) {
$recipe = $recipes->get($id);
foreach ($nutrients as $nutrient => $amount) {
$nutrients[$nutrient] += $recipe->{"{$nutrient}PerServing"}() * $input['amounts'][$key];
$nutrients[$nutrient] += $recipe->{"{$nutrient}PerServing"}() * Number::floatFromString($input['amounts'][$key]);
}
$summary[] = "{$input['amounts'][$key]} {$input['units'][$key]} {$recipe->name}";
}

View File

@ -7,6 +7,8 @@ use App\Models\FoodAmount;
use App\Models\Recipe;
use App\Models\RecipeStep;
use App\Rules\ArrayNotEmpty;
use App\Rules\StringIsDecimalOrFraction;
use App\Support\Number;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@ -63,7 +65,7 @@ class RecipeController extends Controller
'description' => 'nullable|string',
'servings' => 'required|numeric',
'foods_amount' => ['required', 'array', new ArrayNotEmpty],
'foods_amount.*' => 'required_with:foods.*|nullable|numeric|min:0',
'foods_amount.*' => ['required_with:foods.*', 'nullable', new StringIsDecimalOrFraction],
'foods_unit' => ['required', 'array'],
'foods_unit.*' => 'nullable|string',
'foods' => ['required', 'array', new ArrayNotEmpty],
@ -88,7 +90,7 @@ class RecipeController extends Controller
$weight = 0;
foreach (array_filter($input['foods_amount']) as $key => $amount) {
$food_amounts[$key] = new FoodAmount([
'amount' => (float) $amount,
'amount' => Number::floatFromString($amount),
'unit' => $input['foods_unit'][$key],
'weight' => $weight++,
]);

View File

@ -0,0 +1,38 @@
<?php
namespace App\Rules;
use App\Support\Number;
use Illuminate\Contracts\Validation\Rule;
class StringIsDecimalOrFraction implements Rule
{
/**
* Determine if the string is a decimal or fraction, excluding zero.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value): bool
{
try {
$result = Number::floatFromString($value);
return $result != 0;
}
catch (\InvalidArgumentException $e) {
// Allow to pass through, method will return false.
}
return false;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message(): string
{
return 'The :attribute must be a decimal or fraction.';
}
}

51
app/Support/Number.php Normal file
View File

@ -0,0 +1,51 @@
<?php
namespace App\Support;
use Phospr\Fraction;
class Number
{
/**
* Get a float value from a decimal or fraction string.
*
* @param string $value
* Value in decimal or string format.
* @return float
* Float representation of the value.
*
* @throws \InvalidArgumentException
*/
public static function floatFromString(string $value): float {
if ((float) $value == $value) {
$result = (float) $value;
}
else {
$result = Fraction::fromString($value)->toFloat();
}
return $result;
}
/**
* Get a string faction representation of a float.
*
* @todo Handle repeating values like 1/3, 2/3, etc.
*
* @see https://rosettacode.org/wiki/Convert_decimal_number_to_rational#PHP
*
* @param float $value
* Value to convert to string fraction.
* @return string
* String fraction.
*/
public static function fractionStringFromFloat(float $value): string {
$fraction = (string) Fraction::fromFloat($value);
if ($fraction === '33333333/100000000') {
$fraction = '1/3';
}
elseif ($fraction === '66666667/100000000') {
$fraction = '2/3';
}
return $fraction;
}
}

View File

@ -10,7 +10,8 @@
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^8.12",
"laravel/tinker": "^2.5"
"laravel/tinker": "^2.5",
"phospr/fraction": "^1.2"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^2.9",

45
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6798cd035bece9ee986d5e3a59036303",
"content-hash": "7054301f9158e201ddff5b37f41a1c5a",
"packages": [
{
"name": "asm89/stack-cors",
@ -1829,6 +1829,49 @@
},
"time": "2020-11-07T02:01:34+00:00"
},
{
"name": "phospr/fraction",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/phospr/fraction.git",
"reference": "3f195b920bca0ba4eac8575e397af283782c699d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phospr/fraction/zipball/3f195b920bca0ba4eac8575e397af283782c699d",
"reference": "3f195b920bca0ba4eac8575e397af283782c699d",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "4.0.*"
},
"type": "library",
"autoload": {
"psr-4": {
"Phospr\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Tom Haskins-Vaughan",
"email": "tom@tomhv.uk"
}
],
"description": "A composer-installable fractions library",
"support": {
"issues": "https://github.com/phospr/fraction/issues",
"source": "https://github.com/phospr/fraction/tree/master"
},
"time": "2016-12-21T15:33:12+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.7.5",

View File

@ -72,11 +72,10 @@
<x-inputs.input id="serving_size"
class="block mt-1"
type="number"
step="any"
type="text"
name="serving_size"
size="10"
:value="old('serving_size', $food->serving_size)"/>
:value="old('serving_size', \App\Support\Number::fractionStringFromFloat($food->serving_size))"/>
</div>
<!-- Serving unit -->

View File

@ -33,7 +33,7 @@
</div>
@endif
<div class="font-bold">
Serving size {{ $food->serving_size }}
Serving size {{ \App\Support\Number::fractionStringFromFloat($food->serving_size) }}
{{ $food->serving_unit }}
({{ $food->serving_weight }}g)
</div>

View File

@ -60,11 +60,10 @@
<x-inputs.label for="recipes" :value="__('Recipe')" class="col-span-4"/>
@for ($i = 0; $i < 10; $i++)
<div>
<x-inputs.input type="number"
<x-inputs.input type="text"
name="amounts[]"
class="block w-full"
:value="old('amounts.' . $i)"
step="any" />
:value="old('amounts.' . $i)" />
</div>
<div class="col-span-2">
<x-inputs.select name="units[]"

View File

@ -69,10 +69,10 @@
<h3 class="pt-2 mb-2 font-extrabold">Ingredients</h3>
@for($i = 0; $i < 20; $i++)
<div class="flex flex-row space-x-4 mb-4">
<x-inputs.input type="number"
<x-inputs.input type="text"
name="foods_amount[]"
:value="old('foods_amount.' . $i)"
step="any" />
size="5"
:value="old('foods_amount.' . $i)" />
<x-inputs.select name="foods_unit[]"
:options="$food_units"
:selectedValue="old('foods_unit.' . $i)">

View File

@ -13,7 +13,7 @@
<h3 class="mb-2 mt-4 font-bold">Ingredients</h3>
@foreach($recipe->foodAmounts as $ia)
<div class="flex flex-row space-x-2 mb-2">
<div>{{ $ia->amount }}</div>
<div>{{ \App\Support\Number::fractionStringFromFloat($ia->amount) }}</div>
@if($ia->unit)<div>{{ $ia->unit }}</div>@endif
<div>{{ $ia->food->name }}</div>
</div>