Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enum from_value when Flags #3518

Merged
merged 6 commits into from
Nov 20, 2016
Merged

Conversation

bmulvihill
Copy link
Contributor

Fixes #3448

from_value will return the same as new for Enum with Flags

@zatherz
Copy link
Contributor

zatherz commented Nov 14, 2016

Bump, it's been a week. It's literally just one button press to merge.

@bcardiff
Copy link
Member

Before this PR on non flag enums, if a number that does not belongs to the enum is given as an argument, then nil is returned.

After this PR if the enum acts as flag the user can build enum with values outside the declared constants. Maybe a bitwise & should be applied before returning the enum value.

Also, if this is 0, and there is no 0 as enum value, I would expect nil to be returned.

@asterite
Copy link
Member

Hi,

Thanks for the PR but it's not quite right. The idea is that from_value validates that the given value is valid. For example, given:

@[Flags]
enum ColorFlags
  Red = 1
  Green = 2
  Blue = 4
end

Then:

ColorFlags.from_value?(1) # => Red
ColorFlags.from_value?(3) # => Red, Green
ColorFlags.from_value?(5) # => Red, Blue
ColorFlags.from_value?(7) # => Red, Green, Blue

# But...
ColorFlags.from_value?(8) # => nil
ColorFlags.from_value?(9) # => nil
...

So this is not the same as new.

@bmulvihill bmulvihill closed this Nov 14, 2016
@bmulvihill
Copy link
Contributor Author

bmulvihill commented Nov 15, 2016

@asterite I have been looking into this more, and have been working on updating my implementation, I have a question though, in your above example from_value?(9), shouldn't that return Red since the bitwise & of 9 and 1 == 1?

@drhuffman12
Copy link

Maybe use some sort of base-of-x math?

@bmulvihill bmulvihill reopened this Nov 15, 2016
@bmulvihill
Copy link
Contributor Author

@asterite @bcardiff I made some updates, I had one outstanding question above regarding the example you gave.

@asterite
Copy link
Member

@bmulvihill 9 is not a valid value, because yes, 1 is in it, but it's not an exact combination of all the enum values. One way to implement this, I guess, is to subtract each of the enum values from the given value, if the value is greater than the enum value. In the end the value should end up being zero. I don't know if there's another way to do this.

@bcardiff
Copy link
Member

Another approach would be to use bitwise operators to valid the value.

Given

@[Flags]
enum ColorFlags
  Red = 1
  Green = 2
  Blue = 4
end

if the mask = ColorFlags::Red | ColorFlags::Green | ColorFlags::Blue is built,
then for any v value given to from_value?(v) if v & mask != v it means there are some unexpected flag values.
There is a corner case with 0 though. 0 & mask == 0 but might not be a valid enum value.

@bmulvihill
Copy link
Contributor Author

@bcardiff I implemented a mask to determine valid values, the behavior now seems to be what is expected.

arr = values.select {|v| (v.to_i & value) == v.to_i && v.to_i != 0 }
return arr.reduce { |types, e| types | e } unless arr.empty?
mask = {% for member, i in @type.constants %}\
{% if i != 0 %} | {% end %}\
Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIK \ at line endings are considered bad practice.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but in macros the alternative is to join everything in a single line and it is less readable IMO.
Since the macro has test coverage I think we are fine here.

Copy link
Member

@bcardiff bcardiff left a comment

Choose a reason for hiding this comment

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

I think is fine to merge this. All cases are handled now.
@asterite?

mask = {% for member, i in @type.constants %}\
{% if i != 0 %} | {% end %}\
{{@type}}::{{member}}.value{% end %}
return if (mask & value != value) || (value == 0 && !values.map(&.to_i).includes?(0))
Copy link
Member

Choose a reason for hiding this comment

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

There's a map here, so this creates a temporary array. The implementation of this method should be efficient, without allocating memory.

Copy link
Member

Choose a reason for hiding this comment

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

I think values.any? { |val| val.to_i == 0 } is right.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ill update it

Copy link
Member

@RX14 RX14 Nov 16, 2016

Choose a reason for hiding this comment

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

Actually, using none? and dropping the not is probably nicer.

@Sija
Copy link
Contributor

Sija commented Nov 19, 2016

GTG?

@asterite asterite merged commit 584060e into crystal-lang:master Nov 20, 2016
@asterite
Copy link
Member

@bmulvihill Thank you! ❤️

firejox pushed a commit to firejox/crystal that referenced this pull request Dec 12, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants