nutriscore 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ require_relative 'nutriscore/version'
2
+ require_relative 'nutriscore/fr'
3
+ require_relative 'nutriscore/uk'
@@ -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,5 @@
1
+ require_relative 'fr/cheese_score'
2
+ require_relative 'fr/drinks_score'
3
+ require_relative 'fr/fats_score'
4
+ require_relative 'fr/mineral_water_score'
5
+ require_relative 'fr/specific_score'
@@ -0,0 +1,8 @@
1
+ require_relative 'general_score'
2
+
3
+ module Nutriscore
4
+ module FR
5
+ class CheeseScore < GeneralScore
6
+ end
7
+ end
8
+ 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,11 @@
1
+ require_relative '../../common/score_class_range'
2
+
3
+ module Nutriscore
4
+ module FR
5
+ module MineralWaterScoreClass
6
+ def score_class
7
+ Nutriscore::Common::ScoreClassRange.new('A', 'A')
8
+ end
9
+ end
10
+ end
11
+ 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,10 @@
1
+ require_relative 'drinks_score'
2
+ require_relative 'concerns/mineral_water_score_class'
3
+
4
+ module Nutriscore
5
+ module FR
6
+ class MineralWaterScore < DrinksScore
7
+ include MineralWaterScoreClass
8
+ end
9
+ end
10
+ 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,2 @@
1
+ require_relative 'uk/drinks_score'
2
+ require_relative 'uk/specific_score'
@@ -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,11 @@
1
+ require_relative 'specific_score'
2
+ require_relative 'concerns/drinks_less_healthy'
3
+
4
+ module Nutriscore
5
+ module UK
6
+ # this is for general products
7
+ class DrinksScore < SpecificScore
8
+ include DrinksLessHealthy
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,4 @@
1
+ module Nutriscore
2
+ VERSION = '0.2.0'
3
+ VERSION_DATE = '2021-06-07'
4
+ end
@@ -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: []