inci_score 3.1.3 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a79fad4040ad0788ff5c79163b3ad4416a01d2c129e03f5a5af7d76cee14f76
4
- data.tar.gz: de8dcf441618726801b223e635268b1a8d6785bd27596f42854284c948e7c19e
3
+ metadata.gz: 65b6b9212d839ed17a978457cc654b835e34e6a7d68dad7c61cbd3f54eaac8cf
4
+ data.tar.gz: ab6f3cebda2cbf8875aa203edc747864f0953559bbf8472f4f757e3a1b505b8b
5
5
  SHA512:
6
- metadata.gz: d4f6fe4ebd15bc1a65f3249688aa28da08d23af9117ac48d5df299f9129cfdf721623c1c5e853986bb24f73b87c0eb3f68e690e3ff009eb90045ba8e8a375dac
7
- data.tar.gz: 846d875bc0d60304640c68b31a7d5d2505a7ce572a0a52983518058d816844bfe7f47f60bea53b86c680c718b82653f888671f63ecbdbd12560d3b114b28bb2a
6
+ metadata.gz: b51ecb14b357c1926f34ee19d558b5aed024bbda9e5070149abe046f9ef713e40d03749d3ce3d78ee6c1fd48358e9530fb84bed301e7024de18af5466bab5865
7
+ data.tar.gz: 5974307d97068c706bcc483854fd27c6a12e8761d86f3328cfbb1e561cfb06432ddeefcd18aa9d15fee0f3ad8a90260de4919f5a99884119462d93982d361f86
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Commerce Layer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -11,9 +11,6 @@
11
11
  * [CLI](#cli)
12
12
  * [Benchmark](#benchmark)
13
13
  * [Levenshtein in C](#levenshtein-in-c)
14
- * [Platform](#platform)
15
- * [Wrk](#wrk)
16
- * [Results](#results)
17
14
 
18
15
  ## Scope
19
16
  This gem computes the score of cosmetic components basing on the information provided by the [Biodizionario site](http://www.biodizionario.it/) by Fabrizio Zago.
@@ -70,8 +67,8 @@ In such case the score is computed anyway by considering only recognized compone
70
67
  Is still possible to query the object for its state:
71
68
 
72
69
  ```ruby
73
- inci = InciScore::Computer.new(src: 'ingredients:aqua,noent1,noent2').call
74
- inci.valid # false
70
+ inci = InciScore::Computer.new(src: 'ingredients:aqua,noent1,noent2')
71
+ inci.valid? # false
75
72
  inci.unrecognized # ["noent1", "noent2"]
76
73
  ```
77
74
 
@@ -82,32 +79,15 @@ You can collect INCI data by using the available CLI interface:
82
79
  inci_score --src="ingredients: aqua, dimethicone, pej-10, noent"
83
80
 
84
81
  TOTAL SCORE:
85
- 47.1803
82
+ 47.18
86
83
  VALID STATE:
87
- true
88
- COMPONENTS (hazard - name):
89
- aqua
90
- dimethicone
91
- peg-10
84
+ true
85
+ PRECISION:
86
+ 75.0
87
+ COMPONENTS:
88
+ aqua\n dimethicone\n peg-10
92
89
  UNRECOGNIZED:
93
- noent
94
- ```
95
-
96
- #### HTTP server
97
- The CLI interface exposes a Web layer based on the [Puma](http://puma.io/) application server.
98
- The HTTP server is started on the specified port by spawning as many workers as your current workstation supports:
99
- ```shell
100
- inci_score --http=9292
101
- ```
102
- Consider all other options are discarded when running HTTP server.
103
-
104
- ##### Triggering a request
105
- The HTTP server responds with a JSON representation of the original *InciScore::Response* object.
106
- You can pass the source string directly as a HTTP parameter (URI escaped):
107
-
108
- ```shell
109
- curl http://127.0.0.1:9292?src=aqua,dimethicone
110
- => {"components":{"aqua":0,"dimethicone":4},"unrecognized":[],"score":53.7629,"valid":true}
90
+ noent
111
91
  ```
112
92
 
113
93
  #### Getting help
@@ -115,7 +95,6 @@ You can get CLI interface help by:
115
95
  ```shell
116
96
  Usage: inci_score --src="aqua, parfum, etc"
117
97
  -s, --src=SRC The INCI list: "aqua, parfum, etc"
118
- --http=PORT Start HTTP server on the specified port
119
98
  -h, --help Prints this help
120
99
  ```
121
100
 
@@ -124,25 +103,10 @@ Usage: inci_score --src="aqua, parfum, etc"
124
103
  ### Levenshtein in C
125
104
  I noticed the APIs slows down dramatically when dealing with unrecognized components to fuzzy match on.
126
105
  I profiled the code by using the [benchmark-ips](https://github.com/evanphx/benchmark-ips) gem, finding the bottleneck was the pure Ruby implementation of the Levenshtein distance algorithm.
127
- After some pointless optimization, i replaced this routine with a C implementation: i opted for the straightforward [Ruby Inline](https://github.com/seattlerb/rubyinline) library to call the C code straight from Ruby.
128
-
129
- ### Platform
130
- I registered these benchmarks with a MacBook PRO 15 mid 2015 having these specs:
131
- * OSX Sierra
132
- * 2,2 GHz Intel Core i7 (4 cores)
133
- * 16 GB 1600 MHz DDR3
134
- * Ruby 2.4
135
-
136
- ### Wrk
137
- As always i used [wrk](https://github.com/wg/wrk) as the loading tool.
138
- I measured the library three times, picking the best lap.
106
+ After some pointless optimization, i replaced this routine with a C implementation: i opted for the straightforward [Ruby Inline](https://github.com/seattlerb/rubyinline) library to call the C code straight from Ruby.
107
+
108
+ Once downloaded source code, run the bench specs by:
109
+
139
110
  ```shell
140
- wrk -t 4 -c 100 -d 30s --timeout 2000 "http://0.0.0.0:9292/?src=<source>"
111
+ bundle exec rake spec:bench
141
112
  ```
142
-
143
- ### Results
144
- | Source | Throughput (req/s) |
145
- | --------------------------: | -----------------: |
146
- | aqua,parfum,zeolite | 20296.75 |
147
- | agua,porfum,zeolithe | 1098.45 |
148
- | agua/water,porfum/fragrance | 1599.47 |
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "inci_score"
5
- require "irb"
6
- require "irb/completion"
3
+ require 'bundler/setup'
4
+ require 'inci_score'
5
+ require 'irb'
6
+ require 'irb/completion'
7
+
7
8
  IRB.start
data/bin/inci_score CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
- lib = File.expand_path("../../lib", __FILE__)
2
+ lib = File.expand_path('../../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
- require "inci_score"
5
+ require 'inci_score'
6
6
 
7
7
  InciScore::CLI.new(args: ARGV.clone).call
@@ -1,38 +1,37 @@
1
- require "optparse"
2
- require "inci_score/computer"
3
- require "inci_score/server"
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'inci_score/computer'
4
5
 
5
6
  module InciScore
6
7
  class CLI
7
- def initialize(args:, io: STDOUT, catalog: InciScore::Catalog.fetch)
8
+ attr_reader :args, :io, :catalog
9
+ attr_accessor :src
10
+
11
+ def initialize(args:, io: STDOUT, catalog: Config::CATALOG)
8
12
  @args = args
9
13
  @io = io
10
14
  @catalog = catalog
11
15
  @src = nil
12
- @port = nil
13
16
  end
14
17
 
15
- def call(server_klass: Server, computer_klass: Computer)
16
- parser.parse!(@args)
17
- return server_klass.new(port: @port, preload: true).run if @port
18
- return @io.puts(%q{Specify inci list as: --src="aqua, parfum, etc"}) unless @src
19
- @io.puts computer_klass.new(src: @src, catalog: @catalog).call
18
+ def call
19
+ parser.parse!(args)
20
+ return io.puts(%q{Specify inci list as: --src='aqua, parfum, etc'}) unless src
21
+ computer = Computer.new(src: src, catalog: catalog)
22
+ io.puts computer.call
20
23
  end
21
24
 
22
25
  private def parser
23
26
  OptionParser.new do |opts|
24
- opts.banner = %q{Usage: inci_score --src="aqua, parfum, etc"}
25
-
26
- opts.on("-sSRC", "--src=SRC", %q{The INCI list: "aqua, parfum, etc"}) do |src|
27
- @src = src
28
- end
27
+ opts.banner = %q{Usage: inci_score --src='aqua, parfum, etc'}
29
28
 
30
- opts.on("--http=PORT", "Start HTTP server on the specified port") do |port|
31
- @port = port
29
+ opts.on('-sSRC', '--src=SRC', %q{The INCI list: 'aqua, parfum, etc'}) do |src|
30
+ self.src = src
32
31
  end
33
32
 
34
- opts.on("-h", "--help", "Prints this help") do
35
- @io.puts opts
33
+ opts.on('-h', '--help', 'Prints this help') do
34
+ io.puts opts
36
35
  exit
37
36
  end
38
37
  end
@@ -1,50 +1,50 @@
1
- require "inci_score/ingredient"
2
- require "inci_score/normalizer"
3
- require "inci_score/recognizer"
4
- require "inci_score/response"
5
- require "inci_score/scorer"
1
+ # frozen_string_literal: true
6
2
 
7
3
  module InciScore
8
4
  class Computer
9
5
  TOLERANCE = 30.0
10
- PERCENT = 100.0
6
+ DECIMALS = 2
11
7
 
12
- def initialize(src:,
13
- catalog: Catalog.fetch,
14
- tolerance: TOLERANCE,
15
- rules: Normalizer::DEFAULT_RULES)
8
+ attr_reader :src, :catalog, :rules, :ingredients, :components, :unrecognized
9
+
10
+ def initialize(src:, catalog: Config::CATALOG, rules: Normalizer::DEFAULT_RULES)
11
+ @unrecognized = []
16
12
  @src = src
17
13
  @catalog = catalog
18
- @tolerance = Float(tolerance)
19
14
  @rules = rules
20
- @unrecognized = []
15
+ @ingredients = Normalizer.new(src: src, rules: rules).call
16
+ @components = fetch_components
17
+ freeze
21
18
  end
22
19
 
23
20
  def call
24
- @response ||= Response.new(components: components.map(&:name),
25
- unrecognized: @unrecognized,
26
- score: score,
27
- valid: valid?)
21
+ Response.new(components: components.map(&:name),
22
+ unrecognized: unrecognized,
23
+ score: score,
24
+ valid: valid?,
25
+ precision: precision)
28
26
  end
29
27
 
30
- private def score
31
- Scorer.new(components.map(&:hazard)).call
28
+ def score
29
+ Scorer.new(components.map(&:hazard)).call.round(DECIMALS)
32
30
  end
33
31
 
34
- private def ingredients
35
- @ingredients ||= Normalizer.new(src: @src, rules: @rules).call
32
+ def precision
33
+ (100 - ((unrecognized.size / Float(ingredients.size)) * 100)).round(DECIMALS)
36
34
  end
37
35
 
38
- private def components
39
- @components ||= ingredients.map do |ingredient|
40
- Recognizer.new(ingredient, @catalog).call.tap do |component|
41
- @unrecognized << ingredient unless component
42
- end
43
- end.compact
36
+ def valid?
37
+ precision >= TOLERANCE
44
38
  end
45
39
 
46
- private def valid?
47
- @unrecognized.size / (ingredients.size / PERCENT) <= @tolerance
40
+ private
41
+
42
+ def fetch_components
43
+ ingredients.map do |ingredient|
44
+ Recognizer.new(ingredient, catalog).call.tap do |component|
45
+ unrecognized << ingredient unless component
46
+ end
47
+ end.compact
48
48
  end
49
49
  end
50
50
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module InciScore
6
+ module Config
7
+ CATALOG = YAML::load_file(File::expand_path('../../../config/catalog.yml', __FILE__)).freeze
8
+ HAZARDS = YAML::load_file(File::expand_path('../../../config/hazards.yml', __FILE__)).freeze
9
+ end
10
+ end
@@ -1,40 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InciScore
2
4
  class Ingredient
3
- SLASH = "/"
4
- SLASH_RULE = /(?<!ate)\//
5
- PARENTHESIS = %w[( ) [ ]]
6
- DETAILS_RULE = /(\(.+\)|\[.+\])/
5
+ SLASH = '/'
6
+ SLASH_RULE = /(?<!ate)\//.freeze
7
+ PARENTHESIS = %w[( ) [ ]].freeze
8
+ DETAILS_RULE = /(\(.+\)|\[.+\])/.freeze
9
+
10
+ attr_reader :raw, :tokens, :values
7
11
 
8
12
  def initialize(raw)
9
13
  @raw = raw.to_s
10
14
  @tokens = @raw.split(SLASH_RULE).map(&:strip)
15
+ @values ||= synonims.unshift(name).compact
16
+ freeze
11
17
  end
12
18
 
13
19
  def to_s
14
20
  values.join(SLASH)
15
21
  end
16
22
 
17
- def values
18
- @values ||= synonims.unshift(name).compact
19
- end
20
-
21
- private def name
22
- return @tokens.first unless parenthesis?
23
- @raw.sub(DETAILS_RULE, "").strip
24
- end
23
+ private
25
24
 
26
- private def synonims
27
- @tokens[1, @tokens.size].to_a
25
+ def name
26
+ return tokens.first unless parenthesis?
27
+ raw.sub(DETAILS_RULE, '').strip
28
28
  end
29
29
 
30
- private def details
31
- return unless parenthesis?
32
- @raw.match(DETAILS_RULE)[1].delete(PARENTHESIS.join("|"))
30
+ def synonims
31
+ tokens[1, tokens.size].to_a
33
32
  end
34
33
 
35
- private def parenthesis?
34
+ def parenthesis?
36
35
  PARENTHESIS.each_slice(2).any? do |pair|
37
- pair.all? { |p| @raw.index(p) }
36
+ pair.all? { |p| raw.index(p) }
38
37
  end
39
38
  end
40
39
  end
@@ -1,24 +1,28 @@
1
- require "inline"
1
+ # frozen_string_literal: true
2
+
3
+ require 'inline'
2
4
 
3
5
  module InciScore
4
6
  class LevenshteinC
5
- C_PROGRAM = File::expand_path("../../../ext/levenshtein.c", __FILE__)
7
+ C_PROGRAM = File::expand_path('../../../ext/levenshtein.c', __FILE__)
6
8
 
7
9
  inline(:C) do |builder|
8
- builder.c File::read(C_PROGRAM)
10
+ builder.c File::read(C_PROGRAM)
9
11
  end
10
12
  end
11
13
 
12
14
  class Levenshtein
15
+ attr_reader :s, :t
16
+
13
17
  def initialize(s, t)
14
- @s = s.downcase.unpack("U*")
15
- @t = t.downcase.unpack("U*")
18
+ @s = s.downcase.unpack('U*')
19
+ @t = t.downcase.unpack('U*')
16
20
  end
17
21
 
18
22
  def call
19
- n, m = @s.length, @t.length
23
+ n, m = s.length, t.length
20
24
 
21
- return 0 if @s == @t
25
+ return 0 if s == t
22
26
  return m if n.zero?
23
27
  return n if m.zero?
24
28
 
@@ -28,7 +32,7 @@ module InciScore
28
32
  n.times do |i|
29
33
  e = i + 1
30
34
  m.times do |j|
31
- c = @s[i] == @t[j] ? 0 : 1
35
+ c = s[i] == t[j] ? 0 : 1
32
36
  ins = d[j + 1] + 1
33
37
  del = e + 1
34
38
  sub = d[j] + c
@@ -43,4 +47,3 @@ module InciScore
43
47
  end
44
48
  end
45
49
  end
46
-
@@ -1,20 +1,22 @@
1
- require "inci_score/normalizer_rules"
1
+ # frozen_string_literal: true
2
+
3
+ require 'inci_score/normalizer_rules'
2
4
 
3
5
  module InciScore
4
6
  class Normalizer
5
- DEFAULT_RULES = [Rules::Replacer, Rules::Downcaser, Rules::Beheader, Rules::Separator, Rules::Tokenizer, Rules::Sanitizer, Rules::Uniquifier]
7
+ DEFAULT_RULES = [Rules::Replacer, Rules::Downcaser, Rules::Beheader, Rules::Separator, Rules::Tokenizer, Rules::Sanitizer, Rules::Uniquifier].freeze
6
8
 
7
- attr_reader :src
9
+ attr_reader :src, :rules
8
10
 
9
11
  def initialize(src:, rules: DEFAULT_RULES)
10
12
  @src = src
11
13
  @rules = rules
14
+ freeze
12
15
  end
13
16
 
14
17
  def call
15
- yield(@rules) if block_given?
16
- @rules.reduce(@src) do |src, rule|
17
- @src = rule.call(src)
18
+ rules.reduce(src) do |_src, rule|
19
+ _src = rule.call(_src)
18
20
  end
19
21
  end
20
22
  end
@@ -1,8 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InciScore
2
4
  class Normalizer
3
5
  module Rules
4
6
  SEPARATOR = ','
5
7
 
8
+ Downcaser = ->(src) { src.downcase }.freeze
9
+
10
+ Tokenizer = ->(src) { src.split(SEPARATOR).map(&:strip) }.freeze
11
+
12
+ Uniquifier = ->(src) { Array(src).uniq }.freeze
13
+
6
14
  module Replacer
7
15
  extend self
8
16
 
@@ -14,7 +22,7 @@ module InciScore
14
22
  ['~', '-'],
15
23
  ['|', 'l'],
16
24
  [' I ', '/']
17
- ]
25
+ ].freeze
18
26
 
19
27
  def call(src)
20
28
  REPLACEMENTS.reduce(src) do |_src, replacement|
@@ -24,14 +32,6 @@ module InciScore
24
32
  end
25
33
  end
26
34
 
27
- module Downcaser
28
- extend self
29
-
30
- def call(src)
31
- src.downcase
32
- end
33
- end
34
-
35
35
  module Beheader
36
36
  extend self
37
37
 
@@ -40,7 +40,8 @@ module InciScore
40
40
 
41
41
  def call(src)
42
42
  sep_index = src.index(TITLE_SEP)
43
- return src if !sep_index || sep_index > MAX_INDEX
43
+ return src unless sep_index
44
+ return src if sep_index > MAX_INDEX
44
45
  src[sep_index+1, src.size]
45
46
  end
46
47
  end
@@ -48,27 +49,19 @@ module InciScore
48
49
  module Separator
49
50
  extend self
50
51
 
51
- SEPARATORS = ["; ", ". ", " ' ", " - ", " : "]
52
+ SEPARATORS = ['; ', '. ', " ' ", ' - ', ' : '].freeze
52
53
 
53
54
  def call(src)
54
55
  SEPARATORS.reduce(src) do |_src, separator|
55
56
  _src = _src.gsub(separator, SEPARATOR)
56
57
  end
57
58
  end
58
- end
59
-
60
- module Tokenizer
61
- extend self
62
-
63
- def call(src)
64
- src.split(SEPARATOR).map(&:strip)
65
- end
66
59
  end
67
60
 
68
61
  module Sanitizer
69
62
  extend self
70
63
 
71
- INVALID_CHARS = /[^\/\[\]\(\)\w\s-]/
64
+ INVALID_CHARS = /[^\/\[\]\(\)\w\s-]/.freeze
72
65
 
73
66
  def call(src)
74
67
  Array(src).map do |token|
@@ -76,14 +69,6 @@ module InciScore
76
69
  end.reject(&:empty?)
77
70
  end
78
71
  end
79
-
80
- module Uniquifier
81
- extend self
82
-
83
- def call(src)
84
- Array(src).uniq
85
- end
86
- end
87
72
  end
88
73
  end
89
74
  end
@@ -1,38 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "inci_score/recognizer_rules"
2
4
 
3
5
  module InciScore
4
6
  class Recognizer
5
- DEFAULT_RULES = [Rules::Key, Rules::Levenshtein, Rules::Digits, Rules::Tokens]
7
+ DEFAULT_RULES = [Rules::Key, Rules::Levenshtein, Rules::Digits, Rules::Hazard, Rules::Tokens].freeze
6
8
 
7
9
  Component = Struct.new(:name, :hazard)
8
10
 
9
- attr_reader :applied
11
+ attr_reader :ingredient, :catalog, :rules, :applied
10
12
 
11
13
  def initialize(ingredient, catalog, rules = DEFAULT_RULES, wrapper = Ingredient)
12
14
  @ingredient = wrapper.new(ingredient)
13
15
  @catalog = catalog
14
16
  @rules = rules
15
17
  @applied = []
18
+ freeze
16
19
  end
17
20
 
18
21
  def call
19
- return if @ingredient.to_s.empty?
22
+ return if ingredient.to_s.empty?
20
23
  component = find_component
21
24
  return unless component
22
- Component.new(component, @catalog[component])
23
- end
25
+ Component.new(component, catalog[component])
26
+ end
27
+
28
+ private
24
29
 
25
- private def find_component
26
- @rules.reduce(nil) do |component, rule|
30
+ def find_component
31
+ rules.reduce(nil) do |component, rule|
27
32
  break(component) if component
28
33
  applied << rule
29
34
  apply(rule)
30
35
  end
31
36
  end
32
37
 
33
- private def apply(rule)
34
- @ingredient.values.map do |value|
35
- rule.call(value, @catalog)
38
+ def apply(rule)
39
+ ingredient.values.map do |value|
40
+ rule.call(value, catalog)
36
41
  end.find(&:itself)
37
42
  end
38
43
  end
@@ -1,4 +1,6 @@
1
- require "inci_score/refinements"
1
+ # frozen_string_literal: true
2
+
3
+ require 'inci_score/refinements'
2
4
 
3
5
  module InciScore
4
6
  using Refinements
@@ -6,13 +8,9 @@ module InciScore
6
8
  module Rules
7
9
  TOLERANCE = 3
8
10
 
9
- module Key
10
- extend self
11
+ Key = ->(src, catalog) { src if catalog.has_key?(src) }
11
12
 
12
- def call(src, catalog)
13
- src if catalog.has_key?(src)
14
- end
15
- end
13
+ Hazard = ->(src, _) { 'generic-hazard' if Config::HAZARDS.any? { |h| src.include?(h) } }
16
14
 
17
15
  module Levenshtein
18
16
  extend self
@@ -54,19 +52,21 @@ module InciScore
54
52
  module Tokens
55
53
  extend self
56
54
 
57
- UNMATCHABLE = %w[extract oil sodium acid sulfate]
58
-
55
+ UNMATCHABLE = %w[extract oil sodium acid sulfate].freeze
56
+
59
57
  def call(src, catalog)
60
58
  tokens(src).each do |token|
61
- catalog.each do |component, _|
59
+ catalog.each do |component, _|
62
60
  return component if component.include?(token)
63
61
  end
64
62
  end
65
63
  nil
66
64
  end
67
65
 
66
+ private
67
+
68
68
  def tokens(src)
69
- (src.split(" ") - UNMATCHABLE).reject { |t| t.size < TOLERANCE }.sort! { |a, b| b.size <=> a.size }
69
+ (src.split(' ') - UNMATCHABLE).reject { |t| t.size < TOLERANCE }.sort! { |a, b| b.size <=> a.size }
70
70
  end
71
71
  end
72
72
  end
@@ -1,4 +1,6 @@
1
- require "inci_score/levenshtein"
1
+ # frozen_string_literal: true
2
+
3
+ require 'inci_score/levenshtein'
2
4
 
3
5
  module InciScore
4
6
  module Refinements