I recently was given this question for an interview application and am wondering where I went wrong. (The whole answer is contained in a skeleton which they gave me which I haven't included here so it's possible I failed due to a mistake outside the files posted here.)
EDIT: So I got feedback from the company:
Cons:
No use of Enumerable methods e.g.
#mapUse of
RuntimeErrorfor validations and error messages. Handling this kind of logic without exceptions tends to be more flexible.- Accessing instance variables from outside the object.
No use of existing mocking libraries.
#cleartest is not an effective test.Everything in a single file.
Ruby's code conventions are not really followed.
I have two questions about this.
- What do they mean by "No use of Enumerable methods e.g.
#map"? - What should I be doing instead of using
set_tablefor my unit tests? I created this because I wanted to be able to create a table withoutcreate_table, so my tests stay independent.
The program is run by:
bitmap_reader = BitmapEditorFileReader()
bitmap_reader.run()
The program should read a file containing string commands, and edit a 2D list based on these commands. The question is framed as a bitmap editor, i.e. the 2D list represents a bitmap image, with each element being a letter which represents a colour. The commands are as follows:
I N M - Create a new M x N image with all pixels coloured white (O).
C - Clears the table, setting all pixels to white (O).
L X Y C - Colours the pixel (X,Y) with colour C.
V X Y1 Y2 C - Draw a vertical segment of colour C in column X between rows Y1 and Y2 (inclusive).
H X1 X2 Y C - Draw a horizontal segment of colour C in row Y between columns X1 and X2 (inclusive).
S - Show the contents of the current image
class BitmapEditorFileReader
def run(file)
return puts "please provide correct file" if file.nil? || !File.exists?(file)
bitmap_editor = BitmapEditor.new
command_reader = CommandReader.new(bitmap_editor)
File.open(file).each do |line|
command_reader.defer_to_method(line)
end
end
end
class BitmapEditor
def create_table(width, height)
_validate_height_and_width(width, height)
@width, @height = width, height
@table = Array.new(height) {Array.new(width, "O")}
end
def clear
_check_table_exists()
@table.each do |row|
row.each do |element|
element = "O"
end
end
end
def colour_pixel(x, y, colour)
_check_table_exists()
x, y = _change_strings_to_integers([x, y])
_validate_rows_and_columns([x], [y])
@table[y-1][x-1] = colour
end
def vertical_line(x, y_1, y_2, colour)
_check_table_exists()
_validate_rows_and_columns([x], [y_1, y_2])
min_y, max_y = [y_1, y_2].minmax
y = min_y
while y <= max_y
colour_pixel(x, y, colour)
y = y + 1
end
end
def horizontal_line(x_1, x_2, y, colour)
_check_table_exists()
_validate_rows_and_columns([x_1, x_2], [y])
min_x, max_x = [x_1, x_2].minmax
x = min_x
while x <= max_x
colour_pixel(x, y, colour)
x = x + 1
end
end
def display
_check_table_exists()
@table.each do |row|
row.each do |pixel|
print pixel
end
puts ""
end
end
def _check_table_exists
if @table == nil
raise "Table has not yet been created. Please create table before other commands"
end
end
def _validate_height_and_width(width, height)
if width <= 0 or height <= 0
raise "Height and Width must both be greater than 0"
end
end
def _validate_rows_and_columns(columns, rows)
rows.each do |row|
if row < 1 or row > @height
raise "Row must be between 1 and the height (inclusive)"
end
end
columns.each do |column|
if column < 1 or column > @width
raise "Column must be between 1 and the width (inclusive)"
end
end
end
def _change_strings_to_integers(args)
integer_args = []
begin
args.each do |arg|
int_arg = arg.to_i
integer_args.push(int_arg)
end
rescue SyntaxError
raise "All numbers must be integers"
end
return integer_args
end
end
class CommandReader
def initialize(bitmap_editor)
@bitmap_editor = bitmap_editor
end
@@command_map = {
"I" => "create_table",
"C" => "clear",
"L" => "colour_pixel",
"V" => "vertical_line",
"H" => "horizontal_line",
"S" => "display"
}
@@argument_types_map = {
"I" => ["Integer", "Integer", "String"],
"C" => [],
"L" => ["Integer", "Integer", "String"],
"V" => ["Integer", "Integer", "Integer", "String"],
"H" => ["Integer", "Integer", "Integer", "String"],
"S" => []
}
def defer_to_method(command_string)
method_code, *args = command_string.split
method_string = @@command_map[method_code]
argument_types = @@argument_types_map[method_code]
args = _convert_args_types(args, argument_types)
@bitmap_editor.send(method_string, *args)
end
def _convert_args_types(args, argument_types)
arg_type = nil
index = nil
begin
args.each_with_index do |arg, index|
arg_type = argument_types[index]
args[index] = send(arg_type, arg)
end
rescue ArgumentError
raise "Argument number #{index} must be of type #{arg_type}"
end
return args
end
end
Unit Tests:
require "bitmap_editor"
require "rspec"
def set_table(bitmap_editor, x, y)
bitmap_editor.instance_variable_set(:@table, Array.new(y) {Array.new(x, "O")})
bitmap_editor.instance_variable_set(:@width, x)
bitmap_editor.instance_variable_set(:@height, y)
end
describe BitmapEditor do
describe '#CreateTable' do
before(:each) do
@bitmap_editor = BitmapEditor.new
end
it 'should raise exception on negative dimension argument' do
expect{@bitmap_editor.create_table(4, -1)}.to raise_error(RuntimeError)
end
it 'should raise exception on zero dimension argument' do
expect{@bitmap_editor.create_table(4, 0)}.to raise_error(RuntimeError)
end
it 'should create table of given positive dimensions' do
@bitmap_editor.create_table(4, 5)
expect(@bitmap_editor.instance_variable_get(:@table).length).to eq(5)
expect(@bitmap_editor.instance_variable_get(:@table)[0].length).to eq(4)
end
end
describe '#Clear' do
before(:each) do
@bitmap_editor = BitmapEditor.new
end
it 'should raise exception if table not created yet' do
expect{@bitmap_editor.clear()}.to raise_error(RuntimeError)
end
it 'should clear table if table created' do
set_table(@bitmap_editor, 4, 5)
@bitmap_editor.clear()
@bitmap_editor.instance_variable_get(:@table).each do |row|
row.each do |element|
expect(element).to eq("O")
end
end
end
end
describe '#ColourPixel' do
before(:each) do
@bitmap_editor = BitmapEditor.new
end
it 'should raise exception if table not created yet' do
expect{@bitmap_editor.colour_pixel(4, 5, "C")}.to raise_error(RuntimeError)
end
it 'should raise exception if negative index given' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.colour_pixel(2, -1, "C")}.to raise_error(RuntimeError)
end
it 'should raise exception if index is larger than table dimension' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.colour_pixel(2, 6, "C")}.to raise_error(RuntimeError)
end
it 'should change colour of element at given location' do
set_table(@bitmap_editor, 4, 5)
@bitmap_editor.colour_pixel(2, 2, "C")
@bitmap_editor.instance_variable_get(:@table).each_with_index do |row, y|
row.each_with_index do |element, x|
if x == 1 and y == 1
expect(element).to eq("C")
else
expect(element).to eq("O")
end
end
end
end
end
describe '#VerticalLine' do
before(:each) do
@bitmap_editor = BitmapEditor.new
end
it 'should raise exception if table not created yet' do
expect{@bitmap_editor.vertical_line(2, 1, 3, "C")}.to raise_exception(RuntimeError)
end
it 'should raise exception if negative vertical index given' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.vertical_line(2, -1, 3, "C")}.to raise_exception(RuntimeError)
end
it 'should raise exception if negative horizontal index given' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.vertical_line(-2, 1, 3, "C")}.to raise_exception(RuntimeError)
end
it 'should raise exception if vertical index larger than table dimension' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.vertical_line(2, 1, 6, "C")}.to raise_exception(RuntimeError)
end
it 'should raise exception if horizontal index larger than table dimension' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.vertical_line(6, 1, 3, "C")}.to raise_exception(RuntimeError)
end
it 'should draw vertical line at given location' do
set_table(@bitmap_editor, 4, 5)
@bitmap_editor.vertical_line(2, 1, 3, "C")
@bitmap_editor.instance_variable_get(:@table).each_with_index do |row, y|
row.each_with_index do |element, x|
if x == 1 and y >= 0 and y <= 2
expect(element).to eq("C")
else
expect(element).to eq("O")
end
end
end
end
end
describe '#HorizontalLine' do
before(:each) do
@bitmap_editor = BitmapEditor.new
end
it 'should raise exception if table not created yet' do
expect{@bitmap_editor.horizontal_line(1, 3, 2, "C")}.to raise_exception(RuntimeError)
end
it 'should raise exception if negative vertical index given' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.horizontal_line(1, 3, -2, "C")}.to raise_exception(RuntimeError)
end
it 'should raise exception if negative horizontal index given' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.horizontal_line(-1, 3, 2, "C")}.to raise_exception(RuntimeError)
end
it 'should raise exception if vertical index larger than table dimension' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.horizontal_line(1, 3, 6, "C")}.to raise_exception(RuntimeError)
end
it 'should raise exception if horizontal index larger than table dimension' do
set_table(@bitmap_editor, 4, 5)
expect{@bitmap_editor.horizontal_line(1, 6, 2, "C")}.to raise_exception(RuntimeError)
end
it 'should draw horizontalline at given location' do
set_table(@bitmap_editor, 4, 5)
@bitmap_editor.horizontal_line(1, 3, 2, "C")
@bitmap_editor.instance_variable_get(:@table).each_with_index do |row, y|
row.each_with_index do |element, x|
if y == 1 and x >= 0 and x <= 2
expect(element).to eq("C")
else
expect(element).to eq("O")
end
end
end
end
end
describe '#Display' do
before(:each) do
@bitmap_editor = BitmapEditor.new
end
it 'should raise exception if table not created yet' do
expect{@bitmap_editor.display()}.to raise_exception(RuntimeError)
end
it 'should display table if table created' do
set_table(@bitmap_editor, 4, 5)
$stdout = StringIO.new
@bitmap_editor.display()
output = "OOOO\nOOOO\nOOOO\nOOOO\nOOOO\n"
expect(output).to eq($stdout.string)
end
end
end
describe CommandReader do
class MockBitmapEditor
def initialize
@call_record = {
"create_table" => nil,
"clear" => nil,
"colour_pixel" => nil,
"vertical_line" => nil,
"horizontal_line" => nil,
"display" => nil
}
end
def create_table(*args)
@call_record["create_table"] = args
end
def clear
@call_record["clear"] = []
end
def colour_pixel(*args)
@call_record["colour_pixel"] = args
end
def vertical_line(*args)
@call_record["vertical_line"] = args
end
def horizontal_line(*args)
@call_record["horizontal_line"] = args
end
def display
@call_record["display"] = []
end
def get_args_of_call(method_string)
return @call_record[method_string]
end
end
describe '#DeferToMethod' do
before(:each) do
@mock_editor = MockBitmapEditor.new
@command_reader = CommandReader.new(@mock_editor)
end
it 'should call create_table method' do
command = "I 1 2"
@command_reader.defer_to_method(command)
expect(@mock_editor.get_args_of_call("create_table")).to eq([1, 2])
end
it 'should call clear method' do
command = "C"
@command_reader.defer_to_method(command)
expect(@mock_editor.get_args_of_call("clear")).to eq([])
end
it 'should call colour_pixel method' do
command = "L 3 4 A"
@command_reader.defer_to_method(command)
expect(@mock_editor.get_args_of_call("colour_pixel")).to eq([3, 4, "A"])
end
it 'should call vertical_line method' do
command = "V 5 6 7 B"
@command_reader.defer_to_method(command)
expect(@mock_editor.get_args_of_call("vertical_line")).to eq([5, 6, 7, "B"])
end
it 'should call horizontal_line method' do
command = "H 8 9 10 C"
@command_reader.defer_to_method(command)
expect(@mock_editor.get_args_of_call("horizontal_line")).to eq([8, 9, 10, "C"])
end
it 'should call print method' do
command = "S"
@command_reader.defer_to_method(command)
expect(@mock_editor.get_args_of_call("display")).to eq([])
end
end
end