diff --git a/lib/ProductOpener/Food.pm b/lib/ProductOpener/Food.pm
index 78b055b5083d5..a84e025e41d8a 100644
--- a/lib/ProductOpener/Food.pm
+++ b/lib/ProductOpener/Food.pm
@@ -1204,6 +1204,199 @@ my @fruits_vegetables_nuts_by_category_sorted
= sort {$fruits_vegetables_nuts_by_category{$b} <=> $fruits_vegetables_nuts_by_category{$a}}
keys %fruits_vegetables_nuts_by_category;
+=head2 compute_fruit_ratio($product_ref, $prepared)
+
+Compute the fruit % according to the Nutri-Score rules
+
+Warning Also modifies product_ref->{misc_tags}
+
+=head3 return
+
+The fruit ratio
+
+=cut
+
+sub compute_fruit_ratio ($product_ref, $prepared) {
+
+ my $fruits = undef;
+
+ if (defined $product_ref->{nutriments}{"fruits-vegetables-nuts-dried" . $prepared . "_100g"}) {
+ $fruits = 2 * $product_ref->{nutriments}{"fruits-vegetables-nuts-dried" . $prepared . "_100g"};
+ push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-dried";
+
+ if (defined $product_ref->{nutriments}{"fruits-vegetables-nuts" . $prepared . "_100g"}) {
+ $fruits += $product_ref->{nutriments}{"fruits-vegetables-nuts" . $prepared . "_100g"};
+ push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts";
+ }
+
+ $fruits
+ = $fruits * 100 / (100 + $product_ref->{nutriments}{"fruits-vegetables-nuts-dried" . $prepared . "_100g"});
+ }
+ elsif (defined $product_ref->{nutriments}{"fruits-vegetables-nuts" . $prepared . "_100g"}) {
+ $fruits = $product_ref->{nutriments}{"fruits-vegetables-nuts" . $prepared . "_100g"};
+ push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts";
+ }
+ elsif (defined $product_ref->{nutriments}{"fruits-vegetables-nuts-estimate" . $prepared . "_100g"}) {
+ $fruits = $product_ref->{nutriments}{"fruits-vegetables-nuts-estimate" . $prepared . "_100g"};
+ $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_estimate} = 1;
+ push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-estimate";
+ }
+ # Use the estimate from the ingredients list if we have one
+ elsif (
+ (not defined $fruits)
+ and
+ (defined $product_ref->{nutriments}{"fruits-vegetables-nuts-estimate-from-ingredients" . $prepared . "_100g"})
+ )
+ {
+ $fruits = $product_ref->{nutriments}{"fruits-vegetables-nuts-estimate-from-ingredients" . $prepared . "_100g"};
+ $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_estimate_from_ingredients} = 1;
+ $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_estimate_from_ingredients_value} = $fruits;
+ push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-estimate-from-ingredients";
+ }
+ else {
+ # estimates by category of products. not exact values. it's important to distinguish only between the thresholds: 40, 60 and 80
+ foreach my $category_id (@fruits_vegetables_nuts_by_category_sorted) {
+
+ if (has_tag($product_ref, "categories", $category_id)) {
+ $fruits = $fruits_vegetables_nuts_by_category{$category_id};
+ $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_from_category} = $category_id;
+ $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_from_category_value}
+ = $fruits_vegetables_nuts_by_category{$category_id};
+ push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-from-category";
+ my $category = $category_id;
+ $category =~ s/:/-/;
+ push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-from-category-$category";
+ last;
+ }
+ }
+
+ # If we do not have a fruits estimate, use 0 and add a warning
+ if (not defined $fruits) {
+ $fruits = 0;
+ $product_ref->{nutrition_score_warning_no_fruits_vegetables_nuts} = 1;
+ push @{$product_ref->{misc_tags}}, "en:nutrition-no-fruits-vegetables-nuts";
+ }
+ }
+
+ if ( (defined $product_ref->{nutrition_score_warning_no_fiber})
+ or (defined $product_ref->{nutrition_score_warning_no_fruits_vegetables_nuts}))
+ {
+ push @{$product_ref->{misc_tags}}, "en:nutrition-no-fiber-or-fruits-vegetables-nuts";
+ }
+ else {
+ push @{$product_ref->{misc_tags}}, "en:nutrition-all-nutriscore-values-known";
+ }
+
+ return $fruits;
+}
+
+=head2 saturated_fat_ratio( $product_ref, $prepared )
+
+Compute saturated_fat_ratio as needed for nutriscore
+
+=cut
+
+sub saturated_fat_ratio ($product_ref, $prepared) {
+
+ my $saturated_fat = $product_ref->{nutriments}{"saturated-fat" . $prepared . "_100g"};
+ my $fat = $product_ref->{nutriments}{"fat" . $prepared . "_100g"};
+ my $saturated_fat_ratio = 0;
+ if ((defined $saturated_fat) and ($saturated_fat > 0)) {
+ if ($fat <= 0) {
+ $fat = $saturated_fat;
+ }
+ $saturated_fat_ratio = $saturated_fat / $fat * 100; # in %
+ }
+ return $saturated_fat_ratio;
+}
+
+=head2 saturated_fat_0_because_of_fat_0($product_ref, $prepared)
+
+Detect if we are in the special case where we can detect saturated fat is 0 because fat is 0
+
+=cut
+
+sub saturated_fat_0_because_of_fat_0 ($product_ref, $prepared) {
+ my $fat = $product_ref->{nutriments}{"fat" . $prepared . "_100g"};
+ return ( (!defined $product_ref->{nutriments}{"saturated-fat" . $prepared . "_100g"})
+ && (defined $fat)
+ && ($fat == 0));
+}
+
+=head2 sugar_0_because_of_carbohydrates_0($product_ref, $prepared) {
+
+Detect if we are in the special case where we can detect sugars are 0 because carbohydrates are 0
+
+=cut
+
+sub sugar_0_because_of_carbohydrates_0 ($product_ref, $prepared) {
+ my $carbohydrates = $product_ref->{nutriments}{"carbohydrates" . $prepared . "_100g"};
+ return ( (!defined $product_ref->{nutriments}{"sugars" . $prepared . "_100g"})
+ && (defined $carbohydrates)
+ && ($carbohydrates == 0));
+}
+
+=head2 compute_nutriscore_data( $product_ref, $prepared )
+
+Compute data for nutriscore computation.
+
+Warning: it also modifies $product_ref
+
+=head3 Arguments
+
+=head4 $product_ref - ref to product
+
+=head4 $prepared - string contains either "" or "prepared"
+
+=head4 $fruits - float - fruit % estimation
+
+=head3 return
+
+Ref to a mapping suitable to call compute_nutriscore_score_and_grade
+
+=cut
+
+sub compute_nutriscore_data ($product_ref, $prepared) {
+ # compute data
+ my $saturated_fat_ratio = saturated_fat_ratio($product_ref, $prepared);
+ my $fruits = compute_fruit_ratio($product_ref, $prepared);
+ my $nutriscore_data = {
+ is_beverage => $product_ref->{nutrition_score_beverage},
+ is_water => is_water_for_nutrition_score($product_ref),
+ is_cheese => is_cheese_for_nutrition_score($product_ref),
+ is_fat => is_fat_for_nutrition_score($product_ref),
+
+ energy => $product_ref->{nutriments}{"energy" . $prepared . "_100g"},
+ sugars => $product_ref->{nutriments}{"sugars" . $prepared . "_100g"},
+ saturated_fat => $product_ref->{nutriments}{"saturated-fat" . $prepared . "_100g"},
+ saturated_fat_ratio => $saturated_fat_ratio,
+ sodium => $product_ref->{nutriments}{"sodium" . $prepared . "_100g"} * 1000, # in mg,
+
+ fruits_vegetables_nuts_colza_walnut_olive_oils => $fruits,
+ fiber => (
+ (defined $product_ref->{nutriments}{"fiber" . $prepared . "_100g"})
+ ? $product_ref->{nutriments}{"fiber" . $prepared . "_100g"}
+ : 0
+ ),
+ proteins => $product_ref->{nutriments}{"proteins" . $prepared . "_100g"},
+ };
+
+ # tweak data to take into account special cases
+
+ # if sugar is undefined but carbohydrates is 0, set sugars to 0
+ if (sugar_0_because_of_carbohydrates_0($product_ref, $prepared)) {
+ $nutriscore_data->{sugars} = 0;
+ }
+ # if saturated_fat is undefined but fat is 0, set saturated_fat to 0
+ # as well as saturated_fat_ratio
+ if (saturated_fat_0_because_of_fat_0($product_ref, $prepared)) {
+ $nutriscore_data->{saturated_fat} = 0;
+ $nutriscore_data->{saturated_fat_ratio} = 0;
+ }
+
+ return $nutriscore_data;
+}
+
=head2 compute_nutrition_score( $product_ref )
Determines if we have enough data to compute the Nutri-Score (category + nutrition facts),
@@ -1345,6 +1538,10 @@ sub compute_nutrition_score ($product_ref) {
foreach my $nid ("energy", "fat", "saturated-fat", "sugars", "sodium", "proteins") {
if (not defined $product_ref->{nutriments}{$nid . $prepared . "_100g"}) {
+ # we have two special case where we can deduce data
+ next
+ if ((($nid eq "saturated-fat") && saturated_fat_0_because_of_fat_0($product_ref, $prepared))
+ || (($nid eq "sugars") && sugar_0_because_of_carbohydrates_0($product_ref, $prepared)));
$product_ref->{"nutrition_grades_tags"} = ["unknown"];
add_tag($product_ref, "misc", "en:nutrition-not-enough-data-to-compute-nutrition-score");
$product_ref->{nutrition_score_debug} .= "missing " . $nid . $prepared . " - ";
@@ -1382,109 +1579,9 @@ sub compute_nutrition_score ($product_ref) {
push @{$product_ref->{misc_tags}}, "en:nutrition-grade-computed-for-prepared-product";
}
- # Compute the fruit % according to the Nutri-Score rules
-
- my $fruits = undef;
-
- if (defined $product_ref->{nutriments}{"fruits-vegetables-nuts-dried" . $prepared . "_100g"}) {
- $fruits = 2 * $product_ref->{nutriments}{"fruits-vegetables-nuts-dried" . $prepared . "_100g"};
- push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-dried";
-
- if (defined $product_ref->{nutriments}{"fruits-vegetables-nuts" . $prepared . "_100g"}) {
- $fruits += $product_ref->{nutriments}{"fruits-vegetables-nuts" . $prepared . "_100g"};
- push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts";
- }
-
- $fruits
- = $fruits * 100 / (100 + $product_ref->{nutriments}{"fruits-vegetables-nuts-dried" . $prepared . "_100g"});
- }
- elsif (defined $product_ref->{nutriments}{"fruits-vegetables-nuts" . $prepared . "_100g"}) {
- $fruits = $product_ref->{nutriments}{"fruits-vegetables-nuts" . $prepared . "_100g"};
- push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts";
- }
- elsif (defined $product_ref->{nutriments}{"fruits-vegetables-nuts-estimate" . $prepared . "_100g"}) {
- $fruits = $product_ref->{nutriments}{"fruits-vegetables-nuts-estimate" . $prepared . "_100g"};
- $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_estimate} = 1;
- push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-estimate";
- }
- # Use the estimate from the ingredients list if we have one
- elsif (
- (not defined $fruits)
- and
- (defined $product_ref->{nutriments}{"fruits-vegetables-nuts-estimate-from-ingredients" . $prepared . "_100g"})
- )
- {
- $fruits = $product_ref->{nutriments}{"fruits-vegetables-nuts-estimate-from-ingredients" . $prepared . "_100g"};
- $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_estimate_from_ingredients} = 1;
- $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_estimate_from_ingredients_value} = $fruits;
- push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-estimate-from-ingredients";
- }
- else {
- # estimates by category of products. not exact values. it's important to distinguish only between the thresholds: 40, 60 and 80
- foreach my $category_id (@fruits_vegetables_nuts_by_category_sorted) {
-
- if (has_tag($product_ref, "categories", $category_id)) {
- $fruits = $fruits_vegetables_nuts_by_category{$category_id};
- $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_from_category} = $category_id;
- $product_ref->{nutrition_score_warning_fruits_vegetables_nuts_from_category_value}
- = $fruits_vegetables_nuts_by_category{$category_id};
- push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-from-category";
- my $category = $category_id;
- $category =~ s/:/-/;
- push @{$product_ref->{misc_tags}}, "en:nutrition-fruits-vegetables-nuts-from-category-$category";
- last;
- }
- }
-
- # If we do not have a fruits estimate, use 0 and add a warning
- if (not defined $fruits) {
- $fruits = 0;
- $product_ref->{nutrition_score_warning_no_fruits_vegetables_nuts} = 1;
- push @{$product_ref->{misc_tags}}, "en:nutrition-no-fruits-vegetables-nuts";
- }
- }
-
- if ( (defined $product_ref->{nutrition_score_warning_no_fiber})
- or (defined $product_ref->{nutrition_score_warning_no_fruits_vegetables_nuts}))
- {
- push @{$product_ref->{misc_tags}}, "en:nutrition-no-fiber-or-fruits-vegetables-nuts";
- }
- else {
- push @{$product_ref->{misc_tags}}, "en:nutrition-all-nutriscore-values-known";
- }
-
- my $saturated_fat = $product_ref->{nutriments}{"saturated-fat" . $prepared . "_100g"};
- my $fat = $product_ref->{nutriments}{"fat" . $prepared . "_100g"};
- my $saturated_fat_ratio = 0;
- if ((defined $saturated_fat) and ($saturated_fat > 0)) {
- if ($fat <= 0) {
- $fat = $saturated_fat;
- }
- $saturated_fat_ratio = $saturated_fat / $fat * 100; # in %
- }
-
# Populate the data structure that will be passed to Food::Nutriscore
- $product_ref->{nutriscore_data} = {
- is_beverage => $product_ref->{nutrition_score_beverage},
- is_water => is_water_for_nutrition_score($product_ref),
- is_cheese => is_cheese_for_nutrition_score($product_ref),
- is_fat => is_fat_for_nutrition_score($product_ref),
-
- energy => $product_ref->{nutriments}{"energy" . $prepared . "_100g"},
- sugars => $product_ref->{nutriments}{"sugars" . $prepared . "_100g"},
- saturated_fat => $product_ref->{nutriments}{"saturated-fat" . $prepared . "_100g"},
- saturated_fat_ratio => $saturated_fat_ratio,
- sodium => $product_ref->{nutriments}{"sodium" . $prepared . "_100g"} * 1000, # in mg,
-
- fruits_vegetables_nuts_colza_walnut_olive_oils => $fruits,
- fiber => (
- (defined $product_ref->{nutriments}{"fiber" . $prepared . "_100g"})
- ? $product_ref->{nutriments}{"fiber" . $prepared . "_100g"}
- : 0
- ),
- proteins => $product_ref->{nutriments}{"proteins" . $prepared . "_100g"},
- };
+ $product_ref->{nutriscore_data} = compute_nutriscore_data($product_ref, $prepared);
my ($nutriscore_score, $nutriscore_grade)
= ProductOpener::Nutriscore::compute_nutriscore_score_and_grade($product_ref->{nutriscore_data});
diff --git a/tests/unit/expected_test_results/nutriscore/fr-orange-nectar-0-fat.json b/tests/unit/expected_test_results/nutriscore/fr-orange-nectar-0-fat.json
new file mode 100644
index 0000000000000..46309cdf5d130
--- /dev/null
+++ b/tests/unit/expected_test_results/nutriscore/fr-orange-nectar-0-fat.json
@@ -0,0 +1,201 @@
+{
+ "categories" : "fruit-nectar",
+ "categories_hierarchy" : [
+ "en:plant-based-foods-and-beverages",
+ "en:beverages",
+ "en:plant-based-beverages",
+ "en:fruit-based-beverages",
+ "en:juices-and-nectars",
+ "en:fruit-nectars"
+ ],
+ "categories_lc" : "en",
+ "categories_properties" : {
+ "agribalyse_proxy_food_code:en" : "2061"
+ },
+ "categories_properties_tags" : [
+ "all-products",
+ "categories-known",
+ "agribalyse-food-code-unknown",
+ "agribalyse-proxy-food-code-2061",
+ "agribalyse-proxy-food-code-known",
+ "ciqual-food-code-unknown",
+ "agribalyse-known",
+ "agribalyse-2061"
+ ],
+ "categories_tags" : [
+ "en:plant-based-foods-and-beverages",
+ "en:beverages",
+ "en:plant-based-beverages",
+ "en:fruit-based-beverages",
+ "en:juices-and-nectars",
+ "en:fruit-nectars"
+ ],
+ "food_groups" : "en:sweetened-beverages",
+ "food_groups_tags" : [
+ "en:beverages",
+ "en:sweetened-beverages"
+ ],
+ "ingredients" : [
+ {
+ "id" : "en:orange",
+ "percent" : 47,
+ "percent_estimate" : 47,
+ "percent_max" : 47,
+ "percent_min" : 47,
+ "text" : "Orange",
+ "vegan" : "yes",
+ "vegetarian" : "yes"
+ },
+ {
+ "id" : "en:water",
+ "percent_estimate" : 25.3333333333333,
+ "percent_max" : 33,
+ "percent_min" : 17.6666666666667,
+ "text" : "Water",
+ "vegan" : "yes",
+ "vegetarian" : "yes"
+ },
+ {
+ "id" : "en:sugar",
+ "percent_estimate" : 17.6666666666667,
+ "percent_max" : 25.3333333333333,
+ "percent_min" : 10,
+ "text" : "Sugar",
+ "vegan" : "yes",
+ "vegetarian" : "yes"
+ },
+ {
+ "id" : "en:carrot",
+ "percent" : 10,
+ "percent_estimate" : 10,
+ "percent_max" : 10,
+ "percent_min" : 10,
+ "text" : "Carrots",
+ "vegan" : "yes",
+ "vegetarian" : "yes"
+ }
+ ],
+ "ingredients_analysis" : {},
+ "ingredients_analysis_tags" : [
+ "en:palm-oil-free",
+ "en:vegan",
+ "en:vegetarian"
+ ],
+ "ingredients_hierarchy" : [
+ "en:orange",
+ "en:fruit",
+ "en:citrus-fruit",
+ "en:water",
+ "en:sugar",
+ "en:added-sugar",
+ "en:disaccharide",
+ "en:carrot",
+ "en:vegetable",
+ "en:root-vegetable"
+ ],
+ "ingredients_n" : 4,
+ "ingredients_n_tags" : [
+ "4",
+ "1-10"
+ ],
+ "ingredients_original_tags" : [
+ "en:orange",
+ "en:water",
+ "en:sugar",
+ "en:carrot"
+ ],
+ "ingredients_percent_analysis" : 1,
+ "ingredients_tags" : [
+ "en:orange",
+ "en:fruit",
+ "en:citrus-fruit",
+ "en:water",
+ "en:sugar",
+ "en:added-sugar",
+ "en:disaccharide",
+ "en:carrot",
+ "en:vegetable",
+ "en:root-vegetable"
+ ],
+ "ingredients_text" : "Orange 47%, Water, Sugar, Carrots 10%",
+ "ingredients_with_specified_percent_n" : 2,
+ "ingredients_with_specified_percent_sum" : 57,
+ "ingredients_with_unspecified_percent_n" : 2,
+ "ingredients_with_unspecified_percent_sum" : 43,
+ "known_ingredients_n" : 10,
+ "lc" : "en",
+ "misc_tags" : [
+ "en:nutrition-fruits-vegetables-nuts-estimate-from-ingredients",
+ "en:nutrition-all-nutriscore-values-known",
+ "en:nutriscore-computed"
+ ],
+ "nutriments" : {
+ "energy_100g" : 250,
+ "fat_100g" : 0,
+ "fiber_100g" : 0,
+ "fruits-vegetables-nuts-estimate-from-ingredients_100g" : 57,
+ "fruits-vegetables-nuts-estimate-from-ingredients_serving" : 57,
+ "nutrition-score-fr" : 17,
+ "nutrition-score-fr_100g" : 17,
+ "proteins_100g" : 0.5,
+ "sodium_100g" : 0.2,
+ "sugars_100g" : 12
+ },
+ "nutriscore_data" : {
+ "energy" : 250,
+ "energy_points" : 9,
+ "energy_value" : 250,
+ "fiber" : 0,
+ "fiber_points" : 0,
+ "fiber_value" : 0,
+ "fruits_vegetables_nuts_colza_walnut_olive_oils" : 57,
+ "fruits_vegetables_nuts_colza_walnut_olive_oils_points" : 2,
+ "fruits_vegetables_nuts_colza_walnut_olive_oils_value" : 57,
+ "grade" : "e",
+ "is_beverage" : 1,
+ "is_cheese" : 0,
+ "is_fat" : 0,
+ "is_water" : 0,
+ "negative_points" : 19,
+ "positive_points" : 2,
+ "proteins" : 0.5,
+ "proteins_points" : 0,
+ "proteins_value" : 0.5,
+ "saturated_fat" : 0,
+ "saturated_fat_points" : 0,
+ "saturated_fat_ratio" : 0,
+ "saturated_fat_ratio_points" : 0,
+ "saturated_fat_ratio_value" : 0,
+ "saturated_fat_value" : 0,
+ "score" : 17,
+ "sodium" : 200,
+ "sodium_points" : 2,
+ "sodium_value" : 200,
+ "sugars" : 12,
+ "sugars_points" : 8,
+ "sugars_value" : 12
+ },
+ "nutriscore_grade" : "e",
+ "nutriscore_score" : 17,
+ "nutriscore_score_opposite" : -17,
+ "nutrition_grade_fr" : "e",
+ "nutrition_grades" : "e",
+ "nutrition_grades_tags" : [
+ "e"
+ ],
+ "nutrition_score_beverage" : 1,
+ "nutrition_score_debug" : "",
+ "nutrition_score_warning_fruits_vegetables_nuts_estimate_from_ingredients" : 1,
+ "nutrition_score_warning_fruits_vegetables_nuts_estimate_from_ingredients_value" : 57,
+ "pnns_groups_1" : "Beverages",
+ "pnns_groups_1_tags" : [
+ "beverages",
+ "known"
+ ],
+ "pnns_groups_2" : "Sweetened beverages",
+ "pnns_groups_2_tags" : [
+ "sweetened-beverages",
+ "known"
+ ],
+ "unknown_ingredients_n" : 0
+}
diff --git a/tests/unit/expected_test_results/nutriscore/sunflower-oil-no-sugar-no-sat-fat.json b/tests/unit/expected_test_results/nutriscore/sunflower-oil-no-sugar-no-sat-fat.json
new file mode 100644
index 0000000000000..37e55646a4505
--- /dev/null
+++ b/tests/unit/expected_test_results/nutriscore/sunflower-oil-no-sugar-no-sat-fat.json
@@ -0,0 +1,72 @@
+{
+ "categories" : "sunflower oils",
+ "categories_hierarchy" : [
+ "en:plant-based-foods-and-beverages",
+ "en:plant-based-foods",
+ "en:fats",
+ "en:vegetable-fats",
+ "en:vegetable-oils",
+ "en:sunflower-seeds-and-their-products",
+ "en:sunflower-oils"
+ ],
+ "categories_lc" : "en",
+ "categories_properties" : {
+ "agribalyse_food_code:en" : "17440",
+ "ciqual_food_code:en" : "17440"
+ },
+ "categories_properties_tags" : [
+ "all-products",
+ "categories-known",
+ "agribalyse-food-code-17440",
+ "agribalyse-food-code-known",
+ "agribalyse-proxy-food-code-unknown",
+ "ciqual-food-code-17440",
+ "ciqual-food-code-known",
+ "agribalyse-known",
+ "agribalyse-17440"
+ ],
+ "categories_tags" : [
+ "en:plant-based-foods-and-beverages",
+ "en:plant-based-foods",
+ "en:fats",
+ "en:vegetable-fats",
+ "en:vegetable-oils",
+ "en:sunflower-seeds-and-their-products",
+ "en:sunflower-oils"
+ ],
+ "food_groups" : "en:fats",
+ "food_groups_tags" : [
+ "en:fats-and-sauces",
+ "en:fats"
+ ],
+ "lc" : "en",
+ "misc_tags" : [
+ "en:nutriscore-not-computed",
+ "en:nutrition-not-enough-data-to-compute-nutrition-score",
+ "en:nutriscore-missing-nutrition-data",
+ "en:nutriscore-missing-nutrition-data-saturated-fat"
+ ],
+ "nutriments" : {
+ "carbohydrates_100g" : 0,
+ "energy_100g" : 3378,
+ "fat_100g" : 100,
+ "fiber_100g" : 0,
+ "proteins_100g" : 0,
+ "sodium_100g" : 0
+ },
+ "nutrition_grades_tags" : [
+ "unknown"
+ ],
+ "nutrition_score_beverage" : 0,
+ "nutrition_score_debug" : "missing saturated-fat",
+ "pnns_groups_1" : "Fat and sauces",
+ "pnns_groups_1_tags" : [
+ "fat-and-sauces",
+ "known"
+ ],
+ "pnns_groups_2" : "Fats",
+ "pnns_groups_2_tags" : [
+ "fats",
+ "known"
+ ]
+}
diff --git a/tests/unit/expected_test_results/nutriscore/sunflower-oil-no-sugar.json b/tests/unit/expected_test_results/nutriscore/sunflower-oil-no-sugar.json
new file mode 100644
index 0000000000000..4d1406454db9d
--- /dev/null
+++ b/tests/unit/expected_test_results/nutriscore/sunflower-oil-no-sugar.json
@@ -0,0 +1,114 @@
+{
+ "categories" : "sunflower oils",
+ "categories_hierarchy" : [
+ "en:plant-based-foods-and-beverages",
+ "en:plant-based-foods",
+ "en:fats",
+ "en:vegetable-fats",
+ "en:vegetable-oils",
+ "en:sunflower-seeds-and-their-products",
+ "en:sunflower-oils"
+ ],
+ "categories_lc" : "en",
+ "categories_properties" : {
+ "agribalyse_food_code:en" : "17440",
+ "ciqual_food_code:en" : "17440"
+ },
+ "categories_properties_tags" : [
+ "all-products",
+ "categories-known",
+ "agribalyse-food-code-17440",
+ "agribalyse-food-code-known",
+ "agribalyse-proxy-food-code-unknown",
+ "ciqual-food-code-17440",
+ "ciqual-food-code-known",
+ "agribalyse-known",
+ "agribalyse-17440"
+ ],
+ "categories_tags" : [
+ "en:plant-based-foods-and-beverages",
+ "en:plant-based-foods",
+ "en:fats",
+ "en:vegetable-fats",
+ "en:vegetable-oils",
+ "en:sunflower-seeds-and-their-products",
+ "en:sunflower-oils"
+ ],
+ "food_groups" : "en:fats",
+ "food_groups_tags" : [
+ "en:fats-and-sauces",
+ "en:fats"
+ ],
+ "lc" : "en",
+ "misc_tags" : [
+ "en:nutrition-no-fruits-vegetables-nuts",
+ "en:nutrition-no-fiber-or-fruits-vegetables-nuts",
+ "en:nutriscore-computed"
+ ],
+ "nutriments" : {
+ "carbohydrates_100g" : 0,
+ "energy_100g" : 3378,
+ "fat_100g" : 100,
+ "fiber_100g" : 0,
+ "nutrition-score-fr" : 11,
+ "nutrition-score-fr_100g" : 11,
+ "proteins_100g" : 0,
+ "saturated-fat_100g" : 10,
+ "sodium_100g" : 0
+ },
+ "nutriscore_data" : {
+ "energy" : 3378,
+ "energy_points" : 10,
+ "energy_value" : 3378,
+ "fiber" : 0,
+ "fiber_points" : 0,
+ "fiber_value" : 0,
+ "fruits_vegetables_nuts_colza_walnut_olive_oils" : 0,
+ "fruits_vegetables_nuts_colza_walnut_olive_oils_points" : 0,
+ "fruits_vegetables_nuts_colza_walnut_olive_oils_value" : 0,
+ "grade" : "d",
+ "is_beverage" : 0,
+ "is_cheese" : 0,
+ "is_fat" : 1,
+ "is_water" : 0,
+ "negative_points" : 11,
+ "positive_points" : 0,
+ "proteins" : 0,
+ "proteins_points" : 0,
+ "proteins_value" : 0,
+ "saturated_fat" : 10,
+ "saturated_fat_points" : 9,
+ "saturated_fat_ratio" : 10,
+ "saturated_fat_ratio_points" : 1,
+ "saturated_fat_ratio_value" : 10,
+ "saturated_fat_value" : 10,
+ "score" : 11,
+ "sodium" : 0,
+ "sodium_points" : 0,
+ "sodium_value" : 0,
+ "sugars" : 0,
+ "sugars_points" : 0,
+ "sugars_value" : 0
+ },
+ "nutriscore_grade" : "d",
+ "nutriscore_score" : 11,
+ "nutriscore_score_opposite" : -11,
+ "nutrition_grade_fr" : "d",
+ "nutrition_grades" : "d",
+ "nutrition_grades_tags" : [
+ "d"
+ ],
+ "nutrition_score_beverage" : 0,
+ "nutrition_score_debug" : "",
+ "nutrition_score_warning_no_fruits_vegetables_nuts" : 1,
+ "pnns_groups_1" : "Fat and sauces",
+ "pnns_groups_1_tags" : [
+ "fat-and-sauces",
+ "known"
+ ],
+ "pnns_groups_2" : "Fats",
+ "pnns_groups_2_tags" : [
+ "fats",
+ "known"
+ ]
+}
diff --git a/tests/unit/nutriscore.t b/tests/unit/nutriscore.t
index 10740e77679c4..a251bfef8c760 100644
--- a/tests/unit/nutriscore.t
+++ b/tests/unit/nutriscore.t
@@ -103,6 +103,42 @@ my @tests = (
}
],
+ # if no sugar but carbohydrates is 0, consider sugar 0
+ [
+ "sunflower-oil-no-sugar",
+ {
+ lc => "en",
+ categories => "sunflower oils",
+ nutriments => {
+ energy_100g => 3378,
+ fat_100g => 100,
+ "saturated-fat_100g" => 10,
+ carbohydrates_100g => 0,
+ sodium_100g => 0,
+ fiber_100g => 0,
+ proteins_100g => 0
+ }
+ }
+ ],
+
+ # if no sugar but carbohydrates is 0, consider sugar 0
+ # still saturated fat missing will block
+ [
+ "sunflower-oil-no-sugar-no-sat-fat",
+ {
+ lc => "en",
+ categories => "sunflower oils",
+ nutriments => {
+ energy_100g => 3378,
+ fat_100g => 100,
+ carbohydrates_100g => 0,
+ sodium_100g => 0,
+ fiber_100g => 0,
+ proteins_100g => 0
+ }
+ }
+ ],
+
# saturated fat 1.03 should be rounded to 1.0 which is not strictly greater than 1.0
[
"breakfast-cereals",
@@ -191,6 +227,7 @@ my @tests = (
ingredients_text => "Fresh milk 80%, sugar"
}
],
+
[
"dairy-drink-with-less-than-80-percent-milk",
{
@@ -249,6 +286,25 @@ my @tests = (
],
+ # if fat is 0 and we have no saturated fat, we consider it 0
+ [
+ "fr-orange-nectar-0-fat",
+ {
+ lc => "en",
+ categories => "fruit-nectar",
+ ingredients_text => "Orange 47%, Water, Sugar, Carrots 10%",
+ nutriments => {
+ energy_100g => 250,
+ fat_100g => 0,
+ sugars_100g => 12,
+ sodium_100g => 0.2,
+ fiber_100g => 0,
+ proteins_100g => 0.5
+ },
+ }
+
+ ],
+
# spring waters
["spring-water-no-nutrition", {lc => "en", categories => "spring water", nutriments => {}}],
["flavored-spring-water-no-nutrition", {lc => "en", categories => "flavoured spring water", nutriments => {}}],