-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrecommendation_index.rb
More file actions
138 lines (117 loc) · 3.99 KB
/
Copy pathrecommendation_index.rb
File metadata and controls
138 lines (117 loc) · 3.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# frozen_string_literal: true
module MarketRecommendation
class RecommendationIndex
INDEX_KEYS = %i[naics_desc state_cd product_desc].freeze
attr_reader :csv_path, :state, :naics, :product, :raw_data
# @param csv_path [String]
# @param state [String] searched state
# @param naics [String] Naics description
# @param product [String] CL, GL, BOP, etc
# @param search_criteria [Hash] # possible use for new filters
def initialize(csv_path, state:, naics: nil, product: nil)
@csv_path = csv_path
@state = valid_state(state)
@naics = naics
@product = product
@indexed_data = nil
@raw_data = load_csv
end
# @return [Array<Hash>] An array of matching rows from the CSV.
def call
find(**query_builder)
rescue StandardError => e
log_error(error_debugging(e))
end
# @return [Hash] - Indexed data set
def indexed_data
@indexed_data ||= build_index(raw_data)
end
# @params query [Hash] builds the keys to search
# @return [Array<Hash>] return matching rows
def find(**query)
query_keys = query.keys.map(&:to_sym).sort
return [] unless query_keys.all? { |key| raw_data.first.key?(key) }
values = index_key(query_keys, query)
indexed_data[values] || []
end
private
# @return [Hash]
def query_builder
{
naics_desc: naics,
state_cd: state,
product_desc: product
}.reject { |_key, value| value.to_s.strip.empty? }
end
# Builds keys and utilizes helper methods to create
# a searchable index of hashses
# @return [Hash]
def build_index(data)
index = {}
data.each do |row|
index_key_combinations.each do |keys_for_index|
key = index_key(keys_for_index, row)
next if key.any?(&:nil?) || key.any? { |v| v.to_s.strip.empty? }
index[key] ||= []
index[key] << row
end
end
index
end
# This method takes the possible indexes and creates all
# combinations of the index keys.
# @return [Array<Symbol>]
def index_key_combinations
@index_key_combinations ||= (1..INDEX_KEYS.size).flat_map do |n|
INDEX_KEYS.combination(n).to_a
end.sort
end
# Create a new key out of the combinations of indexes
# @return [Array]
def index_key(keys, row)
keys.sort.map { |k| row[k] }
end
# Reads and parses the CSV file into an array of hashes.
# @return [Array<Hash>] An array of hashes, where keys are symbolized.
def load_csv
unless File.exist?(csv_path)
raise Errno::ENOENT, "CSV file not found at path: #{csv_path}"
end
CSV.read(csv_path, headers: true, header_converters: :symbol).map(&:to_h)
rescue CSV::MalformedCSVError => e
error_string = "Malformed CSV error in #{csv_path}: #{e.message}"
log_error(error_string)
end
def error_debugging(error)
<<~ERROR_MESSAGES
Error processing CSV data:
- CSV Path: #{csv_path}
- State: #{state}
- NAICS: #{naics}
- Product: #{product}
- Error: #{error.message}
- Backtrace: #{error.backtrace.join("\n")}
ERROR_MESSAGES
end
# @param message [String] The error message to log.
def log_error(message)
timestamp = Time.now.zone.strftime('%Y-%m-%d %H:%M:%S')
Rails.logger.error("[#{timestamp} | #{self.class}] #{message}")
end
# This ensures our user entered valid data for the state.
# @return [String] The state code in uppercase.
# @raise [ArgumentError] if the state identifier is blank or invalid.
def valid_state(state)
state = state.to_s.strip
raise ArgumentError, 'State identifier cannot be blank.' if state.blank?
valid_state = US_STATES.find do |k, v|
k.casecmp?(state) || v.casecmp?(state)
end
if valid_state.present?
valid_state.first.upcase
else
raise ArgumentError, "Invalid state identifier provided: #{state}"
end
end
end
end