I've designed a class NumberSet which contains an array on which I have to implement certain filters, which I've designed as classes. The code is:
class NumberSet
include Enumerable
def initialize
@arr=[]
end
def each (&block)
@arr.each do |member|
block.call (member)
end
end
def << number
@arr<<number unless @arr.include?(number)
end
def [] (sito)
@arr.select{|number| sito.filter(number)}
end
end
class Filter
attr_reader :filter
def initialize &block
@filter = block
end
def filter number
@filter.call(number)
end
end
class SignFilter
def initialize(sign)
@sign = sign
end
def filter number
return true if(@sign==:negative && number<0)
return true if(@sign==:positive && number>0)
return true if(@sign==:nonnegative && number >= 0)
return true if(@sign==:nonpositive && number <= 0)
end
end
class TypeFilter
def initialize(sign)
@sign = sign
end
def filter number
return true if (@sign==:complex && number.is_a?(Complex) == true)
return true if (@sign==:integer && number.is_a?(Integer) == true)
return true if (@sign==:real && (number.is_a?(Rational) == true
|| number.is_a?Float) == true))
end
end
All works well, but I have to also define the &
and |
operators so they work like intersection and union, meaning that &
should get only numbers that satisfy all filters and |
numbers that satisfy at least one of the filters.
Also, the syntax
numbers[SignFilter.new(:non_negative) & Filter.new { |number| number.even? }]
must be valid.
How do I define them so they work correctly?
To answer some of the comments, I am looking for a way to make the operators &
, |
work the way I want them. The problem is not how to write the syntax, but rather what should the code between def
and end
be.
You can define them just like any other method. I just tried this as an example:
class Adder
def &(number_to_add)
number_to_add + 20
end
end
a = Adder.new
a & 20
#=> 40
Just as a note because this looks like smell:
def filter number
return true if(@sign==:negative && number<0)
return true if(@sign==:positive && number>0)
return true if(@sign==:nonnegative && number >= 0)
return true if(@sign==:nonpositive && number <= 0)
end
Why try this:
class SignFilter
@@signs = {negative: "<", positive: ">", nonnegative: ">=", nonpositive: "<="}
@@signs.each do |meth,sign|
define_method meth do |number|
number.send(sign,0)
end
end
def initialize(sign)
@sign = sign
end
def filter(number)
self.public_send(@sign,number)
end
end
You can do the same with TypeFilter (as a hint Numeric
has a #real?
method) or define the methods individually. I know it is not an answer to your actual question I just like clean code. :)
Here's something to meditate on...
def filter number
return true if(@sign==:negative && number<0)
return true if(@sign==:positive && number>0)
return true if(@sign==:nonnegative && number >= 0)
return true if(@sign==:nonpositive && number <= 0)
end
can be written as:
def filter number
( @sign == :negative && number < 0 ) ||
( @sign == :positive && number > 0 ) ||
( @sign == :nonnegative && number >= 0 ) ||
( @sign == :nonpositive && number <= 0 )
end
Why? Because each of the tests returns either true or false. If one returns true, none of the others joined by ||
will be tested because ||
short-circuits, and the final value Ruby sees will be true, which will be returned. If the first fails, ||
will tell Ruby to check the next, and on down the chain.
This mimics the behavior of the original code, but there's a possible problem in either. In a successful result, true is returned. Failure is going to be nil. Is that what you want, or do you want false returned to make the method only return true/false? In Ruby nil is a "falsey" value so it'll work the same but for consistency you might want to only return true/false.
Also, as a coding-style recommendation, use whitespace between your operators.
@sign==:negative && number<0
is not as readable as
@sign == :negative && number < 0
Readability is very important when debugging, or after passing the code on to someone else for maintenance. People like to say they'll fix it later, but later comes, the code remains the same and gets pushed into production, where it waits to irritate some poor soul at 3AM.
How about something like this?
class Filter
def initialize(&filter_block)
@filter_block = filter_block
end
def filter(number)
@filter_block.call(number)
end
def &(other)
CompositeFilterAND.new(self, other)
end
def |(other)
CompositeFilterOR.new(self, other)
end
end
class CompositeFilterAND
attr_accessor :left, :right
def initialize(left, right)
@left, @right = left, right
end
def filter(number)
@left.filter(number) && @right.filter(number) # This is the magic.
end
end
class CompositeFilterOR
attr_accessor :left, :right
def initialize(left, right)
@left, @right = left, right
end
def filter(number)
@left.filter(number) || @right.filter(number) # And this.
end
end