Reputation: 303
# Erlang/OTP 24, Elixir 1.12.3
bmp_signature = <<66, 77>>
#=> "BM"
<<^bmp_signature, _::binary>> = <<66, 77, 30, 0>>
#=> ** (MatchError) no match of right hand side value: <<66, 77, 30, 0>>
Why is this happening?
In short, I'd like to pattern match bitstrings in a cycle, rather than writing method definitions by hand. So instead of this:
@bmp_signature <<66, 77>>
…
def type(<<@bmp_signature, _::binary>>), do: :bmp
…
…something like this:
@signatures %{
"bmp" => <<66, 77>>,
…
}
def detect_type(file) do
find_value_func = fn {extension, signature} ->
case file do
<<^signature, _::binary>> -> extension
_ -> false
end
end
Enum.find_value(@signatures, find_value_func)
end
Is this solvable without metaprogramming?
Upvotes: 2
Views: 696
Reputation: 303
Erlang's :binary.match/2 is the right tool for the job:
defmodule Type do
@signatures [
{"bmp", <<66, 77>>},
{"jpg", <<255, 216, 255>>},
]
def detect(file_binary) do
find_value_func = fn {extension, signature} ->
case :binary.match(file_binary, signature) do
{0, _} -> extension
_ -> nil
end
end
Enum.find_value(@signatures, find_value_func)
end
end
Type.detect(<<66, 77, 0, 0>>) #=> "bmp"
Type.detect(<<55, 66, 0, 0>>) #=> nil
Type.detect(<<255, 216, 255, 0>>) #=> "jpg"
A big thank you goes to @Everett for his advice to look elsewhere.
Upvotes: -1
Reputation: 121020
Asking whether this is doable without metaprogramming is like asking to solve a list reverse without both Enum
module and recursion.
In elixir, there is metaprogramming to solve exactly this kind of task. It makes the code clean, succinct, and manageable.
defmodule Type do
@signatures %{
bmp: <<66, 77>>
}
Enum.each(@signatures, fn {name, bom} ->
def detect(<<unquote(bom), _::binary>>), do: unquote(name)
end)
def detect(_), do: nil
end
Type.detect(<<66, 77, 30, 0>>)
#⇒ :bmp
Type.detect(<<66, 30, 0>>)
#⇒ nil
It’s still can be done in an ugly not idiomatic way without metaprogramming, though.
defmodule Type do
@signatures [
{<<66, 77>>, :bmp},
{<<77, 66>>, :pmb}
]
def detect(signature, candidates \\ @signatures) do
for <<c <- signature>>, reduce: @signatures do
acc ->
Enum.reduce(acc, [], fn
{"", name}, acc -> [{"", name} | acc]
{<<^c, rest::binary>>, name}, acc -> [{rest, name} | acc]
_, acc -> acc
end)
end
end
end
case Type.detect(<<66, 77, 30, 0>>) do
[{"", type}] -> {:ok, type}
[] -> {:error, :none}
[few] -> {:error, few: few}
end
#⇒ {:ok, :bmp}
Upvotes: 3
Reputation: 9638
Your syntax is slightly off. Remember that the pin operator ^
pins only a single value. In your example, you were trying to pin it to 2 values.
So if the thing you are trying to match on is a binary with 2 values that you are aware of, then you would need to pin both of them, e.g.
iex> <<bmp_sig1, bmp_sig2>> = <<66, 77>>
iex> <<^bmp_sig1, ^bmp_sig2, rest::binary>> = <<66, 77, 88, 23, 44, 89>>
<<66, 77, 88, 23, 44, 89>>
iex> rest
<<88, 23, 44, 89>>
The binary syntax <<>>
isn't the only way to do this -- you can accomplish the same with regular strings (assuming the values are in fact strings):
iex> x = "apple"
"apple"
iex> "ap" <> rest = x
"apple"
iex> rest
"ple"
The rub here is that you can't pin a prefix because you need a literal value in order to do the match. This is because the length of the binary isn't known beforehand.
If you know your "signatures" always have 2, 3, or 4 characters, you can code your variables to be pinned appropriately. However, if you must deal with a an unknown length, then you'd probably need to rely on something more like a regular expression or String.starts_with?/2
or similar.
Upvotes: 2