coding_challenge 0.1.0 → 0.1.2

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 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
  - - ">="