How to [elegantly] extract values for keys in map, reject nils and determine if all values match true

Ok, here’s the problem:

Given a map of variable keys and values, select a set of those keys which may be either nil, false, or true. Only return true if there is at least one true value and no false values.

Here is an example map I have, where I would be extracting the 3 matches_ keys:

%{
  random_key: 123,
  another: 456,
  matches_1: nil,
  matches_2: true,
  matches_3: false
}

In the above, the check would fail because the matches include false. Here is another false (as no true values):

%{
  random_key: 123,
  another: 456,
  matches_1: nil,
  matches_2: nil,
  matches_3: nil
}

Here is a pass:

%{
  random_key: 123,
  another: 456,
  matches_1: nil,
  matches_2: true,
  matches_3: nil
}

My attempt at this:

  def pass?(map) do
    Map.take(map, [:matches_1, :matches_2, :matches_3])
    |> Map.values
    |> Enum.reject(&is_nil/1)
    |> (fn(values) -> length(values) > 0 && Enum.all?(values, fn x -> x == true end) end).()
  end

Would love to see if this can be more elegantly expressed. The crux of the conditions and what I’m not that happy with is the last line:

(fn(values) -> length(values) > 0 && Enum.all?(values, fn x -> x == true end) end).()

1 Like

Would this work?

@spec pass?(map) :: boolean
def pass?(%{matches_1: matches_1, matches_2: matches_2, matches_3: matches_3}) do
  [matches_1, matches_2, matches_3]
  |> Enum.reject(&is_nil/1)
  |> Enum.find(true, &(!&1))
end

Probably not. Let me think again.

What would happen in the case of nil in those conditions? A nil on matches_1 would continue to matches_2?

That appears to be testing that all matches return true?

Just saw the edit, cheers! I like the direction it is going with pattern matching. Hadn’t thought of using that.

But the edit will return true if any values match true, even if other values are false.

Idiomatic answers to un-idiomatic data are always the trickiest. Is there a way to not do matches_1, matches_2 and instead have matches: [nil, true false] ?

The matches themselves map to database table columns and I’d prefer to keep the matches as descriptive as possible during operations, that’s the only reason it seems a little odd. I could transform the structure into what you suggest first… though that is essentially what I’m doing with Map.values.

Gotcha. Here’s my take:

def pass?(map) do
  map
  |> Map.take([:matches_1, :matches_2, :matches_3])
  |> Map.values
  |> Enum.map(fn
    nil -> true
    val -> val
  end)
  |> Enum.all?
end
2 Likes

Clever! I like.

Hold on… what is nil -> true doing? Any ref docs?

essentially yeah, although I’m realizing that my logic isn’t what you requested. “Only return true if there is at least one true value and no false values.”

I think a variation on @idi527’s solution might work:

def pass?(map) do
  map
  |> Map.take([:matches_1, :matches_2, :matches_3])
  |> Map.values
  |> Enum.reject(&is_nil/1)
  |> Enum.find(false, &(&1))
end
1 Like

Was thinking that, threw me off a little :stuck_out_tongue:

That looks more like it, thanks. I think Enum.find should be Enum.find(enumerable, ...)

Do you have any ref docs for the nil -> true mapping, and the &(&1)? Or some keywords to give me something to go on?

This is my attempt. You can try with DecideMap.is_pass?(map).

defmodule DecideMap do
  def is_pass?(map) do
    values =
      map
      |> Map.take([:matches_1, :matches_2, :matches_3])
      |> Map.values()
      |> Enum.reject(&is_nil/1)

    with true <- false not in values,
         true <- true in values do
      true
    else
      _ -> false
    end
  end
end
1 Like

Just started to separate it out into values. Cheers!

well the nil -> true thing is just normal pattern matching. If you had a regular function it’d look like:

def map_nil_to_true(nil), do: true
def map_nil_to_true(value), do: value

As an anonymous function this just looks like:

fn
  nil -> true
  value -> value
end

Basically, anonymous functions can have multiple bodies just like named functions.

&(&1) is anonymous function short hand, you can read about it here:
https://hexdocs.pm/elixir/Kernel.SpecialForms.html#&/1-anonymous-functions

4 Likes

Enum.reduce_while/3

defmodule Demo do

  def run(map, keys) do
    check = fn (key, has_true) ->
      case Map.get(map, key) do
        true -> {:cont, true}
        false -> {:halt, false}
        _ -> {:cont, has_true}
      end
    end

    Enum.reduce_while(keys, false, check)
  end

end

sample1 = %{
  random_key: 123,
  another: 456,
  matches_1: nil,
  matches_2: true,
  matches_3: false
}

sample2 = %{
  random_key: 123,
  another: 456,
  matches_1: nil,
  matches_2: nil,
  matches_3: nil
}

sample3 = %{
  random_key: 123,
  another: 456,
  matches_1: nil,
  matches_2: true,
  matches_3: nil
}

keys = [:matches_1, :matches_2, :matches_3]

IO.inspect(Demo.run(sample1, keys)) // false
IO.inspect(Demo.run(sample2, keys)) // false
IO.inspect(Demo.run(sample3, keys)) // true
4 Likes

With that values you could just do this:

false not in values and true in values

so it could be simpler:

defmodule DecideMap do
  def is_pass?(map) do
    values =
      map
      |> Map.take([:matches_1, :matches_2, :matches_3])
      |> Map.values()
      |> Enum.reject(&is_nil/1)

    false not in values and true in values
  end
end

with is there, just in case you want to easily add other kind of conditions in the future.

4 Likes

I really like that! Very simple to follow.

A slightly different spin:

def pass?(%{matches_1: m1, matches_2: m2, matches_3: m3}) do
  [true, false] -- [m1, m2, m3] == [false]
end

edit One more:

def pass?(%{matches_1: m1, matches_2: m2, matches_3: m3}) do
  [true] == [m1, m2, m3]
    |> Enum.reject(&is_nil/1)
    |> Enum.uniq()
end
1 Like

That last one is really cool! I need to make more use of pattern matching and assertive programming.