coding_challenge 0.1.0 → 0.1.2

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: b402c49ac44e0b69892c35722a391481078caba6dea4b116b830d98716de84c8
4
- data.tar.gz: 19fc88c46f3c15a813004a82b1ff63d42ce72bfd25627f1fb5e9d42be3654037
3
+ metadata.gz: bfa7e08afbb3d98f9be768a7b97f9ec2c3d39ae40d5d4a1db52e4c8f2da4b87b
4
+ data.tar.gz: d85e74ec08bb62fb508dc06f760acb8f6a562399b797cbb19239d374dbe9a9c0
5
5
  SHA512:
6
- metadata.gz: 730739aea7cb1626cfe6211aea8fff5a4035ec3e852e64a4a4b6783f8b58a39b45c67bab546face8a3473ce80ad772b6b56654f3c610602e06a223dabace5d8e
7
- data.tar.gz: 253be807c4c0b9746aed688ee21a7b615721ce55bc8601db3757f59da8794b3a69788b61593868310f0c8d1750cf518be6228204a22ebce8be4f51b0c7203d67
6
+ metadata.gz: ce40b7010eaadf8b84842db73b690938f311627498bbedd01df1d8e09382aa1377e54ed0a8307f232ed7725277ba6201c40f5a7e5f8073f050cc1d77e9d9a975
7
+ data.tar.gz: fb13542e35ab806616132e23a8f2166d10d95d55b24cd46f62242e6e2182f63ad226e8bb1d76930c72955da03b872c36d759efcab914dd56f0ca500b442a58d6
data/.gitignore CHANGED
@@ -9,3 +9,5 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+
13
+ .gem
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- coding_challenge (0.1.0)
4
+ coding_challenge (0.1.2)
5
5
  colorize (~> 0.8.1)
6
6
  lolcat (~> 100.0, >= 100.0.1)
7
7
  thor (~> 1.0, >= 1.0.1)
data/README.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # CodingChallenge
2
2
 
3
- Had fun with this. Published it as gem.
3
+ Had fun with this. I published it as gem here: https://rubygems.org/gems/coding_challenge.
4
+
5
+ ## Quickstart
6
+
7
+ `gem install coding_challenge` then
8
+ `coding_challenge start`
9
+
10
+ ## Screenshots
11
+
12
+ 1. using CLI menu to input arguements
13
+ ![gif1](./gif1.gif)
14
+ 2. inputting arguments directly from commandline
15
+ ![gif2](./gif2.gif)
4
16
 
5
17
  ## Installation
6
18
 
@@ -10,7 +22,7 @@ Clone repo. Then in project directory `bundle install`.
10
22
 
11
23
  2. via rubygems
12
24
 
13
- ``gem install coding_challenge```
25
+ `gem install coding_challenge`
14
26
 
15
27
  ## Usage
16
28
 
@@ -20,91 +32,96 @@ To run with a cool CLI UI at the start, either do:
20
32
  - (IF INSTALLED AS GEM) from CLI do: `coding_challenge start [product type] [options]`
21
33
 
22
34
  To run without a cool CLI UI at the start but have it appear after, either do:
23
- _ (IF CLONED) Go to project directory and: `./exe/coding_challenge start [product type] [options] --skip_intro_animation=true`
24
- _ (IF INSTALLED AS GEM) from CLI do: `coding_challenge start [product type] [options] --skip_intro_animation=true`
35
+
36
+ - (IF CLONED) Go to project directory and: `./exe/coding_challenge start [product type] [options] --skip_intro_animation=true`
37
+ - (IF INSTALLED AS GEM) from CLI do: `coding_challenge start [product type] [options] --skip_intro_animation=true`
25
38
 
26
39
  ## Testing
27
40
 
28
41
  Clone and go to project directory and do `rspec spec`
29
42
 
30
- ## Code Highlight
31
-
32
- I use an object to group together the query args and results like so
33
-
34
- ```
35
- class Query
36
- attr_reader :product_type, :options, :results
37
- attr_writer :performed_at, :results
38
-
39
- def initialize(query_args)
40
- @product_type = query_args[0]
41
- @options = query_args.slice(1, query_args.length)
42
- @performed_at = nil
43
- @results = nil
44
- end
43
+ ## Code Explanation
45
44
 
