Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
52 changes: 51 additions & 1 deletion spec/std/enum_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ private enum SpecEnumWithCaseSensitiveMembers
Foo = 2
end

private enum SpecEnumWithUnicodeMembers
Föö = 1
Bár = 2
end

describe Enum do
describe "#to_s" do
it "for simple enum" do
Expand Down Expand Up @@ -307,7 +312,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 All @@ -334,12 +339,57 @@ describe Enum do
SpecEnumWithCaseSensitiveMembers.parse("foo").should eq SpecEnumWithCaseSensitiveMembers::FOO
SpecEnumWithCaseSensitiveMembers.parse("FOO").should eq SpecEnumWithCaseSensitiveMembers::FOO
SpecEnumWithCaseSensitiveMembers.parse("Foo").should eq SpecEnumWithCaseSensitiveMembers::FOO

SpecEnumWithUnicodeMembers.parse("Föö").should eq SpecEnumWithUnicodeMembers::Föö
expect_raises(ArgumentError, "Unknown enum SpecEnumWithUnicodeMembers value: Fööd") do
SpecEnumWithUnicodeMembers.parse("Fööd")
end
SpecEnumWithUnicodeMembers.parse("FÖÖ").should eq SpecEnumWithUnicodeMembers::Föö
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") { 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

SpecEnumWithUnicodeMembers.parse("Föö".to_slice).should eq SpecEnumWithUnicodeMembers::Föö
expect_raises(ArgumentError, "Unknown enum SpecEnumWithUnicodeMembers value: Fööd") do
SpecEnumWithUnicodeMembers.parse("Fööd".to_slice)
end
SpecEnumWithUnicodeMembers.parse("FÖÖ".to_slice).should eq SpecEnumWithUnicodeMembers::Föö
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
72 changes: 65 additions & 7 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: #{String.new(slice)}")
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,13 +528,30 @@ 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
byte_counter = 0
pos = 0
while pos < slice.size
byte = slice.unsafe_fetch(pos)

if byte < 0x80
# The byte is ASCII, so it contains the full Char value
pos += 1
next if byte == '-'.ord || byte == '_'.ord
byte_counter += 1
return nil if byte == 0
return nil if byte_counter > {{max_size * 4}}
# Setting the 6th bit on an alphabetical ASCII byte is a downcase.
appender << ('A'.ord <= byte <= 'Z'.ord ? byte | 0x20_u8 : byte)
else
# Multi-byte characters need to be decoded so `Char#downcase` can
# apply the same Unicode case mapping as the compile-time
# normalization of the member names.
char, width = decode_utf8_char(slice, pos)
pos += width
downcased = char.downcase
byte_counter += downcased.bytesize
return nil if byte_counter > {{max_size * 4}}
downcased.each_byte { |b| appender << b }
end
end
# Temporarily map all constants to their normalized value in order to
Expand All @@ -551,6 +576,39 @@ abstract struct Enum
{% end %}
end

# Decodes the UTF8-encoded character starting at `pos` in `slice` and
# returns it along with its width in bytes. Invalid byte sequences decode
# to `Char::REPLACEMENT` with a width of 1, the same as `Char::Reader` does.
#
# TODO: This should probably live somewhere other than `Enum`.
private def self.decode_utf8_char(slice : Bytes, pos : Int) : {Char, Int32}
first = slice[pos].to_u32!

second = slice.fetch(pos + 1) { 0u32 }
return {Char::REPLACEMENT, 1} if first < 0xc2 || second & 0xc0 != 0x80

return {((first << 6) &+ second &- 0x3080).unsafe_chr, 2} if first < 0xe0

third = slice.fetch(pos + 2) { 0u32 }
if third & 0xc0 != 0x80 ||
(first == 0xe0 && second < 0xa0) || # overlong encoding
(first == 0xed && second >= 0xa0) # UTF-16 surrogate?
return {Char::REPLACEMENT, 1}
end

return {((first << 12) &+ (second << 6) &+ third &- 0xe2080).unsafe_chr, 3} if first < 0xf0

fourth = slice.fetch(pos + 3) { 0u32 }
if fourth & 0xc0 != 0x80 ||
(first == 0xf0 && second < 0x90) || # overlong encoding
(first == 0xf4 && second >= 0x90) || # beyond Unicode range?
first > 0xf4 # beyond Unicode range?
return {Char::REPLACEMENT, 1}
end

{((first << 18) &+ (second << 12) &+ (third << 6) &+ fourth &- 0x3c82080).unsafe_chr, 4}
end

def clone
self
end
Expand Down
Loading