Advanced Java-Like Enums in Ruby

Advanced Java-like enums in Ruby

class MyEnum
attr_accessor :value
def initialize(value)
@value = value
end

VALUE1 = new("Value 1")
VALUE2 = new("Value 2")

class << self
private :new
end
end

MyEnum::VALUE2 # Enum with value "Value 2"
MyEnum.new # Error

A more elaborate solution that allows you to define arbitrary "enum classes" and also gives you ordinal():

def enum(*values, &class_body)
Class.new( Class.new(&class_body) ) do
attr_reader :ordinal

def initialize(ordinal, *args, &blk)
super(*args, &blk)
@ordinal = ordinal
end

values.each_with_index do |(name, *parameters), i|
const_set(name, new(i, *parameters))
end

class <<self
private :new
end
end
end

# Usage:
MyEnum = enum([:VALUE1, "Value 1"], [:VALUE2, "Value 2"]) do
attr_reader :str
def initialize(str)
@str = str
end
end

MyEnum::VALUE1.str #=> "Value 1"
MyEnum::VALUE2.ordinal #=> 1

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

Methods in Enums

Yes, Java enums can have functions.

http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html

Example from this page:

public enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7);

private final double mass; // in kilograms
private final double radius; // in meters
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
private double mass() { return mass; }
private double radius() { return radius; }

// universal gravitational constant (m3 kg-1 s-2)
public static final double G = 6.67300E-11;

double surfaceGravity() {
return G * mass / (radius * radius);
}
double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("Usage: java Planet <earth_weight>");
System.exit(-1);
}
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight/EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Your weight on %s is %f%n",
p, p.surfaceWeight(mass));
}
}

Ruby: How do you configure an enum in a fixture?

According to the enum docs you can refer to the enumerable through the class like this:

User.permissions[:permission_staff]

And the factories are just ruby code - so they should be able to access the value in the same way

testuser1:
id: 1
username: sam
permission: <%= User.permissions[:permission_staff] %>

Ways to save enums in database

We never store enumerations as numerical ordinal values anymore; it makes debugging and support way too difficult. We store the actual enumeration value converted to string:

public enum Suit { Spade, Heart, Diamond, Club }

Suit theSuit = Suit.Heart;

szQuery = "INSERT INTO Customers (Name, Suit) " +
"VALUES ('Ian Boyd', %s)".format(theSuit.name());

and then read back with:

Suit theSuit = Suit.valueOf(reader["Suit"]);

The problem was in the past staring at Enterprise Manager and trying to decipher:

Name          Suit
------------ ----
Kylie Guénin 2
Ian Boyd 1

verses

Name          Suit
------------ -------
Kylie Guénin Diamond
Ian Boyd Heart

the latter is much easier. The former required getting at the source code and finding the numerical values that were assigned to the enumeration members.

Yes it takes more space, but the enumeration member names are short, and hard drives are cheap, and it is much more worth it to help when you're having a problem.

Additionally, if you use numerical values, you are tied to them. You cannot nicely insert or rearrange the members without having to force the old numerical values. For example, changing the Suit enumeration to:

public enum Suit { Unknown, Heart, Club, Diamond, Spade }

would have to become :

public enum Suit { 
Unknown = 4,
Heart = 1,
Club = 3,
Diamond = 2,
Spade = 0 }

in order to maintain the legacy numerical values stored in the database.

How to sort them in the database

The question comes up: lets say i wanted to order the values. Some people may want to sort them by the enum's ordinal value. Of course, ordering the cards by the numerical value of the enumeration is meaningless:

SELECT Suit FROM Cards
ORDER BY SuitID; --where SuitID is integer value(4,1,3,2,0)

Suit
------
Spade
Heart
Diamond
Club
Unknown

That's not the order we want - we want them in enumeration order:

SELECT Suit FROM Cards
ORDER BY CASE SuitID OF
WHEN 4 THEN 0 --Unknown first
WHEN 1 THEN 1 --Heart
WHEN 3 THEN 2 --Club
WHEN 2 THEN 3 --Diamond
WHEN 0 THEN 4 --Spade
ELSE 999 END

The same work that is required if you save integer values is required if you save strings:

SELECT Suit FROM Cards
ORDER BY Suit; --where Suit is an enum name

Suit
-------
Club
Diamond
Heart
Spade
Unknown

But that's not the order we want - we want them in enumeration order:

SELECT Suit FROM Cards
ORDER BY CASE Suit OF
WHEN 'Unknown' THEN 0
WHEN 'Heart' THEN 1
WHEN 'Club' THEN 2
WHEN 'Diamond' THEN 3
WHEN 'Space' THEN 4
ELSE 999 END

My opinion is that this kind of ranking belongs in the user interface. If you are sorting items based on their enumeration value: you're doing something wrong.

But if you wanted to really do that, i would create a Suits dimension table:



Leave a reply



Submit