From 7c2fb517898cf30acf499ec1d649e00b0a092da3 Mon Sep 17 00:00:00 2001 From: benbenben2 <110821832+benbenben2@users.noreply.github.com> Date: Fri, 15 Dec 2023 09:22:07 +0100 Subject: [PATCH] feat: dq_label_nutrient_levels (#9410) * dq_label_nutrient_levels * make lint * apply comment + add tests --- lib/ProductOpener/DataQualityFood.pm | 558 +++++++++++++++++++- lib/ProductOpener/Units.pm | 76 ++- stop_words.txt | 1 + taxonomies/data_quality.txt | 229 ++++++++ taxonomies/labels.txt | 234 +++++++- tests/unit/dataqualityfood.t | 124 ----- tests/unit/dataqualityfood_labels.t | 762 +++++++++++++++++++++++++++ 7 files changed, 1824 insertions(+), 160 deletions(-) create mode 100644 tests/unit/dataqualityfood_labels.t diff --git a/lib/ProductOpener/DataQualityFood.pm b/lib/ProductOpener/DataQualityFood.pm index b70c1fe1cf05c..1e46b944afaab 100644 --- a/lib/ProductOpener/DataQualityFood.pm +++ b/lib/ProductOpener/DataQualityFood.pm @@ -52,6 +52,7 @@ use ProductOpener::Store qw(:all); use ProductOpener::Tags qw(:all); use ProductOpener::Food qw(:all); use ProductOpener::Ecoscore qw(:all); +use ProductOpener::Units qw(extract_standard_unit); use Data::DeepAccess qw(deep_exists); @@ -1780,13 +1781,14 @@ sub check_categories ($product_ref) { Checks related to specific product labels. -Vegan label: check that there is no non-vegan ingredient. - -Vegetarian label: check that there is no non-vegetarian ingredient. - =cut sub check_labels ($product_ref) { + # compare label claim and ingredients + + # Vegan label: check that there is no non-vegan ingredient. + # Vegetarian label: check that there is no non-vegetarian ingredient. + # this also include en:vegan that is a child of en:vegetarian if (defined $product_ref->{labels_tags} && has_tag($product_ref, "labels", "en:vegetarian")) { if (defined $product_ref->{ingredients}) { @@ -1841,6 +1843,554 @@ sub check_labels ($product_ref) { } } + # In EU, compare label claim and nutrition + # https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A02006R1924-20141213 + my @eu_countries = ( + "en:austria", "en:belgium", "en:bulgaria", "en:croatia", "en:cyprus", "en:czech republic", + "en:denmark", "en:france", "en:estonia", "en:finland", "en:germany", "en:greece", + "en:hungary", "en:ireland", "en:italy", "en:latvia", "en:lithuania", "en:luxembourg", + "en:malta", "en:netherlands", "en:poland", "en:portugal", "en:romania", "en:slovakia", + "en:slovenia", "en:spain", "en:sweden" + ); + my $european_product = 0; + foreach my $eu_country (@eu_countries) { + if (has_tag(($product_ref, "countries", $eu_country))) { + $european_product = 1; + last; + } + } + + if ( (defined $product_ref->{nutriments}) + and (defined $product_ref->{labels_tags}) + and ($european_product == 1)) + { + # maximal values differs depending if the product is + # solid (higher maxmal values) or + # liquid (lower maximal values) + my $solid = 1; + if (defined $product_ref->{quantity}) { + my $standard_unit = extract_standard_unit($product_ref->{quantity}); + if ((defined $standard_unit) and ($standard_unit eq "ml")) { + $solid = 0; + } + } + + if ( (defined $product_ref->{nutriments}{"energy-kcal_100g"}) + or (defined $product_ref->{nutriments}{"energy-kj_value"})) + { + # In EU, a claim that a food is low in energy may only be made where the product + # does not contain more than 40 kcal (170 kJ)/100 g for solids or + # more than 20 kcal (80 kJ)/100 ml for liquids. + # (not handled) For table-top sweeteners the limit of 4 kcal (17 kJ)/portion, + # with equivalent sweetening properties to 6 g of sucrose + # (approximately 1 teaspoon of sucrose), applies. + if ( + (has_tag($product_ref, "labels", "en:low-energy")) + and ( + ( + ($solid == 1) and (($product_ref->{nutriments}{"energy-kcal_100g"} > 40) + or ($product_ref->{nutriments}{"energy-kj_100g"} > 170)) + ) + or ( + ($solid == 0) + and ( ($product_ref->{nutriments}{"energy-kcal_100g"} > 20) + or ($product_ref->{nutriments}{"energy-kj_100g"} > 80)) + ) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", "en:low-energy-label-claim-but-energy-above-limitation"); + } + + # TODO: checks label and children - limitation applied on child should not apply on parent in some cases + + # In EU, a claim that a food is energy free may only be made where the product + # does not contain more than 4 kcal (17 kJ)/100 ml. + # (not handled) For table-top sweeteners the limit of 0,4 kcal (1,7 kJ)/portion, + # with equivalent sweetening properties to 6 g of sucrose + # (approximately 1 teaspoon of sucrose), applies. + if ( + (has_tag($product_ref, "labels", "en:energy-free")) + and ( + ( + ($product_ref->{nutriments}{"energy-kcal_100g"} > 4) + or ($product_ref->{nutriments}{"energy-kj_100g"} > 17) + ) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:energy-free-label-claim-but-energy-above-limitation"); + } + } + + if (defined $product_ref->{nutriments}{fat_100g}) { + # In EU, a claim that a food is low fat may only be made where the product contains + # no more than 3 g of fat per 100 g for solids or + # 1,5 g of fat per 100 ml for liquids + # (1,8 g of fat per 100 ml for semi-skimmed milk). + if ( + (has_tag($product_ref, "labels", "en:low-fat")) + and ( + (($solid == 1) and ($product_ref->{nutriments}{fat_100g} > 3)) + or ( ($solid == 0) + and ($product_ref->{nutriments}{fat_100g} > 1.5) + and (!has_tag($product_ref, "categories", "en:semi-skimmed-milks"))) + or ( ($solid == 0) + and ($product_ref->{nutriments}{fat_100g} > 1.8) + and (has_tag($product_ref, "categories", "en:semi-skimmed-milks"))) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", "en:low-fat-label-claim-but-fat-above-limitation"); + } + + # In EU, a claim that a food is fat free may only be made where + # the product contains no more than 0,5 g of fat per 100 g or 100 ml. + if ((has_tag($product_ref, "labels", "en:no-fat")) && ($product_ref->{nutriments}{fat_100g} > 0.5)) { + add_tag($product_ref, "data_quality_warnings", "en:no-fat-label-claim-but-fat-above-0.5"); + } + + # In EU, a claim that a food is high in monounsaturated fat may only be made where + # at least 45 % of the fatty acids present in the product derive from monounsaturated fat + # under the condition that monounsaturated fat provides more than 20 % of energy of the product. + if ( + (has_tag($product_ref, "labels", "en:high-monounsaturated-fat")) + and (defined $product_ref->{nutriments}{"monounsaturated-fat_100g"}) + and ( + ( + $product_ref->{nutriments}{"monounsaturated-fat_100g"} + < ($product_ref->{nutriments}{fat_100g} * 45 / 100) + ) + or ( + ( + $product_ref->{nutriments}{"monounsaturated-fat_100g"} + * $energy_from_nutrients{europe}{"fat"}{"kcal"} + ) < (20 * $product_ref->{nutriments}{"energy-kcal_value_computed"} / 100) + ) + or ( + ( + $product_ref->{nutriments}{"monounsaturated-fat_100g"} + * $energy_from_nutrients{europe}{"fat"}{"kj"} + ) < (20 * $product_ref->{nutriments}{"energy-kj_value_computed"} / 100) + ) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:high-monounsaturated-fat-label-claim-but-monounsaturated-fat-under-limitation"); + } + + # In EU, a claim that a food is high in polyunsaturated fat may only be made where + # at least 45 % of the fatty acids present in the product derive from polyunsaturated fat + # under the condition that polyunsaturated fat provides more than 20 % of energy of the product. + if ( + (has_tag($product_ref, "labels", "en:rich-in-polyunsaturated-fatty-acids")) + and (defined $product_ref->{nutriments}{"polyunsaturated-fat_100g"}) + and ( + ( + $product_ref->{nutriments}{"polyunsaturated-fat_100g"} + < ($product_ref->{nutriments}{fat_100g} * 45 / 100) + ) + or ( + ( + $product_ref->{nutriments}{"polyunsaturated-fat_100g"} + * $energy_from_nutrients{europe}{"fat"}{"kcal"} + ) < (20 * $product_ref->{nutriments}{"energy-kcal_value_computed"} / 100) + ) + or ( + ( + $product_ref->{nutriments}{"polyunsaturated-fat_100g"} + * $energy_from_nutrients{europe}{"fat"}{"kj"} + ) < (20 * $product_ref->{nutriments}{"energy-kj_value_computed"} / 100) + ) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:rich-in-polyunsaturated-fatty-acids-label-claim-but-polyunsaturated-fat-under-limitation"); + } + + # In EU, a claim that a food is high in unsaturated fat may only be made where + # at least 70 % of the fatty acids present in the product derive from unsaturated fat + # under the condition that unsaturated fat provides more than 20 % of energy of the product. + if ( + (has_tag($product_ref, "labels", "en:rich-in-unsaturated-fatty-acids")) + and (defined $product_ref->{nutriments}{"unsaturated-fat_100g"}) + and ( + ( + $product_ref->{nutriments}{"unsaturated-fat_100g"} + < ($product_ref->{nutriments}{fat_100g} * 45 / 100) + ) + or ( + ( + $product_ref->{nutriments}{"unsaturated-fat_100g"} + * $energy_from_nutrients{europe}{"fat"}{"kcal"} + ) < (20 * $product_ref->{nutriments}{"energy-kcal_value_computed"} / 100) + ) + or ( + ( $product_ref->{nutriments}{"unsaturated-fat_100g"} + * $energy_from_nutrients{europe}{"fat"}{"kj"}) + < (20 * $product_ref->{nutriments}{"energy-kj_value_computed"} / 100)) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:rich-in-unsaturated-fatty-acids-label-claim-but-unsaturated-fat-under-limitation"); + } + + } + + if (defined $product_ref->{nutriments}{"saturated-fat_100g"}) { + # In EU, a claim that a food is fat free may only be made if + # the sum of saturated fatty acids and trans-fatty acids in the product does not exceed 1,5 g per 100 g for solids or + # 0,75 g/100 ml for liquids and in either case + # the sum of saturated fatty acids and trans-fatty acids must not provide more than 10 % of energy + + # use computed enegy because input energy may be undefined + if ( + (has_tag($product_ref, "labels", "en:low-content-of-saturated-fat")) + and ( + (($solid == 1) and ($product_ref->{nutriments}{"saturated-fat_100g"} > 1.5)) + or ( ($solid == 0) + and ($product_ref->{nutriments}{"saturated-fat_100g"} > 0.75)) + or ( + ( + ( + ( + $product_ref->{nutriments}{"saturated-fat_100g"} + + $product_ref->{nutriments}{"trans-fat_100g"} || 0 + ) * $energy_from_nutrients{europe}{"fat"}{"kj"} + ) > (10 * $product_ref->{nutriments}{"energy-kj_value_computed"} / 100) + ) + or ( + ( + ( + $product_ref->{nutriments}{"saturated-fat_100g"} + + $product_ref->{nutriments}{"trans-fat_100g"} || 0 + ) * $energy_from_nutrients{europe}{"fat"}{"kcal"} + ) > (10 * $product_ref->{nutriments}{"energy-kcal_value_computed"} / 100) + ) + ) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:low-saturated-fat-label-claim-but-fat-above-limitation"); + } + + # In EU, a claim that a food does not contain sturated fat may only be made where + # the sum of saturated fat and trans-fatty acids does not exceed 0,1 g of saturated fat per 100 g or 100 ml. + if ( (has_tag($product_ref, "labels", "en:saturated-fat-free")) + and (($product_ref->{nutriments}{"saturated-fat_100g"} > 0.1))) + { + add_tag($product_ref, "data_quality_warnings", "en:saturated-fat-free-label-claim-but-fat-above-0.1"); + } + } + + if (defined $product_ref->{nutriments}{sugars_100g}) { + # In EU, a claim that a food is low sugar may only be made where the product contains + # no more than 5 g of sugars per 100 g for solids or + # 2,5 g of sugars per 100 ml for liquids. + if ( + (has_tag($product_ref, "labels", "en:low-sugar")) + and ( + (($solid == 1) and ($product_ref->{nutriments}{sugars_100g} > 5)) + or ( ($solid == 0) + and ($product_ref->{nutriments}{sugars_100g} > 2.5)) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", "en:low-sugar-label-claim-but-sugar-above-limitation"); + } + + # In EU, a claim that a food is sugar-free may only be made where the product contains + # no more than 0,5 g of sugars per 100 g or 100 ml. + if ( (has_tag($product_ref, "labels", "en:no-sugar")) + and (($product_ref->{nutriments}{sugars_100g} > 0.5))) + { + add_tag($product_ref, "data_quality_warnings", "en:sugar-free-label-claim-but-sugar-above-limitation"); + } + } + + # In EU, a claim that a food is with no added sugars may only be made where + # the product does not contain any added mono- or disaccharides or + # any other food used for its sweetening properties. + # (not handled) If sugars are naturally present in the food, the following indication should also appear on the label: + # ‘CONTAINS NATURALLY OCCURRING SUGARS’. + if ( (has_tag($product_ref, "labels", "en:no-added-sugar")) + and (has_tag($product_ref, "ingredients", "en:added-sugar"))) + { + add_tag($product_ref, "data_quality_warnings", "en:no-added-sugar-label-claim-but-contains-added-sugar"); + } + + # In EU, a claim that a food is low sodium or low salt may only be made where + # the product contains no more than 0,12 g of sodium, or the equivalent value for salt, per 100 g or per 100 ml. + # (not handled) For waters, other than natural mineral waters falling within the scope of Directive 80/777/EEC, + # this value should not exceed 2 mg of sodium per 100 ml. + if ( + ( + ((defined $product_ref->{nutriments}{sodium_100g}) and ($product_ref->{nutriments}{sodium_100g} > 0.12)) + or ((defined $product_ref->{nutriments}{salt_100g}) and ($product_ref->{nutriments}{salt_100g} > 0.3)) + ) + and (has_tag($product_ref, "labels", "en:low-sodium") or has_tag($product_ref, "labels", "en:low-salt")) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:low-sodium-or-low-salt-label-claim-but-sodium-or-salt-above-limitation"); + } + + # In EU, a claim that a food is very low in sodium/salt may only be made where + # the product contains no more than 0,04 g of sodium, or the equivalent value for salt, per 100 g or per 100 ml. + # This claim shall not be used for natural mineral waters and other waters. + if ( + ( + ((defined $product_ref->{nutriments}{sodium_100g}) and ($product_ref->{nutriments}{sodium_100g} > 0.04)) + or ((defined $product_ref->{nutriments}{salt_100g}) and ($product_ref->{nutriments}{salt_100g} > 0.1)) + ) + and ( has_tag($product_ref, "labels", "en:very-low-sodium") + or has_tag($product_ref, "labels", "en:very-low-salt")) + and (!has_tag($product_ref, "categories", "en:waters")) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:very-low-sodium-or-very-low-salt-label-claim-but-sodium-or-salt-above-limitation"); + } + + # In EU, a claim that a food is sodium-free or salt-free may only be made where the product contains + # no more than 0,005 g of sodium, or the equivalent value for salt, per 100 g. + if ( + ( + ( (defined $product_ref->{nutriments}{sodium_100g}) + and ($product_ref->{nutriments}{sodium_100g} > 0.005)) + or + ((defined $product_ref->{nutriments}{salt_100g}) and ($product_ref->{nutriments}{salt_100g} > 0.0125)) + ) + and (has_tag($product_ref, "labels", "en:no-sodium") or has_tag($product_ref, "labels", "en:no-salt")) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:sodium-free-or-salt-free-label-claim-but-sodium-or-salt-above-limitation"); + } + + # In EU, a claim that sodium/salt has not been added to a food may only be made where the product + # does not contain any added sodium/salt or any other ingredient containing added sodium/salt and + # the product contains no more than 0,12 g sodium, or the equivalent value for salt, per 100 g or 100 ml. + if ( + ( + ((defined $product_ref->{nutriments}{sodium_100g}) and ($product_ref->{nutriments}{sodium_100g} > 0.12)) + or ((defined $product_ref->{nutriments}{salt_100g}) and ($product_ref->{nutriments}{salt_100g} > 0.3)) + or (has_tag($product_ref, "ingredients", "en:salt")) + ) + and ( has_tag($product_ref, "labels", "en:no-added-sodium") + or has_tag($product_ref, "labels", "en:no-added-salt")) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:no-added-sodium-or-no-added-salt-label-claim-but-sodium-or-salt-above-limitation"); + } + + if (defined $product_ref->{nutriments}{fiber_100g}) { + # In EU, a claim that a food is a source of fibre may only be made where the product contains + # at least 3 g of fibre per 100 g or + # at least 1,5 g of fibre per 100 kcal. + if ( + (has_tag($product_ref, "labels", "en:source-of-fibre")) + and ( (($solid == 1) and ($product_ref->{nutriments}{fiber_100g} < 3)) + or ($product_ref->{nutriments}{fiber_100g} * $energy_from_nutrients{europe}{"fiber"}{"kcal"}) + < (1.5 * $product_ref->{nutriments}{"energy-kcal_value_computed"} / 100)) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:source-of-fibre-label-claim-but-fibre-below-limitation"); + } + # In EU, a claim that a food is high in fibre may only be made where the product contains + # at least 6 g of fibre per 100 g or + # at least 3 g of fibre per 100 kcal. + if ( + (has_tag($product_ref, "labels", "en:high-fibres")) + and ( (($solid == 1) and ($product_ref->{nutriments}{fiber_100g} < 6)) + or ($product_ref->{nutriments}{fiber_100g} * $energy_from_nutrients{europe}{"fiber"}{"kcal"}) + < (3 * $product_ref->{nutriments}{"energy-kcal_value_computed"} / 100)) + ) + { + add_tag($product_ref, "data_quality_warnings", "en:high-fibres-label-claim-but-fibre-below-limitation"); + } + } + + if (defined $product_ref->{nutriments}{proteins_100g}) { + # In EU, a claim that a food is a source of protein may only be made where + # at least 12 % of the energy value of the food is provided by protein. + if ( (has_tag($product_ref, "labels", "en:source-of-proteins")) + and ($product_ref->{nutriments}{proteins_100g} * $energy_from_nutrients{europe}{"proteins"}{"kcal"}) + < (12 * $product_ref->{nutriments}{"energy-kcal_value_computed"} / 100)) + { + add_tag($product_ref, "data_quality_warnings", + "en:source-of-proteins-label-claim-but-proteins-below-limitation"); + } + + # In EU, a claim that a food is high in protein may only be made where + #at least 20 % of the energy value of the food is provided by protein. + if ( (has_tag($product_ref, "labels", "en:high-proteins")) + and ($product_ref->{nutriments}{proteins_100g} * $energy_from_nutrients{europe}{"proteins"}{"kcal"}) + < (20 * $product_ref->{nutriments}{"energy-kcal_value_computed"} / 100)) + { + add_tag($product_ref, "data_quality_warnings", + "en:high-proteins-label-claim-but-proteins-below-limitation"); + } + } + + # See annex from: https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:31990L0496 + my %vitamins_and_minerals_labelling = ( + europe => { + "vitamin-a" => { + "en:vitamin-a-source" => 0.0008, # 800 µg + "en:rich-in-vitamin-a" => 0.0016, + }, + "vitamin-d" => { + "en:vitamin-d-source" => 0.000005, # 5 µg + "en:rich-in-vitamin-d" => 0.00001, + }, + "vitamin-e" => { + "en:vitamin-e-source" => 0.01, # 10 mg + "en:rich-in-vitamin-e" => 0.02, + }, + "vitamin-c" => { + "en:vitamin-c-source" => 0.06, # 10 mg + "en:rich-in-vitamin-c" => 0.12, + }, + "vitamin-b1" => { + "en:vitamin-b1-source" => 0.0014, # 1.4 mg, thiamin + "en:rich-in-vitamin-b1" => 0.0028, + }, + "vitamin-b2" => { + "en:vitamin-b2-source" => 0.0016, # 1.6 mg, riboflavin + "en:rich-in-vitamin-b2" => 0.0032, + }, + "vitamin-b3" => { + "en:vitamin-b3-source" => 0.018, # 18 mg, niacin + "en:rich-in-vitamin-b3" => 0.036, + }, + "vitamin-b6" => { + "en:vitamin-b6-source" => 0.002, # 2 mg + "en:rich-in-vitamin-b6" => 0.004, + }, + "vitamin-b9" => { + "en:vitamin-b9-source" => 0.0002, # 200 µg, folacin + "en:rich-in-vitamin-b9" => 0.0004, + }, + "vitamin-b12" => { + "en:vitamin-b12-source" => 0.000001, # 1 µg + "en:rich-in-vitamin-b12" => 0.000002, + }, + "biotin" => { + "en:source-of-biotin" => 0.00015, # 0.15 mg + "en:high-in-biotin" => 0.0003, + }, + "pantothenic-acid" => { + "en:source-of-pantothenic-acid" => 0.006, # 6 mg + "en:high-in-pantothenic-acid" => 0.012, + }, + "calcium" => { + "en:calcium-source" => 0.8, # 800 mg + "en:high-in-calcium" => 1.6, + }, + "phosphorus" => { + "en:phosphore-source" => 0.8, # 800 mg + "en:high-in-phosphore" => 1.6, + }, + "iron" => { + "en:iron-source" => 0.014, # 14 mg + "en:high-in-iron" => 0.028, + }, + "magnesium" => { + "en:magnesium-source" => 0.3, # 300 mg + "en:high-in-magnesium" => 0.6, + }, + "zinc" => { + "en:zinc-source" => 0.015, # 15 mg + "en:high-in-zinc" => 0.03, + }, + "iodine" => { + "en:iodine-source" => 0.00015, # 150 µg + "en:high-in-iodine" => 0.0003, + }, + }, + ); + foreach my $vit_or_min (keys %{$vitamins_and_minerals_labelling{europe}}) { + foreach my $vit_or_min_label (keys %{$vitamins_and_minerals_labelling{europe}{$vit_or_min}}) { + if ( + (defined $product_ref->{nutriments}{$vit_or_min . "_100g"}) + and (has_tag($product_ref, "labels", $vit_or_min_label)) + and ($product_ref->{nutriments}{$vit_or_min . "_100g"} + < $vitamins_and_minerals_labelling{europe}{$vit_or_min}{$vit_or_min_label}) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:" + . substr($vit_or_min_label, 3) + . "-label-claim-but-$vit_or_min-below-$vitamins_and_minerals_labelling{europe}{$vit_or_min}{$vit_or_min_label}" + ); + } + } + } + + # In EU, a claim that a food is a source of omega-3 may only be made where the product contains + # at least 0,3 g alpha-linolenic acid per 100 g + # not handled: and per 100 kcal, or + # at least 40 mg of the sum of eicosapentaenoic acid and docosahexaenoic acid per 100 g + # not handled: and per 100 kcal. + if ( + (has_tag($product_ref, "labels", "en:source-of-omega-3")) + and ( + ( + (defined $product_ref->{nutriments}{"alpha-linolenic-acid_100g"}) + and ($product_ref->{nutriments}{"alpha-linolenic-acid_100g"} < 0.3) + ) + or ( + (defined $product_ref->{nutriments}{"eicosapentaenoic-acid_100g"}) + and (defined $product_ref->{nutriments}{"docosahexaenoic-acid_100g"}) + and ( + ( + $product_ref->{nutriments}{"eicosapentaenoic-acid_100g"} + + $product_ref->{nutriments}{"docosahexaenoic-acid_100g"} + ) < 0.04 + ) + ) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:source-of-omega-3-label-claim-but-ala-or-sum-of-epa-and-dha-below-limitation"); + } + + # In EU, a claim that a food is high in omega-3 may only be made where the product contains + # at least 0,6 g alpha-linolenic acid per 100 g and per 100 kcal, or + # at least 80 mg of the sum of eicosapentaenoic acid and docosahexaenoic acid per 100 g and per 100 kcal. + if ( + (has_tag($product_ref, "labels", "en:high-in-omega-3")) + and ( + ( + (defined $product_ref->{nutriments}{"alpha-linolenic-acid_100g"}) + and ($product_ref->{nutriments}{"alpha-linolenic-acid_100g"} < 0.6) + ) + or ( + (defined $product_ref->{nutriments}{"eicosapentaenoic-acid_100g"}) + and (defined $product_ref->{nutriments}{"docosahexaenoic-acid_100g"}) + and ( + ( + $product_ref->{nutriments}{"eicosapentaenoic-acid_100g"} + + $product_ref->{nutriments}{"docosahexaenoic-acid_100g"} + ) < 0.08 + ) + ) + ) + ) + { + add_tag($product_ref, "data_quality_warnings", + "en:high-in-omega-3-label-claim-but-ala-or-sum-of-epa-and-dha-below-limitation"); + } + } return; } diff --git a/lib/ProductOpener/Units.pm b/lib/ProductOpener/Units.pm index b58d41cef01d7..ee3d042e2fe79 100644 --- a/lib/ProductOpener/Units.pm +++ b/lib/ProductOpener/Units.pm @@ -47,6 +47,7 @@ BEGIN { &normalize_serving_size &normalize_quantity + &extract_standard_unit ); # symbols to export on request %EXPORT_TAGS = (all => [@EXPORT_OK]); @@ -222,21 +223,24 @@ sub mmoll_to_unit ($value, $unit) { return g_to_unit($value, $unit); } -=head2 normalize_quantity($quantity) +=head2 parse_quantity_unit($quantity) -Returns the size in g or ml for the whole product. Eg.: -normalize_quantity(1 barquette de 40g) returns 40 -normalize_quantity(20 tranches 500g) returns 500 -normalize_quantity(6x90g) returns 540 -normalize_quantity(2kg) returns 2000 +Returns the quantity ($q), the multiplicator ($m, optional) and the unit ($u) +that may be found in the quantity field entered by contributors -Returns undef if no quantity was detected. +parse_quantity_unit(1 barquette de 40g) returns (40, 1, g) +parse_quantity_unit(20 tranches 500g) returns (500, 20, g) +parse_quantity_unit(6x90g) returns (90, 6, g) +parse_quantity_unit(2kg) returns (2, undef, kg) + +Returns (undef, undef, undef) if no quantity was detected. =cut -sub normalize_quantity ($quantity) { +sub parse_quantity_unit ($quantity, $standard_unit_bool = undef) { my $q = undef; + my $m = undef; my $u = undef; # 12 pots x125 g @@ -248,20 +252,64 @@ sub normalize_quantity ($quantity) { =~ /(?\d+)(\s(\p{Letter}| )+)?(\s)?( de | of |x|\*)(\s)?(?$number_regexp)(\s)?(?$units_regexp)\b/i ) { - my $m = $+{number}; + $m = $+{number}; $q = lc($+{quantity}); $u = $+{unit}; - $q = convert_string_to_number($q); - $q = unit_to_g($q * $m, $u); } elsif ($quantity =~ /(?$number_regexp)(\s)?(?$units_regexp)\s*\b/i) { $q = lc($+{quantity}); $u = $+{unit}; - $q = convert_string_to_number($q); - $q = unit_to_g($q, $u); } - return $q; + return ($q, $m, $u); +} + +=head2 normalize_quantity($quantity) + +Returns the size in g or ml for the whole product. Eg.: +normalize_quantity(1 barquette de 40g) returns 40 +normalize_quantity(20 tranches 500g) returns 500 +normalize_quantity(6x90g) returns 540 +normalize_quantity(2kg) returns 2000 + +Returns undef if no quantity was detected. + +=cut + +sub normalize_quantity ($quantity_field) { + my ($quantity, $multiplier, $unit) = parse_quantity_unit($quantity_field); + + $quantity = convert_string_to_number($quantity); + + if (defined $multiplier) { + $quantity = unit_to_g($quantity * $multiplier, $unit); + } + else { + $quantity = unit_to_g($quantity, $unit); + } + return $quantity; +} + +=head2 extract_standard_unit($quantity) + +Returns the standard_unit corresponding to the extracted unit + +extract_standard_unit(1 barquette de 40g, 1) returns g +extract_standard_unit(2kg) returns g +extract_standard_unit(33cl) returns ml + +Returns undef if no unit was detected. + +=cut + +sub extract_standard_unit ($quantity_field) { + my (undef, undef, $unit) = parse_quantity_unit($quantity_field); + + # search in the map of all synonyms in all languages ($units_names) + $unit = lc($unit); + my $unit_id = $units_names{$unit}; # $unit_id can be undefined + + return $units{$unit_id}{standard_unit}; # standard_unit can be undefined } =head2 normalize_serving_size($serving) diff --git a/stop_words.txt b/stop_words.txt index d2e4d0f99f357..b51d348b45698 100644 --- a/stop_words.txt +++ b/stop_words.txt @@ -152,6 +152,7 @@ Mousquetaires msgctxt multiline Multiline +multiplicator NaN naturel nd diff --git a/taxonomies/data_quality.txt b/taxonomies/data_quality.txt index cb0c724bdc891..a874f220eef26 100644 --- a/taxonomies/data_quality.txt +++ b/taxonomies/data_quality.txt @@ -2918,3 +2918,232 @@ description:en:This product has a vegan label, however some ingredients are not warnings -$product_ref = { - labels_tags => ["en:vegetarian", "en:vegan",], - ingredients => [ - { - id => "en:lentils", - vegan => "yes", - vegetarian => "yes" - }, - { - id => "en:green-bell-pepper", - vegan => "yes", - vegetarian => "yes" - }, - { - id => "en:totoro", - } - ], -}; -ProductOpener::DataQuality::check_quality($product_ref); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegan-label-but-non-vegan-ingredient', - 'raise error only when vegan is no and label is vegan', 0 -); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegetarian-label-but-non-vegetarian-ingredient', - 'raise error only when vegetarian is no and label is vegan', 0 -); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegan-label-but-could-not-confirm-for-all-ingredients', - 'raise warning because vegan or non-vegan is unknown for an ingredient', 1 -); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegetarian-label-but-could-not-confirm-for-all-ingredients', - 'raise warning because vegetarian or non-vegetarian is unknown for an ingredient', 1 -); -# non-vegan/non-vegetarian ingredient -> error -$product_ref = { - labels_tags => ["en:vegetarian", "en:vegan",], - ingredients => [ - { - id => "en:lentils", - vegan => "yes", - vegetarian => "yes" - }, - { - id => "en:green-bell-pepper", - vegan => "yes", - vegetarian => "yes" - }, - { - id => "en:chicken", - vegan => "no", - vegetarian => "no" - } - ], -}; -ProductOpener::DataQuality::check_quality($product_ref); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegan-label-but-non-vegan-ingredient', - 'raise error only when vegan is no and label is vegan', 1 -); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegetarian-label-but-non-vegetarian-ingredient', - 'raise error only when vegetarian is no and label is vegan', 1 -); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegan-label-but-could-not-confirm-for-all-ingredients', - 'raise warning because vegan or non-vegan is unknown for an ingredient', 0 -); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegetarian-label-but-could-not-confirm-for-all-ingredients', - 'raise warning because vegetarian or non-vegetarian is unknown for an ingredient', 0 -); -# non-vegan/vegatarian ingredient -> error -$product_ref = { - labels_tags => ["en:vegetarian", "en:vegan",], - ingredients => [ - { - id => "en:lentils", - vegan => "yes", - vegetarian => "yes" - }, - { - id => "en:green-bell-pepper", - vegan => "yes", - vegetarian => "yes" - }, - { - id => "en:honey", - vegan => "no", - vegetarian => "yes" - } - ], -}; -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegan-label-but-non-vegan-ingredient', - 'raise error only when vegan is no and label is vegan', 1 -); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegetarian-label-but-non-vegetarian-ingredient', - 'raise error only when vegetarian is no and label is vegan', 0 -); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegan-label-but-could-not-confirm-for-all-ingredients', - 'raise warning because vegan or non-vegan is unknown for an ingredient', 0 -); -check_quality_and_test_product_has_quality_tag( - $product_ref, - 'en:vegetarian-label-but-could-not-confirm-for-all-ingredients', - 'raise warning because vegetarian or non-vegetarian is unknown for an ingredient', 0 -); # product quantity warnings and errors $product_ref = {product_quantity => "123456789",}; check_quality_and_test_product_has_quality_tag( diff --git a/tests/unit/dataqualityfood_labels.t b/tests/unit/dataqualityfood_labels.t new file mode 100644 index 0000000000000..8c6e4ffdcc540 --- /dev/null +++ b/tests/unit/dataqualityfood_labels.t @@ -0,0 +1,762 @@ +#!/usr/bin/perl -w + +use Modern::Perl '2017'; +use utf8; + +use Test::More; + +use ProductOpener::DataQuality qw/:all/; +use ProductOpener::DataQualityFood qw/:all/; +use ProductOpener::Tags qw/:all/; +use ProductOpener::Ingredients qw/:all/; + +sub check_quality_and_test_product_has_quality_tag($$$$) { + my $product_ref = shift; + my $tag = shift; + my $reason = shift; + my $yesno = shift; + ProductOpener::DataQuality::check_quality($product_ref); + if ($yesno) { + ok(has_tag($product_ref, 'data_quality', $tag), $reason) + or diag explain {tag => $tag, yesno => $yesno, product => $product_ref}; + } + else { + ok(!has_tag($product_ref, 'data_quality', $tag), $reason) + or diag explain {tag => $tag, yesno => $yesno, product => $product_ref}; + } + + return; +} + +# vegan label but non-vegan ingredients +# unknown ingredient -> warnings +my $product_ref = { + labels_tags => ["en:vegetarian", "en:vegan",], + ingredients => [ + { + id => "en:lentils", + vegan => "yes", + vegetarian => "yes" + }, + { + id => "en:green-bell-pepper", + vegan => "yes", + vegetarian => "yes" + }, + { + id => "en:totoro", + } + ], +}; +ProductOpener::DataQuality::check_quality($product_ref); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegan-label-but-non-vegan-ingredient', + 'raise error only when vegan is no and label is vegan', 0 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegetarian-label-but-non-vegetarian-ingredient', + 'raise error only when vegetarian is no and label is vegan', 0 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegan-label-but-could-not-confirm-for-all-ingredients', + 'raise warning because vegan or non-vegan is unknown for an ingredient', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegetarian-label-but-could-not-confirm-for-all-ingredients', + 'raise warning because vegetarian or non-vegetarian is unknown for an ingredient', 1 +); +# non-vegan/non-vegetarian ingredient -> error +$product_ref = { + labels_tags => ["en:vegetarian", "en:vegan",], + ingredients => [ + { + id => "en:lentils", + vegan => "yes", + vegetarian => "yes" + }, + { + id => "en:green-bell-pepper", + vegan => "yes", + vegetarian => "yes" + }, + { + id => "en:chicken", + vegan => "no", + vegetarian => "no" + } + ], +}; +ProductOpener::DataQuality::check_quality($product_ref); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegan-label-but-non-vegan-ingredient', + 'raise error only when vegan is no and label is vegan', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegetarian-label-but-non-vegetarian-ingredient', + 'raise error only when vegetarian is no and label is vegan', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegan-label-but-could-not-confirm-for-all-ingredients', + 'raise warning because vegan or non-vegan is unknown for an ingredient', 0 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegetarian-label-but-could-not-confirm-for-all-ingredients', + 'raise warning because vegetarian or non-vegetarian is unknown for an ingredient', 0 +); +# non-vegan/vegatarian ingredient -> error +$product_ref = { + labels_tags => ["en:vegetarian", "en:vegan",], + ingredients => [ + { + id => "en:lentils", + vegan => "yes", + vegetarian => "yes" + }, + { + id => "en:green-bell-pepper", + vegan => "yes", + vegetarian => "yes" + }, + { + id => "en:honey", + vegan => "no", + vegetarian => "yes" + } + ], +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegan-label-but-non-vegan-ingredient', + 'raise error only when vegan is no and label is vegan', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegetarian-label-but-non-vegetarian-ingredient', + 'raise error only when vegetarian is no and label is vegan', 0 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegan-label-but-could-not-confirm-for-all-ingredients', + 'raise warning because vegan or non-vegan is unknown for an ingredient', 0 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vegetarian-label-but-could-not-confirm-for-all-ingredients', + 'raise warning because vegetarian or non-vegetarian is unknown for an ingredient', 0 +); + +# labels claim vs input nutrition data, based on EU regulation: https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A02006R1924-20141213 +# TODO TESTS WITH CATEGORIES AND ALL LABELS IN CASE TAXO CHANGE OVER TIME, SO THAT IT IS DETECTED WHEN RUNNING TESTS +# product quantity warnings and errors +# all labels and alerts maximum (minimum) when there are 2 levels and it should be below (above) an expected limitation +# all positives + +# non-EU countries, should not raise facet +$product_ref = { + countries_tags => ["en:united-states",], + labels_tags => ["en:low-energy",], + nutriments => { + "energy-kcal_100g" => 100, # is above limitation + "energy-kj_100g" => 420, # is not above limitation + }, + quantity => "500 mg", + }, + check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-energy-label-claim-but-energy-above-limitation', + 'non-EU countries, should not raise facet', + 0 + ); + +# solid +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => [ + "en:low-energy", "en:energy-free", + "en:low-fat", "en:no-fat", + "en:high-monounsaturated-fat", "en:rich-in-polyunsaturated-fatty-acids", + "en:rich-in-unsaturated-fatty-acids", "en:low-content-of-saturated-fat", + "en:saturated-fat-free", "en:low-sugar", + "en:no-sugar", "en:low-sodium", + "en:low-salt", "en:very-low-sodium", + "en:very-low-salt", "en:no-sodium", + "en:no-salt", "en:no-added-sodium", + "en:no-added-salt", "en:source-of-fibre", + "en:high-fibres", "en:vitamin-a-source", + "en:rich-in-vitamin-a", "en:vitamin-d-source", + "en:rich-in-vitamin-d", "en:vitamin-e-source", + "en:rich-in-vitamin-e", "en:vitamin-c-source", + "en:rich-in-vitamin-c", "en:vitamin-b1-source", + "en:rich-in-vitamin-b1", "en:vitamin-b2-source", + "en:rich-in-vitamin-b2", "en:vitamin-b3-source", + "en:rich-in-vitamin-b3", "en:vitamin-b6-source", + "en:rich-in-vitamin-b6", "en:vitamin-b9-source", + "en:rich-in-vitamin-b9", "en:vitamin-b12-source", + "en:rich-in-vitamin-b12", "en:source-of-biotin", + "en:high-in-biotin", "en:source-of-pantothenic-acid", + "en:high-in-pantothenic-acid", "en:calcium-source", + "en:high-in-calcium", "en:phosphore-source", + "en:high-in-phosphore", "en:iron-source", + "en:high-in-iron", "en:magnesium-source", + "en:high-in-magnesium", "en:zinc-source", + "en:high-in-zinc", "en:iodine-source", + "en:high-in-iodine", "en:source-of-omega-3", + "en:high-in-omega-3", + ], + nutriments => { + "energy-kcal_100g" => 100, # is above 40 and 4 + "energy-kj_100g" => 420, # is above 170 and 17 + fat_100g => 4, # is above 3 and 0.5 + "monounsaturated-fat_100g" => 1, # less than 45% of the fat + "polyunsaturated-fat_100g" => 1, # less than 45% of the fat + "unsaturated-fat_100g" => 1, # less than 45% of the fat + "saturated-fat_100g" => 2, # above 1.5 + sugars_100g => 6, # above 5 and 0.5 + sodium_100g => 0.4, # above 0.12 and 0.04 and 0.005 + salt_100g => 1, # above 0.3 and 0.1 and 0.0125 + fiber_100g => 2, # below 3 and 6 + "vitamin-a_100g" => 0.0007, # below 0.0008 and 0.0016 + "vitamin-d_100g" => 0.000004, # below 0.000005 and 0.00001 + "vitamin-e_100g" => 0.009, # below 0.01 and 0.02 + "vitamin-c_100g" => 0.05, # below 0.06 and 0.12 + "vitamin-b1_100g" => 0.0013, # below 0.0014 and 0.0028 + "vitamin-b2_100g" => 0.0015, # below 0.0016 and 0.0032 + "vitamin-b3_100g" => 0.017, # below 0.018 and 0.036 + "vitamin-b6_100g" => 0.001, # below 0.002 and 0.004 + "vitamin-b9_100g" => 0.0001, # below 0.0002 and 0.0004 + "vitamin-b12_100g" => 0.0000009, # below 0.000001 and 0.000002 + "biotin_100g" => 0.00014, # below 0.00015 and 0.0003 + "pantothenic-acid_100g" => 0.005, # below 0.006 and 0.012 + "calcium_100g" => 0.7, # below 0.8 and 1.6 + "phosphorus_100g" => 0.7, # below 0.8 and 1.6 + "iron_100g" => 0.013, # below 0.014 and 0.028 + "magnesium_100g" => 0.2, # below 0.3 and 0.6 + "zinc_100g" => 0.014, # below 0.015 and 0.03 + "iodine_100g" => 0.00014, # below 0.00015 and 0.0003 + "alpha-linolenic-acid_100g" => 0.2 # below 0.3 and 0.6 + }, + quantity => "500 mg", +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-energy-label-claim-but-energy-above-limitation', + 'above limitation for low energy label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:energy-free-label-claim-but-energy-above-limitation', + 'above limitation for energy free label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-fat-label-claim-but-fat-above-limitation', + 'above limitation for low fat label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:no-fat-label-claim-but-fat-above-0.5', + 'above limitation for no fat label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-monounsaturated-fat-label-claim-but-monounsaturated-fat-under-limitation', + 'under limitation for high-monounsaturated fat label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-polyunsaturated-fatty-acids-label-claim-but-polyunsaturated-fat-under-limitation', + 'under limitation for high-polyunsaturated fat label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-unsaturated-fatty-acids-label-claim-but-unsaturated-fat-under-limitation', + 'under limitation for unsaturated fat label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-saturated-fat-label-claim-but-fat-above-limitation', + 'under limitation for low saturated fat label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:saturated-fat-free-label-claim-but-fat-above-0.1', + 'above limitation for saturated fat free label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-sugar-label-claim-but-sugar-above-limitation', + 'above limitation for low sugar label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:sugar-free-label-claim-but-sugar-above-limitation', + 'above limitation for no sugar label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-sodium-or-low-salt-label-claim-but-sodium-or-salt-above-limitation', + 'above limitation for low sodium label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-sodium-or-low-salt-label-claim-but-sodium-or-salt-above-limitation', + 'above limitation for low salt label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:very-low-sodium-or-very-low-salt-label-claim-but-sodium-or-salt-above-limitation', + 'above limitation for very low sodium label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:very-low-sodium-or-very-low-salt-label-claim-but-sodium-or-salt-above-limitation', + 'above limitation for very low salt label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:sodium-free-or-salt-free-label-claim-but-sodium-or-salt-above-limitation', + 'above limitation for no sodium label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:sodium-free-or-salt-free-label-claim-but-sodium-or-salt-above-limitation', + 'above limitation for no salt label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:no-added-sodium-or-no-added-salt-label-claim-but-sodium-or-salt-above-limitation', + 'above limitation for no added salt label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:source-of-fibre-label-claim-but-fibre-below-limitation', + 'under limitation for source of fibres label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-fibres-label-claim-but-fibre-below-limitation', + 'under limitation for high-fibres label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-a-source-label-claim-but-vitamin-a-below-0.0008', + 'under limitation for vitamin-a source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-a-label-claim-but-vitamin-a-below-0.0016', + 'under limitation for rich in vitamin-a label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-d-source-label-claim-but-vitamin-d-below-5e-06', + 'under limitation for vitamin-d source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-d-label-claim-but-vitamin-d-below-1e-05', + 'under limitation for rich in vitamin-d label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-e-source-label-claim-but-vitamin-e-below-0.01', + 'under limitation for vitamin-e source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-e-label-claim-but-vitamin-e-below-0.02', + 'under limitation for rich in vitamin-e label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-c-source-label-claim-but-vitamin-c-below-0.06', + 'under limitation for vitamin-c source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-c-label-claim-but-vitamin-c-below-0.12', + 'under limitation for rich in vitamin-c label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-b1-source-label-claim-but-vitamin-b1-below-0.0014', + 'under limitation for vitamin-b1 source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-b1-label-claim-but-vitamin-b1-below-0.0028', + 'under limitation for rich in vitamin-b1 label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-b2-source-label-claim-but-vitamin-b2-below-0.0016', + 'under limitation for vitamin-b3 source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-b2-label-claim-but-vitamin-b2-below-0.0032', + 'under limitation for rich in vitamin-b3 label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-b3-source-label-claim-but-vitamin-b3-below-0.018', + 'under limitation for vitamin-b3 source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-b3-label-claim-but-vitamin-b3-below-0.036', + 'under limitation for rich in vitamin-b3 label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-b6-source-label-claim-but-vitamin-b6-below-0.002', + 'under limitation for vitamin-b6 source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-b6-label-claim-but-vitamin-b6-below-0.004', + 'under limitation for rich in vitamin-b6 label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-b9-source-label-claim-but-vitamin-b9-below-0.0002', + 'under limitation for vitamin-b9 source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-b9-label-claim-but-vitamin-b9-below-0.0004', + 'under limitation for rich in vitamin-b9 label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:vitamin-b12-source-label-claim-but-vitamin-b12-below-1e-06', + 'under limitation for vitamin-b12 source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-vitamin-b12-label-claim-but-vitamin-b12-below-2e-06', + 'under limitation for rich in vitamin-b12 label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:source-of-biotin-label-claim-but-biotin-below-0.00015', + 'under limitation for biotin source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-biotin-label-claim-but-biotin-below-0.0003', + 'under limitation for high in biotin label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:source-of-pantothenic-acid-label-claim-but-pantothenic-acid-below-0.006', + 'under limitation for pantothenic-acid source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-pantothenic-acid-label-claim-but-pantothenic-acid-below-0.012', + 'under limitation for high in pantothenic-acid label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:calcium-source-label-claim-but-calcium-below-0.8', + 'under limitation for calcium source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-calcium-label-claim-but-calcium-below-1.6', + 'under limitation for high in calcium label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:phosphore-source-label-claim-but-phosphorus-below-0.8', + 'under limitation for phosphore source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-phosphore-label-claim-but-phosphorus-below-1.6', + 'under limitation for high in phosphore label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:iron-source-label-claim-but-iron-below-0.014', + 'under limitation for iron source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-iron-label-claim-but-iron-below-0.028', + 'under limitation for high in iron label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:magnesium-source-label-claim-but-magnesium-below-0.3', + 'under limitation for magnesium source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-magnesium-label-claim-but-magnesium-below-0.6', + 'under limitation for high in magnesium label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:zinc-source-label-claim-but-zinc-below-0.015', + 'under limitation for zinc source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-zinc-label-claim-but-zinc-below-0.03', + 'under limitation for high in zinc label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:iodine-source-label-claim-but-iodine-below-0.00015', + 'under limitation for iodine source label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-iodine-label-claim-but-iodine-below-0.0003', + 'under limitation for high in iodine label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:source-of-omega-3-label-claim-but-ala-or-sum-of-epa-and-dha-below-limitation', + 'under limitation for source of omega-3 label', 1 +); + +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-omega-3-label-claim-but-ala-or-sum-of-epa-and-dha-below-limitation', + 'under limitation for high in omega-3 label', 1 +); + +## en:high-monounsaturated-fat-label-claim-but-monounsaturated-fat-under-limitation +## due to monounsaturated providing less than 20% of total energy +## same for poly- (20% of total energy) +## same for saturated (10% of total energy) +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => [ + "en:high-monounsaturated-fat", "en:rich-in-polyunsaturated-fatty-acids", + "en:rich-in-unsaturated-fatty-acids", "en:low-content-of-saturated-fat", + ], + nutriments => { + "energy-kcal_value" => 200, # needed to calculate computed energy values + "energy-kj_value" => 840, # needed to calculate computed energy values + fat_100g => 4, + fat_value => 4, # needed to calculate computed energy values + "monounsaturated-fat_100g" => 3, # more than 45% of the fat + "polyunsaturated-fat_100g" => 3, # more than 45% of the fat + "unsaturated-fat_100g" => 3, # more than 45% of the fat + "saturated-fat_100g" => 3, # more than 45% of the fat + "carbohydrates_value" => 10, # needed to calculate computed energy values + "proteins_value" => 40, # needed to calculate computed energy values + # "energy-kcal_value_computed" => 276, # for information only + # "energy-kj_value_computed" => 1168, # for information only + }, + quantity => "500 mg", +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-monounsaturated-fat-label-claim-but-monounsaturated-fat-under-limitation', + 'under limitation of total energy for high-monounsaturated fat label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-polyunsaturated-fatty-acids-label-claim-but-polyunsaturated-fat-under-limitation', + 'under limitation of total energy for high-polyunsaturated fat label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:rich-in-unsaturated-fatty-acids-label-claim-but-unsaturated-fat-under-limitation', + 'under limitation of total energy for unsaturated fat label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-saturated-fat-label-claim-but-fat-above-limitation', + 'under limitation of total energy for saturated fat label', 1 +); +## same as previous for fibres (1.5% and 3% of total energy)) +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => ["en:source-of-fibre", "en:high-fibres",], + nutriments => { + "energy-kcal_value" => 400, # needed to calculate computed energy values + "energy-kj_value" => 1800, # needed to calculate computed energy values + fat_value => 60, # needed to calculate computed energy values + "carbohydrates_value" => 60, # needed to calculate computed energy values + "proteins_value" => 60, # needed to calculate computed energy values + # "energy-kcal_value_computed" => 1020, # for information only + # "energy-kj_value_computed" => 4260, # for information only + fiber_100g => 7, # above 6 + }, + quantity => "500 mg", +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:source-of-fibre-label-claim-but-fibre-below-limitation', + 'under limitation for source of fibres label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-fibres-label-claim-but-fibre-below-limitation', + 'under limitation for high-fibres label', 1 +); +## same as previous for proteins (12% and 20% of total energy) +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => ["en:source-of-proteins", "en:high-proteins",], + nutriments => { + "energy-kcal_value" => 400, # needed to calculate computed energy values + "energy-kj_value" => 1800, # needed to calculate computed energy values + fat_value => 60, # needed to calculate computed energy values + "carbohydrates_value" => 60, # needed to calculate computed energy values + "proteins_value" => 21, # needed to calculate computed energy values + # "energy-kcal_value_computed" => 864, # for information only + # "energy-kj_value_computed" => 3597, # for information only + proteins_100g => 21, # above 20 and 12 + }, + quantity => "500 mg", +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:source-of-proteins-label-claim-but-proteins-below-limitation', + 'under limitation for source of proteins label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-proteins-label-claim-but-proteins-below-limitation', + 'under limitation for high-proteins label', 1 +); + +## no added sugar but added sugar ingredients +## same for salt, sodium +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => ["en:no-added-sugar", "en:no-added-sodium", "en:no-added-salt",], + quantity => "500 mg", + ingredients_tags => ["en:cane-sugar", "en:added-sugar", "en:disaccharide", "en:salt",], +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:no-added-sugar-label-claim-but-contains-added-sugar', + 'added sugar detected beside label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:no-added-sodium-or-no-added-salt-label-claim-but-sodium-or-salt-above-limitation', + 'added sodium detected beside label', 1 +); +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:no-added-sodium-or-no-added-salt-label-claim-but-sodium-or-salt-above-limitation', + 'added salt detected beside label', 1 +); + +## omega-3, trigger with sum of epa and dha instead of ala +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => ["en:source-of-omega-3", "en:high-in-omega-3",], + nutriments => { + "eicosapentaenoic-acid_100g" => 0.01, + "docosahexaenoic-acid_100g" => 0.01, # sum is below 0.04 and 0.08 + }, + quantity => "500 mg", +}; + +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:source-of-omega-3-label-claim-but-ala-or-sum-of-epa-and-dha-below-limitation', + 'under limitation for source of omega-3 label', 1 +); + +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:high-in-omega-3-label-claim-but-ala-or-sum-of-epa-and-dha-below-limitation', + 'under limitation for high in omega-3 label', 1 +); + +# liquid +## low-energy liquid +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => ["en:low-energy",], + nutriments => { + "energy-kcal_100g" => 21, # is above 20 + "energy-kj_100g" => 81, # is above 80 + }, + quantity => "1L", +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-energy-label-claim-but-energy-above-limitation', + 'above limitation for low energy label', 1 +); + +## low-fat + semi-skimmed-milk +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => ["en:low-fat",], + nutriments => { + fat_100g => 1.6, # is above 1.5 (default limit) but below 1.8 (limit for skimmed-milk) + }, + quantity => "1L", + categories_tags => ["en:semi-skimmed-milks"], +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-fat-label-claim-but-fat-above-limitation', + 'below limitation for low fat label for skimmed-milk', 0 +); + +## low saturated fat liquid +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => ["en:low-content-of-saturated-fat",], + nutriments => { + "saturated-fat_100g" => 0.8, # above 0.75 + }, + quantity => "1L", +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-saturated-fat-label-claim-but-fat-above-limitation', + 'under limitation for low saturated fat label', 1 +); + +## low sugar liquid +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => ["en:low-sugar",], + nutriments => { + sugars_100g => 3, # above 2.5 + }, + quantity => "1L", +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:low-sugar-label-claim-but-sugar-above-limitation', + 'above limitation for low sugar label', 1 +); + +## This claim shall not be used for natural mineral waters and other waters. +$product_ref = { + countries_tags => ["en:croatia",], + labels_tags => ["en:very-low-sodium", "en:very-low-salt",], + quantity => "1,5 L", + categories_tags => ["en:waters"], +}; +check_quality_and_test_product_has_quality_tag( + $product_ref, + 'en:very-low-sodium-or-very-low-salt-label-claim-but-sodium-or-salt-above-limitation', + 'label should not be triggered for waters category', 0 +); + +done_testing();