46
- def formatted_results
47
- results_str = "Performed At #{@performed_at}\n"
48
- results_str += " Product Type Arg: #{@product_type}\n"
49
- results_str += " Options Args: #{@options.join(', ')}\n"
50
- results_str += " Results:\n"
51
- results_str += " #{@results.join("\n ")}"
45
+ Since the product list is represented as an array, there is NO
46
+ way to solve this problem in O(1) time.
52
47
 
53
- results_str
54
- end
55
- end
56
- ```
48
+ At the very least, any solution is going to require 1 full iteration through the product list.
49
+ What we can do, is create a hash based schema that will group all of the product types, option types,
50
+ and option values as keys in a logical hierachy so that afterwards, detecting the prescence of data can be done
51
+ simply by attempting to access it from the hash as a key O(1) time.
57
52
 
58
- This an instance of this class is then passed to an instance of a class I made in lib/coding_challenge/commands/util/Inventory.rb as an arg to this method
53
+ I tried to do this compactly/elegantly by creating the following method and having it execute
54
+ immediately after reading the products list:
59
55
 
60
56
  ```
61
- def handle_query(query)
62
- remaining_props_seen = {}
63
- remaining_props = []
64
-
65
- @products_list.each do |product|
66
- next if product['product_type'] != query.product_type
67
-
68
- search_start = query.options.length
69
- option_types = product['options'].keys
70
-
71
- (search_start..option_types.length - 1).each do |i|
72
- option_type = option_types[i]
73
- option_value = product['options'][option_type]
74
-
75
- if remaining_props_seen[option_type].nil?
76
- remaining_props_seen[option_type] = {}
77
- remaining_props_seen[option_type][option_value] = true
78
- remaining_props.push("#{option_type.capitalize}: #{option_value}")
79
- elsif !remaining_props_seen[option_type][option_value]
80
- remaining_props_seen[option_type][option_value] = true
81
- remaining_props[i - search_start] += ", #{option_value}"
82
- end
57
+ def index_product_schema(products_list)
58
+ products_schema = {}
59
+ products_list.each do |p|
60
+ if !products_schema.key?(p['product_type'])
61
+ products_schema[p['product_type']] = p['options'].transform_values { |o| Hash[o, true] }
62
+ else
63
+ products_schema[p['product_type']].merge!(p['options']) { |_, o, n| o.merge(Hash[n, true]) }
83
64
  end
84
65
  end
66
+ products_schema
67
+ end
85
68
  ```
86
69
 
87
- This is the main query algorithm.
70
+ The above operation going to require one loop through the product list and then for each item,
71
+ a nested loop that runs for the number of option types that exist for that item.
88
72
 
89
- A cool thing you can do is specify for the Inventory class to load in a product list in a JSON file from a URL or alternate file path. You can do this from the CLI menu.
90
- This method invokes some private methods I created for handling the cases but the logic is captured here:
73
+ The generated products schema would look like this:
91
74
 
92
- ```
93
- def load_products_list_from_source(source_type, source_uri)
94
- if source_type == 'FILE PATH'
95
- load_from_file_path(source_uri)
96
- elsif source_type == 'URL'
97
- load_from_file_url(source_uri)
98
- end
75
+ `{"tshirt"=>{"gender"=>{"male"=>true, "female"=>true}, "color"=>{"red"=>true, "green"=>true, "navy"=>true, "white"=>true, "black"=>true}, "size"=>{"small"=>true, "medium"=>true, "large"=>true, "extra-large"=>true, "2x-large"=>true}}, "mug"=>{"type"=>{"coffee-mug"=>true, "travel-mug"=>true}}, "sticker"=>{"size"=>{"x-small"=>true, "small"=>true, "medium"=>true, "large"=>true, "x-large"=>true}, "style"=>{"matte"=>true, "glossy"=>true}}}`
76
+
77
+ Run Time ~> sum of O(num_option_types<sub>i</sub>) where i goes from 1 to the length of the products_list array
99
78
 
