PullMonkey Blog


31 Mar

Intersection of Array of Arrays - Ruby


Have you ever needed to retrieve the intersection of arrays in an array. Well, I did.

Let's say that you have a has and belongs to many (habtm) association between articles and tags, (I.e., a tag habtm articles and an article habtm tags). This means that tag.articles will return a list of articles that have that tag and article.tags will return the tags of the article. So if a user wants to search your blog for all articles that have 'tag1' and 'tag2', you would return all common articles.

Picture a search page with a list of check boxes, one for each tag that you have. The user can click as many tags as they want and then it is your job to find the common articles between them. The search POST contains the ids of the selected tags, like params[:tag_ids] = ["1", "3", "15"] or something. Let's go with this example.

1) The first step is to find those tags:

1
>> tags = Tag.find(params[:tag_ids])

2) Next, we want to get the articles associated with each tag, we will work with the ids:

1
2
>> tags.map(&:article_ids)
=> [[1,2,3],[2,3,4,5],[2,3,6]]

So we have our array of arrays of article ids.

3) Turn each sub array into a string like "[1,2,3]"

1
2
>> tags.map(&:article_ids).map(&:to_json)
=> ["[1, 2, 3]", "[2, 3, 4, 5]", "[2, 3, 6]"]

4) Join by the intersection symbol (&) like this:

1
2
>> tags.map(&:article_ids).map(&:to_json).join("&")
=> "[1, 2, 3]&[2, 3, 4, 5]&[2, 3, 6]"

5) Run eval() against this string:

1
2
>> eval(tags.map(&:article_ids).map(&:to_json).join("&"))
=> [2,3]

This means that the tags we selected have common articles with ids of 2 and 3.

So a one-liner would be:

1
2
>> Article.find(eval(Tag.find(params[:tag_ids]).map(&:article_ids).map(&:to_json).join("&")))
=> <returns an array of article objects>





2 Responses Filed under: Home, development, ruby Tags:
30 Mar

Dynamic Select Boxes - Ruby on Rails


UPDATE: There is a dynamic select boxes for rails 3 tutorial now, so if this isn't working for you, check it out.

I have seen this asked a lot in the forums, so I thought I would write up a little tutorial.

For this tutorial I am going to have three select boxes. The first select box will be a super category of the next two select boxes and the second select box will be a super category of the third select box. I hope that makes sense. To demonstrate, I thought I would use Genre -> Artist -> Song. So let's get started:

Create your models and build your migrations:

1
2
3
4

ruby script/generate model genre name:string
ruby script/generate model artist name:string genre_id:integer
ruby script/generate model song title:string artist_id:integer

Populate your genres, artists and songs through a migration:

1
2

ruby script/generate migration create_hierarchy

Contents of migration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

class CreateHierarchy < ActiveRecord::Migration
  def self.up
    g1 = Genre.create(:name => "Genre 1")
    g2 = Genre.create(:name => "Genre 2")
    g3 = Genre.create(:name => "Genre 3")

    a1 = Artist.create(:name => "Artist 1", :genre_id => g1.id)
    a2 = Artist.create(:name => "Artist 2", :genre_id => g1.id)
    a3 = Artist.create(:name => "Artist 3", :genre_id => g2.id)
    a4 = Artist.create(:name => "Artist 4", :genre_id => g2.id)
    a5 = Artist.create(:name => "Artist 5", :genre_id => g3.id)
    a6 = Artist.create(:name => "Artist 6", :genre_id => g3.id)

    Song.create(:title => "Song 1",  :artist_id => a1.id)
    Song.create(:title => "Song 2",  :artist_id => a1.id)
    Song.create(:title => "Song 3",  :artist_id => a2.id)
    Song.create(:title => "Song 4",  :artist_id => a2.id)
    Song.create(:title => "Song 5",  :artist_id => a3.id)
    Song.create(:title => "Song 6",  :artist_id => a3.id)
    Song.create(:title => "Song 7",  :artist_id => a4.id)
    Song.create(:title => "Song 8",  :artist_id => a4.id)
    Song.create(:title => "Song 9",  :artist_id => a5.id)
    Song.create(:title => "Song 10", :artist_id => a5.id)
    Song.create(:title => "Song 11", :artist_id => a6.id)
    Song.create(:title => "Song 12", :artist_id => a6.id)
  end

  def self.down
