eco-helpers 2.0.13 → 2.0.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -2
  3. data/eco-helpers.gemspec +6 -4
  4. data/lib/eco-helpers.rb +2 -0
  5. data/lib/eco/api/common/base_loader.rb +14 -0
  6. data/lib/eco/api/common/people/default_parsers/date_parser.rb +11 -1
  7. data/lib/eco/api/common/people/default_parsers/login_providers_parser.rb +1 -1
  8. data/lib/eco/api/common/people/default_parsers/policy_groups_parser.rb +11 -11
  9. data/lib/eco/api/common/people/person_entry.rb +9 -2
  10. data/lib/eco/api/common/people/supervisor_helpers.rb +27 -0
  11. data/lib/eco/api/common/session/file_manager.rb +2 -2
  12. data/lib/eco/api/common/session/mailer.rb +0 -1
  13. data/lib/eco/api/common/session/s3_uploader.rb +0 -1
  14. data/lib/eco/api/common/session/sftp.rb +0 -1
  15. data/lib/eco/api/common/version_patches/exception.rb +8 -4
  16. data/lib/eco/api/error.rb +5 -3
  17. data/lib/eco/api/microcases.rb +3 -1
  18. data/lib/eco/api/microcases/append_usergroups.rb +0 -1
  19. data/lib/eco/api/microcases/people_cache.rb +2 -2
  20. data/lib/eco/api/microcases/people_load.rb +2 -2
  21. data/lib/eco/api/microcases/people_refresh.rb +2 -2
  22. data/lib/eco/api/microcases/people_search.rb +6 -6
  23. data/lib/eco/api/microcases/preserve_default_tag.rb +23 -0
  24. data/lib/eco/api/microcases/preserve_filter_tags.rb +28 -0
  25. data/lib/eco/api/microcases/preserve_policy_groups.rb +30 -0
  26. data/lib/eco/api/microcases/set_account.rb +0 -1
  27. data/lib/eco/api/organization.rb +1 -0
  28. data/lib/eco/api/organization/people.rb +7 -0
  29. data/lib/eco/api/organization/people_analytics.rb +60 -0
  30. data/lib/eco/api/organization/presets_factory.rb +116 -93
  31. data/lib/eco/api/organization/presets_integrity.json +58 -0
  32. data/lib/eco/api/organization/presets_values.json +5 -4
  33. data/lib/eco/api/policies/default_policies/99_user_access_policy.rb +0 -30
  34. data/lib/eco/api/session.rb +1 -20
  35. data/lib/eco/api/session/batch.rb +23 -7
  36. data/lib/eco/api/session/batch/job.rb +3 -0
  37. data/lib/eco/api/session/config.rb +16 -15
  38. data/lib/eco/api/session/config/api.rb +4 -0
  39. data/lib/eco/api/session/config/apis.rb +80 -0
  40. data/lib/eco/api/session/config/files.rb +7 -0
  41. data/lib/eco/api/session/config/people.rb +3 -19
  42. data/lib/eco/api/usecases/default_cases.rb +4 -1
  43. data/lib/eco/api/usecases/default_cases/abstract_policygroup_abilities_case.rb +161 -0
  44. data/lib/eco/api/usecases/default_cases/analyse_people_case.rb +76 -0
  45. data/lib/eco/api/usecases/default_cases/codes_to_tags_case.rb +2 -3
  46. data/lib/eco/api/usecases/default_cases/reset_landing_page_case.rb +11 -1
  47. data/lib/eco/api/usecases/default_cases/restore_db_case.rb +1 -2
  48. data/lib/eco/api/usecases/default_cases/supers_cyclic_identify_case.rb +72 -0
  49. data/lib/eco/api/usecases/default_cases/supers_hierarchy_case.rb +59 -0
  50. data/lib/eco/api/usecases/default_cases/to_csv_case.rb +104 -26
  51. data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +62 -36
  52. data/lib/eco/cli.rb +0 -10
  53. data/lib/eco/cli/config/default/options.rb +19 -17
  54. data/lib/eco/cli/config/default/people_filters.rb +3 -3
  55. data/lib/eco/cli/config/default/usecases.rb +77 -25
  56. data/lib/eco/cli/config/default/workflow.rb +12 -3
  57. data/lib/eco/cli/config/help.rb +1 -0
  58. data/lib/eco/cli/config/options_set.rb +106 -13
  59. data/lib/eco/cli/config/use_cases.rb +33 -33
  60. data/lib/eco/cli/scripting/args_helpers.rb +30 -3
  61. data/lib/eco/data.rb +1 -0
  62. data/lib/eco/data/crypto/encryption.rb +3 -3
  63. data/lib/eco/data/files/directory.rb +28 -20
  64. data/lib/eco/data/files/helpers.rb +6 -4
  65. data/lib/eco/data/fuzzy_match.rb +119 -0
  66. data/lib/eco/data/fuzzy_match/array_helpers.rb +75 -0
  67. data/lib/eco/data/fuzzy_match/chars_position_score.rb +37 -0
  68. data/lib/eco/data/fuzzy_match/ngrams_score.rb +73 -0
  69. data/lib/eco/data/fuzzy_match/pairing.rb +102 -0
  70. data/lib/eco/data/fuzzy_match/result.rb +67 -0
  71. data/lib/eco/data/fuzzy_match/results.rb +53 -0
  72. data/lib/eco/data/fuzzy_match/score.rb +44 -0
  73. data/lib/eco/data/fuzzy_match/stop_words.rb +35 -0
  74. data/lib/eco/data/fuzzy_match/string_helpers.rb +69 -0
  75. data/lib/eco/version.rb +1 -1
  76. metadata +86 -10
  77. data/lib/eco/api/microcases/refresh_abilities.rb +0 -19
  78. data/lib/eco/api/organization/presets_reference.json +0 -59
  79. data/lib/eco/api/usecases/default_cases/refresh_abilities_case.rb +0 -30
