Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix unit conversion #465

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions internal/models/recipe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,7 @@ func TestRecipe_ConvertMeasurementSystem(t *testing.T) {
{
name: "imperial to metric",
in: models.Recipe{
Description: "Preheat the oven to 351 °F (177 °C). " +
"Stir in flour, chocolate chips, and walnuts. " +
"Drop spoonfuls of dough 1.18 inches apart onto ungreased baking sheets. " +
"Bake in the preheated oven until edges are nicely browned, about 10 minutes.",
Description: "Preheat the oven to 351 °F (177 °C). Stir in flour, chocolate chips, and walnuts. Drop spoonfuls of dough 3 cm apart onto ungreased baking sheets. Bake in the preheated oven until edges are nicely browned, about 10 minutes.",
Ingredients: []string{
"1 cup butter, softened",
"2 eggs",
Expand All @@ -166,7 +163,7 @@ func TestRecipe_ConvertMeasurementSystem(t *testing.T) {
},
to: units.MetricSystem,
want: models.Recipe{
Description: "Preheat the oven to 177 °C (177 °C). " +
Description: "Preheat the oven to 351 °F (177 °C). " +
"Stir in flour, chocolate chips, and walnuts. " +
"Drop spoonfuls of dough 3 cm apart onto ungreased baking sheets. " +
"Bake in the preheated oven until edges are nicely browned, about 10 minutes.",
Expand All @@ -181,7 +178,7 @@ func TestRecipe_ConvertMeasurementSystem(t *testing.T) {
"14.79 ml plus 5 ml adobo sauce (or to taste (from canned chipotle peppers))",
},
Instructions: []string{
"Preheat the oven to 177 °C (175 °C).",
"Preheat the oven to 350 degrees F (175 degrees C).",
"Stir in flour, chocolate chips, and walnuts.",
"Drop spoonfuls of dough 5.08 cm apart onto ungreased baking sheets.",
"Bake in the preheated oven until edges are nicely browned, about 10 minutes.",
Expand Down
3 changes: 2 additions & 1 deletion internal/models/schema-recipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ func (k *Keywords) UnmarshalJSON(data []byte) error {
}
k.Values = strings.TrimSpace(strings.Join(xs, ","))
case map[string]any:
fmt.Println("fuck")
fmt.Println("oops")
}

k.Values = strings.TrimRight(k.Values, ",")
Expand Down Expand Up @@ -604,6 +604,7 @@ func (i *Ingredients) UnmarshalJSON(data []byte) error {
{"œ", "œ"},
{"é", "é"},
{"ï", "ï"},
{"&", "&"},
}

for _, v := range xv {
Expand Down
2 changes: 1 addition & 1 deletion internal/scraper/scraper_A_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ func TestScraper_A(t *testing.T) {
Values: []string{
"3 ounces cream cheese", "3 ounces quality blue cheese, crumbled",
"1/2 cup half and half", "1/4 cup sour cream",
"1/2 teaspoon Simon & Garfunkel Seasoning or Poultry Seasoning",
"1/2 teaspoon Simon & Garfunkel Seasoning or Poultry Seasoning",
"1/2 cup melted salted butter", "2 cloves minced or pressed garlic",
"1/2 cup Frank's Original RedHot Sauce",
"24 whole chicken wings ((about 4 pounds (1.8 kg) for 24 whole wings))",
Expand Down
6 changes: 3 additions & 3 deletions internal/scraper/scraper_B_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -679,9 +679,9 @@ func TestScraper_B(t *testing.T) {
"1 tbsp oil (for frying)", "1 onion (chopped)", "2 garlic cloves (chopped)",
"1 tsp paprika powder", "1 tsp chili powder (or more if you like it spicier)",
"1 tsp cumin",
"1x14 oz can black beans (8.5oz (240g) rinsed & drained, or sub other beans)",
"1 cup corn (rinsed & drained)", "14 oz chopped tomatoes (or passata)",
"salt and pepper (to taste)", "4 bell peppers (halved & seeded)",
"1x14 oz can black beans (8.5oz (240g) rinsed & drained, or sub other beans)",
"1 cup corn (rinsed & drained)", "14 oz chopped tomatoes (or passata)",
"salt and pepper (to taste)", "4 bell peppers (halved & seeded)",
"1 cup vegan grated cheese", "1 recipe Vegan Aioli",
"green hot peppers (or jalapeños, sliced)",
"scallions (sliced)",
Expand Down
6 changes: 3 additions & 3 deletions internal/scraper/scraper_E_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,18 +350,18 @@ func TestScraper_E(t *testing.T) {
Ingredients: &models.Ingredients{
Values: []string{
"2 pounds potatoes (peeled)", "3 large eggplants", "Olive oil (to brush)",
"Sea salt & pepper (to sprinkle)",
"Sea salt & pepper (to sprinkle)",
"3 cups cooked lentils ((about 1 1/2 cups dried))", "2 cups passata",
"1 cup chopped tomatoes", "1 Tbsp olive oil", "1 large onion (chopped)",
"2 cloves of garlic (minced)", "2 bay leaves", "1-2 tsp dried thyme",
"1 tsp oregano", "1 tsp paprika", "1 tsp coconut sugar (or brown sugar)",
"1 pinch of cinnamon",
"Sea salt & pepper (to taste)",
"Sea salt & pepper (to taste)",
"2 Tbsp vegan butter",
"2 cups plant-based milk",
"3 1/2 Tbsp cornstarch",
"2 Tbsp nutritional yeast",
"Sea salt & pepper (to taste)",
"Sea salt & pepper (to taste)",
"1 pinch of nutmeg",
"Vegan cheese (to taste (optional))",
},
Expand Down
2 changes: 1 addition & 1 deletion internal/scraper/scraper_G_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ func TestScraper_G(t *testing.T) {
"1 lime",
"1 garlic clove",
"​25g cashew nuts",
"​10g coriander & mint",
"​10g coriander & mint",
"​80g spinach",
"​130g bulgur wheat",
"​160g blanched peas",
Expand Down
10 changes: 5 additions & 5 deletions internal/scraper/scraper_S_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ func TestScraper_S(t *testing.T) {
"1 large egg, at room temperature",
"1 teaspoon salt",
"14 Tablespoons (196g) unsalted butter, cold",
"2 and 1/2 cups (313g) all-purpose flour (spooned & leveled), plus more for generously flouring hands, surface, and dough",
"2/3 cup filling (see recipe notes for options & cheese filling)",
"2 and 1/2 cups (313g) all-purpose flour (spooned & leveled), plus more for generously flouring hands, surface, and dough",
"2/3 cup filling (see recipe notes for options & cheese filling)",
"1 large egg",
"2 Tablespoons (30ml) milk",
"1 cup (120g) confectioners’ sugar",
Expand Down Expand Up @@ -194,7 +194,7 @@ func TestScraper_S(t *testing.T) {
Image: &models.Image{Value: anUploadedImage.String()},
Ingredients: &models.Ingredients{
Values: []string{
"2 large boneless skinless chicken breasts", "Salt & pepper (to taste)",
"2 large boneless skinless chicken breasts", "Salt & pepper (to taste)",
"Flour (for dredging)", "1 tablespoon olive oil",
"2 tablespoons butter (divided)", "1 whole head garlic (cloves peeled)",
"1/2 cup chicken broth or stock", "1/2 teaspoon lemon juice",
Expand Down Expand Up @@ -971,7 +971,7 @@ func TestScraper_S(t *testing.T) {
"1 tablespoon Dijon mustard",
"4 tablespoons pickle juice",
"2 tablespoons water",
"Coarse salt & black pepper (to taste)",
"Coarse salt & black pepper (to taste)",
},
},
Instructions: &models.Instructions{
Expand Down Expand Up @@ -1952,7 +1952,7 @@ func TestScraper_S(t *testing.T) {
Values: []string{
"4 chicken breasts (roughly 2 lbs; boneless skinless chicken thighs may also be used * see note 1)",
"11.5 oz can of corn kernels (drained; 341 mL)",
"15 oz can of black beans (drained & rinsed; optional)",
"15 oz can of black beans (drained & rinsed; optional)",
"1 red onion (sliced into strips)",
"1/2 cup chicken stock",
"2 cloves garlic",
Expand Down
2 changes: 1 addition & 1 deletion internal/scraper/scraper_V_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ func TestScraper_V(t *testing.T) {
"200 to 250 grams Paneer ((Indian cottage cheese) - cubed or diced)",
"1 teaspoon dry fenugreek leaves ((kasuri methi) - optional)",
"½ to 1 teaspoon Garam Masala (or tandoori masala)",
"2 to 3 tablespoons light cream (or half & half or 1 to 2 tablespoons heavy cream - optional)",
"2 to 3 tablespoons light cream (or half & half or 1 to 2 tablespoons heavy cream - optional)",
"¼ to 1 teaspoon sugar (- optional, add as required depending on the sourness of the tomatoes)",
"salt (as required)",
"1 to 2 tablespoons coriander leaves (- chopped, (cilantro) - optional)",
Expand Down
7 changes: 2 additions & 5 deletions internal/server/handlers_settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ func TestHandlers_Settings_MeasurementSystems(t *testing.T) {
"4.73 dl semisweet chocolate chips",
},
Instructions: []string{
"Preheat the oven to 177 °C (175 °C).",
"Preheat the oven to 350 degrees F (175 degrees C).",
"Stir in flour, chocolate chips, and walnuts.",
"Drop spoonfuls of dough 5.08 cm apart onto ungreased baking sheets.",
"Bake in the preheated oven until edges are nicely browned, about 10 minutes.",
Expand Down Expand Up @@ -642,10 +642,7 @@ func TestHandlers_Settings_MeasurementSystems(t *testing.T) {
system: units.ImperialSystem,
want: models.Recipes{
{
Description: "Preheat the oven to 351 °F (351 °F). " +
"Stir in flour, chocolate chips, and walnuts. " +
"Drop spoonfuls of dough 1.18 inches apart onto ungreased baking sheets. " +
"Bake in the preheated oven until edges are nicely browned, about 10 minutes.",
Description: "Preheat the oven to 177 °C (351 °F). Stir in flour, chocolate chips, and walnuts. Drop spoonfuls of dough 1.18 inches apart onto ungreased baking sheets. Bake in the preheated oven until edges are nicely browned, about 10 minutes.",
Ingredients: []string{
"1 cup butter, softened",
"2 eggs",
Expand Down
7 changes: 6 additions & 1 deletion internal/services/sqlite_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ func (s *SQLiteService) addRecipe(r models.Recipe, userID int64, settings models
}
}

if settings.MeasurementSystem == units.ImperialSystem {
for i, ingredient := range r.Ingredients {
r.Ingredients[i] = units.ReplaceDecimalFractions(ingredient)
}
}

tx, err := s.DB.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return 0, err
Expand Down Expand Up @@ -390,7 +396,6 @@ func (s *SQLiteService) addRecipeTx(ctx context.Context, tx *sql.Tx, r models.Re
r.Ingredients = slices.DeleteFunc(extensions.Unique(r.Ingredients), func(s string) bool { return s == "" })
for i, ingredient := range r.Ingredients {
var ingredientID int64
ingredient = units.ReplaceDecimalFractions(ingredient)
err = tx.QueryRowContext(ctx, statements.InsertIngredient, ingredient).Scan(&ingredientID)
if err != nil {
return 0, err
Expand Down
59 changes: 59 additions & 0 deletions internal/units/measurements.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/neurosnap/sentences"
"github.com/reaper47/recipya/internal/utils/extensions"
"github.com/reaper47/recipya/internal/utils/regex"
"log/slog"
"math"
"regexp"
"slices"
Expand Down Expand Up @@ -761,7 +762,12 @@ func ConvertSentence(input string, from, to System) (string, error) {
return input, errors.New("the measurement system is unchanged")
}

if regex.UnitMetric.MatchString(input) && regex.UnitImperial.MatchString(input) {
return input, nil
}

input = ReplaceVulgarFractions(input)
input = sumQuantitiesWithSeparator(input)

var (
irregular string
Expand Down Expand Up @@ -799,6 +805,59 @@ func ConvertSentence(input string, from, to System) (string, error) {
return converted, nil
}

func sumQuantitiesWithSeparator(input string) string {
excluded := []string{
"to", // English
"à", // French
"zu", // German
"para", // Portuguese
"до", // Ukrainian
"için", // Turkish
"kwa", // Swahili
"naar", // Dutch
"προς", // Greek
"do", // Polish
"till", // Swedish
}

matches := regex.QuantityWithSeparator.FindAllStringSubmatch(input, -1)
if len(matches) > 0 && len(matches[0]) > 3 && !slices.ContainsFunc(excluded, func(s string) bool {
return strings.Contains(matches[0][0], s)
}) {
inputAttr := slog.String("input", input)

left, err := strconv.ParseFloat(matches[0][1], 64)
if err != nil {
slog.Warn("Could not convert left quantity from string to integer", inputAttr, "integer", matches[0][1])
}

var right float64
parts := strings.Split(matches[0][2], "/")
if len(parts) > 1 {
a, err := strconv.ParseFloat(parts[0], 64)
if err != nil {
slog.Warn("Could not convert numerator from string to integer", inputAttr, "integer", parts[0])
}

b, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
slog.Warn("Could not convert denominator from string to integer", inputAttr, "integer", parts[1])
}

right = a / b
} else {
right, err = strconv.ParseFloat(matches[0][2], 64)
if err != nil {
slog.Warn("Could not convert right quantity from string to integer", inputAttr, "integer", matches[0][2])
}
}

return regex.QuantityWithSeparator.ReplaceAllString(input, strconv.FormatFloat(left+right, 'f', -1, 64))
}

return input
}

func parseIrregularQuantity(input string, matches []string, re *regexp.Regexp, to System) (string, error) {
match := strings.Replace(matches[1], "-", " ", 1)
parts := strings.Split(match, " ")
Expand Down
14 changes: 14 additions & 0 deletions internal/units/measurements_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,13 @@ func TestConvertSentence(t *testing.T) {
to: units.MetricSystem,
want: "Rope with 5 ml of rice.",
},
{
name: "tsp to mL special",
in: "1 and 1/2 teaspoons cornstarch*",
from: units.ImperialSystem,
to: units.MetricSystem,
want: "7.5 ml cornstarch*",
},
{
name: "tsp to dL",
in: "Rope with 200tsp. of rice.",
Expand Down Expand Up @@ -548,6 +555,13 @@ func TestConvertSentence(t *testing.T) {
to: units.MetricSystem,
want: "Rope with 946.35 L of rice.",
},
{
name: "no conversion required",
in: "2 and 1/4 cups (280g) all-purpose flour (spooned & leveled)",
from: units.ImperialSystem,
to: units.MetricSystem,
want: "2 and 1/4 cups (280g) all-purpose flour (spooned & leveled)",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions internal/utils/regex/regex.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ var DimensionPattern = regexp.MustCompile(`(\d+)\s*x\s*(\d+).`)
// Quantity detects quantities, i.e. 1ml, 1 ml, 1l and 1 l.
var Quantity = regexp.MustCompile(`(?i)\d+\s*((ml|l\b)(°[cf])?|°[cf])`)

// QuantityWithSeparator detects quantities separated by a word.
var QuantityWithSeparator = regexp.MustCompile(`(?i)(\d+)\s+[a-zA-Z]+\s+(\d+(/\d+)?)`)

// Regular expressions related to HTML.
var (
// URL matches an HTTP or HTTPS address.
Expand Down
18 changes: 18 additions & 0 deletions internal/utils/regex/regex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,24 @@ func TestRegex_Quantity(t *testing.T) {
}
}

func TestRegex_QuantityWithSeparator(t *testing.T) {
valid := []string{
"1 and 1/2 teaspoons vanilla extract",
}
assertRegex(t, valid, regex.QuantityWithSeparator)

invalid := []string{
"1 teaspoon vanilla extract",
}
for _, s := range invalid {
t.Run("regex is invalid "+s, func(t *testing.T) {
if regex.QuantityWithSeparator.MatchString(s) {
t.Errorf("got true when want false for %q", s)
}
})
}
}

func TestRegex_Anchor(t *testing.T) {
t.Run("anchor is valid", func(t *testing.T) {
a := `<a slot="guide-links-primary" href="https://www.youtube.com/about/press/" style="display: none;">`
Expand Down
Loading
Loading