aspin
aspin

Reputation: 317

Elixir: pattern matching two same arguments to a function

I'm iterating through a list of strings, and I want to return the contents of a string if the beginning of it matches the provided string.

e.g.

strings = [ "GITHUB:github.com", "STACKOVERFLOW:stackoverflow.com" ]
IO.puts fn(strings, "GITHUB") // => "github.com"

This is what I thinking so far:

def get_tag_value([ << tag_name, ": ", tag_value::binary >> | rest ], tag_name), do: tag_value

def get_tag_value([ _ | rest], tag_name), do: get_tag_value(rest, tag_name)

def get_tag_value([], tag_name), do: ""

But I get this:

** (CompileError) lib/file.ex:31: a binary field without size is only allowed at the end of a binary pattern and never allowed in binary generators

Which makes sense, but then I'm not quite sure how to go about doing this. How would I match a substring to a different variable provided as an argument?

Upvotes: 1

Views: 2167

Answers (5)

ryanwinchester
ryanwinchester

Reputation: 12157

There are many ways to skin this cat.

For example:

def get_tag_value(tag, strings) do
  strings
  |> Enum.find("", &String.starts_with?(&1, tag <> ":"))
  |> String.split(":", parts: 2)
  |> Enum.at(1, "")
end

or if you still wanted to explicitly use recursion:

def get_tag_value(_tag, []), do: ""

def get_tag_value(tag, [str | rest]) do
  if String.starts_with?(str, tag <> ":") do
    String.split(str, ":", parts: 2) |> Enum.at(1, "")
  else
    get_tag_value(tag, rest)
  end
end

Are just two of many possible ways.

However, you won't be able to pattern match the string in the function head without knowing it (or at least the length) beforehand.

Upvotes: 2

Dogbert
Dogbert

Reputation: 222428

Here's how I'd do this making most use of pattern matching and no call to String.starts_with? or String.split:

defmodule A do
  def find(strings, string) do
    size = byte_size(string)
    Enum.find_value strings, fn
      <<^string::binary-size(size), ":", rest::binary>> -> rest
      _ -> nil
    end
  end
end

strings = ["GITHUB:github.com", "STACKOVERFLOW:stackoverflow.com"]

IO.inspect A.find(strings, "GITHUB")
IO.inspect A.find(strings, "STACKOVERFLOW")
IO.inspect A.find(strings, "GIT")
IO.inspect A.find(strings, "FOO")

Output:

"github.com"
"stackoverflow.com"
nil
nil

Upvotes: 2

Brujo Benavides
Brujo Benavides

Reputation: 1958

This would be my take in Erlang:

get_tag_value(Tag, Strings) ->
    L = size(Tag),
    [First | _] = [Val || <<Tag:L/binary, $:, Val/binary>> <- Strings]
    First.

The same in Elixir (there are probably more idiomatic ways of writing it, tho):

def gtv(tag, strings) do
  l = :erlang.size(tag)
  [first | _ ] =
    for << t :: binary - size(l), ":", value :: binary >> <- strings,
        t == tag,
        do: value
  first
end

Upvotes: 0

M&#225;t&#233;
M&#225;t&#233;

Reputation: 2345

You can use a combination of Enum.map and Enum.filter to get the matching pairs you're looking for:

def get_tag_value(tag_name, tags) do
  tags
  |> Enum.map(&String.split(&1, ":")) # Creates a list of [tag_name, tag_value] elements
  |> Enum.filter(fn([tn, tv]) -> tn == tag_name end) # Filters for the tag name you're after
  |> List.last # Potentially gets you the pair [tag_name, tag_value] OR empty list
end

And in the end you can either call List.last/1 again to either get an empty list (no match found) or the tag value.

Alternatively you can use a case statement to return a different kind of result, like a :nomatch atom:

def get_tag_value(tag_name, tags) do
  matches = tags
    |> Enum.map(&String.split(&1, ":")) # Creates a list of [tag_name, tag_value] elements
    |> Enum.filter(fn([tn, tv]) -> tn == tag_name end) # Filters for the tag name you're after
    |> List.last # Potentially gets you the pair [tag_name, tag_value] OR empty list
  case matches do
    [] -> :nomatch
    [_, tag_value] -> tag_value
  end
end

Upvotes: 0

guitarman
guitarman

Reputation: 3320

iex(1)> strings = [ "GITHUB:github.com", "STACKOVERFLOW:stackoverflow.com" ]

iex(2)> Enum.filter(strings, fn(s) -> String.starts_with?(s, "GITHUB") end)
iex(3)> |> Enum.map(fn(s) -> [_, part_2] = String.split(s, ":"); part_2 end)

# => ["github.com"]

In Enum.filter/2 I select all strings they start with "GITHUB" and I get a new List. Enum.map/2 iterates through the new List and splits each string at the colon to return the second part only. Result is a List with all parts after the colon, where the original string starts with "GITHUB".

Be aware, that If there's an item like "GITHUBgithub.com" without colon, you get a MatchError. To avoid this either use String.starts_with?(s, "GITHUB:") to filter the right strings or avoid the pattern matching like I did in Enum.map/2 or use pattern matching for an empty list like @ryanwinchester did it.

Upvotes: 0

Related Questions