Topic: Representing multidimensional data

Not sure if this should go in Models or Views section as they're related, but I'll try it here

Okay, this has been driving me nuts for a good while.  I've been trying to avoid the problem, every time I come back to it I get stuck.  I can't avoid it any longer and I'm just getting frustrated beyond belief simply because I should be able to figure it out but I just can't.

Plainly, I need to represent a 2d space in Rails using ActiveRecord.  I want to do it with as many Rails niceties as possible.  I'll be doing this a lot for various applications, so I'm looking for a generalized way that could be a candidate for an 'acts_as'.  A good example would a chess board.  You need to know what, if anything, is in each space and a way to locate that space.  Further, I need to be able to search for the data that is in these spaces, i.e. 'Give me all boards where white knight 1 is at c8' There are techniques on representing 2d, 3d and higher spaces in an SQL database, which is essentially a 1d space, but I can't get it to play nice with Rails.

First shot is to just do a standard way of representing something like this in SQL:

class Board
  has_many :squares
  (columns id, name)

class Square
  belongs_to :board
  (columns id, board_id, x, y, associated data)


This works, and it works fine.  With default scoping you can reduce the number of queries to 2.  The problem comes from getting and setting data in the way Rails likes for convenience. 

My first shot ignored all the Rails convenience, generating the forms and reconstructing the data by hand in the controller, shuttling along JSON objects using jQuery to populate forms.  This became hugely messy and felt like I was writing a prettier PHP.  There has to be a better way.

Because this is essentially a 2d array it needs to be represented in a 2d fashion, an actual use for a table!  However, the accepted way to generate forms with associated data is with fields_for.  Rails 2.3 brought a handy view construction with fields_for as such:

form_for @board do |b|
  b.fields_for :squares do |s|
    #yielded construction

This works fantastic if you had a linear set, such as a list, or a singular association (Person belongs_to :organization).  The problem is the yield to fields_for is called for every association.  In the case of the chess board, this would give you 64 yields in a linear fashion.  What you want to do is to yield 8 (the size of a row of a chessboard) times each to give you a chance to close out the row.  I've done this with a weirdly convoluted template logic something like

row = 0
column = 0
<tr>
b.fields_for :squares do |s|
  #form construction here
  column = column.succ
  if column % 8 == 0
    row = row.succ
    </tr>
    next if row % 8 == 0
    <tr>

Which to me is hugely chaotic, but it does work for the most part.  Something similar could be accomplished by checking the row and column number from the record, but it wouldn't be much better I think.

Another thought I had in mind would be to change the associations deeper like

class Board; has_many :rows
class Row; belongs_to :board; has_many :squares
class Square; belongs_to :row

Which can give you a double level association that you can treat like a 2d array which allows you to keep track of position, and can  be accomplished with a self-referencing table (has_many :squares, :class => :row, :foreign_key :row_number), but that may end up being convoluted again.

I've seen a presentation of someone else who is doing something similar, and they got around the problem by making an intermediate class between AR and the data.  This seems like something that I shouldn't have to do and would require a lot more code to prop it up to fit in to the Rails structure (or maybe less than I think).

Re: Representing multidimensional data

Interesting problem.

How about just having rows and columns as a many to many association? In the chess board demonstration each relationship between a row and a column describes an occupied square and where no relationship exists the square in unoccupied. By using a has_many through rather than habtm you can assign to the join table what is occupying the square, following your example the id of a chess piece.

Re: Representing multidimensional data

That's an interesting idea I haven't thought of.  I'll have to think about it and work it out with views next time I'm back on that project to see how well it works out.  Good thought!

Re: Representing multidimensional data

mark_d wrote:

Interesting problem.

How about just having rows and columns as a many to many association? In the chess board demonstration each relationship between a row and a column describes an occupied square and where no relationship exists the square in unoccupied. By using a has_many through rather than habtm you can assign to the join table what is occupying the square, following your example the id of a chess piece.

I'm trying to implement a similar multi-dimensional model.  I am using your suggestion and the complex-forms-example from alloy on github http://github.com/alloy/complex-form-ex … ree/master

I have 4 models:

#da_matrix.rb
class DaMatrix < ActiveRecord::Base

has_many :interests, :dependent => :destroy
  accepts_nested_attributes_for :interests, :allow_destroy => true

has_many :alternatives, :dependent => :destroy
  accepts_nested_attributes_for :alternatives, :allow_destroy => true


#interest.rb
class Interest < ActiveRecord::Base
belongs_to :da_matrix
has_many   :oid_ranks
has_many   :alternatives, :through => oid_ranks


#alternative.rb
class Alternative < ActiveRecord::Base
belongs_to :da_matrix
has_many   :oid_ranks
has_many   :interests, :through => oid_ranks
accepts_nested_attributes_for :oid_ranks, :allow_destroy => true

#oid_rank
class OidRank <ActiveRecord::Base

belongs_to :alternatives
belongs_to :interests


I can add alternatives(rows)  and interests(cols) to the matrix fine.  I am confused about how to add the oid_ranks correctly.

This is my da_matrices_controller

#da_matrices_controller.rb
class DaMatricesController < ApplicationController

def index
   @da_matrices = DaMatrix.find(:all, :order => :name)
end

def show
   @da_matrix = DaMatrix.find(params[:id])
end

def new
    @da_matrix = DaMatrix.new
    @da_matrix.interests.build
    @da_matrix.alternatives.build
   
    @da_matrix.interests.each {
      @da_matrix.alternatives.each {
         |r| r.oid_ranks.build
      }
    }


I would like to allow the user to add the interests and alternatives...then when they submit the form see something a table like this:

Alternatives   |  Interest[0]   |   Interest[1]   |  Total
----------------------------------------------------------
Alternative[1] |     0          |      0          |   0
----------------------------------------------------------
Alternative[2] |     0          |      0          |   0


the user than can input the decimal oid_ranks at the intersection of each interest & alternative & submit will calculate the total

I'm not even sure how to "build" the oid_ranks or if I should define a private function in the da_matrices controller that creates an oid_rank with the default 0 value for the matrix.

Any direction would be greatly appreciated

Natalie

Last edited by natdsuarez (2009-04-19 19:47:09)