зеркало из https://github.com/nextcloud/cookbook.git
Merge pull request #1723 from j0hannesr0th/master
Enhance recipe recalculation algorithm
This commit is contained in:
Коммит
838080d6c3
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче