Packages
beancount_ex
0.6.0
An idiomatic Elixir interface to Beancount that serves as the long-term behavioral oracle for a future native Elixir General Ledger.
Current section
Files
Jump to
Current section
Files
lib/beancount/parser/cost.ex
defmodule Beancount.Parser.Cost do
@moduledoc false
alias Beancount.CostSpec
alias Beancount.Parser.{Error, Lexer}
@spec parse(binary(), keyword()) :: {:ok, CostSpec.t()} | {:error, Error.t()}
def parse(text, opts \\ []) do
text = String.trim(text)
cond do
String.starts_with?(text, "{{") and String.ends_with?(text, "}}") ->
inner = String.slice(text, 2..-3//1)
parse_total_only(inner, opts)
String.starts_with?(text, "{") and String.ends_with?(text, "}") ->
inner = String.slice(text, 1..-2//1)
parse_braced(inner, opts)
true ->
error("invalid cost spec", opts)
end
end
defp parse_total_only(inner, opts) do
with {:ok, tokens} <- tokenize(inner, opts),
{:ok, amount, currency, rest} <- parse_amount_currency(tokens, opts),
:ok <- ensure_empty(rest, opts) do
{:ok, %CostSpec{total_amount: amount, total_currency: currency, merge: false}}
end
end
defp parse_braced(inner, opts) do
with {:ok, tokens} <- tokenize(inner, opts) do
parse_braced_tokens(tokens, opts)
end
end
defp parse_braced_tokens([date | rest], opts) do
if date_token?(date) do
with {:ok, date} <- parse_date_token(date, opts),
{:ok, _extras, merge} <- parse_extras(rest, opts) do
{:ok, %CostSpec{date: date, merge: merge}}
end
else
parse_braced_tokens_label_or_amount([date | rest], opts)
end
end
defp parse_braced_tokens_label_or_amount([label | rest], opts) do
if quoted_token?(label) do
with {:ok, label} <- parse_quoted(label, opts),
{:ok, _extras, merge} <- parse_extras(rest, opts) do
{:ok, %CostSpec{label: label, merge: merge}}
end
else
parse_braced_amount([label | rest], opts)
end
end
defp parse_braced_amount(tokens, opts) do
with {:ok, per_amount, per_currency, rest} <- parse_amount_currency(tokens, opts) do
parse_braced_amount_rest(per_amount, per_currency, rest, opts)
end
end
defp parse_braced_amount_rest(per_amount, per_currency, ["#", amount, currency | extras], opts) do
with {:ok, total_amount} <- parse_number_token(amount, opts),
{:ok, total_currency} <- parse_commodity_token(currency, opts),
{:ok, _extras, merge} <- parse_extras(extras, opts) do
{:ok,
%CostSpec{
per_amount: per_amount,
per_currency: per_currency,
total_amount: total_amount,
total_currency: total_currency,
merge: merge
}}
end
end
defp parse_braced_amount_rest(per_amount, per_currency, rest, opts) do
with {:ok, date, label, merge} <- parse_per_extras(rest, opts) do
{:ok,
%CostSpec{
per_amount: per_amount,
per_currency: per_currency,
date: date,
label: label,
merge: merge
}}
end
end
defp parse_per_extras(tokens, opts) do
{date, label, merge, rest} = split_extras(tokens, opts)
case rest do
[] -> {:ok, date, label, merge}
_ -> error("unexpected tokens in cost spec", opts)
end
end
defp parse_extras(tokens, opts) do
{date, label, merge, rest} = split_extras(tokens, opts)
case rest do
[] -> {:ok, {date, label}, merge}
_ -> error("unexpected tokens in cost spec", opts)
end
end
defp split_extras(tokens, opts) do
{date, rest} =
case tokens do
[token | rest] ->
if date_token?(token) do
{:ok, date} = parse_date_token(token, opts)
{date, rest}
else
{nil, tokens}
end
_ ->
{nil, tokens}
end
{label, rest} =
case rest do
[token | rest] ->
if quoted_token?(token) do
{:ok, label} = parse_quoted(token, opts)
{label, rest}
else
{nil, rest}
end
_ ->
{nil, rest}
end
{merge, rest} =
case rest do
["merge" | rest] -> {true, rest}
_ -> {false, rest}
end
{date, label, merge, rest}
end
defp parse_amount_currency([amount, currency | rest], opts) do
with {:ok, amount} <- parse_number_token(amount, opts),
{:ok, currency} <- parse_commodity_token(currency, opts) do
{:ok, amount, currency, rest}
end
end
defp parse_amount_currency(_tokens, opts), do: error("expected amount and currency", opts)
defp tokenize(inner, opts) do
parts =
inner
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.flat_map(&Lexer.split_tokens/1)
if parts == [] do
error("empty cost spec", opts)
else
{:ok, parts}
end
end
defp ensure_empty([], _opts), do: :ok
defp ensure_empty(_rest, opts), do: error("unexpected tokens in cost spec", opts)
defp date_token?(token), do: Regex.match?(~r/^\d{4}-\d{2}-\d{2}$/, token)
defp quoted_token?(token),
do: String.starts_with?(token, "\"") and String.ends_with?(token, "\"")
defp parse_date_token(token, _opts), do: Date.from_iso8601(token)
defp parse_number_token(token, opts), do: Lexer.parse_number(token) |> map_error(opts)
defp parse_commodity_token(token, opts), do: Lexer.parse_commodity(token) |> map_error(opts)
defp parse_quoted(token, opts) do
case Lexer.parse_quoted_string(token) do
{:ok, value, ""} -> {:ok, value}
_ -> error("invalid quoted string in cost spec", opts)
end
end
defp map_error({:ok, value, ""}, _opts), do: {:ok, value}
defp map_error(_, opts), do: error("invalid token in cost spec", opts)
defp error(message, opts) do
{:error,
%Error{message: message, line: Keyword.get(opts, :line), column: Keyword.get(opts, :column)}}
end
end