nutriscore 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +126 -0
- data/lib/nutriscore.rb +3 -0
- data/lib/nutriscore/common/nutrients.rb +26 -0
- data/lib/nutriscore/common/range.rb +87 -0
- data/lib/nutriscore/common/score.rb +42 -0
- data/lib/nutriscore/common/score_class_range.rb +34 -0
- data/lib/nutriscore/fr.rb +5 -0
- data/lib/nutriscore/fr/cheese_score.rb +8 -0
- data/lib/nutriscore/fr/concerns/drinks_score_class.rb +28 -0
- data/lib/nutriscore/fr/concerns/general_score_class.rb +28 -0
- data/lib/nutriscore/fr/concerns/mineral_water_score_class.rb +11 -0
- data/lib/nutriscore/fr/drinks_score.rb +61 -0
- data/lib/nutriscore/fr/fats_score.rb +44 -0
- data/lib/nutriscore/fr/general_score.rb +33 -0
- data/lib/nutriscore/fr/mineral_water_score.rb +10 -0
- data/lib/nutriscore/fr/negative_score.rb +80 -0
- data/lib/nutriscore/fr/positive_score.rb +46 -0
- data/lib/nutriscore/fr/specific_score.rb +22 -0
- data/lib/nutriscore/uk.rb +2 -0
- data/lib/nutriscore/uk/concerns/drinks_less_healthy.rb +16 -0
- data/lib/nutriscore/uk/concerns/general_less_healthy.rb +16 -0
- data/lib/nutriscore/uk/drinks_score.rb +11 -0
- data/lib/nutriscore/uk/general_score.rb +33 -0
- data/lib/nutriscore/uk/negative_score.rb +82 -0
- data/lib/nutriscore/uk/positive_score.rb +66 -0
- data/lib/nutriscore/uk/specific_score.rb +24 -0
- data/lib/nutriscore/version.rb +4 -0
- data/nutriscore.gemspec +25 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 32fc807c0ac252fb44029dd894b01c6e1a73cdb018818683265bcfbaf28e6414
|
4
|
+
data.tar.gz: e52bbc975c607d50c68f308ccb0c374841a8cfc209b2e176b88c2b1ae22cf97c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 60a6712c438ea99a6ea46560327fa25f8519bc80aefb8e65a3eaf272568f53a4ce08219731656b02f8eada7ed28bc48385173107ed767221d9002334b3d2cda3
|
7
|
+
data.tar.gz: 36571b3a8c1930043e961ef776f5bbdfa958da5f2759cd029ab555ac5354dbbbc9e2251aa11242a9006cc1b27efa401759764bdbd0f8e629529700c346f2c392
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Questionmark
|
4
|
+
Copyright (c) 2019 wvengen
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
14
|
+
copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# Nutriscore
|
2
|
+
|
3
|
+
Consumer food products in some countries have a nutritional score, indicating
|
4
|
+
how healthy the product is to eat. Each country has its own approach, but in
|
5
|
+
the European Union the _Nutri-Score_ in several countries.
|
6
|
+
|
7
|
+
This gem implements the Nutri-Score for Ruby and includes adaptations for different
|
8
|
+
countries. The maintainer would be open to integrating other nutritional scores as well.
|
9
|
+
|
10
|
+
_**Note:** this is currently under development, the API may change without notice,
|
11
|
+
and scoring has not yet been fully verified. Be very careful using this in production._
|
12
|
+
|
13
|
+
## Nutri-Score
|
14
|
+
|
15
|
+
There are currently two versions of the Nutri-Score in use. The first was
|
16
|
+
[developed](https://www.food.gov.uk/business-guidance/nutrient-profiling-model-for-children)
|
17
|
+
in the UK by the Food Standards Agency and
|
18
|
+
[currently maintainted](https://www.gov.uk/government/publications/the-nutrient-profiling-model)
|
19
|
+
by the Department of Health. It is used for regulating food advertisements.
|
20
|
+
|
21
|
+
In France, an adapted
|
22
|
+
[Nutri-Score](https://www.santepubliquefrance.fr/Sante-publique-France/Nutri-Score)
|
23
|
+
was adopted in 2017 for use as a voluntary label on the packaging.
|
24
|
+
In 2018, Belgium and Spain adopted the same scheme.
|
25
|
+
|
26
|
+
Other countries are evaluating adopting the Nutri-Score as well, including
|
27
|
+
The Netherlands. There is also a
|
28
|
+
[European citizen's initiative](http://ec.europa.eu/citizens-initiative/public/initiatives/ongoing/details/2019/000008)
|
29
|
+
to adopt it Europe-wide.
|
30
|
+
|
31
|
+
## Installation
|
32
|
+
|
33
|
+
```
|
34
|
+
gem install nutriscore
|
35
|
+
```
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
The input for all nutritional scores are a product category and nutritional values.
|
40
|
+
Which nutrients are required depends on the product category.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require 'nutriscore'
|
44
|
+
|
45
|
+
# Fruit fromage frais
|
46
|
+
product_a = {
|
47
|
+
energy: 459, # kJ/100g
|
48
|
+
fat_saturated: 1.8, # g/100g
|
49
|
+
sugar: 13.4, # g/100g
|
50
|
+
sodium: 0.1 / 1000, # g/100g
|
51
|
+
fruits_vegetables_nuts: 8, # g/100g (= weight-%)
|
52
|
+
fibres: 0.6, # g/100g
|
53
|
+
proteins: 6.5, # g/100g
|
54
|
+
}
|
55
|
+
|
56
|
+
# Compute the french Nutri-Score for a generic product.
|
57
|
+
score = Nutriscore::FR::SpecificScore.new(product_a)
|
58
|
+
#<Nutriscore::FR::SpecificScore score=0
|
59
|
+
# positive_score=#<Nutriscore::FR::PositiveScore score=4
|
60
|
+
# fruits_vegetables_nuts=0 fibres=0 proteins=4>
|
61
|
+
# negative_score=#<Nutriscore::FR::NegativeScore score=4
|
62
|
+
# energy=1 fat_saturated=1 sugar=2 sodium=0>>
|
63
|
+
score.score.single
|
64
|
+
# => 0
|
65
|
+
score.score_class.single
|
66
|
+
# => "B"
|
67
|
+
```
|
68
|
+
|
69
|
+
To be able to work with incomplete information, results are returned as ranges.
|
70
|
+
The use of `.single` in the above example converts these to a single value (it
|
71
|
+
returns `nil` if there is not enough information to get a single result). The
|
72
|
+
following example shows what happens when data is missing.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
score = Nutriscore::FR::SpecificScore.new(product_a.merge({ sodium: nil }))
|
76
|
+
score.score
|
77
|
+
# => 0..10
|
78
|
+
score.score.single
|
79
|
+
# => nil
|
80
|
+
score.score_class
|
81
|
+
# => "B".."C"
|
82
|
+
score.score_class.single
|
83
|
+
# => nil
|
84
|
+
```
|
85
|
+
|
86
|
+
Please only use `#single` and the regular Ruby `Range` methods on `#score` and `#score_class`.
|
87
|
+
Other methods do exist, but are not guaranteed to be stable across releases.
|
88
|
+
|
89
|
+
Different categories can use different score classes:
|
90
|
+
* `Nutriscore::FR::CheeseScore` for cheese
|
91
|
+
* `Nutriscore::FR::FatsScore` for vegetable and animal oils and fats
|
92
|
+
* `Nutriscore::FR::MineralWaterScore` for mineral water
|
93
|
+
* `Nutriscore::FR::DrinksScore` for other drinks
|
94
|
+
* `Nutriscore::FR::SpecificScore` for other food products
|
95
|
+
|
96
|
+
## UK
|
97
|
+
|
98
|
+
The UK has the same basis for computation, but it is used to determine
|
99
|
+
whether a product can be advertised (it must not be less healthy).
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
score = Nutriscore::UK::SpecificScore.new(product_a)
|
103
|
+
score.score
|
104
|
+
# => 0
|
105
|
+
score.less_healthy?
|
106
|
+
# => false
|
107
|
+
```
|
108
|
+
|
109
|
+
The method `#less_healthy?` is UK-specific, and returns `true`, `false`, or `nil`
|
110
|
+
if there is not enough information to make a judgement.
|
111
|
+
|
112
|
+
By default, the fibres measurement method is AOAC (which is preferred), but
|
113
|
+
it is possible to use fibres values measured with the NSP method:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
# Acceptable values for the fibres_method are: :aoac and :nsp.
|
117
|
+
score = Nutriscore::UK::SpecificScore.new(product_a, fibres_method: :nsp)
|
118
|
+
```
|
119
|
+
|
120
|
+
Different categories can use different score classes:
|
121
|
+
* `Nutriscore::EN::SpecificScore` for food products
|
122
|
+
* `Nutriscore::EN::DrinksScore` for drinks
|
123
|
+
|
124
|
+
## License
|
125
|
+
|
126
|
+
This software is distributed under the [MIT license](LICENSE).
|
data/lib/nutriscore.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'range'
|
2
|
+
|
3
|
+
module Nutriscore
|
4
|
+
module Common
|
5
|
+
class Nutrients
|
6
|
+
|
7
|
+
def self.wrap(o)
|
8
|
+
if o.is_a?(Nutrients)
|
9
|
+
o
|
10
|
+
else
|
11
|
+
Nutrients.new(o)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(h)
|
16
|
+
@h = h.transform_values {|v| v && Range.wrap(v) }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def method_missing(m)
|
22
|
+
@h[m.to_sym]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Nutriscore
|
2
|
+
module Common
|
3
|
+
# Range class that supports addition, substraction and comparison.
|
4
|
+
# Assumes the objects that the range is composed of is a {{Numeric}}.
|
5
|
+
#
|
6
|
+
# Note that the end range is always included (+exclude_end+ is +false+).
|
7
|
+
class Range < ::Range
|
8
|
+
# Returns a {{Nutriscore::Common::Range}} object from a {{Numeric}} or {{Range}}.
|
9
|
+
def self.wrap(a)
|
10
|
+
if Numeric === a
|
11
|
+
Range.new(a, a)
|
12
|
+
elsif Range === a
|
13
|
+
a
|
14
|
+
elsif ::Range === a
|
15
|
+
Range.new(a.first, a.last)
|
16
|
+
else
|
17
|
+
raise ArgumentError
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def +(a)
|
22
|
+
if Numeric === a
|
23
|
+
Range.new(min + a, max + a)
|
24
|
+
elsif ::Range === a
|
25
|
+
Range.new(min + a.min, max + a.max)
|
26
|
+
else
|
27
|
+
raise ArgumentError
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def -(a)
|
32
|
+
if Numeric === a
|
33
|
+
Range.new(min - a, max - a)
|
34
|
+
elsif ::Range === a
|
35
|
+
Range.new(min - a.max, max - a.min)
|
36
|
+
else
|
37
|
+
raise ArgumentError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def *(a)
|
42
|
+
if Numeric === a
|
43
|
+
Range.new(min * a, max * a)
|
44
|
+
elsif ::Range === a
|
45
|
+
Range.new(min * a.min, max * a.max)
|
46
|
+
else
|
47
|
+
raise ArgumentError
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def /(a)
|
52
|
+
if Numeric === a
|
53
|
+
Range.new(min / a, max / a)
|
54
|
+
elsif ::Range === a
|
55
|
+
Range.new(min / a.max, max / a.min)
|
56
|
+
else
|
57
|
+
raise ArgumentError
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def ==(a)
|
62
|
+
if Numeric === a
|
63
|
+
single == a
|
64
|
+
else
|
65
|
+
super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
if min == max
|
71
|
+
min.to_s
|
72
|
+
else
|
73
|
+
super
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def inspect
|
78
|
+
to_s
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns single value if possible, +nil+ if there is a range of values.
|
82
|
+
def single
|
83
|
+
min if min == max
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'nutrients'
|
2
|
+
require_relative 'range'
|
3
|
+
|
4
|
+
module Nutriscore
|
5
|
+
module Common
|
6
|
+
class Score
|
7
|
+
attr_reader :nutrients
|
8
|
+
|
9
|
+
def self.nutrient_keys
|
10
|
+
[]
|
11
|
+
end
|
12
|
+
|
13
|
+
def score
|
14
|
+
self.class.nutrient_keys.map(&method(:public_send)).reduce(&:+)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(nutrients)
|
18
|
+
@nutrients = Nutrients.wrap(nutrients)
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"#<#{self.class} score=#{score} #{inspect_nutrients}>"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def score_value(value, extremes)
|
28
|
+
if value.nil?
|
29
|
+
Range.wrap(extremes)
|
30
|
+
else
|
31
|
+
Range.new(yield(value.min), yield(value.max))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect_nutrients
|
36
|
+
self.class.nutrient_keys.map do |key|
|
37
|
+
"#{key}=#{public_send(key)}"
|
38
|
+
end.join(" ")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Nutriscore
|
2
|
+
module Common
|
3
|
+
# Range of possible score classes.
|
4
|
+
#
|
5
|
+
# This is a standard Ruby {{::Range}} for {{String}}s with some additional helper
|
6
|
+
# methods, similar to {{Nutriscore::Common::Range}} (without the computation parts).
|
7
|
+
class ScoreClassRange < ::Range
|
8
|
+
def ==(a)
|
9
|
+
if String === a
|
10
|
+
single == a
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
if min == max
|
18
|
+
min.to_s
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns single value if possible, +nil+ if there is a range of values.
|
29
|
+
def single
|
30
|
+
min if min == max
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative '../../common/score_class_range'
|
2
|
+
|
3
|
+
module Nutriscore
|
4
|
+
module FR
|
5
|
+
module DrinksScoreClass
|
6
|
+
def score_class
|
7
|
+
return nil if score.nil?
|
8
|
+
|
9
|
+
Nutriscore::Common::ScoreClassRange.new(
|
10
|
+
score_class_single(score.min),
|
11
|
+
score_class_single(score.max)
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def score_class_single(score)
|
18
|
+
if !score then nil
|
19
|
+
# mineral water has 'A'
|
20
|
+
elsif score < 2 then 'B'
|
21
|
+
elsif score < 6 then 'C'
|
22
|
+
elsif score < 10 then 'D'
|
23
|
+
else 'E'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative '../../common/score_class_range'
|
2
|
+
|
3
|
+
module Nutriscore
|
4
|
+
module FR
|
5
|
+
module GeneralScoreClass
|
6
|
+
def score_class
|
7
|
+
return nil if score.nil?
|
8
|
+
|
9
|
+
Nutriscore::Common::ScoreClassRange.new(
|
10
|
+
score_class_single(score.min),
|
11
|
+
score_class_single(score.max)
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def score_class_single(score)
|
18
|
+
if !score then nil
|
19
|
+
elsif score < 0 then 'A'
|
20
|
+
elsif score < 3 then 'B'
|
21
|
+
elsif score < 11 then 'C'
|
22
|
+
elsif score < 19 then 'D'
|
23
|
+
else 'E'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative '../common/score'
|
2
|
+
require_relative 'concerns/drinks_score_class'
|
3
|
+
|
4
|
+
module Nutriscore
|
5
|
+
module FR
|
6
|
+
# this is for drinks
|
7
|
+
class DrinksScore < Nutriscore::Common::Score
|
8
|
+
include DrinksScoreClass
|
9
|
+
|
10
|
+
def self.nutrient_keys
|
11
|
+
[:energy, :sugar, :fruits_vegetables]
|
12
|
+
end
|
13
|
+
|
14
|
+
def energy
|
15
|
+
score_value(@nutrients.energy, 0..10) do |v|
|
16
|
+
if v == 0 then 0
|
17
|
+
elsif v <= 30 then 1
|
18
|
+
elsif v <= 60 then 2
|
19
|
+
elsif v <= 90 then 3
|
20
|
+
elsif v <= 120 then 4
|
21
|
+
elsif v <= 150 then 5
|
22
|
+
elsif v <= 180 then 6
|
23
|
+
elsif v <= 210 then 7
|
24
|
+
elsif v <= 240 then 8
|
25
|
+
elsif v <= 270 then 9
|
26
|
+
else 10
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def sugar
|
32
|
+
score_value(@nutrients.sugar, 0..10) do |v|
|
33
|
+
if v == 0 then 0
|
34
|
+
elsif v < 1.5 then 1
|
35
|
+
elsif v < 3 then 2
|
36
|
+
elsif v < 4.5 then 3
|
37
|
+
elsif v < 6 then 4
|
38
|
+
elsif v < 7.5 then 5
|
39
|
+
elsif v < 9 then 6
|
40
|
+
elsif v < 10.5 then 7
|
41
|
+
elsif v < 12 then 8
|
42
|
+
elsif v < 13.5 then 9
|
43
|
+
else 10
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def fruits_vegetables
|
49
|
+
# the text mentions % but here we use g/100ml
|
50
|
+
# we'd need to either ask for %, ask for g/100g, or require a density ...
|
51
|
+
score_value(@nutrients.fruits_vegetables, 0..10) do |v|
|
52
|
+
if v > 80 then 10
|
53
|
+
elsif v > 60 then 4
|
54
|
+
elsif v > 40 then 2
|
55
|
+
else 0
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative '../common/score'
|
2
|
+
require_relative 'concerns/general_score_class'
|
3
|
+
|
4
|
+
module Nutriscore
|
5
|
+
module FR
|
6
|
+
# this is for vegetable and animal fats and oils
|
7
|
+
class FatsScore < Nutriscore::Common::Score
|
8
|
+
include GeneralScoreClass
|
9
|
+
|
10
|
+
def self.nutrient_keys
|
11
|
+
[:fat_saturated, :fat_total]
|
12
|
+
end
|
13
|
+
|
14
|
+
def score
|
15
|
+
fat_saturated
|
16
|
+
end
|
17
|
+
|
18
|
+
def fat_saturated
|
19
|
+
score_value(fat_saturated_pct, 0..10) do |v|
|
20
|
+
if v < 10 then 0
|
21
|
+
elsif v < 16 then 1
|
22
|
+
elsif v < 22 then 2
|
23
|
+
elsif v < 28 then 3
|
24
|
+
elsif v < 34 then 4
|
25
|
+
elsif v < 40 then 5
|
26
|
+
elsif v < 46 then 6
|
27
|
+
elsif v < 52 then 7
|
28
|
+
elsif v < 58 then 8
|
29
|
+
elsif v < 64 then 9
|
30
|
+
else 10
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def fat_saturated_pct
|
38
|
+
fat_saturated = @nutrients.fat_saturated
|
39
|
+
fat_total = @nutrients.fat_total
|
40
|
+
fat_saturated * 100 / fat_total if fat_saturated && fat_total
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative '../common/nutrients'
|
2
|
+
require_relative 'concerns/general_score_class'
|
3
|
+
require_relative 'positive_score'
|
4
|
+
require_relative 'negative_score'
|
5
|
+
|
6
|
+
module Nutriscore
|
7
|
+
module FR
|
8
|
+
class GeneralScore
|
9
|
+
include GeneralScoreClass
|
10
|
+
|
11
|
+
attr_reader :positive, :negative
|
12
|
+
|
13
|
+
def self.nutrient_keys
|
14
|
+
PositiveScore.nutrient_keys | NegativeScore.nutrient_keys
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(nutrients)
|
18
|
+
@nutrients = Nutriscore::Common::Nutrients.wrap(nutrients)
|
19
|
+
@positive = PositiveScore.new(@nutrients)
|
20
|
+
@negative = NegativeScore.new(@nutrients)
|
21
|
+
end
|
22
|
+
|
23
|
+
def score
|
24
|
+
@negative.score - @positive.score
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
"#<#{self.class} score=#{score} " +
|
29
|
+
"positive_score=#{@positive.inspect} negative_score=#{@negative.inspect}>"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require_relative '../common/score'
|
2
|
+
|
3
|
+
module Nutriscore
|
4
|
+
module FR
|
5
|
+
class NegativeScore < Nutriscore::Common::Score
|
6
|
+
|
7
|
+
def self.nutrient_keys
|
8
|
+
[:energy, :fat_saturated, :sugar, :sodium]
|
9
|
+
end
|
10
|
+
|
11
|
+
def energy
|
12
|
+
score_value(@nutrients.energy, 0..10) do |v|
|
13
|
+
if v > 3350 then 10
|
14
|
+
elsif v > 3015 then 9
|
15
|
+
elsif v > 2680 then 8
|
16
|
+
elsif v > 2345 then 7
|
17
|
+
elsif v > 2010 then 6
|
18
|
+
elsif v > 1675 then 5
|
19
|
+
elsif v > 1340 then 4
|
20
|
+
elsif v > 1005 then 3
|
21
|
+
elsif v > 670 then 2
|
22
|
+
elsif v > 335 then 1
|
23
|
+
else 0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def fat_saturated
|
29
|
+
score_value(@nutrients.fat_saturated, 0..10) do |v|
|
30
|
+
if v > 10 then 10
|
31
|
+
elsif v > 9 then 9
|
32
|
+
elsif v > 8 then 8
|
33
|
+
elsif v > 7 then 7
|
34
|
+
elsif v > 6 then 6
|
35
|
+
elsif v > 5 then 5
|
36
|
+
elsif v > 4 then 4
|
37
|
+
elsif v > 3 then 3
|
38
|
+
elsif v > 2 then 2
|
39
|
+
elsif v > 1 then 1
|
40
|
+
else 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def sugar
|
46
|
+
score_value(@nutrients.sugar, 0..10) do |v|
|
47
|
+
if v > 45 then 10
|
48
|
+
elsif v > 40 then 9
|
49
|
+
elsif v > 36 then 8
|
50
|
+
elsif v > 31 then 7
|
51
|
+
elsif v > 27 then 6
|
52
|
+
elsif v > 22.5 then 5
|
53
|
+
elsif v > 18 then 4
|
54
|
+
elsif v > 13.5 then 3
|
55
|
+
elsif v > 9 then 2
|
56
|
+
elsif v > 4.5 then 1
|
57
|
+
else 0
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def sodium
|
63
|
+
score_value(@nutrients.sodium, 0..10) do |v|
|
64
|
+
if v > 900 then 10
|
65
|
+
elsif v > 810 then 9
|
66
|
+
elsif v > 720 then 8
|
67
|
+
elsif v > 630 then 7
|
68
|
+
elsif v > 540 then 6
|
69
|
+
elsif v > 450 then 5
|
70
|
+
elsif v > 360 then 4
|
71
|
+
elsif v > 270 then 3
|
72
|
+
elsif v > 180 then 2
|
73
|
+
elsif v > 90 then 1
|
74
|
+
else 0
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative '../common/score'
|
2
|
+
|
3
|
+
module Nutriscore
|
4
|
+
module FR
|
5
|
+
class PositiveScore < Nutriscore::Common::Score
|
6
|
+
|
7
|
+
def self.nutrient_keys
|
8
|
+
[:fruits_vegetables_nuts, :fibres, :proteins]
|
9
|
+
end
|
10
|
+
|
11
|
+
def fruits_vegetables_nuts
|
12
|
+
score_value(@nutrients.fruits_vegetables_nuts, 0..5) do |v|
|
13
|
+
if v > 80 then 5
|
14
|
+
elsif v > 60 then 2
|
15
|
+
elsif v > 40 then 1
|
16
|
+
else 0
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def fibres
|
22
|
+
score_value(@nutrients.fibres, 0..5) do |v|
|
23
|
+
if v > 4.7 then 5
|
24
|
+
elsif v > 3.7 then 4
|
25
|
+
elsif v > 2.8 then 3
|
26
|
+
elsif v > 1.9 then 2
|
27
|
+
elsif v > 0.9 then 1
|
28
|
+
else 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def proteins
|
34
|
+
score_value(@nutrients.proteins, 0..5) do |v|
|
35
|
+
if v > 8.0 then 5
|
36
|
+
elsif v > 6.4 then 4
|
37
|
+
elsif v > 4.8 then 3
|
38
|
+
elsif v > 3.2 then 2
|
39
|
+
elsif v > 1.6 then 1
|
40
|
+
else 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'general_score'
|
2
|
+
require_relative '../common/range'
|
3
|
+
|
4
|
+
module Nutriscore
|
5
|
+
module FR
|
6
|
+
# this is for general products
|
7
|
+
class SpecificScore < GeneralScore
|
8
|
+
def score
|
9
|
+
if @negative.score.min < 11 || @positive.fruits_vegetables_nuts.max >= 5
|
10
|
+
@negative.score - @positive.score
|
11
|
+
elsif @negative.score.max >= 11 && @positive.fruits_vegetables_nuts.min < 5
|
12
|
+
@negative.score - @positive.score_without_proteins
|
13
|
+
else
|
14
|
+
Range.new(
|
15
|
+
@negative.score.min - @positive.score.max,
|
16
|
+
@negative.score.max - @positive.score_without_proteins.min
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Nutriscore
|
2
|
+
module UK
|
3
|
+
module DrinksLessHealthy
|
4
|
+
def less_healthy?
|
5
|
+
# 'A drink is classified as 'less healthy' where it scores 1 point or more.'
|
6
|
+
if score.min >= 1
|
7
|
+
true
|
8
|
+
elsif score.max < 1
|
9
|
+
false
|
10
|
+
else
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Nutriscore
|
2
|
+
module UK
|
3
|
+
module GeneralLessHealthy
|
4
|
+
def less_healthy?
|
5
|
+
# 'A food is classified as 'less healthy' where it scores 4 points or more.'
|
6
|
+
if score.min >= 4
|
7
|
+
true
|
8
|
+
elsif score.max < 4
|
9
|
+
false
|
10
|
+
else
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative '../common/nutrients'
|
2
|
+
require_relative 'concerns/general_less_healthy'
|
3
|
+
require_relative 'positive_score'
|
4
|
+
require_relative 'negative_score'
|
5
|
+
|
6
|
+
module Nutriscore
|
7
|
+
module UK
|
8
|
+
class GeneralScore
|
9
|
+
include GeneralLessHealthy
|
10
|
+
|
11
|
+
attr_reader :positive, :negative
|
12
|
+
|
13
|
+
def self.nutrient_keys
|
14
|
+
PositiveScore.nutrient_keys | NegativeScore.nutrient_keys
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(nutrients, fibres_method: :aoac)
|
18
|
+
@nutrients = Nutriscore::Common::Nutrients.wrap(nutrients)
|
19
|
+
@positive = PositiveScore.new(@nutrients, fibres_method: fibres_method)
|
20
|
+
@negative = NegativeScore.new(@nutrients)
|
21
|
+
end
|
22
|
+
|
23
|
+
def score
|
24
|
+
@negative.score - @positive.score
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
"#<#{self.class} score=#{score} " +
|
29
|
+
"positive_score=#{@positive.inspect} negative_score=#{@negative.inspect}>"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require_relative '../common/score'
|
2
|
+
|
3
|
+
module Nutriscore
|
4
|
+
module UK
|
5
|
+
# This is called 'A'-points in the documentation
|
6
|
+
class NegativeScore < Nutriscore::Common::Score
|
7
|
+
|
8
|
+
def self.nutrient_keys
|
9
|
+
[:energy, :fat_saturated, :sugar, :sodium]
|
10
|
+
end
|
11
|
+
|
12
|
+
def energy
|
13
|
+
score_value(@nutrients.energy, 0..10) do |v|
|
14
|
+
if v > 3350 then 10
|
15
|
+
elsif v > 3015 then 9
|
16
|
+
elsif v > 2680 then 8
|
17
|
+
elsif v > 2345 then 7
|
18
|
+
elsif v > 2010 then 6
|
19
|
+
elsif v > 1675 then 5
|
20
|
+
elsif v > 1340 then 4
|
21
|
+
elsif v > 1005 then 3
|
22
|
+
elsif v > 670 then 2
|
23
|
+
elsif v > 335 then 1
|
24
|
+
else 0
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def fat_saturated
|
30
|
+
score_value(@nutrients.fat_saturated, 0..10) do |v|
|
31
|
+
if v > 10 then 10
|
32
|
+
elsif v > 9 then 9
|
33
|
+
elsif v > 8 then 8
|
34
|
+
elsif v > 7 then 7
|
35
|
+
elsif v > 6 then 6
|
36
|
+
elsif v > 5 then 5
|
37
|
+
elsif v > 4 then 4
|
38
|
+
elsif v > 3 then 3
|
39
|
+
elsif v > 2 then 2
|
40
|
+
elsif v > 1 then 1
|
41
|
+
else 0
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def sugar
|
47
|
+
score_value(@nutrients.sugar, 0..10) do |v|
|
48
|
+
if v > 45 then 10
|
49
|
+
elsif v > 40 then 9
|
50
|
+
elsif v > 36 then 8
|
51
|
+
elsif v > 31 then 7
|
52
|
+
elsif v > 27 then 6
|
53
|
+
elsif v > 22.5 then 5
|
54
|
+
elsif v > 18 then 4
|
55
|
+
elsif v > 13.5 then 3
|
56
|
+
elsif v > 9 then 2
|
57
|
+
elsif v > 4.5 then 1
|
58
|
+
else 0
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def sodium
|
64
|
+
score_value(@nutrients.sodium, 0..10) do |v|
|
65
|
+
v *= 1000 if v # comparison is in mg/100g
|
66
|
+
if v > 900 then 10
|
67
|
+
elsif v > 810 then 9
|
68
|
+
elsif v > 720 then 8
|
69
|
+
elsif v > 630 then 7
|
70
|
+
elsif v > 540 then 6
|
71
|
+
elsif v > 450 then 5
|
72
|
+
elsif v > 360 then 4
|
73
|
+
elsif v > 270 then 3
|
74
|
+
elsif v > 180 then 2
|
75
|
+
elsif v > 90 then 1
|
76
|
+
else 0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative '../common/score'
|
2
|
+
|
3
|
+
module Nutriscore
|
4
|
+
module UK
|
5
|
+
# This is called 'C'-points in the documentation
|
6
|
+
class PositiveScore < Nutriscore::Common::Score
|
7
|
+
|
8
|
+
def initialize(nutrients, fibres_method: :aoac, **opts)
|
9
|
+
super(nutrients, **opts)
|
10
|
+
@fibres_method = fibres_method
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.nutrient_keys
|
14
|
+
[:fruits_vegetables_nuts, :fibres, :proteins]
|
15
|
+
end
|
16
|
+
|
17
|
+
def fruits_vegetables_nuts
|
18
|
+
score_value(@nutrients.fruits_vegetables_nuts, 0..5) do |v|
|
19
|
+
if v > 80 then 5
|
20
|
+
elsif v > 60 then 2
|
21
|
+
elsif v > 40 then 1
|
22
|
+
else 0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def fibres
|
28
|
+
score_value(@nutrients.fibres, 0..5) do |v|
|
29
|
+
if @fibres_method == :aoac
|
30
|
+
if v > 4.7 then 5
|
31
|
+
elsif v > 3.7 then 4
|
32
|
+
elsif v > 2.8 then 3
|
33
|
+
elsif v > 1.9 then 2
|
34
|
+
elsif v > 0.9 then 1
|
35
|
+
else 0
|
36
|
+
end
|
37
|
+
elsif @fibres_method == :nsp
|
38
|
+
if v > 3.5 then 5
|
39
|
+
elsif v > 2.8 then 4
|
40
|
+
elsif v > 2.1 then 3
|
41
|
+
elsif v > 1.4 then 2
|
42
|
+
elsif v > 0.7 then 1
|
43
|
+
else 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def proteins
|
50
|
+
score_value(@nutrients.proteins, 0..5) do |v|
|
51
|
+
if v > 8.0 then 5
|
52
|
+
elsif v > 6.4 then 4
|
53
|
+
elsif v > 4.8 then 3
|
54
|
+
elsif v > 3.2 then 2
|
55
|
+
elsif v > 1.6 then 1
|
56
|
+
else 0
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def score_without_proteins
|
62
|
+
(self.class.nutrient_keys - [:proteins]).map(&method(:public_send)).compact.reduce(&:+)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'general_score'
|
2
|
+
require_relative '../common/range'
|
3
|
+
|
4
|
+
module Nutriscore
|
5
|
+
module UK
|
6
|
+
# this is for general products
|
7
|
+
class SpecificScore < GeneralScore
|
8
|
+
def score
|
9
|
+
# 'If a food or drink scores 11 or more ‘A’ points then it cannot score points for protein
|
10
|
+
# unless it also scores 5 points for fruit, vegetables and nuts.'
|
11
|
+
if @negative.score.min < 11 || @positive.fruits_vegetables_nuts.max >= 5
|
12
|
+
@negative.score - @positive.score
|
13
|
+
elsif @negative.score.max >= 11 && @positive.fruits_vegetables_nuts.min < 5
|
14
|
+
@negative.score - @positive.score_without_proteins
|
15
|
+
else
|
16
|
+
Range.new(
|
17
|
+
@negative.score.min - @positive.score.max,
|
18
|
+
@negative.score.max - @positive.score_without_proteins.min
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/nutriscore.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$:.unshift(File.expand_path(File.dirname(__FILE__) + '/lib'))
|
2
|
+
require 'nutriscore/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'nutriscore'
|
6
|
+
s.version = Nutriscore::VERSION
|
7
|
+
s.date = Nutriscore::VERSION_DATE
|
8
|
+
s.summary = 'Nutri-score computation for food products.'
|
9
|
+
s.authors = ['wvengen']
|
10
|
+
s.email = ['dev-ruby@willem.engen.nl']
|
11
|
+
s.homepage = 'https://github.com/q-m/nutriscore-ruby'
|
12
|
+
s.license = 'MIT'
|
13
|
+
s.metadata = {
|
14
|
+
'bug_tracker_uri' => 'https://github.com/q-m/nutriscore-ruby/issues',
|
15
|
+
'source_code_uri' => 'https://github.com/q-m/nutriscore-ruby',
|
16
|
+
}
|
17
|
+
|
18
|
+
s.files = `git ls-files *.gemspec lib`.split("\n")
|
19
|
+
s.executables = `git ls-files bin`.split("\n").map(&File.method(:basename))
|
20
|
+
s.extra_rdoc_files = ['README.md', 'LICENSE']
|
21
|
+
s.require_paths = ['lib']
|
22
|
+
|
23
|
+
s.add_development_dependency 'rspec'
|
24
|
+
s.add_development_dependency 'rspec-its'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nutriscore
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- wvengen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-06-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec-its
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- dev-ruby@willem.engen.nl
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files:
|
47
|
+
- README.md
|
48
|
+
- LICENSE
|
49
|
+
files:
|
50
|
+
- LICENSE
|
51
|
+
- README.md
|
52
|
+
- lib/nutriscore.rb
|
53
|
+
- lib/nutriscore/common/nutrients.rb
|
54
|
+
- lib/nutriscore/common/range.rb
|
55
|
+
- lib/nutriscore/common/score.rb
|
56
|
+
- lib/nutriscore/common/score_class_range.rb
|
57
|
+
- lib/nutriscore/fr.rb
|
58
|
+
- lib/nutriscore/fr/cheese_score.rb
|
59
|
+
- lib/nutriscore/fr/concerns/drinks_score_class.rb
|
60
|
+
- lib/nutriscore/fr/concerns/general_score_class.rb
|
61
|
+
- lib/nutriscore/fr/concerns/mineral_water_score_class.rb
|
62
|
+
- lib/nutriscore/fr/drinks_score.rb
|
63
|
+
- lib/nutriscore/fr/fats_score.rb
|
64
|
+
- lib/nutriscore/fr/general_score.rb
|
65
|
+
- lib/nutriscore/fr/mineral_water_score.rb
|
66
|
+
- lib/nutriscore/fr/negative_score.rb
|
67
|
+
- lib/nutriscore/fr/positive_score.rb
|
68
|
+
- lib/nutriscore/fr/specific_score.rb
|
69
|
+
- lib/nutriscore/uk.rb
|
70
|
+
- lib/nutriscore/uk/concerns/drinks_less_healthy.rb
|
71
|
+
- lib/nutriscore/uk/concerns/general_less_healthy.rb
|
72
|
+
- lib/nutriscore/uk/drinks_score.rb
|
73
|
+
- lib/nutriscore/uk/general_score.rb
|
74
|
+
- lib/nutriscore/uk/negative_score.rb
|
75
|
+
- lib/nutriscore/uk/positive_score.rb
|
76
|
+
- lib/nutriscore/uk/specific_score.rb
|
77
|
+
- lib/nutriscore/version.rb
|
78
|
+
- nutriscore.gemspec
|
79
|
+
homepage: https://github.com/q-m/nutriscore-ruby
|
80
|
+
licenses:
|
81
|
+
- MIT
|
82
|
+
metadata:
|
83
|
+
bug_tracker_uri: https://github.com/q-m/nutriscore-ruby/issues
|
84
|
+
source_code_uri: https://github.com/q-m/nutriscore-ruby
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubygems_version: 3.0.3.1
|
101
|
+
signing_key:
|
102
|
+
specification_version: 4
|
103
|
+
summary: Nutri-score computation for food products.
|
104
|
+
test_files: []
|