Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion spec/std/enum_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ describe Enum do
SpecEnum::Two.hash.should_not eq(SpecEnum::Three.hash)
end

it ".parse" do
it ".parse(String)" do
SpecEnum.parse("Two").should eq(SpecEnum::Two)
SpecEnum2.parse("FortyTwo").should eq(SpecEnum2::FortyTwo)
SpecEnum2.parse("forty_two").should eq(SpecEnum2::FortyTwo)
Expand Down Expand Up @@ -336,10 +336,43 @@ describe Enum do
SpecEnumWithCaseSensitiveMembers.parse("Foo").should eq SpecEnumWithCaseSensitiveMembers::FOO
end

it ".parse(Bytes)" do
SpecEnum.parse("Two".to_slice).should eq(SpecEnum::Two)
SpecEnum2.parse("FortyTwo".to_slice).should eq(SpecEnum2::FortyTwo)
SpecEnum2.parse("forty_two".to_slice).should eq(SpecEnum2::FortyTwo)
expect_raises(ArgumentError, "Unknown enum SpecEnum value: #{"Four".to_slice}") { SpecEnum.parse("Four".to_slice) }

SpecEnum.parse("TWO".to_slice).should eq(SpecEnum::Two)
SpecEnum.parse("TwO".to_slice).should eq(SpecEnum::Two)
SpecEnum2.parse("FORTY_TWO".to_slice).should eq(SpecEnum2::FortyTwo)
SpecEnum2.parse("FORTY___TWO".to_slice).should eq(SpecEnum2::FortyTwo)
SpecEnum2.parse("FORTY___TWO_".to_slice).should eq(SpecEnum2::FortyTwo)

SpecEnum2.parse("FORTY_FOUR".to_slice).should eq(SpecEnum2::FORTY_FOUR)
SpecEnum2.parse("forty_four".to_slice).should eq(SpecEnum2::FORTY_FOUR)
SpecEnum2.parse("FORTY-FOUR".to_slice).should eq(SpecEnum2::FORTY_FOUR)
SpecEnum2.parse("forty-four".to_slice).should eq(SpecEnum2::FORTY_FOUR)
SpecEnum2.parse("FortyFour".to_slice).should eq(SpecEnum2::FORTY_FOUR)
SpecEnum2.parse("FORTYFOUR".to_slice).should eq(SpecEnum2::FORTY_FOUR)
SpecEnum2.parse("fortyfour".to_slice).should eq(SpecEnum2::FORTY_FOUR)

PrivateEnum.parse("FOO".to_slice).should eq(PrivateEnum::FOO)
PrivateEnum.parse("BAR".to_slice).should eq(PrivateEnum::BAR)
PrivateEnum.parse("QUX".to_slice).should eq(PrivateEnum::QUX)

SpecEnumWithCaseSensitiveMembers.parse("foo".to_slice).should eq SpecEnumWithCaseSensitiveMembers::FOO
SpecEnumWithCaseSensitiveMembers.parse("FOO".to_slice).should eq SpecEnumWithCaseSensitiveMembers::FOO
SpecEnumWithCaseSensitiveMembers.parse("Foo".to_slice).should eq SpecEnumWithCaseSensitiveMembers::FOO
end

it ".parse?" do
SpecEnum.parse?("Two").should eq(SpecEnum::Two)
SpecEnum.parse?("Four").should be_nil
SpecEnum.parse?("Fo-ur").should be_nil

SpecEnum.parse?("Two".to_slice).should eq(SpecEnum::Two)
SpecEnum.parse?("Four".to_slice).should be_nil
SpecEnum.parse?("Fo-ur".to_slice).should be_nil
end

it "clones" do
Expand Down
24 changes: 16 additions & 8 deletions src/enum.cr
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,10 @@ abstract struct Enum
parse?(string) || raise ArgumentError.new("Unknown enum #{self} value: #{string}")
end

def self.parse(slice : Bytes) : self
parse?(slice) || raise ArgumentError.new("Unknown enum #{self} value: #{slice}")
Comment thread
jgaskins marked this conversation as resolved.
Outdated
end

# Returns the enum member that has the given name, or
# `nil` if no such member exists. The comparison is made by using
# `String#camelcase` and `String#downcase` between *string* and
Expand All @@ -508,6 +512,10 @@ abstract struct Enum
#
# If multiple members match the same normalized string, the first one is returned.
def self.parse?(string : String) : self?
parse? string.to_slice
end

def self.parse?(slice : Bytes) : self?
{% begin %}
# FIXME: There is no `StringLiteral#bytesize` or any other adequate means
# to figure out how much space we actually need. Maybe some regex could
Expand All @@ -520,14 +528,14 @@ abstract struct Enum
{% max_size = @type.constants.map(&.size).sort.last %}
buffer = uninitialized UInt8[{{ max_size * 4 + 1 }}]
appender = buffer.to_unsafe.appender
char_counter = 0
string.each_char do |char|
next if char == '-' || char == '_'
char_counter += 1
return nil if char_counter > {{max_size}}
char.downcase &.each_byte do |byte|
appender << byte
end
byte_counter = 0
slice.each do |byte|
next if byte == '-'.ord || byte == '_'.ord
byte_counter += 1
return nil if byte_counter > {{max_size}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: We cannot compare a byte index with a char size.
As mentioned above, there is no StringLiteral#bytesize equivalent. So keeping this optimization is a bit tricky. Perhaps we could calculate the max bytesize at runtime?
But it should be fine to just use max_size * 4 as limit, same as for the buffer size.

@jgaskins jgaskins Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I didn't think about enum members with non-ASCII names. There weren't any tests for them, so I added some, including a couple with case-folding, in e3a80b8.

A discussion for another time is whether we should add support for Bytes to Char::Reader. Or maybe some different construct that's allocated on the stack since Char::Reader is a reference type and we're trying to avoid heap allocations. I'm not sure how to articulate my thoughts on this, but the summary is that I've now implemented parsing bytes into UTF-8 chars in two different PRs (here and in #17065) and that feels like we need a better abstraction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Char::Reader is a struct with value semantics

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I must've gotten it confused with something else. We can disregard that part, then.

# Setting the 6th bit on an alphabetical ASCII byte is effectively a
# downcase.
appender << ('A'.ord <= byte <= 'Z'.ord ? byte | 0x20_u8 : byte)
end
# Temporarily map all constants to their normalized value in order to
# avoid duplicates in the `case` conditions.
Expand Down
Loading