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: new additives, non-nutritive sweeteners for new Nutri-Score #9005

Merged
merged 9 commits into from
Sep 20, 2023
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
15 changes: 9 additions & 6 deletions docs/api/ref/schemas/product_nutrition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ type: object
description: |
Nutrition fields of a product
properties:
fruits-vegetables-nuts_100g_estimate:
type: integer
description: |
An estimate, from ingredients list of the percentage of vegetable and nuts.
This is an important information for Nutri-Score computation.
no_nutrition_data:
type: string
description: |
Expand Down Expand Up @@ -45,8 +40,16 @@ properties:
type: number
fat:
type: number
fruits-vegetables-nuts-estimate-from-ingredients_100g:
fruits-vegetables-legumes-estimate-from-ingredients:
type: number
description: |
An estimate, from the ingredients list of the percentage of fruits, vegetable and legumes.
This is an important information for Nutri-Score (2023 version) computation.
fruits-vegetables-nuts-estimate-from-ingredients:
type: number
description: |
An estimate, from the ingredients list of the percentage of fruits, vegetable and nuts.
This is an important information for Nutri-Score (2021 version) computation.
nova-group:
type: integer
nutrition-score-fr:
Expand Down
12 changes: 10 additions & 2 deletions lib/ProductOpener/Food.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ sub compute_nutriscore_data ($product_ref, $prepared, $nutriments_field, $versio

# The 2021 and 2023 version of the Nutri-Score need different nutrients
if ($version eq "2021") {
# fruits, vegetables, nuts, olive / rapeseed / walnut oils
# fruits, vegetables, nuts, olive / rapeseed / walnut oils - 2021
my $fruits_vegetables_nuts_colza_walnut_olive_oils
= compute_nutriscore_2021_fruits_vegetables_nuts_colza_walnut_olive_oil($product_ref, $prepared);

Expand Down Expand Up @@ -1302,12 +1302,14 @@ sub compute_nutriscore_data ($product_ref, $prepared, $nutriments_field, $versio
}
}
else {
# fruits, vegetables, legumes - 2023
my $fruits_vegetables_legumes = compute_nutriscore_2023_fruits_vegetables_legumes($product_ref, $prepared);

my $is_fat_oil_nuts_seeds = is_fat_oil_nuts_seeds_for_nutrition_score($product_ref);
my $is_beverage = $product_ref->{nutrition_score_beverage};

$nutriscore_data_ref = {
is_beverage => $product_ref->{nutrition_score_beverage},
is_beverage => $is_beverage,
is_water => is_water_for_nutrition_score($product_ref),
is_cheese => is_cheese_for_nutrition_score($product_ref),
is_fat_oil_nuts_seeds => $is_fat_oil_nuts_seeds,
Expand Down Expand Up @@ -1335,6 +1337,12 @@ sub compute_nutriscore_data ($product_ref, $prepared, $nutriments_field, $versio
$nutriscore_data_ref->{energy_from_saturated_fat} = $nutriscore_data_ref->{saturated_fat} * 37;
}
}

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

# tweak data to take into account special cases
Expand Down
54 changes: 41 additions & 13 deletions lib/ProductOpener/Ingredients.pm
Original file line number Diff line number Diff line change
Expand Up @@ -6039,16 +6039,44 @@ sub extract_ingredients_classes_from_text ($product_ref) {
= $product_ref->{ingredients_that_may_be_from_palm_oil_n} + $product_ref->{ingredients_from_palm_oil_n};
}

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

return;
}

=head2 determine_if_the_product_contains_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.

=cut

sub determine_if_the_product_contains_sweeteners ($product_ref) {

delete $product_ref->{with_sweeteners};
if (defined $product_ref->{'additives_tags'}) {
foreach my $additive (@{$product_ref->{'additives_tags'}}) {
my $e = $additive;
$e =~ s/\D//g;
if (($e >= 950) and ($e <= 968)) {
$product_ref->{with_sweeteners} = 1;
last;
}
}
delete $product_ref->{with_non_nutritive_sweeteners};

if (
get_matching_regexp_property_from_tags(
'additives', $product_ref->{'additives_tags'},
'additives_classes:en', 'sweetener'
)
)
{
$product_ref->{with_sweeteners} = 1;
}

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

return;
Expand Down Expand Up @@ -6421,8 +6449,8 @@ sub add_ingredients_matching_function ($ingredients_ref, $match_function_ref) {
if (defined $ingredient_ref->{percent}) {
$count += $ingredient_ref->{percent};
}
elsif (defined $ingredient_ref->{percent_min}) {
$count += $ingredient_ref->{percent_min};
elsif (defined $ingredient_ref->{percent_estimate}) {
$count += $ingredient_ref->{percent_estimate};
Comment on lines +6452 to +6453
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you check it's not referenced in OpenAPI (if it is, change it there also, and add a description if there is none yet !)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the doc we just said "estimate", not "minimum estimate", so the doc is correct.
I added the new "fruits-vegetables-legumes-estimate-from-ingredients" field.

}
# We may not have percent_min if the ingredient analysis failed because of seemingly impossible values
# in that case, try to get the possible percent values in nested sub ingredients
Expand All @@ -6440,7 +6468,7 @@ sub add_ingredients_matching_function ($ingredients_ref, $match_function_ref) {

=head2 estimate_ingredients_matching_function ( $product_ref, $match_function_ref, $nutrient_id = undef )

This function analyzes the ingredients to estimate the minimum percentage of ingredients of a specific type
This function analyzes the ingredients to estimate the percentage of ingredients of a specific type
(e.g. fruits/vegetables/legumes for the Nutri-Score).

=head3 Parameters
Expand All @@ -6457,7 +6485,7 @@ If the $nutrient_id argument is defined, we also store the nutrient value in $pr

=head3 Return value

Minimum percentage of ingredients matching the function.
Estimated percentage of ingredients matching the function.

=cut

Expand Down
9 changes: 5 additions & 4 deletions lib/ProductOpener/Nutriscore.pm
Original file line number Diff line number Diff line change
Expand Up @@ -718,17 +718,18 @@ sub compute_nutriscore_score_2023 ($nutriscore_data_ref) {

# Beverages with non-nutritive sweeteners have 4 extra negative points
if ($nutriscore_data_ref->{is_beverage}) {
if ($nutriscore_data_ref->{has_sweeteners}) {
$nutriscore_data_ref->{"sweeteners_points"} = 4;
if ($nutriscore_data_ref->{with_non_nutritive_sweeteners}) {
$nutriscore_data_ref->{"non_nutritive_sweeteners_points"} = 4;
}
else {
$nutriscore_data_ref->{"sweeteners_points"} = 0;
$nutriscore_data_ref->{"non_nutritive_sweeteners_points"} = 0;
}
}

# Negative points

$nutriscore_data_ref->{negative_nutrients} = [$energy, "sugars", $saturated_fat, "salt", "sweeteners"];
$nutriscore_data_ref->{negative_nutrients}
= [$energy, "sugars", $saturated_fat, "salt", "non_nutritive_sweeteners"];
$nutriscore_data_ref->{negative_points} = 0;
foreach my $nutrient (@{$nutriscore_data_ref->{negative_nutrients}}) {
$nutriscore_data_ref->{negative_points} += ($nutriscore_data_ref->{$nutrient . "_points"} || 0);
Expand Down
101 changes: 91 additions & 10 deletions lib/ProductOpener/Tags.pm
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ BEGIN {
&get_property
&get_property_with_fallbacks
&get_inherited_property
&get_property_from_tags
&get_inherited_property_from_tags
&get_matching_regexp_property_from_tags
&get_inherited_property_from_categories_tags
&get_inherited_properties
&get_tags_grouped_by_property
Expand Down Expand Up @@ -386,6 +389,89 @@ sub get_inherited_property ($tagtype, $canon_tagid, $property) {
return;
}

=head2 get_property_from_tags ($tagtype, $tags_ref, $property)

Return the value of a property for the first tag of a list that has this property.

=head3 Parameters

=head4 $tagtype

=head4 $tags_ref Reference to a list of tags

=head4 $property

=cut

sub get_property_from_tags ($tagtype, $tags_ref, $property) {

my $value;
if (defined $tags_ref) {
foreach my $tagid (@$tags_ref) {
$value = get_property($tagtype, $tagid, $property);
last if $value;
}
}
return $value;
}

=head2 get_inherited_property_from_tags ($tagtype, $tags_ref, $property)

Return the value of an inherited property for the first tag of a list that has this property.

=head3 Parameters

=head4 $tagtype

=head4 $tags_ref Reference to a list of tags

=head4 $property

=cut

sub get_inherited_property_from_tags ($tagtype, $tags_ref, $property) {

my $value;
if (defined $tags_ref) {
foreach my $tagid (@$tags_ref) {
$value = get_inherited_property($tagtype, $tagid, $property);
last if $value;
}
}
return $value;
}

=head2 get_matching_regexp_property_from_tags ($tagtype, $tags_ref, $property, $regexp)

Return the value of a property for the first tag of a list that has this property that matches the regexp.

=head3 Parameters

=head4 $tagtype

=head4 $tags_ref Reference to a list of tags

=head4 $property

=head4 $regexp

=cut

sub get_matching_regexp_property_from_tags ($tagtype, $tags_ref, $property, $regexp) {

my $matching_value;
if (defined $tags_ref) {
foreach my $tagid (@$tags_ref) {
my $value = get_property($tagtype, $tagid, $property);
if ((defined $value) and ($value =~ /$regexp/)) {
$matching_value = $value;
last;
}
}
}
return $matching_value;
}

=head2 get_inherited_property_from_categories_tags ($product_ref, $property) {

Iterating from the most specific category, try to get a property for a tag by exploring the taxonomy (using parents).
Expand All @@ -402,18 +488,13 @@ The property if found.
=cut

sub get_inherited_property_from_categories_tags ($product_ref, $property) {
my $category_match;

if ((defined $product_ref->{categories_tags}) and (scalar @{$product_ref->{categories_tags}} > 0)) {

# Start with most specific category first
foreach my $category (reverse @{$product_ref->{categories_tags}}) {

$category_match = get_inherited_property("categories", $category, $property);
last if $category_match;
}
if (defined $product_ref->{categories_tags}) {
# We reverse the list of categories in order to have the most specific categories first
return get_inherited_property_from_tags("categories", [reverse @{$product_ref->{categories_tags}}], $property);
}
return $category_match;

return;
}

=head2 get_inherited_properties ($tagtype, $canon_tagid, $properties_names_ref, $fallback_lcs = ["xx", "en"]) {
Expand Down
1 change: 1 addition & 0 deletions stop_words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ naturel
nd
NGO
NGINX
NNS
nodejs
nutri
Nutri
Expand Down
Loading