Merge pull request #1723 from j0hannesr0th/master

Enhance recipe recalculation algorithm
This commit is contained in:
Christian Wolf 2023-06-30 19:53:25 +02:00 коммит произвёл GitHub
Родитель 21b3ea0068 d3feaec60b
Коммит 838080d6c3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 113 добавлений и 48 удалений

Просмотреть файл

@ -5,6 +5,8 @@
[#1573](https://github.com/nextcloud/cookbook/pull/1573) @j0hannesr0th [#1573](https://github.com/nextcloud/cookbook/pull/1573) @j0hannesr0th
- Add copy to clipboard action for ingredients - Add copy to clipboard action for ingredients
[#1602](https://github.com/nextcloud/cookbook/pull/1602) @j0hannesr0th [#1602](https://github.com/nextcloud/cookbook/pull/1602) @j0hannesr0th
- Enhance recipe recalculation algorithm
[#1723](https://github.com/nextcloud/cookbook/pull/1723) @j0hannesr0th
### Fixed ### Fixed
- Fix translation string to not contain quotes - Fix translation string to not contain quotes

Просмотреть файл

@ -69,14 +69,27 @@
<span> <span>
<button <button
:disabled="recipeYield === 1" :disabled="recipeYield === 1"
@click="recalculateIngredients(false)" @click="changeRecipeYield(false)"
> >
<span class="icon-view-previous" /> <span class="icon-view-previous" />
</button> </button>
{{ recipeYield }} <input
<button @click="recalculateIngredients"> v-model="recipeYield"
type="number"
min="0"
/>
<button @click="changeRecipeYield">
<span class="icon-view-next" /> <span class="icon-view-next" />
</button> </button>
<button
v-if="
recipeYield !==
$store.state.recipe.recipeYield
"
@click="restoreOriginalRecipeYield"
>
<span class="icon-history" />
</button>
</span> </span>
</p> </p>
</div> </div>
@ -125,16 +138,17 @@
</template> </template>
{{ t("cookbook", "Copy ingredients") }} {{ t("cookbook", "Copy ingredients") }}
</NcButton> </NcButton>
<h3 v-if="parsedIngredients.length"> <h3 v-if="scaledIngredients.length">
{{ t("cookbook", "Ingredients") }} {{ t("cookbook", "Ingredients") }}
</h3> </h3>
<ul v-if="parsedIngredients.length"> <ul v-if="scaledIngredients.length">
<RecipeIngredient <RecipeIngredient
v-for="(ingredient, idx) in parsedIngredients" v-for="(ingredient, idx) in scaledIngredients"
:key="'ingr' + idx" :key="'ingr' + idx"
:ingredient="ingredient" :ingredient="ingredient"
:ingredient-has-correct-syntax=" :ingredient-has-correct-syntax="
validateIngredientSyntax(ingredient) /* yieldCalculator.isValidIngredientSyntax(ingredient) */
ingredientsWithValidSyntax[idx]
" "
:recipe-ingredients-have-subgroups=" :recipe-ingredients-have-subgroups="
recipeIngredientsHaveSubgroups recipeIngredientsHaveSubgroups
@ -305,6 +319,7 @@ import api from "cookbook/js/api-interface"
import helpers from "cookbook/js/helper" import helpers from "cookbook/js/helper"
import normalizeMarkdown from "cookbook/js/title-rename" import normalizeMarkdown from "cookbook/js/title-rename"
import { showSimpleAlertModal } from "cookbook/js/modals" import { showSimpleAlertModal } from "cookbook/js/modals"
import yieldCalculator from "cookbook/js/yieldCalculator"
import ContentCopyIcon from "icons/ContentCopy.vue" import ContentCopyIcon from "icons/ContentCopy.vue"
@ -505,8 +520,20 @@ export default {
visibleInfoBlocks() { visibleInfoBlocks() {
return this.$store.state.config?.visibleInfoBlocks ?? {} return this.$store.state.config?.visibleInfoBlocks ?? {}
}, },
scaledIngredients() {
return yieldCalculator.recalculateIngredients(
this.parsedIngredients,
this.recipeYield,
this.$store.state.recipe.recipeYield
)
},
ingredientsWithValidSyntax() {
return this.parsedIngredients.map(
yieldCalculator.isValidIngredientSyntax
)
},
ingredientsSyntaxCorrect() { ingredientsSyntaxCorrect() {
return this.parsedIngredients.every(this.validateIngredientSyntax) return this.ingredientsWithValidSyntax.every((x) => x)
}, },
}, },
watch: { watch: {
@ -576,6 +603,11 @@ export default {
} }
} }
}, },
recipeYield() {
if (this.recipeYield < 0) {
this.restoreOriginalRecipeYield()
}
},
}, },
mounted() { mounted() {
this.$log.info("RecipeView mounted") this.$log.info("RecipeView mounted")
@ -662,48 +694,11 @@ export default {
this.recipeYield = this.$store.state.recipe.recipeYield this.recipeYield = this.$store.state.recipe.recipeYield
}, },
recalculateIngredients(increaseYield = true) { changeRecipeYield(increase = true) {
this.recipeYield = increaseYield this.recipeYield = +this.recipeYield + (increase ? 1 : -1)
? this.recipeYield + 1
: this.recipeYield - 1
this.parsedIngredients = this.parsedIngredients.map(
(ingredient) => {
if (this.validateIngredientSyntax(ingredient)) {
const amount = parseFloat(ingredient.split(" ")[0])
const unitAndIngredient = ingredient
.split(" ")
.slice(1)
.join(" ")
const newAmount =
amount *
(increaseYield
? this.recipeYield / (this.recipeYield - 1)
: this.recipeYield / (this.recipeYield + 1))
// Remove decimal places if they are .00
return `${newAmount
.toFixed(2)
.replace(/[.]00$/, "")} ${unitAndIngredient}`
}
return ingredient
}
)
},
validateIngredientSyntax(ingredient) {
/*
Explanation:
^: Start of string
(?:\d+(?:\.\d+)?|\.\d+): Non-capturing group that matches either a positive float value or a positive integer value. The first alternative matches one or more digits, followed by an optional decimal part consisting of a dot and one or more digits. The second alternative matches a decimal point followed by one or more digits.
(?:\s.+$|\s\S+$): Non-capturing group that matches a whitespace character followed by any character with unlimited length or any special character with unlimited length. The first alternative matches a whitespace character followed by any character(s) until the end of the string. The second alternative matches a whitespace character followed by any non-whitespace character(s) until the end of the string.
$: End of string
*/
const ingredientRegExp = /^(?:\d+(?:\.\d+)?|\.\d+)(?:\s.+$|\s\S+$)/
return ingredientRegExp.test(ingredient)
}, },
copyIngredientsToClipboard() { copyIngredientsToClipboard() {
const ingredientsToCopy = this.parsedIngredients.join("\n") const ingredientsToCopy = this.scaledIngredients.join("\n")
if (navigator.clipboard) { if (navigator.clipboard) {
navigator.clipboard navigator.clipboard
@ -736,6 +731,9 @@ export default {
document.body.removeChild(input) document.body.removeChild(input)
} }
}, },
restoreOriginalRecipeYield() {
this.recipeYield = this.$store.state.recipe.recipeYield
},
}, },
} }
</script> </script>

