Thursday, January 17, 2008

Setting topic flags

Recently I've been creating a forum. I wanted to have a possibility to mark specific topics with some flags like "sticky" or "info". The requirement was that SQL could handle them easily and in proper manner (sorting). So I came up with this thing.

First I overridden the default accessor on rails model AbstractForum for flags field:

def flags
BitFlag.new(self)
end

BitFlag class wraps around the AbstractForum having access to all methods.

Then the constructor for BitFlag:

class BitFlag

def initialize(object, method = :flags)
@object = object
@method = method
@object[@method] = 0 unless @object[@method]
end

end

As You can see, it takes the object that we want to work with and method symbol (:flags for default). After setting up everything (including setting object's flags to zero when they are nil) I started REAL coding ;)

First some thought on this. I came up with an idea to have the flags in one INT field to have an easy way of adding new flags. Each flag should be the power of 2 and then we are sure, that every combination is unique (yeah, You DO know that, don't You? ;P). Having to check whether a number is power of 2 I came up with this one:
def self.pow2?(flag)
flag != 0 && (flag & (flag - 1) == 0)
end

When a number is power of 2?
- its bits are entirely reversed comparing to number-1 ...
- ... and it's not zero

Ok, so now having this tricky-yet-super-cool method, it's all piece of cake ;)
def <<(flag)
raise ArgumentError.new("#{flag} is not a power of 2") unless BitFlag.pow2?(flag)
@object[@method] |= flag
self
rescue ArgumentError => e
puts "Forum flag warning: " + e.message
self
end

def >>(flag)
raise ArgumentError.new("#{flag} is not a power of 2") unless BitFlag.pow2?(flag)
@object[@method] &= ~flag
self
rescue ArgumentError => e
puts "Forum flag warning: " + e.message
self
end

Those two are the core - first one adds a flag and second removes it. Adding is simply logical OR and removing is AND with logical negation of number. We have to check the pow2? so that the user is not able to give invalid number (it would destroy everything). Returning self in both cases gives as the ability to chain the insertion of flags:

forum.flags << 2 << 8 << 3 << 16


I've also added some other methods like include?(flag) or clear to provide some basic functionality.

After some playing, we then just do

forum.save!


and we're done - model is saved.

Now when it comes to SQL this is very easy.. you just do (ActiveRecord and Postgres):

Forum.find(:all,
:select => "*, (flags & Forum::Sticky) as sticky, (flags & Forum::INFO) as info",
:order => "sticky DESC, info DESC, replied_at DESC")


and there you are! Results are nicely sorted within one simple query (don't forget to include attr_reader :sticky, :info, :whatever to include the flags in the query result)

0 comment(s):