100
- unless @products_list.nil?
101
- @source_type = source_type
102
- @source_uri = source_uri
79
+ Next, the following method below will execute using the product_schema produced by `index_product_schema`.
80
+
81
+ You can see that validating the precesence of/accessing the options schema for a particular
82
+ product type is done in O(1) in the first line.
83
+
84
+ In an option schema for a given product type, the keys are option types and the values are hashes that contain keys
85
+ every possible option value for a given option type.
86
+ ex for sticker:
87
+
88
+ `{"size"=>{"x-small"=>true, "small"=>true, "medium"=>true, "large"=>true, "x-large"=>true}, "style"=>{"matte"=>true, "glossy"=>true}}`
89
+ We can now iterate through all of the option types/option values pairs using an index value (arg_position) to keep
90
+ track of our position in the hash.
91
+
92
+ Validating an options argument against possible option values is done using the index to match up
93
+ the CLI argument at the position of the index to the current option types/option values pair. We do this in O(1)
94
+ time by seeing if the argument exists as a key in the option values pair.
95
+
96
+ The current option values hash is transformed into a friendly string by joining the keys
97
+ together with commas.
98
+
99
+ The main loop will execute for the number of option types for a given product type regardless of the
100
+ size of the cli arguments input, which is a respective constant for each product type, so it is there for O(1).
101
+
102
+ ```
103
+ def handle_query(query)
104
+ product_options_schema = @products_schema[query.product_type.downcase]
105
+ is_invalid_product_type = product_options_schema.nil?
106
+ raise InvalidProductTypeError, query.product_type if is_invalid_product_type
107
+
108
+ results = []
109
+ product_options_schema.each_with_index do |(option_type, option_values_map), arg_position|
110
+ option_argument = query.options[arg_position]
111
+ is_argument_provided = !option_argument.nil?
112
+
113
+ if is_argument_provided
114
+ is_invalid_argument = !option_values_map.key?(option_argument)
115
+ raise InvalidOptionError.new(query.product_type, option_type, option_argument) if is_invalid_argument
116
+ else
117
+ possible_option_values = option_values_map.keys
118
+ results << "#{option_type.capitalize}: #{possible_option_values.join(', ')}"
119
+ end
103
120
  end
104
121
 
105
- @products_list
122
+ query.results = results
123
+ query
106
124
  end
107
-
108
125
  ```
109
126
 
110
127
  ## Contributing
@@ -118,7 +135,3 @@ Everyone interacting in the CodingChallenge project's codebases, issue trackers,
118
135
  ## Copyright
119
136
 
120
137
  Copyright (c) 2020 Jorge Navarro. See [MIT License](LICENSE.txt) for further details.
121
-
122
- ```
123
-
124
- ```
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.summary = 'A hiring coding challenge.'
13
13
  spec.description = 'A Ruby CLI app made for hiring coding challenge.'
14
14
  spec.homepage = 'https://github.com/Jnavarr56/REDACTED-3-coding-challenge'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
16
16
 
17
17
  # blank for now
18
18
  # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
Binary file
Binary file
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../command'
4
- require_relative './util/Inventory'
5
- require_relative './util/Query'
4
+ require_relative './util/inventory'
5
+ require_relative './util/query'
6
6
  require_relative './util/animation'
7
+ require_relative './util/invalid_product_type_error'
8
+ require_relative './util/invalid_option_error'
9
+ require_relative './util/file_read_error'
7
10
 
8
11
  require 'tty-spinner'
