G
Gavin Kistner
(This isn't a library I plan on maintaining, but ANN felt like the
right prefix for offering up code for mass enjoyment.)
At work yesterday I needed to do some algebra on some matrix math to
reverse-derive a value. (To be specific, I needed to figure out how
to draw the three euler rotation values out of a ZXY rotation
matrix.) After spending 2 minutes writing out equations and fearing I
was making a mistake, I turned to Excel and some stupid text
manipulation to produce my formulae. After 10 minutes of messing
around, I got the answer wrong anyhow.
I decided to write a little Ruby library to do the task for me. I
wrote a matrix class which knows how to do matrix math on strings,
producing formulae from them. After the initial pass produced a lot
of values like "0*sinX" and "0 + 0 + 1*(sinX)" I made it a bit
smarter, so that 0s and 1s properly simplify the equations during
calculation.
It doesn't produce perfectly-reduced equations by any means, but it
did the trick. In the end, I got my result
Sample output follows, followed by the code itself. Enjoy!
M1:
---------------------------------------------------------------------
0 | 1 | 2
3 | 4 | 5
M2:
---------------------------------------------------------------------
x | y | z
3q | 4r | 5s
M1 scaled by 2:
---------------------------------------------------------------------
0 | 2 | 4
6 | 8 | 10
M2 scaled by 2:
---------------------------------------------------------------------
x * 2 | y * 2 | z * 2
3q * 2 | 4r * 2 | 5s * 2
M1 + M2:
---------------------------------------------------------------------
x | 1 + y | 2 + z
3 + 3q | 4 + 4r | 5 + 5s
X:
------------------------------------------------------------------------
-----
1 | 0 | 0
0 | cosX | sinX
0 | -sinX | cosX
Y:
------------------------------------------------------------------------
-----
cosY | 0 | -sinY
0 | 1 | 0
sinY | 0 | cosY
Z:
------------------------------------------------------------------------
-----
cosZ | sinZ | 0
-sinZ | cosZ | 0
0 | 0 | 1
X - Y:
------------------------------------------------------------------------
-----
1 - cosY | 0 | sinY
0 | cosX - 1 | sinX
-sinY | -sinX | cosX - cosY
X * Y * Z:
------------------------------------------------------------------------
-----
cosY * cosZ | cosY *
sinZ | -sinY
((sinX * sinY) * cosZ) + (cosX * -sinZ) | ((sinX * sinY) * sinZ) +
(cosX * cosZ) | sinX * cosY
((cosX * sinY) * cosZ) + (-sinX * -sinZ) | ((cosX * sinY) * sinZ) + (-
sinX * cosZ) | cosX * cosY
Z * X * Y:
------------------------------------------------------------------------
-----
(cosZ * cosY) + ((sinZ * sinX) * sinY) | sinZ * cosX | (cosZ * -
sinY) + ((sinZ * sinX) * cosY)
(-sinZ * cosY) + ((cosZ * sinX) * sinY) | cosZ * cosX | (-sinZ * -
sinY) + ((cosZ * sinX) * cosY)
cosX * sinY | -sinX
| cosX * cosY
stringmatrix.rb
------------------------------------------------------------------------
-----
# A two-dimensional matrix of arbitrary size, with common matrix
# math methods. When entries in the matrix are strings, the math
# methods produce strings representing the equation.
#
# Strings and numbers mix nicely so that multiplying by 0 wipes
# out the string properly, and multiplying by 1 or adding 0
# leaves the original unchanged.
#
# For example:
# x = StringMatrix.parse <<END
# 1 0 0
# 0 cosX sinX
# 0 -sinX cosX
# END
#
# y = StringMatrix.parse <<END
# cosY | 0 | -sinY
# 0 | 1 | 0
# sinY | 0 | cosY
# END
#
# puts x, ' ', y, ' ', x - y, ' ', x * y
#
# #=> 1 | 0 | 0
# #=> 0 | cosX | sinX
# #=> 0 | -sinX | cosX
# #=>
# #=> cosY | 0 | -sinY
# #=> 0 | 1 | 0
# #=> sinY | 0 | cosY
# #=>
# #=> 1 - cosY | 0 | sinY
# #=> 0 | cosX - 1 | sinX
# #=> -sinY | -sinX | cosX - cosY
# #=>
# #=> cosY | 0 | -sinY
# #=> sinX * sinY | cosX | sinX * cosY
# #=> cosX * sinY | -sinX | cosX * cosY
class StringMatrix
# Add two values intelligently
def self.add( v1, v2 )
if v1==0
v2
elsif v2==0
v1
elsif Numeric===v1 && Numeric===v2
v1+v2
else
self.operator_join( v1, v2, '+' )
end
end
# Subtract two values intelligently
def self.subtract( v1, v2 )
if v2==0
v1
elsif v1==0
if v2 =~ /^-\((.+)\)$/ || v2 =~ /^-(.+)$/
$1
elsif v2 =~ /\s/
"-(#{v2})"
else
"-#{v2}"
end
elsif Numeric===v1 && Numeric===v2
v1-v2
else
self.operator_join( v1, v2, '-' )
end
end
# Multiply two values intelligently
def self.multiply( v1, v2 )
if v1==1
v2
elsif v2==1
v1
elsif v1==0 || v2==0
0
elsif Numeric===v1 && Numeric===v2
v1*v2
else
self.operator_join( v1, v2, '*' )
end
end
# Join two values semi-intelligently
def self.operator_join( v1, v2, op_str )
out = ( Numeric === v1 || v1 =~ /^\S+$/ ) ? "#{v1}" : "(#{v1})"
out << " #{op_str} "
out << ( ( Numeric === v2 || v2 =~ /^\S+$/ ) ? "#{v2}" : "(#
{v2})" )
end
attr_reader :width, :height
# Creates a new matrix of the specified _width_ and _height_,
optionally
# specifying a _default_value_ to fill each cell.
def initialize( width, height, default_value='' )
@width = width
@height = height
@values = Array.new( width ){ Array.new(height)
{ default_value } }
end
# Reads the value from column _x_, row _y_.
#
# StringMatrices are 1-based, not zero-based.
# (The first item in the matrix is 1,1 and the last is
_width_,_height_)
def []( x, y )
if !y
@values[ x-1 ].dup
elsif !x
a = []
1.upto(@width){ |x|
a << @values[ x-1 ][ y-1 ]
}
a
else
@values[ x-1 ][ y-1 ]
end
end
# Sets the value in column _x_, row _y_ to _val_.
#
# StringMatrices are 1-based, not zero-based.
# (The first item in the matrix is 1,1 and the last is
_width_,_height_)
def []=( x, y, val )
@values[ x-1 ][ y-1 ] = val
end
# Adds the supplied _right_matrix_ from the current matrix
# and returns the result. (The original matrix is not modified.)
def +( right_matrix )
raise "Size mismatch" if width != right_matrix.width ||
height != right_matrix.height
out = self.class.new( width, height )
1.upto( @height ){ |y|
1.upto( @width ){ |x|
out[ x, y ] = self.class.add( self[ x, y ],
right_matrix[ x, y ] )
}
}
out
end
# Subtracts the supplied _right_matrix_ from the current matrix
# and returns the result. (The original matrix is not modified.)
def -( right_matrix )
raise "Size mismatch" if width != right_matrix.width ||
height != right_matrix.height
out = self.class.new( width, height )
1.upto( @height ){ |y|
1.upto( @width ){ |x|
out[ x, y ] = self.class.subtract( self[ x, y ],
right_matrix[ x, y ] )
}
}
out
end
# Performs matrix multiplication between the two matrices.
# (The original matrix is not modified.)
def *( right_matrix )
raise "Size mismatch" if width != right_matrix.height ||
height != right_matrix.width
out = self.class.new( width, height )
1.upto( @height ){ |y|
1.upto( @width ){ |x|
row = self[ nil, y ]
col = right_matrix[ x, nil ]
val = row.zip( col ).inject( 0 ){ |sum, pair|
self.class.add( sum, self.class.multiply( *pair ) )
}
out[ x, y ] = val
}
}
out
end
# Scales the matrix, multiplying each value by _scale_value_ and
returning
# the resulting matrix.
# (The original matrix is not modified.)
def scale( scale_value )
out = self.class.new( width, height )
1.upto( @height ){ |y|
1.upto( @width ){ |x|
out[ x, y ] = self.class.multiply( self[ x, y ],
scale_value )
}
}
out
end
# Parses a multi-line string for use as a StringMatrix
#
# Lines in the string may be delimited by tabs, vertical bars,
or commas.
# The most common item is used as the separator; if none of the
above are
# present in the string, spaces are used.
def self.parse( raw_str )
best_count = 1
split_char = [ "\t", '|', ',' ].inject( ' ' ){ |split_char,
char|
count = raw_str.scan( char ).length
if count > best_count
best_count = count
char
else
split_char
end
}
values = []
y = 0
raw_str.each_line{ |line|
line.split( split_char ).each_with_index{ |val, x|
val.strip!
( values[ x ] ||= [] )[ y ] = case val
when /^\d+$/ then val.to_i
when /^\d+\.\d+$/ then val.to_f
else val
end
}
y += 1
}
width = values.length
height = y
out = self.new( width, height, '' )
1.upto( height ){ |y|
1.upto( width ){ |x|
out[ x, y ] = values[ x-1 ][ y-1 ]
}
}
out
end
def to_s( no_padding=false )
out = ''
column_widths = @values.collect{ |col|
no_padding ? 0 : col.inject(0){ |max_len,val|
len = val.to_s.length
max_len > len ? max_len : len
}
}
1.upto(@height){ |y|
1.upto(@width){ |x|
out << self[ x, y ].to_s.centered_in( column_widths
[ x-1 ] )
out << " | " unless x == @width
}
out << "\n" unless y == @height
}
out
end
end
class String
# Returns a copy of the string, centered (by padding both sides
with spaces)
# within the specified width.
#
# If width is smaller than the length of the string, the string
itself is returned.
def centered_in( width )
out = self.dup
remains = width - out.length
if remains > 0
back = remains / 2
front = remains - back
out = " "*front + out + " "*back
end
out
end
end
if $0 == __FILE__
m1 = StringMatrix.parse( "0,1,2\n3,4,5" )
m2 = StringMatrix.parse( "x,y,z\n3q,4r,5s" )
puts <<-ENDOUTPUT
M1:
---------------------------------------------------------------------
#{ m1 }
M2:
---------------------------------------------------------------------
#{ m2 }
M1 scaled by 2:
---------------------------------------------------------------------
#{ m1.scale( 2 ) }
M2 scaled by 2:
---------------------------------------------------------------------
#{ m2.scale( 2 ) }
M1 + M2:
---------------------------------------------------------------------
#{ m1 + m2 }
ENDOUTPUT
x = StringMatrix.parse <<-ENDMATRIX
1 0 0
0 cosX sinX
0 -sinX cosX
ENDMATRIX
y = StringMatrix.parse <<-ENDMATRIX
cosY | 0 | -sinY
0 | 1 | 0
sinY | 0 | cosY
ENDMATRIX
z = StringMatrix.parse <<-ENDMATRIX
cosZ sinZ 0
-sinZ cosZ 0
0 0 1
ENDMATRIX
puts <<-ENDOUT
X:
------------------------------------------------------------------------
-----
#{x}
Y:
------------------------------------------------------------------------
-----
#{y}
Z:
------------------------------------------------------------------------
right prefix for offering up code for mass enjoyment.)
At work yesterday I needed to do some algebra on some matrix math to
reverse-derive a value. (To be specific, I needed to figure out how
to draw the three euler rotation values out of a ZXY rotation
matrix.) After spending 2 minutes writing out equations and fearing I
was making a mistake, I turned to Excel and some stupid text
manipulation to produce my formulae. After 10 minutes of messing
around, I got the answer wrong anyhow.
I decided to write a little Ruby library to do the task for me. I
wrote a matrix class which knows how to do matrix math on strings,
producing formulae from them. After the initial pass produced a lot
of values like "0*sinX" and "0 + 0 + 1*(sinX)" I made it a bit
smarter, so that 0s and 1s properly simplify the equations during
calculation.
It doesn't produce perfectly-reduced equations by any means, but it
did the trick. In the end, I got my result
Sample output follows, followed by the code itself. Enjoy!
M1:
---------------------------------------------------------------------
0 | 1 | 2
3 | 4 | 5
M2:
---------------------------------------------------------------------
x | y | z
3q | 4r | 5s
M1 scaled by 2:
---------------------------------------------------------------------
0 | 2 | 4
6 | 8 | 10
M2 scaled by 2:
---------------------------------------------------------------------
x * 2 | y * 2 | z * 2
3q * 2 | 4r * 2 | 5s * 2
M1 + M2:
---------------------------------------------------------------------
x | 1 + y | 2 + z
3 + 3q | 4 + 4r | 5 + 5s
X:
------------------------------------------------------------------------
-----
1 | 0 | 0
0 | cosX | sinX
0 | -sinX | cosX
Y:
------------------------------------------------------------------------
-----
cosY | 0 | -sinY
0 | 1 | 0
sinY | 0 | cosY
Z:
------------------------------------------------------------------------
-----
cosZ | sinZ | 0
-sinZ | cosZ | 0
0 | 0 | 1
X - Y:
------------------------------------------------------------------------
-----
1 - cosY | 0 | sinY
0 | cosX - 1 | sinX
-sinY | -sinX | cosX - cosY
X * Y * Z:
------------------------------------------------------------------------
-----
cosY * cosZ | cosY *
sinZ | -sinY
((sinX * sinY) * cosZ) + (cosX * -sinZ) | ((sinX * sinY) * sinZ) +
(cosX * cosZ) | sinX * cosY
((cosX * sinY) * cosZ) + (-sinX * -sinZ) | ((cosX * sinY) * sinZ) + (-
sinX * cosZ) | cosX * cosY
Z * X * Y:
------------------------------------------------------------------------
-----
(cosZ * cosY) + ((sinZ * sinX) * sinY) | sinZ * cosX | (cosZ * -
sinY) + ((sinZ * sinX) * cosY)
(-sinZ * cosY) + ((cosZ * sinX) * sinY) | cosZ * cosX | (-sinZ * -
sinY) + ((cosZ * sinX) * cosY)
cosX * sinY | -sinX
| cosX * cosY
stringmatrix.rb
------------------------------------------------------------------------
-----
# A two-dimensional matrix of arbitrary size, with common matrix
# math methods. When entries in the matrix are strings, the math
# methods produce strings representing the equation.
#
# Strings and numbers mix nicely so that multiplying by 0 wipes
# out the string properly, and multiplying by 1 or adding 0
# leaves the original unchanged.
#
# For example:
# x = StringMatrix.parse <<END
# 1 0 0
# 0 cosX sinX
# 0 -sinX cosX
# END
#
# y = StringMatrix.parse <<END
# cosY | 0 | -sinY
# 0 | 1 | 0
# sinY | 0 | cosY
# END
#
# puts x, ' ', y, ' ', x - y, ' ', x * y
#
# #=> 1 | 0 | 0
# #=> 0 | cosX | sinX
# #=> 0 | -sinX | cosX
# #=>
# #=> cosY | 0 | -sinY
# #=> 0 | 1 | 0
# #=> sinY | 0 | cosY
# #=>
# #=> 1 - cosY | 0 | sinY
# #=> 0 | cosX - 1 | sinX
# #=> -sinY | -sinX | cosX - cosY
# #=>
# #=> cosY | 0 | -sinY
# #=> sinX * sinY | cosX | sinX * cosY
# #=> cosX * sinY | -sinX | cosX * cosY
class StringMatrix
# Add two values intelligently
def self.add( v1, v2 )
if v1==0
v2
elsif v2==0
v1
elsif Numeric===v1 && Numeric===v2
v1+v2
else
self.operator_join( v1, v2, '+' )
end
end
# Subtract two values intelligently
def self.subtract( v1, v2 )
if v2==0
v1
elsif v1==0
if v2 =~ /^-\((.+)\)$/ || v2 =~ /^-(.+)$/
$1
elsif v2 =~ /\s/
"-(#{v2})"
else
"-#{v2}"
end
elsif Numeric===v1 && Numeric===v2
v1-v2
else
self.operator_join( v1, v2, '-' )
end
end
# Multiply two values intelligently
def self.multiply( v1, v2 )
if v1==1
v2
elsif v2==1
v1
elsif v1==0 || v2==0
0
elsif Numeric===v1 && Numeric===v2
v1*v2
else
self.operator_join( v1, v2, '*' )
end
end
# Join two values semi-intelligently
def self.operator_join( v1, v2, op_str )
out = ( Numeric === v1 || v1 =~ /^\S+$/ ) ? "#{v1}" : "(#{v1})"
out << " #{op_str} "
out << ( ( Numeric === v2 || v2 =~ /^\S+$/ ) ? "#{v2}" : "(#
{v2})" )
end
attr_reader :width, :height
# Creates a new matrix of the specified _width_ and _height_,
optionally
# specifying a _default_value_ to fill each cell.
def initialize( width, height, default_value='' )
@width = width
@height = height
@values = Array.new( width ){ Array.new(height)
{ default_value } }
end
# Reads the value from column _x_, row _y_.
#
# StringMatrices are 1-based, not zero-based.
# (The first item in the matrix is 1,1 and the last is
_width_,_height_)
def []( x, y )
if !y
@values[ x-1 ].dup
elsif !x
a = []
1.upto(@width){ |x|
a << @values[ x-1 ][ y-1 ]
}
a
else
@values[ x-1 ][ y-1 ]
end
end
# Sets the value in column _x_, row _y_ to _val_.
#
# StringMatrices are 1-based, not zero-based.
# (The first item in the matrix is 1,1 and the last is
_width_,_height_)
def []=( x, y, val )
@values[ x-1 ][ y-1 ] = val
end
# Adds the supplied _right_matrix_ from the current matrix
# and returns the result. (The original matrix is not modified.)
def +( right_matrix )
raise "Size mismatch" if width != right_matrix.width ||
height != right_matrix.height
out = self.class.new( width, height )
1.upto( @height ){ |y|
1.upto( @width ){ |x|
out[ x, y ] = self.class.add( self[ x, y ],
right_matrix[ x, y ] )
}
}
out
end
# Subtracts the supplied _right_matrix_ from the current matrix
# and returns the result. (The original matrix is not modified.)
def -( right_matrix )
raise "Size mismatch" if width != right_matrix.width ||
height != right_matrix.height
out = self.class.new( width, height )
1.upto( @height ){ |y|
1.upto( @width ){ |x|
out[ x, y ] = self.class.subtract( self[ x, y ],
right_matrix[ x, y ] )
}
}
out
end
# Performs matrix multiplication between the two matrices.
# (The original matrix is not modified.)
def *( right_matrix )
raise "Size mismatch" if width != right_matrix.height ||
height != right_matrix.width
out = self.class.new( width, height )
1.upto( @height ){ |y|
1.upto( @width ){ |x|
row = self[ nil, y ]
col = right_matrix[ x, nil ]
val = row.zip( col ).inject( 0 ){ |sum, pair|
self.class.add( sum, self.class.multiply( *pair ) )
}
out[ x, y ] = val
}
}
out
end
# Scales the matrix, multiplying each value by _scale_value_ and
returning
# the resulting matrix.
# (The original matrix is not modified.)
def scale( scale_value )
out = self.class.new( width, height )
1.upto( @height ){ |y|
1.upto( @width ){ |x|
out[ x, y ] = self.class.multiply( self[ x, y ],
scale_value )
}
}
out
end
# Parses a multi-line string for use as a StringMatrix
#
# Lines in the string may be delimited by tabs, vertical bars,
or commas.
# The most common item is used as the separator; if none of the
above are
# present in the string, spaces are used.
def self.parse( raw_str )
best_count = 1
split_char = [ "\t", '|', ',' ].inject( ' ' ){ |split_char,
char|
count = raw_str.scan( char ).length
if count > best_count
best_count = count
char
else
split_char
end
}
values = []
y = 0
raw_str.each_line{ |line|
line.split( split_char ).each_with_index{ |val, x|
val.strip!
( values[ x ] ||= [] )[ y ] = case val
when /^\d+$/ then val.to_i
when /^\d+\.\d+$/ then val.to_f
else val
end
}
y += 1
}
width = values.length
height = y
out = self.new( width, height, '' )
1.upto( height ){ |y|
1.upto( width ){ |x|
out[ x, y ] = values[ x-1 ][ y-1 ]
}
}
out
end
def to_s( no_padding=false )
out = ''
column_widths = @values.collect{ |col|
no_padding ? 0 : col.inject(0){ |max_len,val|
len = val.to_s.length
max_len > len ? max_len : len
}
}
1.upto(@height){ |y|
1.upto(@width){ |x|
out << self[ x, y ].to_s.centered_in( column_widths
[ x-1 ] )
out << " | " unless x == @width
}
out << "\n" unless y == @height
}
out
end
end
class String
# Returns a copy of the string, centered (by padding both sides
with spaces)
# within the specified width.
#
# If width is smaller than the length of the string, the string
itself is returned.
def centered_in( width )
out = self.dup
remains = width - out.length
if remains > 0
back = remains / 2
front = remains - back
out = " "*front + out + " "*back
end
out
end
end
if $0 == __FILE__
m1 = StringMatrix.parse( "0,1,2\n3,4,5" )
m2 = StringMatrix.parse( "x,y,z\n3q,4r,5s" )
puts <<-ENDOUTPUT
M1:
---------------------------------------------------------------------
#{ m1 }
M2:
---------------------------------------------------------------------
#{ m2 }
M1 scaled by 2:
---------------------------------------------------------------------
#{ m1.scale( 2 ) }
M2 scaled by 2:
---------------------------------------------------------------------
#{ m2.scale( 2 ) }
M1 + M2:
---------------------------------------------------------------------
#{ m1 + m2 }
ENDOUTPUT
x = StringMatrix.parse <<-ENDMATRIX
1 0 0
0 cosX sinX
0 -sinX cosX
ENDMATRIX
y = StringMatrix.parse <<-ENDMATRIX
cosY | 0 | -sinY
0 | 1 | 0
sinY | 0 | cosY
ENDMATRIX
z = StringMatrix.parse <<-ENDMATRIX
cosZ sinZ 0
-sinZ cosZ 0
0 0 1
ENDMATRIX
puts <<-ENDOUT
X:
------------------------------------------------------------------------
-----
#{x}
Y:
------------------------------------------------------------------------
-----
#{y}
Z:
------------------------------------------------------------------------