Alright, the journey begins. I received the first specification! If you don’t know what this is about perhaps you missed out the story. The first part of the story nor the sotry itslef is relevant for understanding the code that follows. Just a way to keep it interesting and mysterious, until something working comes out of box. Now let me continue the story:
First the creator explained me that after years of debate he settled down on ruby as language for Inox. He then asked me to implement a Point class. At first I objected as why such a class would be of any use and why not represent a point as an array [x, y].
Spec
He told to observe how beautiful ruby’s OO model is. And how pragmatic it is to perform computation and handle numbers like object. For cross-platform, cross-language, cross-OS, and dirty debugging, it would be preferable to have a clean object to represent a point with the following specification:
describe Point do
before :each do
@p = Point.new
end
it "should have a property x" do
@p.should respond_to(:x)
end
it "should have a property y" do
@p.should respond_to(:y)
end
it "should have default x value of 0" do
@p.x.should == 0
end
it "should have default y value of 0" do
@p.y.should == 0
end
it "should have a setter for x" do
@p.x = 1313
@p.x.should == 1313
end
it "should have setter for y" do
@p.y = 1313
@p.y.should == 1313
end
it "should only accept numeric values for initialization" do
lambda { Point.new('1', '2') }.should raise_error
end
it "should only accept numeric values for x" do
lambda { @p.x = "String" }.should raise_error
end
it "should only accept numeric values for y" do
lambda { @p.y = "String" }.should raise_error
end
it "should being able to compare ==" do
p2 = Point.new
@p.should == p2
p2.x = 1313
@p.should_not == p2
@p.should_not == "Test"
end
it "should be castable to an array" do
@p.should respond_to(:to_a)
@p.to_a.should == [0,0]
end
it "should be castable to an hash" do
@p.should respond_to(:to_hash)
@p.to_hash.should == {:x => 0, :y => 0}
end
it "should be castable to Point" do
@p.should respond_to(:to_point)
@p.to_point.should == @p
end
it "an array of two elements should be castable to Point" do
p = [0,0].to_point
p.should == @p
end
it "an array contain more than 2 elements raise error" do
lambda { p = [1,2,3].to_point }.should raise_error
end
it "a hash containing (
=> value, :y =>value ) should be castable to Point" do
p = {:x => 0, :y => 0}.to_point
p.should == @p
end
it "a hash to_point should raise if the keys x and y are not found" do
lambda { p = {}.to_point }.should raise_error
end
end
The Source
At first sight it seams like it is an easy task. Well it is, but the first attempt spawned more than 200 lines of code, for solving this simple matter. I couldn’t let it be that way and after a few rethink, I settled down for the following implementation:
module Inox
# Representation of a Point, defined as Object for the ability to
# handle it in a good OO manner and furthur extention and
# modification.
#
# Struct is the base class of Point and as such Point inherits
# the already powerful methods of Struct.
#
# The assignment checks for Numeric kind of values
# to prevent errors.
class Point < Struct.new(:x, :y)
# added type check to enforce Numeric values
def initialize(*args)
super(*(args.empty? ? [0,0] : args))
unless self.x.kind_of? Numeric and self.y.kind_of? Numeric
self.x = 0
self.y = 0
raise "#{args} not a two Numeric values"
end
end
# added type check to enforce Numeric values
def x=(value)
raise "#{num} not a Numeric value" unless value.kind_of? Numeric
super(value)
end
# added type check to enforce Numeric values
def y=(value)
raise "#{num} not a Numeric value" unless value.kind_of? Numeric
super(value)
end
def to_hash
{:x => self.x, :y => self.y}
end
def to_point
self
end
end
end
# Extending the ability of Array to handle
# extra Inox types
class Array
# Cast an Array holding two Numerics to an instance of the class Point
#
# [1,3].to_point == Point.new(1,3)
def to_point
raise "Cannot convert #{self} to Point" unless length == 2
Inox::Point.new(self[0], self[1])
end
end
# Extending the ability of the Hash class to handle
# extra Inox types
class Hash
# Cast a Hash containing two keys [:x, :y] with Numeric values
# to an instance of the class Point
#
# {:x => 1, y => 3}.to_point == Point.new(1,3)
#
# Extra keys in the hash doesn't interfer
def to_point
raise "Cannot find keys 'x' and 'y'" unless key?(:x) and key?(:y)
Inox::Point.new(self[:x], self[:y])
end
end
Ruby Inox on GitHub
This file is part of the Ruby Inox project. Inox is a work in progress and this file might not be up to date. The status and latest files are available on GitHub
Related posts:
- Ruby Inox Part 3: Actions Here is the pursuit of a series of related articles...
- Brain damaged assertions chain After a while, when I am coding and have already...
- Ruby Inox Part 3.1: Actions Here is a short update for the Ruby Inox Part3:...
- Ruby Inox Part 4: Property Last time, we took a look at Actions. A standard...
- Ruby Inox Part 4.1: Property In Ruby Inox Part 4: Property I described I would...
Tags: cross language, Inox, oo model, Point class, Ruby