A long-lived project that still receives updates
Bit array for ActiveRecord
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
 Dependencies

Development

Runtime

 Project Readme

ActiveFlag - Bit array for ActiveRecord

Build Status

Store up to 64 multiple flags (bit array) in a single integer column with ActiveRecord. From a UI standpoint, it can be used as a multi-select checkbox storage.

Perfect solution to store multiple boolean values such as preferences, notification settings, achievement status, profile options, etc. in a single column.

  • Single column to group multiple boolean values. You don't need to have many separate columns. You don't even need a migration when you add a new flag item to the list.
  • Fast bitwise operations. WHERE languages & 3 > 0 is faster than WHERE (english = true) OR (spanish = true) OR ...

If you want a simple enum column, take a look at EnumAccessor.

If you need to work with huge bit arrays, take a look at Bitwise.

Usage

class Profile < ActiveRecord::Base
  flag :languages, [:english, :spanish, :chinese, :french, :japanese]
end

# {:english=>1, :spanish=>2, :chinese=>4, :french=>8, :japanese=>16 }

# Instance methods
profile.languages                           #=> #<ActiveFlag::Value: {:english, :japanese}>
profile.languages.english?                  #=> true
profile.languages.set?(:english)            #=> true
profile.languages.unset?(:english)          #=> false

profile.languages.set(:spanish)
profile.languages.unset(:japanese)
profile.languages.raw                       #=> 3
profile.languages.to_a                      #=> [:english, :spanish]

profile.languages = [:spanish, :japanese]   # Direct assignment that works with forms

# Class methods
Profile.languages.maps                      #=> {:english=>1, :spanish=>2, :chinese=>4, :french=>8, :japanese=>16 }
Profile.languages.humans                    #=> {:english=>"English", :spanish=>"Spanish", :chinese=>"Chinese", :french=>"French", :japanese=>"Japanese"}
Profile.languages.pairs                     #=> {"English"=>:english, "Spanish"=>:spanish, "Chinese"=>:chinese, "French"=>:french, "Japanese"=>:japanese}
Profile.languages.to_array(3)               #=> [:english, :spanish]

# Scope methods
Profile.where_languages(:french, :spanish)  #=> SELECT * FROM profiles WHERE languages & 10 > 0
Profile.where_all_languages(:french, :spanish)  #=> SELECT * FROM profiles WHERE languages & 10 = 10
Profile.where_not_languages(:french, :spanish)  #=> SELECT * FROM profiles WHERE languages & 10 = 0
Profile.where_not_all_languages(:french, :spanish)  #=> SELECT * FROM profiles WHERE languages & 10 < 10
Profile.languages.set_all!(:chinese)        #=> UPDATE "profiles" SET languages = COALESCE(languages, 0) | 4
Profile.languages.unset_all!(:chinese)      #=> UPDATE "profiles" SET languages = COALESCE(languages, 0) & ~4

Install

gem 'active_flag'

Migration

It is recommended to set 0 by default.

t.integer :languages,                    null: false, default: 0, limit: 8
# OR
add_column :users, :languages, :integer, null: false, default: 0, limit: 8

limit: 8 is only required if you need more than 32 flags.

Query

For a querying purpose, use where_[column], where_all_[column], where_not_[column] and where_not_all_[column] scopes.

Profile.where_languages(:french)            #=> SELECT * FROM profiles WHERE languages & 8 > 0

Also takes multiple values.

Profile.where_languages(:french, :spanish)  #=> SELECT * FROM profiles WHERE languages & 10 > 0

By default, it returns profiles that have either French or Spanish.

To get profiles that have both French and Spanish, use:

Profile.where_all_languages(:french, :spanish) #=> SELECT * FROM profiles WHERE languages & 10 = 10

To get profiles that do not have either French or Spanish, use:

Profile.where_not_languages(:french, :spanish) #=> SELECT * FROM profiles WHERE languages & 10 = 0

To get profiles that do not have both French and Spanish, use:

Profile.where_not_all_languages(:french, :spanish) #=> SELECT * FROM profiles WHERE languages & 10 < 10

Translation

ActiveFlag supports i18n just as ActiveModel does.

For instance, create a Japanese translation in config/locales/ja.yml

ja:
  active_flag:
    profile:
      languages:
        english: 英語
        spanish: スペイン語
        chinese: 中国語
        french: フランス語
        japanese: 日本語

and now to_human method returns a translated string.

I18n.locale = :ja
profile.languages.to_human  #=> ['英語', 'スペイン語']

I18n.locale = :en
profile.languages.to_human  #=> ['English', 'Spanish']

Forms

Thanks to the translation support, forms just work as you would expect with the pairs convenience method.

# With FormBuilder

= form_for(@profile) do |f|
  = f.collection_check_boxes :languages, Profile.languages.pairs

# With SimpleForm

= simple_form_for(@profile) do |f|
  = f.input :languages, as: :check_boxes, collection: Profile.languages.pairs

Other solutions

There are plenty of gems that share the same goal. However they have messy syntax than necessary in my opinion, and I wanted a better API to achieve that goal.

Also, ActiveFlag has one of the simplest code base that you can easily reason about or hack on.