mirror of https://github.com/kcal-app/kcal.git
Add support for fractional data entry
This commit is contained in:
parent
3628ab1f51
commit
81f590515d
|
@ -3,6 +3,8 @@
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Food;
|
use App\Models\Food;
|
||||||
|
use App\Rules\StringIsDecimalOrFraction;
|
||||||
|
use App\Support\Number;
|
||||||
use App\Support\Nutrients;
|
use App\Support\Nutrients;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
@ -70,7 +72,7 @@ class FoodController extends Controller
|
||||||
'name' => 'required|string',
|
'name' => 'required|string',
|
||||||
'detail' => 'nullable|string',
|
'detail' => 'nullable|string',
|
||||||
'brand' => 'nullable|string',
|
'brand' => 'nullable|string',
|
||||||
'serving_size' => 'required|numeric',
|
'serving_size' => ['required', new StringIsDecimalOrFraction],
|
||||||
'serving_unit' => 'nullable|string',
|
'serving_unit' => 'nullable|string',
|
||||||
'serving_weight' => 'required|numeric',
|
'serving_weight' => 'required|numeric',
|
||||||
'calories' => 'nullable|numeric',
|
'calories' => 'nullable|numeric',
|
||||||
|
@ -80,6 +82,7 @@ class FoodController extends Controller
|
||||||
'carbohydrates' => 'nullable|numeric',
|
'carbohydrates' => 'nullable|numeric',
|
||||||
'protein' => 'nullable|numeric',
|
'protein' => 'nullable|numeric',
|
||||||
]);
|
]);
|
||||||
|
$attributes['serving_size'] = Number::floatFromString($attributes['serving_size']);
|
||||||
$food->fill(array_filter($attributes))->save();
|
$food->fill(array_filter($attributes))->save();
|
||||||
return redirect(route('foods.show', $food))
|
return redirect(route('foods.show', $food))
|
||||||
->with('message', 'Changes saved!');
|
->with('message', 'Changes saved!');
|
||||||
|
|
|
@ -9,6 +9,8 @@ use App\Models\Food;
|
||||||
use App\Models\JournalEntry;
|
use App\Models\JournalEntry;
|
||||||
use App\Models\Recipe;
|
use App\Models\Recipe;
|
||||||
use App\Rules\ArrayNotEmpty;
|
use App\Rules\ArrayNotEmpty;
|
||||||
|
use App\Rules\StringIsDecimalOrFraction;
|
||||||
|
use App\Support\Number;
|
||||||
use App\Support\Nutrients;
|
use App\Support\Nutrients;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
@ -82,7 +84,7 @@ class JournalEntryController extends Controller
|
||||||
'date' => 'required|date',
|
'date' => 'required|date',
|
||||||
'meal' => 'required|string',
|
'meal' => 'required|string',
|
||||||
'amounts' => ['required', 'array', new ArrayNotEmpty],
|
'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' => ['required', 'array', new ArrayNotEmpty],
|
||||||
'units.*' => 'nullable|string',
|
'units.*' => 'nullable|string',
|
||||||
'foods' => 'required|array',
|
'foods' => 'required|array',
|
||||||
|
@ -119,7 +121,7 @@ class JournalEntryController extends Controller
|
||||||
$food = $foods->get($id);
|
$food = $foods->get($id);
|
||||||
$nutrient_multiplier = Nutrients::calculateFoodNutrientMultiplier(
|
$nutrient_multiplier = Nutrients::calculateFoodNutrientMultiplier(
|
||||||
$food,
|
$food,
|
||||||
$input['amounts'][$key],
|
Number::floatFromString($input['amounts'][$key]),
|
||||||
$input['units'][$key],
|
$input['units'][$key],
|
||||||
);
|
);
|
||||||
foreach ($nutrients as $nutrient => $amount) {
|
foreach ($nutrients as $nutrient => $amount) {
|
||||||
|
@ -134,7 +136,7 @@ class JournalEntryController extends Controller
|
||||||
foreach ($recipes_selected as $key => $id) {
|
foreach ($recipes_selected as $key => $id) {
|
||||||
$recipe = $recipes->get($id);
|
$recipe = $recipes->get($id);
|
||||||
foreach ($nutrients as $nutrient => $amount) {
|
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}";
|
$summary[] = "{$input['amounts'][$key]} {$input['units'][$key]} {$recipe->name}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ use App\Models\FoodAmount;
|
||||||
use App\Models\Recipe;
|
use App\Models\Recipe;
|
||||||
use App\Models\RecipeStep;
|
use App\Models\RecipeStep;
|
||||||
use App\Rules\ArrayNotEmpty;
|
use App\Rules\ArrayNotEmpty;
|
||||||
|
use App\Rules\StringIsDecimalOrFraction;
|
||||||
|
use App\Support\Number;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
@ -63,7 +65,7 @@ class RecipeController extends Controller
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'servings' => 'required|numeric',
|
'servings' => 'required|numeric',
|
||||||
'foods_amount' => ['required', 'array', new ArrayNotEmpty],
|
'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' => ['required', 'array'],
|
||||||
'foods_unit.*' => 'nullable|string',
|
'foods_unit.*' => 'nullable|string',
|
||||||
'foods' => ['required', 'array', new ArrayNotEmpty],
|
'foods' => ['required', 'array', new ArrayNotEmpty],
|
||||||
|
@ -88,7 +90,7 @@ class RecipeController extends Controller
|
||||||
$weight = 0;
|
$weight = 0;
|
||||||
foreach (array_filter($input['foods_amount']) as $key => $amount) {
|
foreach (array_filter($input['foods_amount']) as $key => $amount) {
|
||||||
$food_amounts[$key] = new FoodAmount([
|
$food_amounts[$key] = new FoodAmount([
|
||||||
'amount' => (float) $amount,
|
'amount' => Number::floatFromString($amount),
|
||||||
'unit' => $input['foods_unit'][$key],
|
'unit' => $input['foods_unit'][$key],
|
||||||
'weight' => $weight++,
|
'weight' => $weight++,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -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.';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,8 @@
|
||||||
"fruitcake/laravel-cors": "^2.0",
|
"fruitcake/laravel-cors": "^2.0",
|
||||||
"guzzlehttp/guzzle": "^7.0.1",
|
"guzzlehttp/guzzle": "^7.0.1",
|
||||||
"laravel/framework": "^8.12",
|
"laravel/framework": "^8.12",
|
||||||
"laravel/tinker": "^2.5"
|
"laravel/tinker": "^2.5",
|
||||||
|
"phospr/fraction": "^1.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-ide-helper": "^2.9",
|
"barryvdh/laravel-ide-helper": "^2.9",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "6798cd035bece9ee986d5e3a59036303",
|
"content-hash": "7054301f9158e201ddff5b37f41a1c5a",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "asm89/stack-cors",
|
"name": "asm89/stack-cors",
|
||||||
|
@ -1829,6 +1829,49 @@
|
||||||
},
|
},
|
||||||
"time": "2020-11-07T02:01:34+00:00"
|
"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",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.7.5",
|
"version": "1.7.5",
|
||||||
|
|
|
@ -72,11 +72,10 @@
|
||||||
|
|
||||||
<x-inputs.input id="serving_size"
|
<x-inputs.input id="serving_size"
|
||||||
class="block mt-1"
|
class="block mt-1"
|
||||||
type="number"
|
type="text"
|
||||||
step="any"
|
|
||||||
name="serving_size"
|
name="serving_size"
|
||||||
size="10"
|
size="10"
|
||||||
:value="old('serving_size', $food->serving_size)"/>
|
:value="old('serving_size', \App\Support\Number::fractionStringFromFloat($food->serving_size))"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Serving unit -->
|
<!-- Serving unit -->
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="font-bold">
|
<div class="font-bold">
|
||||||
Serving size {{ $food->serving_size }}
|
Serving size {{ \App\Support\Number::fractionStringFromFloat($food->serving_size) }}
|
||||||
{{ $food->serving_unit }}
|
{{ $food->serving_unit }}
|
||||||
({{ $food->serving_weight }}g)
|
({{ $food->serving_weight }}g)
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -60,11 +60,10 @@
|
||||||
<x-inputs.label for="recipes" :value="__('Recipe')" class="col-span-4"/>
|
<x-inputs.label for="recipes" :value="__('Recipe')" class="col-span-4"/>
|
||||||
@for ($i = 0; $i < 10; $i++)
|
@for ($i = 0; $i < 10; $i++)
|
||||||
<div>
|
<div>
|
||||||
<x-inputs.input type="number"
|
<x-inputs.input type="text"
|
||||||
name="amounts[]"
|
name="amounts[]"
|
||||||
class="block w-full"
|
class="block w-full"
|
||||||
:value="old('amounts.' . $i)"
|
:value="old('amounts.' . $i)" />
|
||||||
step="any" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<x-inputs.select name="units[]"
|
<x-inputs.select name="units[]"
|
||||||
|
|
|
@ -69,10 +69,10 @@
|
||||||
<h3 class="pt-2 mb-2 font-extrabold">Ingredients</h3>
|
<h3 class="pt-2 mb-2 font-extrabold">Ingredients</h3>
|
||||||
@for($i = 0; $i < 20; $i++)
|
@for($i = 0; $i < 20; $i++)
|
||||||
<div class="flex flex-row space-x-4 mb-4">
|
<div class="flex flex-row space-x-4 mb-4">
|
||||||
<x-inputs.input type="number"
|
<x-inputs.input type="text"
|
||||||
name="foods_amount[]"
|
name="foods_amount[]"
|
||||||
:value="old('foods_amount.' . $i)"
|
size="5"
|
||||||
step="any" />
|
:value="old('foods_amount.' . $i)" />
|
||||||
<x-inputs.select name="foods_unit[]"
|
<x-inputs.select name="foods_unit[]"
|
||||||
:options="$food_units"
|
:options="$food_units"
|
||||||
:selectedValue="old('foods_unit.' . $i)">
|
:selectedValue="old('foods_unit.' . $i)">
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<h3 class="mb-2 mt-4 font-bold">Ingredients</h3>
|
<h3 class="mb-2 mt-4 font-bold">Ingredients</h3>
|
||||||
@foreach($recipe->foodAmounts as $ia)
|
@foreach($recipe->foodAmounts as $ia)
|
||||||
<div class="flex flex-row space-x-2 mb-2">
|
<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
|
@if($ia->unit)<div>{{ $ia->unit }}</div>@endif
|
||||||
<div>{{ $ia->food->name }}</div>
|
<div>{{ $ia->food->name }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue