How to implement Enums in Ruby?
Two ways. Symbols (:foo
notation) or constants (FOO
notation).
Symbols are appropriate when you want to enhance readability without littering code with literal strings.
postal_code[:minnesota] = "MN"
postal_code[:new_york] = "NY"
Constants are appropriate when you have an underlying value that is important. Just declare a module to hold your constants and then declare the constants within that.
module Foo
BAR = 1
BAZ = 2
BIZ = 4
end
flags = Foo::BAR | Foo::BAZ # flags = 3
Added 2021-01-17
If you are passing the enum value around (for example, storing it in a database) and you need to be able to translate the value back into the symbol, there's a mashup of both approaches
COMMODITY_TYPE = {
currency: 1,
investment: 2,
}
def commodity_type_string(value)
COMMODITY_TYPE.key(value)
end
COMMODITY_TYPE[:currency]
This approach inspired by andrew-grimm's answer https://stackoverflow.com/a/5332950/13468
I'd also recommend reading through the rest of the answers here since there are a lot of ways to solve this and it really boils down to what it is about the other language's enum that you care about
Enums in Ruby on Rails Form Select Mapping Values
An enumeration in the model seems like the best way to keep track of
these values.
Hell no. ActiveRecord::Enum is designed to connect a integer or any other type thats efficient to store and index to a developer readable label.
When you define your enum as:
enum construction_type: {
brick_block: "Brick/Block",
concrete_slab: "Concrete/Slab",
wood_steel: "Light Framed Wood/Steel",
timber_steel: "Heavy Framed Timber/Steel"
}
You're going to be storing "Heavy Framed Timber/Steel"
as the value in the database which is a downright bad idea as you're asking for denormalization issues if you ever need to change the human friendly labels. The enum mapping should not be expected to change.
If you really want to use an Enum then use the I18n module to provide the human readible versions:
# the name is just an assumption
class Building < ApplicationRecord
enum construction_type: {
brick_block: 0,
concrete_slab: 1,
wood_steel: 2,
timber_steel: 3
}
end
module BuildingsHelper
def construction_type_options
Building.construction_types.keys do |key|
[key, t("activerecord.models.buildings.construction_types.#{ key }")]
end
end
end
But a less hacky alternative is to use a seperate table/model:
class Building
belongs_to :construction_type
end
class ConstructionType
has_many :buildings
end
<%= form_with(model: @buildling) do |form| %>
<%= form.collection_select :construction_type_id,
ConstructionType.all,
:id,
:description
%>
<% end %>
Enums in Ruby and getting string value
Since you cannot use Rails ActiveRecord enum
, then a hash might be useful:
COLORS = { "red" => 0, "blue" => 1, "yellow" => 2 }
store a color code
red_color = COLORS["red"] #red_color = 0
convert from color code, use Hash#key method
COLORS.key(0)
# > "red"
You can even make a helper for that, something like:
def color_code_to_string(code)
COLORS.key(code) # returning a default color in case if wrong code number is a good idea too
end
Enums in Rails: uppercase or lowercase?
In Rails enums should be snake_case
.
Why? Because enums are used to construct method names. And method names in Ruby should be snake_case
according to the community convention. Using ALLCAPS
or CamelCase
can lead to bugs and confusion as Ruby treats such identifiers as constants.
ActiveRecord::Enum is not comparable to a language level enumeration construct such as in Java.
Declare an enum attribute where the values map to integers in the
database, but can be queried by name.
http://api.rubyonrails.org/classes/ActiveRecord/Enum.html
The keys mappings in an ActiveRecord::Enum are not constants. Rather it's just a list which is used with Ruby style metaprogramming that adds methods to make bitmask columns easier (and more fun) to work with.
In your example its actually comparable to:
enum status: [ :ACTIVE, :DRAFT, :INACTIVE ]
An enum type is a special data type that enables for a variable to be
a set of predefined constants. The variable must be equal to one of
the values that have been predefined for it. Common examples include
compass directions (values of NORTH, SOUTH, EAST, and WEST) and the
days of the week.
https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html
Enums in Java are basically glorified switch statements where the class constants denote the possible values.
i18n-tasks custom scanner for enums
This works:
def scan_file(path)
result = []
text = read_file(path)
text.scan(/enum\s([a-zA-Z]*?):\s\{(.*)}, _prefix: true$/).each do |prefix, body|
occurrence = occurrence_from_position(path, text,
Regexp.last_match.offset(0).first)
body.scan(/(\w+):/).flatten.each do |attr|
model = File.basename(path, ".rb")
name = "#{prefix}_#{attr}"
result << ["activerecord.attributes.#{model}.#{name}", occurrence]
end
end
result
end
It's similar to your 'answer' approach, but uses the regex to get all the contents between '{...}', and then uses another regex to grab each enum key name.
The probable reason your 'answer' version raises an error is that it is actually returning a three-dimensional array, not two:
- The outer
.map
is an array of all iterations. - Each iteration returns
retval
, which is an array. - Each element of
retail
is an array of['key', occurrence]
pairs.
Best way to interleave two enums in ruby?
Here's a shorter and more general approach:
def combine_enums_with_ratio(ratios)
return enum_for(__method__, ratios) unless block_given?
counts = ratios.transform_values { |value| Rational(1, value) }
until counts.empty?
begin
enum, _ = counts.min_by(&:last)
yield enum.next
counts[enum] += Rational(1, ratios[enum])
rescue StopIteration
counts.delete(enum)
end
end
end
Instead of two enums, it takes a hash of enum => ratio
pairs.
At first, it creates a counts
hash using the ratio's reciprocal, i.e. enum_a => 3, enum_b => 2
becomes:
counts = { enum_a => 1/3r, enum_b => 1/2r }
Then, within a loop, it fetches the hash's minimum value, which is enum_a
in the above example. It yields its next
value and increment its counts
ratio value:
counts[enum_a] += 1/3r
counts #=> {:enum_a=>(2/3), :enum_b=>(1/2)}
On the next iteration, enum_b
has the smallest value, so its next
value will be yielded and its ratio be incremented:
counts[enum_b] += 1/2r
counts #=> {:enum_a=>(2/3), :enum_b=>(1/1)}
If you keep incrementing enum_a
by (1/3)
and enum_b
by (1/2)
, the yield ratio of their elements will be 3:2.
Finally, the rescue
clause handles enums running out of elements. If this happens, that enum is removed from the counts
hash.
Once the counts
hash is empty, the loop stops.
Example usage with 3 enums:
enum_a = (1..10).each
enum_b = ('a'..'f').each
enum_c = %i[foo bar baz].each
combine_enums_with_ratio(enum_a => 3, enum_b => 2, enum_c => 1).to_a
#=> [1, "a", 2, 3, "b", :foo, 4, "c", 5, 6, "d", :bar, 7, "e", 8, 9, "f", :baz, 10]
# <---------------------> <---------------------> <--------------------->
# 3:2:1 3:2:1 3:2:1
Related Topics
Rails Activerecord Perform Group, Sum and Count in One Query
How to Convert a Bigdecimal to a 2-Decimal-Place String
Check If a String Contains Only Digits in Ruby
How to Calculate Number of Chars Common to Two Strings
Case Statement With Multiple Values in Each 'When' Block
Suppress Ruby Warnings When Running Specs
Pressing Ctrl + a in Selenium Webdriver
Group Hashes by Keys and Sum the Values
Using Net::Http.Get For an Https Url
Is There a Reason That We Cannot Iterate on "Reverse Range" in Ruby
How to Determine If One Array Contains All Elements of Another Array
Ruby MySQL2 Gem Installation on Windows 7
Rails: Access to Current_User from Within a Model in Ruby on Rails
Test If String Is a Number in Ruby on Rails
How to Validate a Date in Rails