validates_mutual_exclusivity

Railsters:

We have a model with 2 variables. Neither may be nil or non-nil at the
same time. They must pass this truth table:

  def test_mutually_exclude
    assert_equal false, Model.new.save
    assert_equal false, Model.new(:thing1 => 2, :thing2 => 1).save
    assert_equal true, Model.new(:thing1 => 2).save
    assert_equal true, Model.new(:thing2 => 1).save
  end

thing1 and thing2 default to nil. We need only one Thing at a time,
and having them both together causes trouble.

What's the minimum set of validates_presence_of or similar validators
required to enforce this? Note the error message itself is not
important.

We have it down to 4 lines, which seems absurdly excessive for Rails,
so what are we missing?

Even better, could someone write validates_mutual_exclusivity, in the
usual 2 lines of code or less?

How about something like:

  validates_each :thing1 do |row, attr, value|
    row.errors.add_to_base 'Supply thing1 or thing2, but not both'
unless value.blank? ^ row.thing2.blank?
  end

Darned line wrapping. That "unless" part goes with the line above.

Even better, could someone write validates_mutual_exclusivity, in the
usual 2 lines of code or less?

How about something like:

  validates_each :thing1 do |row, attr, value|
    row.errors.add_to_base 'Supply thing1 or thing2, but not both' \
      unless value.blank? ^ row.thing2.blank?
  end

Thanks! Now I remember why I don't twiddle bits any more! I will report back if it works.

Next riddle: Our "Community Agreements" discourage 'unless'. So if you distribute a 'not' into a boolean expression, you get if value.nil? Q value.nil?, where Q is the opposite operator from ^, right? If so, what's Q?

!(a ^ b)

expanding the exclusive-or
!((a & !b) | (!a & b))

distribute the outer !
!(a & !b) & !(!a & b)

distribute the inner !'s
(!a | !(!b)) & (!(!a) | !b)

simplify
(!a | b) & (a | !b)

and since: x & (y | z) => (x & y) | (x & z)

((!a | b) & a) | ((!a | b) & !b)

((!a & a) | (b & a)) | ((!a & !b) | (b & !b))

but: x & !x => false
(false | (b & a)) | ((!a & !b) | false)

and: false | y => y
(b & a) | (!a & !b)

and: x & y => y & x
(a & b) | (!a & !b)

which is equivalent to: (if you take a small hop from pure boolean algebra to Ruby)
a ? b : !b

so the original expression's equivalent "if" form is:

     row.errors.add_to_base 'Supply thing1 or thing2, but not both' \
       if (value.blank? ? row.thing2.blank? : !row.thing2.blank?)

if you look back at the original expansion of ^
[ ok, here it is: a ^ b => (a & !b) | (!a & b) ]

you'll note a similar form in the ?: expression's equivalent:
a ? !b : b

If your "Community Agreements" have a similarly silly ban on the ?: expression, then don't take the last hop (or throw the !(a^b) after the 'if').

-Rob

Rob Biedenharn http://agileconsultingllc.com
Rob@AgileConsultingLLC.com

Rob Biedenharn wrote:

> validates_each :thing1 do |row, attr, value|
> row.errors.add_to_base 'Supply thing1 or thing2, but not both' \
> unless value.blank? ^ row.thing2.blank?
> end

That one worked.

> Next riddle: Our "Community Agreements" discourage 'unless'. So if you
> distribute a 'not' into a boolean expression, you get if value.nil? Q
> value.nil?, where Q is the opposite operator from ^, right? If so,
> what's Q?

     row.errors.add_to_base 'Supply thing1 or thing2, but not both' \
       if (value.blank? ? row.thing2.blank? : !row.thing2.blank?)

if you look back at the original expansion of ^
[ ok, here it is: a ^ b => (a & !b) | (!a & b) ]

you'll note a similar form in the ?: expression's equivalent:
a ? !b : b

If your "Community Agreements" have a similarly silly ban on the ?:
expression, then don't take the last hop (or throw the !(a^b) after
the 'if').

Now now - someone with "agile" in their home URI should know better
than to call any Community Agreement "silly".

Our coding standard challenges us to reduce unless abuse, It's still
better than stating the negative at the end of an error line:

   errors.add "bad thing should not happen" unless good_thing()

We stayed with the unless. Thanks for the math - that was awesome!

Great to see that the "standard" isn't absolute -- I guess i read "discourage" as "prohibit". Anyway, I wouldn't be agile if I didn't challenge things that conflicted with my own opinions. (And 'arbitrary' limitations on the use of language features is one of those things that I despise.)

-Rob

Rob Biedenharn http://agileconsultingllc.com
Rob@AgileConsultingLLC.com