9
12
  require 'colorize'
@@ -35,16 +38,20 @@ module CodingChallenge
35
38
  loading_animation('Calculating results...', 2)
36
39
 
37
40
  new_inventory = Inventory.new
38
- new_inventory.load_products_list_from_default
39
- @inventory = new_inventory
40
41
 
41
- new_query = Query.new(cli_args)
42
- query_with_results = @inventory.handle_query(new_query)
42
+ begin
43
+ new_inventory.load_products_list_from_default
44
+ @inventory = new_inventory
45
+ new_query = Query.new(cli_args)
46
+ query_with_results = @inventory.handle_query(new_query)
43
47
 
44
- puts 'Results:'.colorize(:yellow)
45
- puts query_with_results.results
48
+ puts 'Results:'.colorize(:yellow)
49
+ puts query_with_results.results
46
50
 
47
- @queries.push(query_with_results.formatted_results)
51
+ @queries.push(query_with_results.formatted_results)
52
+ rescue StandardError => e
53
+ puts "#{e.class.name}: #{e.message}".colorize(:red)
54
+ end
48
55
  end
49
56
 
50
57
  exited = false
@@ -124,14 +131,11 @@ module CodingChallenge
124
131
  loading_animation('Checking file readability...', 2)
125
132
  begin
126
133
  new_inventory.load_products_list_from_source('FILE PATH', new_filepath)
127
-
128
- raise StandardError if new_inventory.products_list.nil?
129
-
130
134
  @inventory = new_inventory
131
135
  prompt.say("\nFile loaded successfully!")
132
136
  done = true
133
- rescue StandardError
134
- prompt.say('Could not read this file!')
137
+ rescue StandardError => e
138
+ puts "#{e.class.name}: #{e.message}".colorize(:red)
135
139
  end
136
140
  end
137
141
  end
@@ -146,15 +150,14 @@ module CodingChallenge
146
150
  spinner.auto_spin
147
151
  begin
148
152
  new_inventory.load_products_list_from_source('URL', new_file_url)
149
- raise StandardError if new_inventory.products_list.nil?
150
-
151
153
  @inventory = new_inventory
152
154
  prompt.say("\nFile fetched successfully!")
153
155
  done = true
154
- rescue StandardError
155
- prompt.say('Could not fetch this file!')
156
+ spinner.stop
157
+ rescue StandardError => e
158
+ spinner.stop
159
+ puts "#{e.class.name}: #{e.message}".colorize(:red)
156
160
  end
157
- spinner.stop
158
161
  end
159
162
 
160
163
  end
@@ -163,12 +166,10 @@ module CodingChallenge
163
166
  spinner.auto_spin
164
167
  begin
165
168
  new_inventory.load_products_list_from_default
166
- raise StandardError if new_inventory.products_list.nil?
167
-
168
169
  @inventory = new_inventory
169
170
  prompt.say("\nDefault file fetched successfully!")
170
- rescue StandardError
171
- prompt.say('Could not fetch the default file!')
171
+ rescue StandardError => e
172
+ puts "#{e.class.name}: #{e.message}".colorize(:red)
172
173
  end
173
174
  spinner.stop
174
175
  end
@@ -202,11 +203,15 @@ module CodingChallenge
202
203
  puts ''
203
204
 
204
205
  new_query = Query.new(args)
205
- query_with_results = @inventory.handle_query(new_query)
206
- loading_animation('Calculating...', 2)
207
- puts 'Results:'.colorize(:yellow)
208
- puts query_with_results.results
209
- @queries.push(query_with_results.formatted_results)
206
+
207
+ begin
208
+ query_with_results = @inventory.handle_query(new_query)
209
+ puts 'Results:'.colorize(:yellow)
210
+ puts query_with_results.results
211
+ @queries.push(query_with_results.formatted_results)
212
+ rescue StandardError => e
213
+ puts "#{e.class.name}: #{e.message}".colorize(:red)
214
+ end
210
215
 
