diff --git a/190701programming_question.txt b/190701programming_question.txt new file mode 100644 index 0000000..a5d0e87 --- /dev/null +++ b/190701programming_question.txt @@ -0,0 +1,82 @@ +The coding challenge is to create a small and simple database-like program. The challenge consists of four tasks. You are not required to complete all four tasks but you should do enough for us to be able to see the level and style of your coding ability. The main things we are evaluating with this problem are architecture design and your problem solving abilities. + +Ruby is preferred.You can use the standard library but do not use external libraries. We are keen to see how you code to solve a problem. Do not use sqlite or any other database engines so we can fully assess your abilities for this role. + + +1.) Importer and Datastore + + You will be provided with a pipe separated file in the following format. + + File Sample: + + PROJECT|SHOT|VERSION|STATUS|FINISH_DATE|INTERNAL_BID|CREATED_DATE + the hobbit|01|64|scheduled|2010-05-15|45.00|2010-04-01 13:35 + lotr|03|16|finished|2001-05-15|15.00|2001-04-01 06:47 + king kong|42|128|scheduled|2006-07-22|45.00|2006-08-04 07:22 + the hobbit|40|32|finished|2010-05-15|22.80|2010-03-22 01:10 + king kong|42|128|not required|2006-07-22|30.00|2006-10-15 09:14 + + Field Descriptions: + + PROJECT: The project name or code name of the shot. (Text, max size 64 char) + SHOT: The name of the shot. (Text, max size 64 char) + VERSION: The current version of the file. (Integer, between 0 and 65535 inclusive) + STATUS: The current status of the shot. (Text, max size 32 char) + FINISH_DATE: The date the work on the shot is scheduled to end. (Date in YYYY-MM-DD format) + INTERNAL_BID: The amount of days we estimate the work on this shot will take. (Floating-point number, between 0 and 65535) + CREATED_DATE: The time and date when this record is being added to the system. (Timestamp in YYYY-MM-DD HH:MM format) + + Your first task is to parse and import the file into a simple datastore. You may use any file format that you want to implement to store the data. Records in the datastore should be unique by PROJECT, SHOT and VERSION. Subsequent imports with the same logical record should overwrite the earlier records. + +2.) Query tool + + 2.1) select, order and filter + + The next task is to create a query tool that can execute simple queries against the datastore you created in step one. The tool should accept command line args for SELECT, ORDER and FILTER functions. + + Example: + $ ./query -s PROJECT,SHOT,VERSION,STATUS -o FINISH_DATE,INTERNAL_BID + + lotr,3,16,finished + king kong,42,128,not required + the hobbit,40,32,finished + the hobbit,1,64,scheduled + + $ ./query -s PROJECT,SHOT,VERSION,STATUS -f FINISH_DATE=2006-07-22 + + king kong,42,128,not required + + 2.2) group and aggregate functions + + The next step is to add group by and aggregate functions to your query tool. Your tool should support the following aggregates: + + MIN: select the minimum value from a column + MAX: select the maximum value from a column + SUM: select the summation of all values in a column (Only supported for number types) + COUNT: count the distinct values in a column + COLLECT: collect the distinct values in a column + + Example: + $ ./query -s PROJECT,INTERNAL_BID:sum,SHOT:collect -g PROJECT + + the hobbit,67.80,[1,40] + lotr,15.00,[3] + king kong,30.00,[42] + + + 2.3) advanced filter function + + Add a filter function which evaluates boolean AND and OR expressions in the following format: + + PROJECT="the hobbit" AND SHOT=1 OR SHOT=40 + + Assume AND has higher precedence than OR. Parentheses can be added to change the above statement to the more logical: + + PROJECT="the hobbit" AND (SHOT=1 OR SHOT=40) + + Example: + $ ./query -s PROJECT,INTERNAL_BID -f 'PROJECT="the hobbit" OR PROJECT="lotr"' + + the hobbit,45.00 + lotr,15.00 + the hobbit,22.80 diff --git a/lib/ace_of_base.rb b/lib/ace_of_base.rb index b4f6ff9..5369953 100644 --- a/lib/ace_of_base.rb +++ b/lib/ace_of_base.rb @@ -10,4 +10,5 @@ module AceOfBase require 'ace_of_base/record_serialiser' require 'ace_of_base/file_importer' require 'ace_of_base/storage' + require 'ace_of_base/query' end diff --git a/lib/ace_of_base/query.rb b/lib/ace_of_base/query.rb new file mode 100644 index 0000000..e0b7a32 --- /dev/null +++ b/lib/ace_of_base/query.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module AceOfBase + # A query builder for querying our data. + class Query + INVALID_FIELD_MESSAGE = 'Field %s is not a valid field name' + + def initialize(storage) + @fields = [] + @order_bys = [] + @filters = [] + @storage = storage + end + + def select(*fields) + ensure_valid_fields!(fields) + @fields = fields + + self + end + + def order_by(*fields) + ensure_valid_fields!(fields) + @order_bys = fields + self + end + + def filter(field, value) + ensure_valid_fields!([field]) + @filters << [field, value] + self + end + + def execute + @storage + .select(&method(:select_with_filters)) + .sort(&method(:sort_with_order_bys)) + .map(&method(:take_fields)) + end + + private + + attr_reader :fields, :order_bys, :filters, :storage + + def ensure_valid_fields!(fields) + fields.each do |field| + raise ArgumentError, format(INVALID_FIELD_MESSAGE, field) \ + unless Record::ATTRIBUTES.include?(field.to_sym) + end + end + + def take_fields(record) + selected_fields.map { |field| record.public_send(field) } + end + + def sort_with_order_bys(lhs, rhs) + @order_bys + .lazy + .map { |attr| lhs.public_send(attr) <=> rhs.public_send(attr) } + .find(-> { 0 }, &:nonzero?) + end + + def select_with_filters(record) + @filters.all? do |(key, value)| + record.public_send(key) == value + end + end + + def selected_fields + return Record::ATTRIBUTES if fields.empty? + + fields + end + end +end diff --git a/spec/ace_of_base/query_spec.rb b/spec/ace_of_base/query_spec.rb new file mode 100644 index 0000000..d105f38 --- /dev/null +++ b/spec/ace_of_base/query_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'fileutils' + +RSpec.describe AceOfBase::Query do + let(:tmpdir) { Dir.mktmpdir } + let(:storage) { AceOfBase::Storage.new(tmpdir) } + subject { described_class.new(storage) } + + around do |example| + # Import our fixture file. + path = File.expand_path('../fixtures/sample_file.txt', __dir__) + file = File.open(path, 'r') + importer = AceOfBase::FileImporter.import(file) + importer.records.each { |record| storage.store(record) } + + # Run the example + example.run + + # Clean up. + FileUtils.remove_entry(tmpdir, true) + end + + describe 'query with select and order by' do + subject do + super() + .select(:project, :shot, :version, :status) + .order_by(:finish_date, :internal_bid) + .execute + end + + it 'returns the correct data' do + expect(subject) + .to eq([ + ['lotr', '03', 16, 'finished'], + ['king kong', '42', 128, 'not required'], + ['the hobbit', '40', 32, 'finished'], + ['the hobbit', '01', 64, 'scheduled'] + ]) + end + end + + describe 'query with select and filter' do + subject do + super() + .select(:project, :shot, :version, :status) + .filter(:finish_date, Date.civil(2006, 7, 22)) + .execute + end + + it 'returns the correct data' do + expect(subject) + .to eq([['king kong', '42', 128, 'not required']]) + end + end +end