Beancount.Renderer (beancount_ex v0.6.0)

Copy Markdown View Source

Deterministic rendering of directive streams into Beancount text.

Rendering is intentionally pure and deterministic: rendering the same directive stream twice always produces byte-identical output. This property is essential for golden-file regression testing and for using Beancount as a behavioral oracle.

The module also exposes the low-level formatting helpers (format_date/1, format_decimal/1, quote_string/1, ...) used by the individual Beancount.Directive implementations.

Summary

Functions

Format a Date as an ISO-8601 YYYY-MM-DD string.

Format a Decimal using plain (non-scientific) notation.

Render a scalar value as used in metadata and custom directives.

Join header text, metadata lines and body lines into a single fragment.

Quote and escape a string the way Beancount expects.

Render a list of directives into a complete .bean document.

Render a metadata map into indented key: value lines.

Render the postings of a transaction as aligned, indented lines.

Render #tag and ^link suffixes for a transaction header.

Functions

format_date(date)

@spec format_date(Date.t()) :: binary()

Format a Date as an ISO-8601 YYYY-MM-DD string.

Examples

iex> Beancount.Renderer.format_date(~D[2026-01-31])
"2026-01-31"

format_decimal(decimal)

@spec format_decimal(Decimal.t()) :: binary()

Format a Decimal using plain (non-scientific) notation.

Examples

iex> Beancount.Renderer.format_decimal(Decimal.new("-5000"))
"-5000"

iex> Beancount.Renderer.format_decimal(Decimal.new("12.50"))
"12.50"

format_value(value)

@spec format_value(term()) :: binary()

Render a scalar value as used in metadata and custom directives.

  • binaries become quoted strings
  • Decimal and numbers become bare numbers
  • Date becomes an ISO date
  • booleans become TRUE/FALSE
  • atoms become barewords (useful for accounts/currencies)
  • Beancount.Value.Account, Tag, and Amount for custom directives

Examples

iex> Beancount.Renderer.format_value("hello") == Beancount.Renderer.quote_string("hello")
true

iex> Beancount.Renderer.format_value(Decimal.new("42"))
"42"

iex> Beancount.Renderer.format_value(~D[2026-01-01])
"2026-01-01"

iex> Beancount.Renderer.format_value(Beancount.account_value("Assets:Bank"))
"Assets:Bank"

lines_to_fragment(lines)

@spec lines_to_fragment([binary()]) :: binary()

Join header text, metadata lines and body lines into a single fragment.

Used by directives that span multiple lines (such as transactions).

Examples

iex> Beancount.Renderer.lines_to_fragment(["header", "  posting"])
"header\n  posting"

quote_string(string)

@spec quote_string(binary()) :: binary()

Quote and escape a string the way Beancount expects.

Examples

iex> Beancount.Renderer.quote_string(~S(a "quoted" value))
~S("a \"quoted\" value")

render(directives)

@spec render([Beancount.Directive.t()]) :: binary()

Render a list of directives into a complete .bean document.

Directives are separated by a single blank line and the document ends with a trailing newline.

Examples

iex> ledger = [
...>   Beancount.open(~D[2026-01-01], "Assets:Bank", ["USD"]),
...>   Beancount.close(~D[2026-12-31], "Assets:Bank")
...> ]
iex> Beancount.Renderer.render(ledger)
"2026-01-01 open Assets:Bank USD\n\n2026-12-31 close Assets:Bank\n"

render_metadata(metadata, depth \\ 1)

@spec render_metadata(map(), non_neg_integer()) :: [binary()]

Render a metadata map into indented key: value lines.

Keys are emitted in deterministic (sorted) order. Returns an empty list when there is no metadata.

Examples

iex> lines = Beancount.Renderer.render_metadata(%{"note" => "reviewed"})
iex> hd(lines) =~ "note:"
true

iex> Beancount.Renderer.render_metadata(%{})
[]

render_postings(postings)

@spec render_postings([Beancount.Directives.Posting.t()]) :: [binary()]

Render the postings of a transaction as aligned, indented lines.

Amounts are right-aligned so that decimal values line up, matching the conventional Beancount layout. Posting-level metadata is rendered indented beneath its posting.

Examples

iex> postings = [
...>   Beancount.posting("Assets:Bank", Decimal.new("100"), "USD"),
...>   Beancount.posting("Income:Salary", Decimal.new("-100"), "USD")
...> ]
iex> lines = Beancount.Renderer.render_postings(postings)
iex> Enum.all?(lines, &String.starts_with?(&1, "  "))
true

render_tags_and_links(tags, links)

@spec render_tags_and_links([binary()], [binary()]) :: binary()

Render #tag and ^link suffixes for a transaction header.

Tags are rendered before links, each in sorted order for determinism.

Examples

iex> Beancount.Renderer.render_tags_and_links(["trip"], ["invoice-1"])
" #trip ^invoice-1"