diff --git a/frontend/components/Domain/Recipe/RecipeList.vue b/frontend/components/Domain/Recipe/RecipeList.vue
index 4aa2958d231..6df73b021bf 100644
--- a/frontend/components/Domain/Recipe/RecipeList.vue
+++ b/frontend/components/Domain/Recipe/RecipeList.vue
@@ -30,7 +30,7 @@
diff --git a/frontend/composables/recipe-page/use-extract-recipe-yield.test.ts b/frontend/composables/recipe-page/use-extract-recipe-yield.test.ts
index 3bc8e7996e8..a85605528ff 100644
--- a/frontend/composables/recipe-page/use-extract-recipe-yield.test.ts
+++ b/frontend/composables/recipe-page/use-extract-recipe-yield.test.ts
@@ -1,7 +1,20 @@
import { describe, expect, test } from "vitest";
+import { getLowestFraction } from "../recipes/use-fraction";
import { useExtractRecipeYield } from "./use-extract-recipe-yield";
describe("test use extract recipe yield", () => {
+ test("useFraction", () => {
+ expect(getLowestFraction(0.5)).toStrictEqual([0, 1, 2]);
+ expect(getLowestFraction(1.5)).toStrictEqual([1, 1, 2]);
+ expect(getLowestFraction(3.25)).toStrictEqual([3, 1, 4]);
+ expect(getLowestFraction(93452.75)).toStrictEqual([93452, 3, 4]);
+ expect(getLowestFraction(0.125)).toStrictEqual([0, 1, 8]);
+ expect(getLowestFraction(1.0 / 3.0)).toStrictEqual([0, 1, 3]);
+ expect(getLowestFraction(0.142857)).toStrictEqual([0, 1, 7]);
+ expect(getLowestFraction(0.5)).toStrictEqual([0, 1, 2]);
+ expect(getLowestFraction(0.5)).toStrictEqual([0, 1, 2]);
+ });
+
test("when text empty return empty", () => {
const result = useExtractRecipeYield(null, 1);
expect(result).toStrictEqual("");
@@ -18,54 +31,63 @@ describe("test use extract recipe yield", () => {
test("when text matches a mixed fraction, return a scaled fraction", () => {
const val = "10 1/2 units";
- const result = useExtractRecipeYield(val, 1);
+ const result = useExtractRecipeYield(val, 1, false);
expect(result).toStrictEqual(val);
- const resultScaled = useExtractRecipeYield(val, 3);
+ const resultScaled = useExtractRecipeYield(val, 3, false);
expect(resultScaled).toStrictEqual("31 1/2 units");
- const resultScaledPartial = useExtractRecipeYield(val, 2.5);
+ const resultScaledPartial = useExtractRecipeYield(val, 2.5, false);
expect(resultScaledPartial).toStrictEqual("26 1/4 units");
const resultScaledInt = useExtractRecipeYield(val, 4);
expect(resultScaledInt).toStrictEqual("42 units");
});
+ test("when unit precedes number, have correct formatting", () => {
+ const val = "serves 3";
+ const result = useExtractRecipeYield(val, 1, false);
+ expect(result).toStrictEqual(val)
+
+ const resultScaled = useExtractRecipeYield(val, 2.5, false);
+ expect(resultScaled).toStrictEqual("serves 7 1/2")
+ });
+
test("when text matches a fraction, return a scaled fraction", () => {
const val = "1/3 plates";
- const result = useExtractRecipeYield(val, 1);
+ const result = useExtractRecipeYield(val, 1, false);
expect(result).toStrictEqual(val);
- const resultScaled = useExtractRecipeYield(val, 2);
+ const resultScaled = useExtractRecipeYield(val, 2, false);
expect(resultScaled).toStrictEqual("2/3 plates");
const resultScaledInt = useExtractRecipeYield(val, 3);
expect(resultScaledInt).toStrictEqual("1 plates");
- const resultScaledPartial = useExtractRecipeYield(val, 2.5);
+ const resultScaledPartial = useExtractRecipeYield(val, 2.5, false);
expect(resultScaledPartial).toStrictEqual("5/6 plates");
- const resultScaledMixed = useExtractRecipeYield(val, 4);
+ const resultScaledMixed = useExtractRecipeYield(val, 4, false);
expect(resultScaledMixed).toStrictEqual("1 1/3 plates");
});
test("when text matches a decimal, return a scaled, rounded decimal", () => {
const val = "1.25 parts";
const result = useExtractRecipeYield(val, 1);
- expect(result).toStrictEqual(val);
+ expect(result).toStrictEqual("11⁄4 parts");
const resultScaled = useExtractRecipeYield(val, 2);
- expect(resultScaled).toStrictEqual("2.5 parts");
+ expect(resultScaled).toStrictEqual("21⁄2 parts");
const resultScaledInt = useExtractRecipeYield(val, 4);
expect(resultScaledInt).toStrictEqual("5 parts");
const resultScaledPartial = useExtractRecipeYield(val, 2.5);
- expect(resultScaledPartial).toStrictEqual("3.125 parts");
+ expect(resultScaledPartial).toStrictEqual("31⁄8 parts");
const roundedVal = "1.33333333333333333333 parts";
const resultScaledRounded = useExtractRecipeYield(roundedVal, 2);
- expect(resultScaledRounded).toStrictEqual("2.667 parts");
+ expect(resultScaledRounded).toStrictEqual("22⁄3 parts");
});
test("when text matches an int, return a scaled int", () => {
@@ -77,7 +99,7 @@ describe("test use extract recipe yield", () => {
expect(resultScaled).toStrictEqual("10 bowls");
const resultScaledPartial = useExtractRecipeYield(val, 2.5);
- expect(resultScaledPartial).toStrictEqual("12.5 bowls");
+ expect(resultScaledPartial).toStrictEqual("121⁄2 bowls");
const resultScaledLarge = useExtractRecipeYield(val, 10);
expect(resultScaledLarge).toStrictEqual("50 bowls");
@@ -93,13 +115,13 @@ describe("test use extract recipe yield", () => {
expect(resultDivZeroMixed).toStrictEqual(valDivZeroMixed);
});
- test("when text contains a weird or small fraction, return the original string", () => {
+ test("use an approximation for very weird fractions", () => {
const valWeird = "2323231239087/134527431962272135 servings";
- const resultWeird = useExtractRecipeYield(valWeird, 5);
- expect(resultWeird).toStrictEqual(valWeird);
+ const resultWeird = useExtractRecipeYield(valWeird, 5, false);
+ expect(resultWeird).toStrictEqual("1/11581 servings");
const valSmall = "1/20230225 lovable servings";
- const resultSmall = useExtractRecipeYield(valSmall, 12);
+ const resultSmall = useExtractRecipeYield(valSmall, 12, false);
expect(resultSmall).toStrictEqual(valSmall);
});
diff --git a/frontend/composables/recipe-page/use-extract-recipe-yield.ts b/frontend/composables/recipe-page/use-extract-recipe-yield.ts
index 53d17b264b9..237e16c3391 100644
--- a/frontend/composables/recipe-page/use-extract-recipe-yield.ts
+++ b/frontend/composables/recipe-page/use-extract-recipe-yield.ts
@@ -1,4 +1,5 @@
-import { useFraction } from "~/composables/recipes";
+import { getLowestFraction } from "../recipes/use-fraction";
+import { formatFraction } from "../recipes/use-recipe-ingredients";
const matchMixedFraction = /(?:\d*\s\d*\d*|0)\/\d*\d*/;
const matchFraction = /(?:\d*\d*|0)\/\d*\d*/;
@@ -37,9 +38,7 @@ function extractServingsFromFraction(fractionString: string): number | undefined
}
}
-
-
-function findMatch(yieldString: string): [matchString: string, servings: number, isFraction: boolean] | null {
+function findMatch(yieldString: string): [matchString: string, servings: number] | null {
if (!yieldString) {
return null;
}
@@ -53,7 +52,7 @@ function findMatch(yieldString: string): [matchString: string, servings: number,
if (servings === undefined) {
return null;
} else {
- return [match, servings, true];
+ return [match, servings];
}
}
@@ -66,52 +65,39 @@ function findMatch(yieldString: string): [matchString: string, servings: number,
if (servings === undefined) {
return null;
} else {
- return [match, servings, true];
+ return [match, servings];
}
}
const decimalMatch = yieldString.match(matchDecimal);
if (decimalMatch?.length) {
const match = decimalMatch[0];
- return [match, parseFloat(match), false];
+ return [match, parseFloat(match)];
}
const intMatch = yieldString.match(matchInt);
if (intMatch?.length) {
const match = intMatch[0];
- return [match, parseInt(match), false];
+ return [match, parseInt(match)];
}
return null;
}
-function formatServings(servings: number, scale: number, isFraction: boolean): string {
+function formatServings(servings: number, scale: number, includeFormating: boolean): string {
const val = servings * scale;
if (Number.isInteger(val)) {
return val.toString();
- } else if (!isFraction) {
- return (Math.round(val * 1000) / 1000).toString();
}
// convert val into a fraction string
- const { frac } = useFraction();
-
- let valString = "";
- const fraction = frac(val, 10, true);
-
- if (fraction[0] !== undefined && fraction[0] > 0) {
- valString += fraction[0];
- }
-
- if (fraction[1] > 0) {
- valString += ` ${fraction[1]}/${fraction[2]}`;
- }
+ const fraction = getLowestFraction(val);
- return valString.trim();
+ return formatFraction(fraction, includeFormating)
}
-export function useExtractRecipeYield(yieldString: string | null, scale: number): string {
+export function useExtractRecipeYield(yieldString: string | null, scale: number, includeFormating = true): string {
if (!yieldString) {
return "";
}
@@ -121,12 +107,12 @@ export function useExtractRecipeYield(yieldString: string | null, scale: number)
return yieldString;
}
- const [matchString, servings, isFraction] = match;
+ const [matchString, servings] = match;
- const formattedServings = formatServings(servings, scale, isFraction);
+ const formattedServings = formatServings(servings, scale, includeFormating);
if (!formattedServings) {
return yieldString // this only happens with very weird or small fractions
} else {
- return yieldString.replace(matchString, formatServings(servings, scale, isFraction));
+ return yieldString.replace(matchString, " " + formattedServings + " ").replace(/\s\s+/g, " ").trim();
}
}
diff --git a/frontend/composables/recipes/index.ts b/frontend/composables/recipes/index.ts
index c29e234308e..946348c1222 100644
--- a/frontend/composables/recipes/index.ts
+++ b/frontend/composables/recipes/index.ts
@@ -1,4 +1,3 @@
-export { useFraction } from "./use-fraction";
export { useRecipe } from "./use-recipe";
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-recipes";
export { parseIngredientText, useParsedIngredientText } from "./use-recipe-ingredients";
diff --git a/frontend/composables/recipes/use-fraction.ts b/frontend/composables/recipes/use-fraction.ts
index bbb7eac71e6..e51f819f0c6 100644
--- a/frontend/composables/recipes/use-fraction.ts
+++ b/frontend/composables/recipes/use-fraction.ts
@@ -1,76 +1,25 @@
-/* frac.js (C) 2012-present SheetJS -- http://sheetjs.com */
-/* https://developer.aliyun.com/mirror/npm/package/frac/v/0.3.0 Apache license */
+/* Loosely based on this Stackoverflow answer: https://stackoverflow.com/questions/14002113/how-to-simplify-a-decimal-into-the-smallest-possible-fraction#14011299 */
-function frac(x: number, D: number, mixed: boolean) {
- let n1 = Math.floor(x);
- let d1 = 1;
- let n2 = n1 + 1;
- let d2 = 1;
- if (x !== n1)
- while (d1 <= D && d2 <= D) {
- const m = (n1 + n2) / (d1 + d2);
- if (x === m) {
- if (d1 + d2 <= D) {
- d1 += d2;
- n1 += n2;
- d2 = D + 1;
- } else if (d1 > d2) d2 = D + 1;
- else d1 = D + 1;
- break;
- } else if (x < m) {
- n2 = n1 + n2;
- d2 = d1 + d2;
- } else {
- n1 = n1 + n2;
- d1 = d1 + d2;
- }
- }
- if (d1 > D) {
- d1 = d2;
- n1 = n2;
- }
- if (!mixed) return [0, n1, d1];
- const q = Math.floor(n1 / d1);
- return [q, n1 - q * d1, d1];
-}
-function cont(x: number, D: number, mixed: boolean) {
- const sgn = x < 0 ? -1 : 1;
- let B = x * sgn;
- let P_2 = 0;
- let P_1 = 1;
- let P = 0;
- let Q_2 = 1;
- let Q_1 = 0;
- let Q = 0;
- let A = Math.floor(B);
- while (Q_1 < D) {
- A = Math.floor(B);
- P = A * P_1 + P_2;
- Q = A * Q_1 + Q_2;
- if (B - A < 0.00000005) break;
- B = 1 / (B - A);
- P_2 = P_1;
- P_1 = P;
- Q_2 = Q_1;
- Q_1 = Q;
- }
- if (Q > D) {
- if (Q_1 > D) {
- Q = Q_2;
- P = P_2;
- } else {
- Q = Q_1;
- P = P_1;
- }
+export function getLowestFraction(x: number) {
+ const eps = 1.0E-06;
+ let numerator, h1, h2, denominator, k1, k2, a;
+
+ const whole = Math.floor(x);
+ x = x - whole
+ a = Math.floor(x);
+ h1 = 1;
+ k1 = 0;
+ numerator = a;
+ denominator = 1;
+
+ while (x - a > eps * denominator * denominator) {
+ x = 1 / (x - a);
+ a = Math.floor(x);
+ h2 = h1; h1 = numerator;
+ k2 = k1; k1 = denominator;
+ numerator = h2 + a * h1;
+ denominator = k2 + a * k1;
}
- if (!mixed) return [0, sgn * P, Q];
- const q = Math.floor((sgn * P) / Q);
- return [q, sgn * P - q * Q, Q];
-}
-export const useFraction = function () {
- return {
- frac,
- cont,
- };
-};
+ return [whole, numerator, denominator];
+}
diff --git a/frontend/composables/recipes/use-recipe-ingredients.test.ts b/frontend/composables/recipes/use-recipe-ingredients.test.ts
index 4b6a93869f7..28ae6197c6d 100644
--- a/frontend/composables/recipes/use-recipe-ingredients.test.ts
+++ b/frontend/composables/recipes/use-recipe-ingredients.test.ts
@@ -37,7 +37,8 @@ describe(parseIngredientText.name, () => {
test("ingredient text with fraction when unit is null", () => {
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: undefined });
- expect(parseIngredientText(ingredient, false, 1, true)).contain("11").and.to.contain("2");
+ expect(parseIngredientText(ingredient, false, 1, true)).toStrictEqual("11⁄2 Item 1")
+ expect(parseIngredientText(ingredient, false, 1, false)).toStrictEqual("1 1/2 Item 1")
});
test("ingredient text with fraction no formatting", () => {
diff --git a/frontend/composables/recipes/use-recipe-ingredients.ts b/frontend/composables/recipes/use-recipe-ingredients.ts
index e6d1256f78f..fe361a1f2b1 100644
--- a/frontend/composables/recipes/use-recipe-ingredients.ts
+++ b/frontend/composables/recipes/use-recipe-ingredients.ts
@@ -1,7 +1,6 @@
import DOMPurify from "isomorphic-dompurify";
-import { useFraction } from "./use-fraction";
+import { getLowestFraction } from "./use-fraction";
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit, RecipeIngredient } from "~/lib/api/types/recipe";
-const { frac } = useFraction();
export function sanitizeIngredientHTML(rawHtml: string) {
return DOMPurify.sanitize(rawHtml, {
@@ -35,6 +34,22 @@ function useUnitName(unit: CreateIngredientUnit | IngredientUnit | undefined, us
return returnVal;
}
+
+export function formatFraction(fraction: number[], includeFormating = true): string {
+ let result = ""
+ if (fraction[0] !== undefined && fraction[0] > 0) {
+ result += includeFormating ? `${fraction[0]}` : `${fraction[0]} `;
+ }
+
+ if (fraction[1] > 0) {
+ result += includeFormating ?
+ `${fraction[1]}⁄${fraction[2]}` :
+ `${fraction[1]}/${fraction[2]}`;
+ }
+
+ return result
+}
+
export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale = 1, includeFormating = true) {
if (disableAmount) {
return {
@@ -56,19 +71,12 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
if (unit && !unit.fraction) {
returnQty = (quantity * scale).toString();
} else {
- const fraction = frac(quantity * scale, 10, true);
- if (fraction[0] !== undefined && fraction[0] > 0) {
- returnQty += fraction[0];
- }
-
- if (fraction[1] > 0) {
- returnQty += includeFormating ?
- `${fraction[1]}⁄${fraction[2]}` :
- ` ${fraction[1]}/${fraction[2]}`;
- }
+ const fraction = getLowestFraction(quantity * scale);
+ returnQty += formatFraction(fraction, includeFormating)
}
}
+
const unitName = useUnitName(unit, usePluralUnit);
const foodName = useFoodName(food, usePluralFood);