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 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: []