211
216
  0
212
217
  end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative './invalid_product_type_error'
4
+ require_relative './invalid_option_error'
5
+ require_relative './file_read_error'
6
+
3
7
  require 'net/http'
4
8
  require 'json'
5
9
  require 'date'
@@ -11,87 +15,86 @@ class Inventory
11
15
  @source_type = nil
12
16
  @source_uri = nil
13
17
  @products_list = nil
18
+ @products_schema = nil
14
19
  end
15
20
 
16
21
  def handle_query(query)
17
- remaining_props_seen = {}
18
- remaining_props = []
19
-
20
- @products_list.each do |product|
21
- next if product['product_type'] != query.product_type
22
-
23
- search_start = query.options.length
24
- option_types = product['options'].keys
25
-
26
- (search_start..option_types.length - 1).each do |i|
27
- option_type = option_types[i]
28
- option_value = product['options'][option_type]
29
-
30
- if remaining_props_seen[option_type].nil?
31
- remaining_props_seen[option_type] = {}
32
- remaining_props_seen[option_type][option_value] = true
33
- remaining_props.push("#{option_type.capitalize}: #{option_value}")
34
- elsif !remaining_props_seen[option_type][option_value]
35
- remaining_props_seen[option_type][option_value] = true
36
- remaining_props[i - search_start] += ", #{option_value}"
37
- end
22
+ product_options_schema = @products_schema[query.product_type.downcase]
23
+ is_invalid_product_type = product_options_schema.nil?
24
+ raise InvalidProductTypeError, query.product_type if is_invalid_product_type
25
+
26
+ results = []
27
+ product_options_schema.each_with_index do |(option_type, option_values_map), arg_position|
28
+ option_argument = query.options[arg_position]
29
+ is_argument_provided = !option_argument.nil?
30
+
31
+ if is_argument_provided
32
+ is_invalid_argument = !option_values_map.key?(option_argument)
33
+ raise InvalidOptionError.new(query.product_type, option_type, option_argument) if is_invalid_argument
34
+ else
35
+ possible_option_values = option_values_map.keys
36
+ results << "#{option_type.capitalize}: #{possible_option_values.join(', ')}"
38
37
  end
39
38
  end
40
39
 
41
- query.performed_at = DateTime.now
42
- query.results = remaining_props
43
-
40
+ query.results = results
44
41
  query
45
42
  end
46
43
 
47
44
  def load_products_list_from_source(source_type, source_uri)
48
- if source_type == 'FILE PATH'
49
- load_from_file_path(source_uri)
50
- elsif source_type == 'URL'
51
- load_from_file_url(source_uri)
45
+ begin
46
+ products_list = load_from_file_path(source_uri) if source_type == 'FILE PATH'
47
+ products_list = load_from_file_url(source_uri) if source_type == 'URL'
48
+ raise StandardError if products_list.nil?
49
+ rescue StandardError
50
+ raise FileReadError, source_uri
52
51
  end
52
+ set_as_data_source(source_type, source_uri, products_list)
53
+ end
53
54
 
54
- unless @products_list.nil?
55
- @source_type = source_type
56
- @source_uri = source_uri
55
+ def load_products_list_from_default
56
+ begin
57
+ products_list = load_from_file_url(@@DEFAULT_PRODUCTS_LIST_URL)
58
+ raise StandardError if products_list.nil?
59
+ rescue StandardError
60
+ raise FileReadError, source_uri
57
61
  end
58
-
59
- @products_list
62
+ set_as_data_source('DEFAULT', @@DEFAULT_PRODUCTS_LIST_URL, products_list)
60
63
  end
61
64
 
62
- def load_products_list_from_default
63
- load_from_file_url(@@DEFAULT_PRODUCTS_LIST_URL)
65
+ private
64
66
 