65
src/js/yieldCalculator.js Normal file
Просмотреть файл

@ -0,0 +1,65 @@
function isValidIngredientSyntax(ingredient) {
/*
*** Outdated!!! ***
Explanation of ingredientSyntaxRegExp:
^: Start of string
(?:\d+(?:\.\d+)?|\.\d+): Non-capturing group that matches either a positive float value or a positive integer value. The first alternative matches one or more digits, followed by an optional decimal part consisting of a dot and one or more digits. The second alternative matches a decimal point followed by one or more digits.
(?:\s.+$|\s\S+$): Non-capturing group that matches a whitespace character followed by any character with unlimited length or any special character with unlimited length. The first alternative matches a whitespace character followed by any character(s) until the end of the string. The second alternative matches a whitespace character followed by any non-whitespace character(s) until the end of the string.
$: End of string
*/
const ingredientSyntaxRegExp = /^(?:\d+(?:\.\d+)?(?:\/\d+)?)\s?.*$/
// Regular expression to match all possible fractions within a string
const ingredientFractionRegExp = /\b\d+\/\d+\b/g
/*
Explanation of ingredientMultipleSeperatorsRegExp:
/^ - Start of the string
-? - Matches an optional minus sign
\d+ - Matches one or more digits
(?:[.,]\d+){2,} - Non-capturing group that matches a separator (.,) followed by one or more digits.
The {2,} quantifier ensures that there are at least two occurrences of this pattern.
.* - Matches any characters (except newline) zero or more times.
*/
const ingredientMultipleSeperatorsRegExp = /^-?\d+(?:[.,]\d+){2,}.*/
return (
ingredientSyntaxRegExp.test(ingredient) &&
!ingredientFractionRegExp.test(ingredient) &&
!ingredientMultipleSeperatorsRegExp.test(ingredient)
)
}
function isIngredientsArrayValid(ingredients) {
return ingredients.every(isValidIngredientSyntax)
}
function recalculateIngredients(ingredients, currentYield, originalYield) {
return ingredients.map((ingredient, index) => {
if (isValidIngredientSyntax(ingredient)) {
// For some cases, where the unit is not separated from the amount: 100g cheese
const possibleUnit = ingredient
.split(" ")[0]
.replace(/[^a-zA-Z]/g, "")
const amount = parseFloat(ingredients[index].split(" ")[0])
const unitAndIngredient = ingredient.split(" ").slice(1).join(" ")
let newAmount = (amount / originalYield) * currentYield
newAmount = newAmount.toFixed(2).replace(/[.]00$/, "")
return `${newAmount}${possibleUnit} ${unitAndIngredient}`
}
const factor = currentYield / originalYield
const prefix = ((f) => {
if (f === 1) {
return ""
}
return `${f.toFixed(2)}x `
})(factor)
return `${prefix}${ingredient}`
})
}
export default {
isValidIngredientSyntax,
isIngredientsArrayValid,
recalculateIngredients,
}