On Fri, 10 Oct 2008 15:29:02 -0500, Matthew Moss wrote:


>> Just a preview of my solution (inspired by TZFile):
>>
>> 	Mod26=ModularBase.new(26)
>>        Mod8=ModularBase.new(8)
>>
>>       def test_incompat_bases
>>         assert_raises(ArgumentError) do
>>           Mod26.new(1)+Mod8.new(1)
>>         end
>>         assert_nothing_raised do
>>           Mod26.new(1)+Mod8.new(1).to_i
>>         end
>>       end
>>
>> That said, please clarify on the behavior of a couple tests:
>>
>>       #new
>>       def test_add_reverse
>>          a = Mod26.new(15)
>>          assert_equal(???, 15 + a)
>>       end
>>
>>       #new
>>       def test_sub_reverse
>>          a = Mod26.new(15)
>>          assert_equal(???, 1-a)
>>       end
>>
>>       #already defined
>>       def test_mul_int_reverse
>>          a = Mod8.new(15)
>>          assert_equal(77, 11 * a)
>>       end
>>
>> Clearly a Modulo +/*/- an Integer should give a Modulo Should the
>> reverse relation promote the Modulo to an Integer, or promote
>> the Integer to a Modulo (of the same base)?
> 
> 
> This issue requires some sort of design decision, which I intentionally
> left out of the requirements, so you may do what you like in this
> respect, and perhaps explain why you made a particular choice.
> 
> When I started experimenting with my own solution, my design choice was
> that the result type of any operation was the same as the first operand.
> I did this primarily for simplicity, since I didn't need to raise errors
> for incompatible bases, and most operations became trivial. Of course,
> this choice is not without problem, since some operations that might
> normally be communitive (if compatible modulo was enforced) are no
> longer communitive.
> 
> In context with your questions above and my particular design choice:
> 
> 1. I didn't raise any errors for mismatched bases; I used the left
> operand to determine the result's type. 2. As all your reverse tests
> (which should be considered by all implementors) have a left Integer
> argument, all the answers would be the same, and the domain would be the
> full integer set.  So, from top to bottom, I would replace ??? with 30
> and -14.

OK. I decided against that and decided that the operations would be 
commutative, always promoting to modular arithmetic.

Here's my solution:

module ModularBase
  def self.new  base
    c=Class.new do
      include ModularBase
    end
    c.class_eval do
      define_method :base do
        base
      end
    end
    c
  end

  def self.define_op op
    class_eval <<-"end;"
      def #{op} rhs
        case rhs
        when self.class: self.class.new(@value #{op} rhs.instance_variable_get(:@value))
        when Integer: self.class.new(@value #{op} rhs)
        when ModularBase: raise ArgumentError, "Error performing modular arithmetic with incompatible bases"
        else
          raise ArgumentError, "Requires an integer or compatible base"
        end
      end
    end;
  end
  
  #begin defining operations
  
  def initialize value
    @value = value % base
  end

  define_op :+
  define_op :-
  define_op :*

  def == rhs
    case rhs
    when self.class: @value == rhs.instance_variable_get(:@value)
    when ModularBase: false
    when Integer: @value == rhs
    else false
    end
  end

  def coerce other
    [self.class.new(other), self]
  end

  def <=> rhs
    case rhs
    when self.class: @value <=> rhs.instance_variable_get(:@value)
    when Integer: @value <=> rhs
    else raise ArgumentError
    end
  end

  def to_i
    @value
  end
  def to_s
    @value.to_s
  end
end

exit if __FILE__ != $0

require 'test/unit'

class TestModulo < Test::Unit::TestCase
   Mod26=ModularBase.new(26)
   Mod11=ModularBase.new(11)
   Mod8=ModularBase.new(8)

   def test_modulo_equality
      a = Mod26.new(23)
      b = Mod26.new(23)
      c = Mod26.new(179)
      assert_equal(a, b)
      assert_equal(a, c)
   end

   def test_add_zero
      a = Mod26.new(15)
      b = Mod26.new(0)
      assert_equal(a, a + b)
   end

   def test_add
      a = Mod26.new(15)
      b = Mod26.new(19)
      c = Mod26.new(8)
      assert_equal(c, a + b)
   end

   def test_add_reverse
      a = Mod26.new(15)
      assert_equal(4, 15 + a)
   end

   def test_add_int
      a = Mod26.new(15)
      c = Mod26.new(10)
      assert_equal(c, a + 21)
   end

   def test_sub
      a = Mod26.new(15)
      b = Mod26.new(19)
      c = Mod26.new(22)
      assert_equal(c, a - b)
   end

   def test_sub_reverse
      a = Mod26.new(15)
      assert_equal(12, 1-a)
   end

   def test_sub_int
      a = Mod26.new(15)
      c = Mod26.new(1)
      assert_equal(c, a - 66)
   end

   def test_mul
      a = Mod26.new(15)
      b = Mod26.new(7)
      c = Mod26.new(1)
      assert_equal(c, a * b)
   end

   def test_mul_int
      a = Mod26.new(15)
      c = Mod26.new(9)
      assert_equal(c, a * 11)
   end

   def test_mul_int_reverse
      a = Mod8.new(15)
      assert_equal(5, 11 * a)
   end

   def test_non_default_modulo
      a = Mod11.new(15)
      b = Mod11.new(9)

      assert_equal(2, a + b)
      assert_equal(6, a - b)
      assert_equal(3, a * b)
   end

   def test_compare
      assert_equal(1, Mod26.new(15) <=> Mod26.new(30))
   end

   def test_compare_int
      assert_equal(-1, Mod26.new(15) <=> 35)
   end

   def test_sort
      x = [Mod26.new(15), Mod26.new(29), Mod26.new(-6),  Mod26.new(57)]
      y = [3, 5, 15, 20]
      assert_equal(y, x.sort)
   end

   def test_incompat_bases
     assert_raises(ArgumentError) do
       Mod26.new(1)+Mod8.new(1)
     end
     assert_nothing_raised do
       Mod26.new(1)+Mod8.new(1).to_i
     end
   end
end

-- 
Chanoch (Ken) Bloom. PhD candidate. Linguistic Cognition Laboratory.
Department of Computer Science. Illinois Institute of Technology.
http://www.iit.edu/~kbloom1/