65
- unless @products_list.nil?
66
- @source_type = 'DEFAULT'
67
- @source_uri = @@DEFAULT_PRODUCTS_LIST_URL
67
+ def index_product_schema(products_list)
68
+ products_schema = {}
69
+ products_list.each do |p|
70
+ if !products_schema.key?(p['product_type'])
71
+ products_schema[p['product_type']] = p['options'].transform_values { |o| Hash[o, true] }
72
+ else
73
+ products_schema[p['product_type']].merge!(p['options']) { |_, o, n| o.merge(Hash[n, true]) }
74
+ end
68
75
  end
69
-
70
- @products_list
76
+ products_schema
71
77
  end
72
78
 
73
- private
79
+ def set_as_data_source(source_type, source_uri, products_list)
80
+ @products_list = products_list
81
+ @products_schema = index_product_schema(products_list)
82
+ @source_type = source_type
83
+ @source_uri = source_uri
84
+ end
74
85
 
75
86
  def load_from_file_path(file_path)
76
87
  products_list_file = File.open(file_path)
77
88
  products_list_file_content = products_list_file.read
78
89
  products_list_hash = JSON.parse(products_list_file_content)
79
-
80
- @products_list = products_list_hash
81
- rescue StandardError
82
- @products_list = nil
90
+ products_list_hash
83
91
  end
84
92
 
85
93
  def load_from_file_url(url)
86
94
  request_uri = URI(url)
87
- begin
88
- request_response = Net::HTTP.get_response(request_uri)
89
- request_response_content = request_response.body
90
- products_list_hash = JSON.parse(request_response_content)
91
-
92
- @products_list = products_list_hash
93
- rescue StandardError
94
- @products_list = nil
95
- end
95
+ request_response = Net::HTTP.get_response(request_uri)
96
+ request_response_content = request_response.body
97
+ products_list_hash = JSON.parse(request_response_content)
98
+ products_list_hash
96
99
  end
97
100
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FileReadError < StandardError
4
+ def initialize(source_uri)
5
+ super("Could not read file from #{source_uri}!")
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InvalidOptionError < StandardError
4
+ def initialize(product_type, option_type, option_argument)
5
+ super("Product of type #{product_type.upcase} and #{option_type.upcase} option type of value #{option_argument.upcase} not in catalog!")
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InvalidProductTypeError < StandardError
4
+ def initialize(product_type)
5
+ super("Product of type #{product_type.upcase} not in catalog!")
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module CodingChallenge
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coding_challenge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jorge Navarro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-03 00:00:00.000000000 Z
11
+ date: 2020-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -127,6 +127,8 @@ files:
127
127
  - bin/setup
128
128
  - coding_challenge.gemspec
129
129
  - exe/coding_challenge
130
+ - gif1.gif
131
+ - gif2.gif
130
132
  - lib/coding_challenge.rb
131
133
  - lib/coding_challenge/cli.rb
132
134
  - lib/coding_challenge/command.rb
@@ -135,6 +137,9 @@ files:
135
137
  - lib/coding_challenge/commands/util/Inventory.rb
136
138
  - lib/coding_challenge/commands/util/Query.rb
137
139
  - lib/coding_challenge/commands/util/animation.rb
140
+ - lib/coding_challenge/commands/util/file_read_error.rb
141
+ - lib/coding_challenge/commands/util/invalid_option_error.rb
142
+ - lib/coding_challenge/commands/util/invalid_product_type_error.rb
138
143
  - lib/coding_challenge/templates/.gitkeep
139
144
  - lib/coding_challenge/templates/start/.gitkeep
140
145
  - lib/coding_challenge/version.rb
@@ -153,7 +158,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
153
158
  requirements:
154
159
  - - ">="
155
160
  - !ruby/object:Gem::Version
156
- version: 2.3.0
161
+ version: 2.6.0
157
162
  required_rubygems_version: !ruby/object:Gem::Requirement
158
163
  requirements:
159
164
  - - ">="