How to Implement Enums in Ruby

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:

  1. The outer .map is an array of all iterations.
  2. Each iteration returns retval, which is an array.
  3. 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



Leave a reply



Submit