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: Nutri-Score v2 fixes and improvements to knowledge panels #9795

Merged
merged 4 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions docs/api/ref/schemas/product_ingredients.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ properties:
type: string
ingredients_percent_analysis:
type: integer
ingredients_sweeteners_n:
type: integer
description: |
Number of sweeteners additives in the ingredients. Undefined if ingredients are not specified.
ingredients_non_nutritive_sweeteners_n:
type: integer
description: |
Number of non-nutritive sweeteners additives (as specified in the Nutri-Score formula) in the ingredients. Undefined if ingredients are not specified.
ingredients_tags:
type: array
items:
Expand Down
2 changes: 0 additions & 2 deletions lib/ProductOpener/Display.pm
Original file line number Diff line number Diff line change
Expand Up @@ -7394,8 +7394,6 @@ sub display_page ($request_ref) {

sub display_image_box ($product_ref, $id, $minheight_ref) {

# print STDERR "display_image_box : $id\n";

my $img = display_image($product_ref, $id, $small_size);
if ($img ne '') {
my $code = $product_ref->{code};
Expand Down
15 changes: 6 additions & 9 deletions lib/ProductOpener/Food.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,10 @@ Differences with the 2021 version:

=head4 $prepared - string contains either "" or "-prepared"

=head3 Return values

Return undef if no value could be computed or estimated.

=cut

sub compute_nutriscore_2023_fruits_vegetables_legumes ($product_ref, $prepared) {
Expand Down Expand Up @@ -1601,11 +1605,7 @@ sub compute_nutriscore_data ($product_ref, $prepared, $nutriments_field, $versio
salt => $nutriments_ref->{"salt" . $prepared . "_100g"},

fruits_vegetables_legumes => $fruits_vegetables_legumes,
fiber => (
(defined $nutriments_ref->{"fiber" . $prepared . "_100g"})
? $nutriments_ref->{"fiber" . $prepared . "_100g"}
: 0
),
fiber => $nutriments_ref->{"fiber" . $prepared . "_100g"},
proteins => $nutriments_ref->{"proteins" . $prepared . "_100g"},
};

Expand All @@ -1621,9 +1621,7 @@ sub compute_nutriscore_data ($product_ref, $prepared, $nutriments_field, $versio
}

if ($is_beverage) {
if (defined $product_ref->{with_non_nutritive_sweeteners}) {
$nutriscore_data_ref->{with_non_nutritive_sweeteners} = $product_ref->{with_non_nutritive_sweeteners};
}
$nutriscore_data_ref->{non_nutritive_sweeteners} = $product_ref->{ingredients_non_nutritive_sweeteners_n};
}
}

Expand Down Expand Up @@ -2227,7 +2225,6 @@ sub compute_serving_size_data ($product_ref) {

my $unit = get_property("nutrients", "zz:$nid", "unit:en")
; # $unit will be undef if the nutrient is not in the taxonomy
print STDERR "nid: $nid - unit: $unit\n";

# If the nutrient has no unit (e.g. pH), or is a % (e.g. "% vol" for alcohol), it is the same regardless of quantity
# otherwise we adjust the value for 100g
Expand Down
61 changes: 40 additions & 21 deletions lib/ProductOpener/Ingredients.pm
Original file line number Diff line number Diff line change
Expand Up @@ -6425,7 +6425,9 @@ sub preparse_ingredients_text ($ingredients_lc, $text) {

sub extract_ingredients_classes_from_text ($product_ref) {

not defined $product_ref->{ingredients_text} and return;
if (not defined $product_ref->{ingredients_text}) {
return;
}
my $ingredients_lc = $product_ref->{ingredients_lc} || $product_ref->{lc};
my $text = preparse_ingredients_text($ingredients_lc, $product_ref->{ingredients_text});
# do not match anything if we don't have a translation for "and"
Expand Down Expand Up @@ -7060,43 +7062,60 @@ sub extract_ingredients_classes_from_text ($product_ref) {
}

# Determine if the product has sweeteners, and non nutritive sweeteners
determine_if_the_product_contains_sweeteners($product_ref);
count_sweeteners_and_non_nutritive_sweeteners($product_ref);

return;
}

=head2 determine_if_the_product_contains_sweeteners
=head2 count_sweeteners_and_non_nutritive_sweeteners

Check if the product contains sweeteners and non nutritive sweeteners (used for the Nutri-Score for beverages)

The NNS / Non nutritive sweeteners listed in the Nutri-Score Update report beverages_31 01 2023-voted
have been added as a non_nutritive_sweetener:en:yes property in the additives taxonomy.

=head3 Return values

The function sets the following fields in the product_ref hash.

If there are no ingredients specified for the product, the fields are not set.

=head4 ingredients_sweeteners_n

=head4 ingredients_non_nutritive_sweeteners_n

=cut

sub determine_if_the_product_contains_sweeteners ($product_ref) {
sub count_sweeteners_and_non_nutritive_sweeteners ($product_ref) {

# Delete old fields, can be removed once all products have been reprocessed
delete $product_ref->{with_sweeteners};
delete $product_ref->{with_non_nutritive_sweeteners};
delete $product_ref->{without_non_nutritive_sweeteners};

if (
get_matching_regexp_property_from_tags(
'additives', $product_ref->{'additives_tags'},
'additives_classes:en', 'sweetener'
)
)
{
$product_ref->{with_sweeteners} = 1;
# Set the number of sweeteners only if the product has specified ingredients
if (not $product_ref->{ingredients_n}) {
delete $product_ref->{ingredients_sweeteners_n};
delete $product_ref->{ingredients_non_nutritive_sweeteners_n};
}
else {

if (
get_matching_regexp_property_from_tags(
'additives', $product_ref->{'additives_tags'},
'non_nutritive_sweetener:en', 'yes'
)
)
{
$product_ref->{with_non_nutritive_sweeteners} = 1;
$product_ref->{ingredients_sweeteners_n} = 0;
$product_ref->{ingredients_non_nutritive_sweeteners_n} = 0;

# Go through additives and check if the product contains sweeteners and non-nutritive sweeteners
if (defined $product_ref->{additives_tags}) {
foreach my $additive (@{$product_ref->{additives_tags}}) {
my $sweetener_property = get_inherited_property("additives", $additive, "sweetener:en") // "";
if ($sweetener_property eq "yes") {
$product_ref->{ingredients_sweeteners_n}++;
}
my $non_nutritive_sweetener_property
= get_inherited_property("additives", $additive, "non_nutritive_sweetener:en") // "";
if ($non_nutritive_sweetener_property eq "yes") {
$product_ref->{ingredients_non_nutritive_sweeteners_n}++;
}
}
}
}

return;
Expand Down
30 changes: 28 additions & 2 deletions lib/ProductOpener/KnowledgePanels.pm
Original file line number Diff line number Diff line change
Expand Up @@ -920,11 +920,37 @@ sub create_nutriscore_2023_panel ($product_ref, $target_lc, $target_cc, $options
my $components_ref = deep_get($product_ref, "nutriscore", $version, "data", "components", $type) // [];
foreach my $component_ref (@$components_ref) {

my $value = $component_ref->{value};

# Specify if there is a space between the value and the unit
my $space_before_unit = '';

my $unit = $component_ref->{unit};

# If the value is not defined (e.g. fiber or fruits_vegetables_legumes), display "unknown" with no unit
if (not defined $value) {
$value = lc(lang_in_other_lc($target_lc, "unknown"));
$unit = '';
}
else {
# Localize the unit for the number of non-nutritive sweeteners
if ($component_ref->{id} eq "non_nutritive_sweeteners") {
$space_before_unit = ' ';
if ($value > 1) {
$unit = lang_in_other_lc($target_lc, "sweeteners");
}
else {
$unit = lang_in_other_lc($target_lc, "sweetener");
}
}
}

my $component_panel_data_ref = {
"type" => $type,
"id" => $component_ref->{id},
"value" => $component_ref->{value},
"unit" => $component_ref->{unit},
"value" => $value,
"unit" => $unit,
"space_before_unit" => $space_before_unit,
"points" => $component_ref->{points},
"points_max" => $component_ref->{points_max},
};
Expand Down
13 changes: 10 additions & 3 deletions lib/ProductOpener/Numbers.pm
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,21 @@ sub convert_string_to_number ($value) {

Round a number to a maximum number of decimal places.

Return undef if passed an undefined value.

=cut

sub round_to_max_decimal_places ($value, $max_decimal_places) {

# Round to the maximum number of decimal places
my $rounded_value = sprintf("%.${max_decimal_places}f", $value);
my $return_value = undef;

if (defined $value) {

# Round to the maximum number of decimal places
$return_value = sprintf("%.${max_decimal_places}f", $value) + 0;
}

return $rounded_value + 0;
return $return_value;
}

1;
Expand Down
27 changes: 15 additions & 12 deletions lib/ProductOpener/Nutriscore.pm
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ sub add_units_to_positive_and_negative_nutriscore_components ($nutriscore_data_r
# Compute the unit.
my $unit;
if ($component_ref->{id} eq 'non_nutritive_sweeteners') {
$unit = undef;
$unit = 'number';
}
elsif (($component_ref->{id} eq 'fruits_vegetables_legumes')
or ($component_ref->{id} eq 'saturated_fat_ratio'))
Expand Down Expand Up @@ -719,7 +719,6 @@ sub compute_nutriscore_score_2023 ($nutriscore_data_ref) {
}

foreach my $nutrient ($energy, "sugars", $saturated_fat, "salt", "fruits_vegetables_legumes", "fiber", "proteins") {
next if not defined $nutriscore_data_ref->{$nutrient};

my $nutrient_threshold_id = $nutrient;
if ( (defined $nutriscore_data_ref->{is_beverage})
Expand All @@ -731,12 +730,15 @@ sub compute_nutriscore_score_2023 ($nutriscore_data_ref) {

$nutriscore_data_ref->{$nutrient . "_points"} = 0;

foreach my $threshold (@{$points_thresholds_2023{$nutrient_threshold_id}}) {
# The saturated fat ratio table uses the greater or equal sign instead of greater
if ( (($nutrient eq "saturated_fat_ratio") and ($nutriscore_data_ref->{$nutrient} >= $threshold))
or (($nutrient ne "saturated_fat_ratio") and ($nutriscore_data_ref->{$nutrient} > $threshold)))
{
$nutriscore_data_ref->{$nutrient . "_points"}++;
# If the nutrient value is defined, assign points according to the thresholds
if (defined $nutriscore_data_ref->{$nutrient}) {
foreach my $threshold (@{$points_thresholds_2023{$nutrient_threshold_id}}) {
# The saturated fat ratio table uses the greater or equal sign instead of greater
if ( (($nutrient eq "saturated_fat_ratio") and ($nutriscore_data_ref->{$nutrient} >= $threshold))
or (($nutrient ne "saturated_fat_ratio") and ($nutriscore_data_ref->{$nutrient} > $threshold)))
{
$nutriscore_data_ref->{$nutrient . "_points"}++;
}
}
}

Expand Down Expand Up @@ -769,14 +771,15 @@ sub compute_nutriscore_score_2023 ($nutriscore_data_ref) {
# Beverages with non-nutritive sweeteners have 4 extra negative points
if ($nutriscore_data_ref->{is_beverage}) {
push @$negative_components, "non_nutritive_sweeteners";
$nutriscore_data_ref->{non_nutritive_sweeteners_max} = 4;
if ($nutriscore_data_ref->{with_non_nutritive_sweeteners}) {
$nutriscore_data_ref->{non_nutritive_sweeteners_points_max} = 4;
# If we don't have ingredients, assume the product does not contain non-nutritive sweeteners
if ( (defined $nutriscore_data_ref->{non_nutritive_sweeteners})
and ($nutriscore_data_ref->{non_nutritive_sweeteners} > 0))
{
$nutriscore_data_ref->{non_nutritive_sweeteners_points} = 4;
$nutriscore_data_ref->{non_nutritive_sweeteners} = "presence";
}
else {
$nutriscore_data_ref->{non_nutritive_sweeteners_points} = 0;
$nutriscore_data_ref->{non_nutritive_sweeteners} = "absence";
}
}

Expand Down
12 changes: 11 additions & 1 deletion po/common/common.pot
Original file line number Diff line number Diff line change
Expand Up @@ -6797,4 +6797,14 @@ msgstr "Consuming foods rich in fruits, vegetables and legumes reduces the risks

msgctxt "nutrient_info_proteins_benefit"
msgid "Foods that are rich in proteins are usually rich in calcium or iron which are essential minerals with numerous health benefits."
msgstr "Foods that are rich in proteins are usually rich in calcium or iron which are essential minerals with numerous health benefits."
msgstr "Foods that are rich in proteins are usually rich in calcium or iron which are essential minerals with numerous health benefits."

# sweeteners (additives), plural
msgctxt "sweeteners"
msgid "sweeteners"
msgstr ""

# sweetener (additive), singular
msgctxt "sweetener"
msgid "sweetener"
msgstr ""
Loading
Loading