Skip to content

Instantly share code, notes, and snippets.

@henrik
Last active September 21, 2022 03:06
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save henrik/d482d41288d732f97f2d to your computer and use it in GitHub Desktop.
Save henrik/d482d41288d732f97f2d to your computer and use it in GitHub Desktop.
Proof-of-concept of an Elixir construct like `case` but that matches strings against regexps. #elixirlang
defmodule RegexCase do
defmacro regex_case(string, do: lines) do
new_lines = Enum.map lines, fn ({:->, context, [[regex], result]}) ->
condition = quote do: String.match?(unquote(string), unquote(regex))
{:->, context, [[condition], result]}
end
# Base case if nothing matches; "cond" complains otherwise.
base_case = quote do: (true -> nil)
new_lines = new_lines ++ base_case
quote do
cond do
unquote(new_lines)
end
end
end
end
defmodule Run do
import RegexCase
def run do
regex_case "hello" do
~r/x/ -> IO.puts("matches x")
~r/e/ -> IO.puts("matches e")
~r/y/ -> IO.puts("matches y")
end
end
end
Run.run
@henrik
Copy link
Author

henrik commented Feb 22, 2018

License: Public domain. No attribution needed.

@Neophen
Copy link

Neophen commented Aug 12, 2020

how do you have an escape clause:
let's say i want to do something if the item matches and simply return the item if it doesn't:

    regex_case item do
      ~r/x/ -> do_y(item)
      true -> item
    end

@henrik
Copy link
Author

henrik commented Aug 15, 2020

Hi @Neophen! One way would be to allow anelse block:

defmodule RegexCase do
  defmacro regex_case(string, opts) do
    lines = Keyword.fetch!(opts, :do)
    else_case = Keyword.get(opts, :else, nil)

    new_lines = Enum.map lines, fn ({:->, context, [[regex], result]}) ->
      condition = quote do: String.match?(unquote(string), unquote(regex))
      {:->, context, [[condition], result]}
    end

    base_case = quote do: (true -> unquote(else_case))
    new_lines = new_lines ++ base_case

    quote do
      cond do
        unquote(new_lines)
      end
    end
  end
end

defmodule Run do
  import RegexCase

  def run do
    regex_case "hello" do
      ~r/x/ -> IO.puts("matches x")
    else
      IO.puts("no match")
    end
  end
end

Run.run

Another would be to pass through true -> _whatever clauses as-is:

 defmodule RegexCase do
   defmacro regex_case(string, do: lines) do
     new_lines = Enum.map lines, fn
       ({:->, _context, [[true], _result]} = true_case) -> true_case
       ({:->, context, [[regex], result]}) ->
         condition = quote do: String.match?(unquote(string), unquote(regex))
         {:->, context, [[condition], result]}
     end

     # Base case, for when nothing matches and we didn't provide our own base case; "cond" complains otherwise.
     base_case = quote do: (true -> nil)
     new_lines = new_lines ++ base_case

     quote do
       cond do
         unquote(new_lines)
       end
     end
   end
 end

 defmodule Run do
   import RegexCase

   def run do
     regex_case "hello" do
       ~r/x/ -> IO.puts("matches x")
       true -> IO.puts("no match")
     end
   end
 end

 Run.run

Seems it's fine to have the macro always define its own true -> nil clause as a fallback, even if you define your own (that will match instead, because yours is earlier).

@henrik
Copy link
Author

henrik commented Aug 15, 2020

A third idea is to just keep the current code and use an empty regex as the base case, since it will match any string:

regex_case "hello" do
  ~r/x/ -> IO.puts("matches x")
  ~r// -> IO.puts("no match")
end

This one won't work if you're expecting non-string input.

@Neophen
Copy link

Neophen commented Aug 17, 2020

Awesome the second example with provide the true is a nicer syntax! thank you Henrik

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment