Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you pattern match for nil or a blank string in Elixir?

Tags:

elixir

Here is a code sample with tests where I have a function that checks if the value passed in is in an allowable set of values and if it isn't, it raises an exception, but if the value passed in is nil or a blank string, it just returns false. The idea here is that any sort of blank value means is there is no value. If there is a non-blank value that doesn't match one of the valid values, then we do want to raise an exception because that value is invalid. Also imagine this value is coming from a command line argument or Phoenix request param where we can't control the input value be just nil, we have to check for all permutations of an empty value, nil, an empty string or a string containing just spaces.

ExUnit.start()

defmodule ValidColor do
  @valid_colors MapSet.new(~w[red green blue])

  def valid?(color) do
    cond do
      blank?(color) -> false
      MapSet.member?(@valid_colors, color) -> true
      true -> raise "invalid color"
    end
  end

  defp blank?(value) do
    case value do
      nil -> true
      v when is_binary(v) -> v =~ ~r/\A\s*\z/
      _ -> false
    end
  end
end

defmodule ValidColorTest do
  use ExUnit.Case, async: true

  test "nil" do
    refute ValidColor.valid?(nil)
  end

  test "empty string" do
    refute ValidColor.valid?("")
  end

  test "string with only blank characters" do
    refute ValidColor.valid?("     ")
  end

  test "invalid color" do
    assert_raise RuntimeError, "invalid color", fn ->
      ValidColor.valid?("yellow")
    end
  end

  test "valid color" do
    assert ValidColor.valid?("red")
  end
end

This seems to be a case when a blank? helper function is useful, as I have done in this example. There seems to be a sentiment among Elixir programmers that this type of helper method is unnecessary and can be done with pattern matching.

How can you implement a function like this with just pattern matching and not the blank? helper function?

like image 472
pjb3 Avatar asked Sep 24 '18 15:09

pjb3


3 Answers

While all the answers given here are somehow correct, they are not semantically perfect. What you want is actually to treat nil as an empty string and reject it. Do it straightforward:

defp blank?(str_or_nil),
  do: "" == str_or_nil |> to_string() |> String.trim()

Please note, that this solution, unlike other given here so far, works with all the spaces, defined as spaces in UTF-8 specification.

like image 96
Aleksei Matiushkin Avatar answered Sep 27 '22 20:09

Aleksei Matiushkin


The nil case is easy:

defp blank?(nil), do: true

To check if a string consists of only whitespace characters, more involved pattern matching is required:

# Empty string is always blank.
defp blank?(""), do: true

# If the string's first byte is a space, tab, newline, or carriage return,
# we recursively check the remaining part of the string.
defp blank?(<<h, t::binary>>) when h in ' \t\n\r', do: blank?(t)

# Otherwise, it's not blank.
defp blank?(_), do: false
like image 31
Dogbert Avatar answered Sep 27 '22 20:09

Dogbert


You can also use a case/2:

def blank?(str) do
  case str do
    nil -> true
    "" -> true
    " " <> r -> blank?(r)
    _ -> false 
  end
end

NB: in this case we only consider spaces, pattern matching on "\n","\r" and "\t" must also be considered as @dogbert did in his answer.

like image 30
Nathan Ripert Avatar answered Sep 27 '22 20:09

Nathan Ripert