@@ -3,11 +3,13 @@ module Eco
3
3
  module Files
4
4
  DEFAULT_TIMESTAMP_PATTERN = '%Y-%m-%dT%H%M%S'
5
5
 
6
- def self.included(base)
7
- base.send(:include, InstanceMethods)
8
- base.extend(ClassMethods)
6
+ class << self
7
+ def included(base)
8
+ base.send(:include, InstanceMethods)
9
+ base.extend(ClassMethods)
10
+ end
9
11
  end
10
-
12
+
11
13
  module InstanceMethods
12
14
 
13
15
  end
@@ -0,0 +1,119 @@
1
+ require 'fuzzy_match'
2
+ require 'amatch'
3
+ require 'jaro_winkler'
4
+
5
+ require_relative 'fuzzy_match/stop_words'
6
+ require_relative 'fuzzy_match/array_helpers'
7
+ require_relative 'fuzzy_match/string_helpers'
8
+ require_relative 'fuzzy_match/pairing'
9
+ require_relative 'fuzzy_match/chars_position_score'
10
+ require_relative 'fuzzy_match/ngrams_score'
11
+
12
+ module Eco
13
+ module Data
14
+ module FuzzyMatch
15
+
16
+ class << self
17
+ def included(base)
18
+ base.send(:include, InstanceMethods)
19
+ base.extend(ClassMethods)
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ include ArrayHelpers
25
+ include StringHelpers
26
+ include Pairing
27
+ include CharsPositionScore
28
+ include NGramsScore
29
+
30
+ def jaro_winkler(str1, str2)
31
+ options = {
32
+ ignore_case: true,
33
+ weight: 0.25
34
+ }
35
+ JaroWinkler.distance(str1, str2, **options)
36
+ end
37
+
38
+ end
39
+
40
+ module InstanceMethods
41
+ include StopWords
42
+
43
+ attr_accessor :fuzzy_options
44
+
45
+ def fuzzy_options
46
+ @fuzzy_options ||= {}
47
+ end
48
+
49
+ def fuzzy_match(haystack = nil, **options)
50
+ return @fuzzy_match if instance_variable_defined?(:@fuzzy_match)
51
+ @fuzzy_options = options.merge({
52
+ stop_words: PREPOSITIONS + PRONOUNS + ARTICLES
53
+ })
54
+ # make it run with a native C extension (for better performance: ~130 % increase of performance)
55
+ ::FuzzyMatch.engine = :amatch
56
+ haystack = obtain_haystack(haystack).tap do |items|
57
+ if !fuzzy_read_method && found = items.find {|item| !item.is_a?(String)}
58
+ raise "To use non String objects as 'haystack' you should provide `read:` or `options[:read]`. Given element: #{found.class}"
59
+ end
60
+ end
61
+ @fuzzy_match = ::FuzzyMatch.new(haystack, fuzzy_options)
62
+ end
63
+
64
+ # @note
65
+ # - When the `haystack` elements are **non** `String` objects, it excludes the needle itself from the results
66
+ # @param needle [String, Object] object is allowed when `fuzzy_options` includes `read:` key
67
+ # @return [Eco::Data::FuzzyMatch::Results]
68
+ def find_all_with_score(needle, **options)
69
+ results = fuzzy_match(**options).find_all_with_score(needle).each_with_object([]) do |fuzzy_results, results|
70
+ item, dice, lev = fuzzy_results
71
+ unless item == needle
72
+ needle_str = item_string(needle)
73
+ item_str = item_string(item)
74
+ jaro_res = self.class.jaro_winkler(needle_str, item_str)
75
+ ngram_res = self.class.ngrams_score(needle_str, item_str, range: 3..5).ratio
76
+ wngram_res = self.class.words_ngrams_score(needle_str, item_str, range: 3..7).ratio
77
+ pos_res = self.class.chars_position_score(needle_str, item_str).ratio
78
+ results << Result.new(item, item_str, dice, lev, jaro_res, ngram_res, wngram_res, pos_res)
79
+ end
80
+ end
81
+ Results.new(needle, item_string(needle), results)
82
+ end
83
+
84
+ private
85
+
86
+ # @note
87
+ # - When used in an `Enumerable` it will use `to_a`, or `values` if it's a `Hash`
88
+ # @param data [Enumerable, nil]
89
+ # @return [Array<Object>] the non-repeated values of `data`
90
+ def obtain_haystack(data = nil)
91
+ data = self if self.is_a?(Enumerable) && !data
92
+ raise "'data' should be an Enumerable. Given: #{data.class}" unless data.is_a?(Enumerable)
93
+ data = self.is_a?(Hash) ? self.values.flatten : to_a.flatten
94
+ data.uniq.compact
95
+ end
96
+
97
+ def item_string(item, attr = fuzzy_read_method)
98
+ return item if !item || item.is_a?(String) || !attr
99
+ attr = attr.to_sym
100
+ return item.send(attr) if item.respond_to?(attr)
101
+ end
102
+
103
+ def fuzzy_read_method
104
+ fuzzy_options[:read]
105
+ end
106
+
107
+ end
108
+
109
+ class << self
110
+ include FuzzyMatch::ClassMethods
111
+ end
112
+
113
+ end
114
+ end
115
+ end
116
+
117
+ require_relative 'fuzzy_match/score'
118
+ require_relative 'fuzzy_match/result'
119
+ require_relative 'fuzzy_match/results'
@@ -0,0 +1,75 @@
1
+ module Eco
2
+ module Data
3
+ module FuzzyMatch
4
+ module ArrayHelpers
5
+ # Keeps the start order of the `values` and consecutive `values` together/consecutive.
6
+ # @param values [Array] the input array with the values.
7
+ # @param range [Integer, Range] determine the lenght of the generated values.
8
+ # @return [Array<Array<Value>>] combinations of `range` length of `values`.
9
+ def ngrams(values, range=2..3)
10
+ [].tap do |out|
11
+ if range.is_a?(Integer)
12
+ n = range
13
+ values_count = values.length
14
+ values.each_with_index do |word, i|
15
+ min = i
16
+ max = i + (n - 1)
17
+ break if values_count <= max
18
+ out << values[min..max].join(' ')
19
+ end
20
+ out.uniq!
21
+ else
22
+ range.each {|n| out.concat(ngrams(values, n))}
23
+ out.uniq!
24
+ end
25
+ end
26
+ end
27
+
28
+ # Keeps the start order of the `values` of the input `Array` `values`.
29
+ # It does **not** keep consecutive `values` together (it can jump/skip items).
30
+ # @param values [Array] the input array with the values.
31
+ # @param range [Integer, Range] determine the lenght of the generated values.
32
+ # @return [Array<Array<Value>>] combinations of `range` length of `values`
33
+ def combinations(values, range=2..3)
34
+ if range.is_a?(Integer)
35
+ values.combination(range).to_a
36
+ else
37
+ range.flat_map {|size| values.combination(size).to_a}
38
+ end
39
+ end
40
+
41
+ # It includes `combinations` that break the initial order of the `Array`.
42
+ # It does **not** keep consecutive `values` together (it can jump/skip items).
43
+ # @param values [Array] the input array with the values.
44
+ # @param range [Integer, Range] determine the lenght of the generated values.
45
+ # @return [Array<Array<Value>>] permutations of `range` length of `values`
46
+ def permutations(values, range=2..3)
47
+ combinations(values, range).tap do |out|
48
+ range = range.is_a?(Integer)? (range..range) : range
49
+ out.dup.select do |item|
50
+ range.include?(item.length)
51
+ end.each do |comb|
52
+ comb.permutation.to_a.tap do |perms|
53
+ perms.each {|perm| out << perm}
54
+ end
55
+ end
56
+ out.uniq!
57
+ end
58
+ end
59
+
60
+ # Helper to praper facet structure
61
+ # @param values1 [Array] the input array with the values to have their facet against.
62
+ # @param values2 [Array] the input array with the values to facet against.
63
+ # @return [Hash] where `keys` are `values1` and `value` of each `key` all `values2`
64
+ def facet(values1, values2)
65
+ {}.tap do |out|
66
+ next unless values1.is_a?(Enumerable)
67
+ values1 = values1.is_a?(Hash) ? values1.values : values1.to_a
68
+ values1.each {|val| out[val] = values2.dup}
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,37 @@
1
+ module Eco
2
+ module Data
3
+ module FuzzyMatch
4
+ module CharsPositionScore
5
+ # For each character in `str1`, a search is performed on `str2`.
6
+ # The search is deemed successful if a character is found in `str2` within `max_distance` characters of the current position.
7
+ # A score is kept of matching characters.
8
+ # @note This algorithm is best suited for matching mis-spellings.
9
+ # @max_distance [Integer] maximum char position distance to score.
10
+ # @normalized [Boolean] to avoid double ups in normalizing.
11
+ # @return [Score] the score object with the result.
12
+ def chars_position_score(str1, str2, max_distance: 3, normalized: false)
13
+ str1, str2 = normalize_string([str1, str2]) unless normalized
14
+ len1 = str1 && str1.length; len2 = str2 && str2.length
15
+ Score.new(0, len1 || 0).tap do |score|
16
+ next if !str1 || !str2
17
+ next score.increase(score.total) if str1 == str2
18
+ next if len1 < 2
19
+ pos = 0
20
+ len1.times do |i|
21
+ start = pos + 1
22
+ found = false
23
+ if pos = str2.index(str1[i])
24
+ if pos < (start + max_distance)
25
+ found = true
26
+ score.increase
27
+ end
28
+ end
29
+ pos = start unless found
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,73 @@
1
+ module Eco
2
+ module Data
3
+ module FuzzyMatch
4
+ module NGramsScore
5
+ # It does the following:
6
+ # 1. It splits both strings into words
7
+ # 2. Pairs all words by best `ngrams_score` match
8
+ # 3. Gives `0` score to those words of `str2` that lost their pair (a word of `str1` cannot be paired twice)
9
+ # 4. Merges the `ngrams_score` of all the paired words of `str2` against their `str1` word pair
10
+ # @param range [Integer, Range] determine the lenght of the generated values for each `word`.
11
+ # @normalized [Boolean] to avoid double ups in normalizing.
12
+ # @return [Score] the score object with the result.
13
+ def words_ngrams_score(str1, str2, range: 3..5, normalized: false)
14
+ str1, str2 = normalize_string([str1, str2]) unless normalized
15
+ len1 = str1 && str1.length; len2 = str2 && str2.length
16
+
17
+ Score.new(0, 0).tap do |score|
18
+ next if !str2 || !str1
19
+ next score.increase(score.total) if str1 == str2
20
+ next if str1.length < 2 || str1.length < 2
21
+
22
+ paired_words(str1, str2, normalized: true) do |needle, item|
23
+ ngrams_score(needle, item, range: range, normalized: true)
24
+ end.each do |sub_str1, (item, iscore)|
25
+ #puts "pairs '#{sub_str1}' --> '#{item}' (score: #{iscore.ratio})"
26
+ score.merge!(iscore)
27
+ end
28
+ end
29
+ end
30
+
31
+ # A score is kept of matching ngram combinations of `str2`.
32
+ # @note This algorithm is best suited for matching sentences, or 'firstname lastname' compared with 'lastname firstname' combinations.
33
+ # @param range [Integer, Range] determine the lenght of the generated values.
34
+ # @normalized [Boolean] to avoid double ups in normalizing.
35
+ # @return [Score] the score object with the result.
36
+ def ngrams_score(str1, str2, range: 3..5, normalized: false)
37
+ str1, str2 = normalize_string([str1, str2]) unless normalized
38
+ len1 = str1 && str1.length; len2 = str2 && str2.length
39
+
40
+ Score.new(0, len1 || 0).tap do |score|
41
+ next if !str2 || !str1
42
+ next score.increase(score.total) if str1 == str2
43
+ next if str1.length < 2 || str2.length < 2
44
+
45
+ grams = word_ngrams(str2, range, normalized: true)
46
+ next unless grams.length > 0
47
+
48
+ if range.is_a?(Integer)
49
+ item_weight = score.total.to_f / grams.length
50
+ matches = grams.select {|res| str1.include?(gram)}.length
51
+ score.increase(matches * item_weight)
52
+ else
53
+ groups = grams.group_by {|gram| gram.length}
54
+ sorted_lens = groups.keys.sort.reverse
55
+ lens = sorted_lens.length
56
+ group_weight = (1.0 / lens).round(3)
57
+
58
+ groups.each do |len, grams|
59
+ len_max_score = score.total * group_weight
60
+ item_weight = len_max_score / grams.length
61
+ matches = grams.select {|gram| str1.include?(gram)}.length
62
+ #pp "#{len} match: #{matches} (over #{grams.length}) || max_score: #{len_max_score} (over #{score.total})"
63
+ score.increase(matches * item_weight)
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,102 @@
1
+ module Eco
2
+ module Data
3
+ module FuzzyMatch
4
+ module Pairing
5
+
6
+ # Pair words using some algorithm.
7
+ # It does the following:
8
+ # 1. It splits both strings into words.
9
+ # 2. Pairs all words by using `block` to score the best match.
10
+ # 3. Gives `0` score to those words of `str2` that lost their pair (a word of `str1` cannot be paired twice).
11
+ # 4. Merges the `Score` of all the paired words of `str2` against their `str1` word pair.
12
+ # @yield [needle, item] offers a comparison algorithm between two strings.
13
+ # @yieldparam needle [String] the string of reference.
14
+ # @yieldparam item [String] one of the haystack items.
15
+ # @yieldreturn [Eco::Data::FuzzyMatch::Score] the `Score` object with the results of comparing `str1` and `str2`
16
+ # @param str1 [String] the string of reference.
17
+ # @param str2 [String] one of the haystack items.
18
+ # @param format [Symbol] determines the `values` of the returned `Hash`::
19
+ # 1. `:pair` for just pair
20
+ # 2. `:score` for just score
21
+ # 2. `[:pair, :score]` for `Array`
22
+ # @normalized [Boolean] to avoid double ups in normalizing.
23
+ # @return [Hash] where `keys` are the **words** of `str1` and their `values`:
24
+ # 1. if `format` is `:pair` => the `str2` words with highest match.
25
+ # 2. if `format` is `:score` => the `Score` words with highest match.
26
+ # 3. if `format` is `[:pair, :score]` => both in an `Array`.
27
+ def paired_words(str1, str2, format: [:pair, :score], normalized: false)
28
+ str1, str2 = normalize_string([str1, str2]) unless normalized
29
+ return {} if !str2 || !str1
30
+ return score.increase(score.total) if str1 == str2
31
+ return {str1 => nil} if str1.length < 2 || str1.length < 2
32
+
33
+ needles = get_words(str1, normalized: true)
34
+ haystack = get_words(str2, normalized: true)
35
+
36
+ ranking = {}
37
+ faceted = needles.each_with_object({}) do |needle, faceted|
38
+ faceted[needle] = haystack.map do |item|
39
+ {
40
+ pair: item,
41
+ score: yield(needle, item)
42
+ }.tap do |result|
43
+ ranking[item] ||= []
44
+ if result[:score].ratio > 0.05
45
+ ranking[item] << ({needle: needle, score: result[:score]})
46
+ end
47
+ end
48
+ end.sort_by do |result|
49
+ result[:score].ratio
50
+ end.reverse
51
+ end
52
+
53
+ paired = {}
54
+ #scores = {}
55
+ ranking.each do |item, results|
56
+ sorted = results.reject do |result|
57
+ paired.key?(result[:needle])
58
+ end.sort_by do |result|
59
+ result[:score].ratio
60
+ end.reverse
61
+ if result = sorted.shift
62
+ paired[result[:needle]] = {
63
+ pair: item,
64
+ score: result[:score]
65
+ }
66
+ end
67
+ end
68
+
69
+ pending_items = haystack - paired.values
70
+ faceted.reject do |needle, results|
71
+ paired.key?(needle)
72
+ end.each do |needle, results|
73
+ results.select! do |result|
74
+ pending_items.include?(result[:pair]) && result[:score].ratio > 0.05
75
+ end
76
+ if result = results.shift
77
+ paired[needle] = result
78
+ pending_items.delete(result[:pair])
79
+ end
80
+ end
81
+
82
+ pending_needles = needles - paired.keys
83
+ pending_needles.each do |needle|
84
+ paired[needle] = {
85
+ pair: nil,
86
+ score: Score.new(0, needle.length)
87
+ }
88
+ end
89
+ paired.transform_values do |result|
90
+ case format
91
+ when Array
92
+ result.values_at(*format)
93
+ else
94
+ restult[format]
95
+ end
96
+ end
97
+ end
98
+
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,67 @@
1
+ module Eco
2
+ module Data
3
+ module FuzzyMatch
4
+ class Result < Struct.new(:match, :value, :dice, :levenshtein, :jaro_winkler, :ngrams, :words_ngrams, :chars_position)
5
+ ALL_METHODS = [:dice, :levenshtein, :jaro_winkler, :ngrams, :words_ngrams, :chars_position]
6
+
7
+ def dice; super&.round(3); end
8
+ def levenshtein; super&.round(3); end
9
+ def jaro_winkler; super&.round(3); end
10
+ def ngrams; super&.round(3); end
11
+ def words_ngrams; super&.round(3); end
12
+ def chars_position; super&.round(3); end
13
+
14
+ # TODO: print in the order of `order`
15
+ def print
16
+ msg = "(Dice: #{dice}) (Lev Dst: #{levenshtein}) "
17
+ msg << "(Jaro: #{jaro_winkler}) "
18
+ msg << "(Ngram: #{ngrams}) (WNgrams: #{words_ngrams}) "
19
+ msg << "(C Pos: #{chars_position}) "
20
+ msg << "'#{value}'"
21
+ end
22
+
23
+ def all_threshold?(methods = order, threshold = 0.15)
24
+ return true unless threshold
25
+ [methods].flatten.compact.all? {|method| threshold?(method, threshold)}
26
+ end
27
+
28
+ def any_threshold?(methods = order, threshold = 0.15)
29
+ return true unless threshold
30
+ [methods].flatten.compact.any? {|method| threshold?(method, threshold)}
31
+ end
32
+
33
+ def threshold?(method = :dice, threshold = 0.15)
34
+ raise "Uknown method '#{method}'" unless self.respond_to?(method)
35
+ self.send(method) >= threshold
36
+ end
37
+
38
+ def order=(values)
39
+ @order = [values].flatten.compact.tap do |o|
40
+ o = [:words_ngrams, :dice] if o.empty?
41
+ end
42
+ end
43
+
44
+ def order
45
+ @order ||= [:words_ngrams, :dice]
46
+ end
47
+
48
+ def <=>(result)
49
+ compare(result)
50
+ end
51
+
52
+ private
53
+
54
+ def compare(other, order: self.order)
55
+ return 0 unless method = order.first
56
+ raise "Uknown method '#{method}'" unless self.respond_to?(method) && other.respond_to?(method)
57
+ return -1 if self.send(method) > other.send(method)
58
+ return 1 if self.send(method) < other.send(method)
59
+ compare(other, order: order[1..-1])
60
+ end
61
+
62
+
63
+ end
64
+
65
+ end
66
+ end
67
+ end