# you can fill this in if you want.
  end
end

Yes, I know it is generic data, sorry. So anyway, as you can see there are 3 genres, each with 2 artists, for a total of 6 artists each with 2 songs for a total of 12 songs. Each genre has 4 songs through its artists. Ok, so now we need to populate the database:

1
2

rake db:migrate

Now we need to modify our models to set up the associations.

1
2
3
4
5
6
7
8

class Genre < ActiveRecord::Base
  has_many :artists
  has_many :songs, :through => :artists
end
class Artist < ActiveRecord::Base
  has_many :songs
end

That should be it, now let's go to the console and see that all this works:

1
2
3
4
5
6
7
8
9
10

ruby script/console 
Loading development environment (Rails 2.0.2)
>> g = Genre.find(:first)
=> #<Genre id: 1, name: "Genre 1", created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">
>> g.artists
=> [#<Artist id: 1, name: "Artist 1", genre_id: 1, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">, #<Artist id: 2, name: "Artist 2", genre_id: 1, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">]
>> g.songs
=> [#<Song id: 1, title: "Song 1", artist_id: 1, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">, #<Song id: 2, title: "Song 2", artist_id: 1, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">, #<Song id: 3, title: "Song 3", artist_id: 2, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">, #<Song id: 4, title: "Song 4", artist_id: 2, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">]
>> 

Looks like it works :) Ok, now on to the controller. Our controller needs to have some action for the view, I just used index, and two other actions, for remote function calls, I called these update_artists and update_songs. update_artists() is called when a genre is changed and it updates the list of artists and the list of songs based on the genre. update_songs() only updates the songs based on the artist. So let's look at this code:

# the controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

class TestItController < ApplicationController
  def index
    @genres  = Genre.find(:all)
    @artists = Artist.find(:all)
    @songs   = Song.find(:all)
  end

  def update_artists
    # updates artists and songs based on genre selected
    genre = Genre.find(params[:genre_id])
    artists = genre.artists
    songs   = genre.songs

    render :update do |page|
      page.replace_html 'artists', :partial => 'artists', :object => artists
      page.replace_html 'songs',   :partial => 'songs',   :object => songs
    end
  end

  def update_songs
    # updates songs based on artist selected
    artist = Artist.find(params[:artist_id])
    songs  = artist.songs

    render :update do |page|
      page.replace_html 'songs', :partial => 'songs', :object => songs
    end
  end
end

Now as far as views go we have one view (index.html.erb) and two partials (_songs and _artists). Let's take a look at those:

# the _songs partial (_songs.html.erb):

1
2
3

<%= collection_select(nil, :song_id, songs, :id, :title,
                     {:prompt   => "Select a Song"}) %>

# the _artists partial (_artists.html.erb):

1
2
3
4
5
6

<%= collection_select(nil, :artist_id, artists, :id, :name,
                     {:prompt   => "Select an Artist"},
                     {:onchange => "#{remote_function(:url  => {:action => "update_songs"},
                                                      :with => "'artist_id='+value")}"}) %>
<br/>

# and last, but not least, the index view (index.html.erb):

1
2
3
4
5
6
7
8
9

<%= javascript_include_tag :defaults %>
<%= collection_select(nil, :genre_id,  @genres,  :id, :name,
                      {:prompt   => "Select a Genre"},
                      {:onchange => "#{remote_function(:url  => {:action => "update_artists"},
                                                       :with => "'genre_id='+value")}"}) %>
<br/>
<div id="artists"><%= render :partial => 'artists', :object => @artists %></div>
<div id="songs"><%= render :partial => 'songs',   :object => @songs %></div>

Ok, this probably takes some explanation. I will save that for part II, where I will also improve upon what we have so far and include a demo.


For now, if you have any questions, just ask me in the comments.