John Hinnegan
John Hinnegan

Reputation: 5962

Looking for better logical XOR solution with strings

I'm writing some code to evaluate the presence of a bunch of strings, and I want to ensure that only 1 is present. They're mutually exclusive.

class MyClass
  include ActiveModel::Validations
  attr_accessor :a, :b, :c
  validate :mutex_values 

  def initialize(attr = {}) 
    attr.each do |k, v| 
      send("#{k}=", v)
    end 
  end 

  private 
  def mutex_values 
    # here, I want to do this:
    # errors.add(:base, "specify only 1") unless a ^ b ^ c
    # instead I do this
    errors.add(:base, "specify only 1") unless a.present? ^ b.present? ^ c.present?
  end
end 
MyClass.new(:a => "A", :b => "B", :c => "C").valid? 
=> false

Is there another way that doesn't require repetitive use of the .present?? Monkey patch String to define an operator ^? I'm just so used to being able to do if a that it feels unnatural to need what is basically an explicit cast to boolean. I feel like this use case would 'just work' in Ruby.

Upvotes: 0

Views: 429

Answers (3)

Mladen Jablanović
Mladen Jablanović

Reputation: 44080

If you want to check whether exactly one of methods a, b, and c returns non-null value, you can use:

[a, b, c].compact.count == 1

or even, thanks to numbers,

[a, b, c].one?

Upvotes: 1

numbers1311407
numbers1311407

Reputation: 34072

Enumerable actually defines a one? method which does exactly what you need.

# So either...

[a, b, c].one?(&:present?)

# or

[a, b, c].one?(&:presence)

# ... would do the trick in this case.

Unfortunately if you want two? or etcera, you're out of luck.

Upvotes: 2

mu is too short
mu is too short

Reputation: 434685

You could do something like this with count:

errors.add(:base 'specify exactly 1') unless [a, b, c].count(&:present?) == 1

If you want at most one instead of exactly one, then:

errors.add(:base 'specify at most 1') unless [a, b, c].count(&:present?) > 1

For example:

> ['',nil,'6',''].count(&:present?)
=> 1 
> ['',nil,'6',11].count(&:present?)
=> 2 
> ['',nil,''].count(&:present?)
=> 0 

Upvotes: 2

Related Questions