OK, revised version. Not sure its any more “beautiful” than your version but may be food for thought:
case some_string do
<<h1::integer-8, h2::integer-8, version::binary-4, count::binary-4, "$">>
when (h1 in ?A..?Z or h1 in ?0..?9) and (h2 in ?A..?Z or h2 in ?0..?9)->
header = List.to_string [h1, h2]
version = String.to_integer(version, 16)
count = String.to_integer(count, 16)
{:ok, header, version, count}
_other ->
{:error, :invalid}
end
The reason I chose for nimble_parsec is because, down the line, there are many more commands that I have to implement where some have variable byte lenghts for some of the paramters and I don’t want to type those all out. I’d rather use nimble_parse for that.
Using nimble_parsec I tend to prefer using a helper module for combinators rather than inline. I also tend to create semantic functions because sooner or later debugging a nimble_parsec combinator - and providing reasonable messages back to a user on parse errors - becomes a challenge.
Example parser
This is a pattern I usually follow, here just parsing your first example.
defmodule Parse.Helpers do
import NimbleParsec
def version() do
head()
|> concat(hex_integer(4))
|> label("version")
|> tag(:version)
end
def hex_integer(digits) do
ascii_string([?a..?f, ?A..?F, ?0..?9], digits)
|> reduce(:hex_to_integer)
end
def count() do
hex_integer(4)
|> label("count")
|> unwrap_and_tag(:count)
end
def head() do
ascii_string([?A..?Z, ?0..?9], 2)
|> label("head")
end
def tail() do
string("$")
|> label("tail")
|> unwrap_and_tag(:tail)
end
def hex_to_integer([string]) do
String.to_integer(string, 16)
end
end
defmodule Parser do
import NimbleParsec
import Parse.Helpers
defparsec :parse,
version()
|> concat(count())
|> concat(tail())
end
Example usage
iex> Parser.parse "AB98761234$"
{:ok, [version: ["AB", 39030], count: 4660, tail: "$"], "", %{}, {1, 0}, 11}
iex> Parser.parse "AB98761234"
{:error, "expected version, followed by count, followed by tail", "AB98761234",
%{}, {1, 0}, 0}