Retrieving an instance of an enum using a string

This has been posed, sort of, several times in Discourse or stackoverflow.

For this example: @enum condition nil=5 mild sick severe

It is easy to get the enum with an integer:

julia> condition(5)
nil::condition = 5

I have a need to convert a string that matches the enum’s appearance to the actual enum: to go from “nil” to nil::condition = 5.

Here are 2 versions of strtoenum. Which of these would be preferred?

First, using the function Base.Enums.namemap, which is not exported.

function strtoenum(enumgrp::Type{<:Enum{T}}, str::String) where {T<:Integer}
    dict = Base.Enums.namemap(enumgrp)
    found = -1
    srch = Symbol(str)
    for (k,v) in dict
        if v == srch
            found = k
            break
        end
    end
    if found != -1
        return enumgrp(found)
    end
end
julia> @btime strtoenum(condition, "nil")
  68.191 ns (0 allocations: 0 bytes)
nil::condition = 5

Second, using instances, which is documented, rather than namemap:

function strtoenum2(enumgrp::Type{<:Enum{T}}, str::String) where {T<:Integer}
    srch = Symbol(str)
    found = -1
    for val in instances(enumgrp)
        if srch == Symbol(val)
            found = val
            break
        end
    end
    if found != -1
        return found
    end
end
julia> @btime strtoenum2(condition, "nil")
  71.731 ns (0 allocations: 0 bytes)
nil::condition = 5

Break-even in performance; no allocations.

Questions:

  1. Neither raises an error if the input string doesn’t match the name of the enum–instead returning nothing. Should a value error be raised? nothing is probably going to cause an error if you try to use the return value for anything.
  2. Which is preferred?
  3. Is this worth a PR to enum? It’s way slower than condition(5), which has nothing to do. The cost is linear in the number of instances in the enum set.

Thought this through better. Enums aren’t really meant to change (though they can).

I have them setup in advance. I can build lookup table as a dict in both directions. I only need to do this once. I used symbols below, but it would be just as easy to do with strings (or convert results as needed). Then it’s way faster to just do the lookup for individual enums as needed elsewhere in the code.

Doh!

julia> inst = instances(condition)
(nil, mild, sick, severe)

julia> syms = Symbol.(inst)
(:nil, :mild, :sick, :severe)

julia> condsym = Dict(zip(inst, syms))
Dict{condition, Symbol} with 4 entries:
  sick   => :sick
  mild   => :mild
  severe => :severe
  nil    => :nil

julia> symcond = Dict(zip(syms, inst))
Dict{Symbol, condition} with 4 entries:
  :sick   => sick
  :mild   => mild
  :nil    => nil
  :severe => severe

A single lookup is fast:

julia> @btime $symcond[:severe]
  5.831 ns (0 allocations: 0 bytes)
severe::condition = 8

This is what I’ll do and what anybody else can do…

1 Like

If you inted to do that very often, you may try to use a LittleDict from OrderedCollections which should be a lot faster for just a handful of entries.

I’ll give it a shot. Thanks.

Nice. I used eval(Meta.parse(...)), but this is prettier. Of course, ideally parse(condition, "...") would just work, but oh well, I’ll take your solution.

May be a more julian way to do it

Base.tryparse(E::Type{<:Enum}, str::String) =
    let insts = instances(E) ,
        p = findfirst(==(Symbol(str)) ∘ Symbol, insts) ;
        p !== nothing ? insts[p] : nothing
    end
using Test

@enum A AA AB
@test tryparse(A, "AA") == AA
@test tryparse(A, "AB") == AB
@test tryparse(A, "AC") == nothing

NOTA:

  • as of julia v1.7 both algo seems to be o(n) . namemap is dict-based but we search on value not key
    so no pref from perf there.
  • dict are still unordered in base, and the use of iterate with them is still messy. so a pref for instances that is vect-based
  • a more proper way may be to add a valmap = Dict{Symbol, basetype}() near to namemap @
    https://github.com/JuliaLang/julia/blob/master/base/Enums.jl
@enum LogLevel begin
    LL_INFO
    LL_DEBUG
end

Please also note from

Base.tryparse(E::Type{<:Enum}, str::String) =
    let insts = instances(E) ,
        p = findfirst(==(Symbol(str)) ∘ Symbol, insts) ;
        p !== nothing ? insts[p] : nothing
    end

using Test
@testset "now" begin
    @test tryparse(LogLevel, "LL_INFO") == LL_INFO
    @test tryparse(LogLevel, "ll_info") == nothing
    @test tryparse(LogLevel, "INFO") == nothing
    @test tryparse(LogLevel, "info") == nothing
end

Those possibles evolutions :

Base.tryparse(E::Type{<:Enum}, str::String; prefix::String="") =
    let eq(x, e) = lowercase(prefix*x) == lowercase(e) ,
        # eq could be statically dispatchable according to some enum surface lang traits
        insts = instances(E) ,
        p = findfirst(insts .|> string) do e; eq(str,e) end;
        p !== nothing ? insts[p] : nothing
    end

@testset "meanwhile" begin
    @test tryparse(LogLevel, "LL_INFO") == LL_INFO
    @test tryparse(LogLevel, "ll_info") == LL_INFO
    @test tryparse(LogLevel, "INFO"; prefix="LL_") == LL_INFO
    @test tryparse(LogLevel, "info"; prefix="LL_") == LL_INFO
end

particulaly considering Solving the drawbacks of @enum