ExLedger (ex_ledger v0.5.6)

Main module for ExLedger - a ledger-cli format parser and processor.

Provides utility functions for formatting dates and amounts in ledger format.

Summary

Functions

Calculates balances per account.

Balances postings by filling in a single missing amount.

Builds a budget report for periodic transactions.

Builds a new transaction entry based on a payee pattern.

Checks that all accounts used in transactions are declared.

Checks that all commodities used in transactions are declared.

Checks whether a ledger file parses successfully.

Checks a ledger file and returns an error tuple on failure.

Checks that all payees used in transactions are declared.

Checks whether a ledger string parses successfully.

Checks that all tags used in transactions are declared.

Expands include directives and returns the resolved ledger content.

Extracts account declarations from ledger content.

Extracts declared commodities from ledger content.

Extracts declared payees from ledger content.

Extracts declared tags from ledger content.

Returns the earliest regular transaction by date.

Forecasts balances after applying periodic budgets.

Formats account postings into a register report.

Formats an amount into ledger format with currency symbol and 2 decimal places.

Formats a numeric value for a specific currency.

Formats balance assertion failures as a human-readable error message.

Formats balances as a report string.

Formats a budget report table.

Formats a Date struct into ledger register format: YY-Mon-DD

Parses ledger content and returns formatted transactions.

Formats select rows into a tab-separated output.

Formats stats into a report string.

Formats a timeclock report.

Formats transactions into ledger-compatible output.

Returns postings for an account with running balance.

Returns the latest regular transaction by date.

Lists all account names, including declared accounts.

Lists all commodities referenced in postings.

Lists all payees in the transactions.

Lists all tags referenced in postings.

Parses an account declaration line.

Parses an amount string into a structured amount map.

Parses a date string into a Date struct.

Parses a ledger file.

Parses a ledger string with support for include directives and account declarations.

Parses a note line and returns the note tuple.

Parses a posting line.

Parses timeclock entries from input.

Parses a single transaction from a string.

Builds a register view of postings with running balances.

Resolves an account name or alias to its canonical name.

Resolves all account aliases inside transactions.

Selects postings matching a query string.

Builds summary statistics for the ledger.

Builds a timeclock report grouped by account.

Validates balance assertions in transactions.

Validates that a transaction is balanced.

Functions

balance(transaction_or_transactions)

Calculates balances per account.

Examples

iex> transactions = [%{postings: [
...>   %{account: "Assets:Cash", amount: %{value: 10.0, currency: "$"}},
...>   %{account: "Expenses:Food", amount: %{value: -10.0, currency: "$"}}
...> ]}]
iex> [first | _] = ExLedger.balance(transactions)["Assets:Cash"]
iex> first.amount
10.0

balance_by_period(transactions, group_by \\ "none", start_date \\ nil, end_date \\ nil, account_filter \\ nil)

Calculates balances grouped by time period.

Examples

iex> transactions = [%{date: ~D[2024-01-01], payee: "Open", postings: [%{account: "Assets:Cash", amount: %{value: 10.0, currency: "$"}}]}]
iex> result = ExLedger.balance_by_period(transactions, "monthly")
iex> Map.has_key?(result, "balances")
true

balance_postings(transaction_or_postings)

Balances postings by filling in a single missing amount.

Examples

iex> postings = [
...>   %{account: "Assets:Cash", amount: %{value: 10.0, currency: "$"}},
...>   %{account: "Equity:Opening", amount: nil}
...> ]
iex> [_filled, missing] = ExLedger.balance_postings(postings)
iex> missing.amount.value
-10.0

budget_report(transactions, date \\ Date.utc_today())

Builds a budget report for periodic transactions.

Examples

iex> transactions = [
...>   %{kind: :periodic, period: "monthly", postings: [%{account: "Expenses:Rent", amount: %{value: 100.0, currency: "$"}}]},
...>   %{kind: :regular, date: ~D[2024-01-10], postings: [%{account: "Expenses:Rent", amount: %{value: 40.0, currency: "$"}}]}
...> ]
iex> rows = ExLedger.budget_report(transactions, ~D[2024-01-15])
iex> Enum.any?(rows, &(&1.account == "Expenses:Rent"))
true

build_xact(transactions, date, payee_pattern)

Builds a new transaction entry based on a payee pattern.

Examples

iex> transactions = [
...>   %{date: ~D[2024-01-01], payee: "Coffee", postings: [
...>     %{account: "Expenses:Food", amount: %{value: 4.0, currency: "$"}},
...>     %{account: "Assets:Cash", amount: %{value: -4.0, currency: "$"}}
...>   ]}
...> ]
iex> {:ok, output} = ExLedger.build_xact(transactions, ~D[2024-01-15], "Coffee")
iex> String.contains?(output, "Coffee")
true

check_accounts(transactions, accounts)

Checks that all accounts used in transactions are declared.

Examples

iex> transactions = [%{postings: [%{account: "Assets:Cash", amount: %{value: 1.0, currency: "$"}}]}]
iex> ExLedger.check_accounts(transactions, %{"Assets:Cash" => :asset})
:ok

check_commodities(transactions, declared_commodities)

Checks that all commodities used in transactions are declared.

Examples

iex> transactions = [%{postings: [%{account: "Assets:Cash", amount: %{value: 1.0, currency: "$"}}]}]
iex> ExLedger.check_commodities(transactions, MapSet.new(["$"]))
:ok

check_file(path)

Checks whether a ledger file parses successfully.

Examples

iex> is_boolean(ExLedger.check_file("path/to/file.ledger"))
true

check_file_with_error(path)

Checks a ledger file and returns an error tuple on failure.

Examples

iex> result = ExLedger.check_file_with_error("path/to/file.ledger")
iex> match?({:ok, :valid}, result) or match?({:error, _}, result)
true

check_payees(transactions, declared_payees)

Checks that all payees used in transactions are declared.

Examples

iex> transactions = [%{payee: "Store", postings: []}]
iex> ExLedger.check_payees(transactions, MapSet.new(["Store"]))
:ok

check_string(content, base_dir \\ ".")

Checks whether a ledger string parses successfully.

Examples

iex> input = "2024/01/01 Opening
Assets:Cash  $10.00
Equity:Opening

"

iex> ExLedger.check_string(input, ".")
true

check_tags(transactions, contents, declared_tags)

Checks that all tags used in transactions are declared.

Examples

iex> transactions = [%{postings: [%{account: "Expenses:Food", amount: %{value: 5.0, currency: "$"}, tags: ["Food"]}]}]
iex> contents = "; :Food:

"

iex> ExLedger.check_tags(transactions, contents, MapSet.new(["Food"]))
:ok

expand_includes(input, base_dir, seen_files \\ MapSet.new(), source_file \\ nil)

Expands include directives and returns the resolved ledger content.

Examples

iex> input = "2024/01/01 Opening
Assets:Cash  $10.00
Equity:Opening

"

iex> {:ok, expanded} = ExLedger.expand_includes(input, ".")
iex> expanded == input
true

extract_account_declarations(input)

Extracts account declarations from ledger content.

Examples

iex> input = "account Assets:Checking  ; type:asset

"

iex> ExLedger.extract_account_declarations(input)
%{"Assets:Checking" => :asset}

extract_commodity_declarations(input)

Extracts declared commodities from ledger content.

Examples

iex> input = "commodity $

"

iex> ExLedger.extract_commodity_declarations(input) |> MapSet.member?("$")
true

extract_payee_declarations(input)

Extracts declared payees from ledger content.

Examples

iex> input = "payee Coffee Shop

payee Grocery "

iex> ExLedger.extract_payee_declarations(input) |> MapSet.member?("Grocery")
true

extract_tag_declarations(input)

Extracts declared tags from ledger content.

Examples

iex> input = "tag Travel

"

iex> ExLedger.extract_tag_declarations(input) |> MapSet.member?("Travel")
true

first_transaction(transactions)

Returns the earliest regular transaction by date.

Examples

iex> transactions = [%{date: ~D[2024-01-01], payee: "Open", postings: []}]
iex> ExLedger.first_transaction(transactions).payee
"Open"

forecast_balance(transactions, months \\ 1)

Forecasts balances after applying periodic budgets.

Examples

iex> transactions = [
...>   %{kind: :regular, date: ~D[2024-01-01], postings: [%{account: "Assets:Cash", amount: %{value: 10.0, currency: "$"}}]},
...>   %{kind: :periodic, period: "monthly", postings: [%{account: "Assets:Cash", amount: %{value: 5.0, currency: "$"}}]}
...> ]
iex> [first | _] = ExLedger.forecast_balance(transactions, 2)["Assets:Cash"]
iex> first.amount
20.0

format_account_register(postings, account_name)

Formats account postings into a register report.

Examples

iex> postings = [%{date: ~D[2024-01-01], description: "Open", account: "Assets:Cash", amount: %{value: 10.0, currency: "$"}, balance: %{value: 10.0, currency: "$"}}]
iex> String.contains?(ExLedger.format_account_register(postings, "Assets:Cash"), "Assets:Cash")
true

format_amount(amount)

@spec format_amount(Decimal.t() | number()) :: String.t()

Formats an amount into ledger format with currency symbol and 2 decimal places.

Examples

iex> ExLedger.format_amount(4.50)
"    $4.50"

iex> ExLedger.format_amount(-4.50)
"   -$4.50"

iex> ExLedger.format_amount(20.00)
"   $20.00"

format_amount_for_currency(value, currency)

Formats a numeric value for a specific currency.

Examples

iex> ExLedger.format_amount_for_currency(10.5, "$")
"$10.50"

format_assertion_failures(failures)

Formats balance assertion failures as a human-readable error message.

Examples

iex> failure = %{account: "Assets:Cash", expected: %{value: 100.0, currency: "CHF"}, actual: %{value: 50.0, currency: "CHF"}, transaction_date: ~D[2024-01-15], transaction_payee: "Check", source_file: nil, source_line: nil}
iex> String.contains?(ExLedger.format_assertion_failures([failure]), "Assets:Cash")
true

format_balance(balances, show_empty \\ false)

Formats balances as a report string.

Examples

iex> balances = %{"Assets:Checking" => [%{amount: -23.0, currency: "$"}], "Expenses:Utilities" => [%{amount: 23.0, currency: "$"}]}
iex> String.contains?(ExLedger.format_balance(balances), "Assets:Checking")
true

format_budget_report(rows)

Formats a budget report table.

Examples

iex> rows = [%{account: "Expenses:Rent", currency: "$", actual: 40.0, budget: 100.0, remaining: 60.0}]
iex> String.contains?(ExLedger.format_budget_report(rows), "Expenses:Rent")
true

format_date(date)

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

Formats a Date struct into ledger register format: YY-Mon-DD

Examples

iex> ExLedger.format_date(~D[2009-10-29])
"09-Oct-29"

iex> ExLedger.format_date(~D[2009-11-01])
"09-Nov-01"

format_ledger(input)

@spec format_ledger(String.t()) :: {:ok, String.t()} | {:error, term()}

Parses ledger content and returns formatted transactions.

Examples

iex> input = """
...> 2024/01/01 Opening
...>     Assets:Cash  $10.00
...>     Equity:Opening
...> """
iex> ExLedger.format_ledger(input)
{:ok, "2024/01/01 Opening
Assets:Cash  $10.00
Equity:Opening  $-10.00

"}

format_select(fields, rows)

Formats select rows into a tab-separated output.

Examples

iex> ExLedger.format_select(["account"], [%{"account" => "Assets:Cash"}])
"Assets:Cash

"

format_stats(stats)

Formats stats into a report string.

Examples

iex> transactions = [%{date: ~D[2024-01-01], payee: "Store", postings: [%{account: "Assets:Cash", amount: %{value: -5.0, currency: "$"}}]}]
iex> stats = ExLedger.stats(transactions)
iex> is_binary(ExLedger.format_stats(stats))
true

format_timeclock_report(report)

Formats a timeclock report.

Examples

iex> report = %{"Work" => 1.5}
iex> String.contains?(ExLedger.format_timeclock_report(report), "Work")
true

format_transactions(transactions)

Formats transactions into ledger-compatible output.

Examples

iex> input = "2024/01/01 Opening
Assets:Cash  $10.00
Equity:Opening

"

iex> {:ok, transactions} = ExLedger.parse_ledger(input)
iex> String.contains?(ExLedger.format_transactions(transactions), "Opening")
true

get_account_postings(transactions, account_name)

Returns postings for an account with running balance.

Examples

iex> transactions = [
...>   %{date: ~D[2024-01-01], payee: "Open", postings: [
...>     %{account: "Assets:Cash", amount: %{value: 10.0, currency: "$"}}
...>   ]}
...> ]
iex> ExLedger.get_account_postings(transactions, "Assets:Cash") |> length()
1

last_transaction(transactions)

Returns the latest regular transaction by date.

Examples

iex> transactions = [%{date: ~D[2024-01-01], payee: "Open", postings: []}]
iex> ExLedger.last_transaction(transactions).payee
"Open"

list_accounts(transactions, account_map \\ %{})

Lists all account names, including declared accounts.

Examples

iex> transactions = [%{postings: [%{account: "Assets:Cash", amount: %{value: 1.0, currency: "$"}}]}]
iex> ExLedger.list_accounts(transactions, %{"Assets:Bank" => :asset})
["Assets:Bank", "Assets:Cash"]

list_commodities(transactions)

Lists all commodities referenced in postings.

Examples

iex> transactions = [%{postings: [%{account: "Assets:Cash", amount: %{value: 1.0, currency: "$"}}]}]
iex> ExLedger.list_commodities(transactions)
["$"]

list_payees(transactions)

Lists all payees in the transactions.

Examples

iex> transactions = [%{payee: "Store", postings: []}, %{payee: "Cafe", postings: []}]
iex> ExLedger.list_payees(transactions)
["Cafe", "Store"]

list_tags(transactions)

Lists all tags referenced in postings.

Examples

iex> transactions = [%{postings: [%{account: "Expenses:Food", amount: %{value: 5.0, currency: "$"}, tags: ["Food"]}]}]
iex> ExLedger.list_tags(transactions)
["Food"]

parse_account_declaration(input)

Parses an account declaration line.

Examples

iex> ExLedger.parse_account_declaration("account Assets:Checking  ; type:asset")
{:ok, %{name: "Assets:Checking", type: :asset}}

parse_amount(amount_string)

Parses an amount string into a structured amount map.

Examples

iex> {:ok, amount} = ExLedger.parse_amount("$10.00")
iex> amount.value
10.0

parse_date(date_string)

Parses a date string into a Date struct.

Examples

iex> ExLedger.parse_date("2024/01/01")
{:ok, ~D[2024-01-01]}

parse_file(path, opts \\ [])

@spec parse_file(
  String.t(),
  keyword()
) :: {:ok, ExLedger.LedgerParser.parse_result()} | {:error, term()}

Parses a ledger file.

Options

All options are passed to parse_ledger/2. Additionally:

  • :base_dir defaults to the directory containing the file
  • :source_file defaults to the filename

Examples

{:ok, result} = ExLedger.parse_file("journal.ledger")
{:ok, result} = ExLedger.parse_file("journal.ledger", strict: true)

parse_ledger(input, opts \\ [])

Parses a ledger string with support for include directives and account declarations.

Returns {:ok, result} where result is a map containing:

  • :transactions - List of parsed transactions
  • :accounts - Map of account names to their types
  • :prices - List of price directives (P DATE SYMBOL PRICE)
  • :commodities - Map of commodity symbols to their full definitions

Options

  • :base_dir - Base directory for resolving relative include paths (default: ".")
  • :source_file - Source file name for error reporting (default: nil)

Examples

iex> input = "2024/01/01 Opening
Assets:Cash  $10.00
Equity:Opening

"

iex> {:ok, result} = ExLedger.parse_ledger(input)
iex> length(result.transactions)
1

iex> input = "2024/01/01 Opening
Assets:Cash  $10.00
Equity:Opening

"

iex> {:ok, result} = ExLedger.parse_ledger(input, source_file: "journal.ledger")
iex> hd(result.transactions).source_file
"journal.ledger"

parse_note(note_string)

Parses a note line and returns the note tuple.

Examples

iex> ExLedger.parse_note(";:Food:")
{:ok, {:tag, "Food"}}

parse_posting(line)

Parses a posting line.

Examples

iex> {:ok, posting} = ExLedger.parse_posting("Assets:Cash  $10.00")
iex> posting.account
"Assets:Cash"

parse_timeclock_entries(input)

Parses timeclock entries from input.

Examples

iex> input = "i 2024/01/01 09:00:00 Assets:Work  Client

O 2024/01/01 10:00:00 "

iex> ExLedger.parse_timeclock_entries(input) |> length()
1

parse_transaction(input)

Parses a single transaction from a string.

Examples

iex> input = "2024/01/01 Opening
Assets:Cash  $10.00
Equity:Opening

"

iex> {:ok, transaction} = ExLedger.parse_transaction(input)
iex> transaction.payee
"Opening"

register(transactions, account_regex \\ nil)

Builds a register view of postings with running balances.

Examples

iex> transactions = [%{date: ~D[2024-01-01], payee: "Open", postings: [%{account: "Assets:Cash", amount: %{value: 10.0, currency: "$"}}]}]
iex> ExLedger.register(transactions) |> length()
1

resolve_account_name(account_name, account_map)

Resolves an account name or alias to its canonical name.

Examples

iex> accounts = %{"Assets:Checking" => :asset, "checking" => "Assets:Checking"}
iex> ExLedger.resolve_account_name("checking", accounts)
"Assets:Checking"

resolve_transaction_aliases(transactions, account_map)

Resolves all account aliases inside transactions.

Examples

iex> transactions = [%{postings: [%{account: "checking", amount: %{value: -10.0, currency: "$"}}]}]
iex> accounts = %{"Assets:Checking" => :asset, "checking" => "Assets:Checking"}
iex> ExLedger.resolve_transaction_aliases(transactions, accounts)
[%{postings: [%{account: "Assets:Checking", amount: %{value: -10.0, currency: "$"}}]}]

select(transactions, query)

Selects postings matching a query string.

Examples

iex> transactions = [%{date: ~D[2024-01-01], payee: "Store", postings: [%{account: "Assets:Cash", amount: %{value: -5.0, currency: "$"}, tags: []}]}]
iex> ExLedger.select(transactions, "account from posts where account=~/Assets/")
{:ok, ["account"], [%{"account" => "Assets:Cash"}]}

stats(transactions)

Builds summary statistics for the ledger.

Examples

iex> transactions = [%{date: ~D[2024-01-01], payee: "Store", postings: [%{account: "Assets:Cash", amount: %{value: -5.0, currency: "$"}}]}]
iex> ExLedger.stats(transactions).unique_accounts
1

timeclock_report(entries)

Builds a timeclock report grouped by account.

Examples

iex> entries = [%{account: "Work", duration_seconds: 3600}]
iex> ExLedger.timeclock_report(entries)["Work"]
1.0

validate_balance_assertions(transactions, opts \\ [])

Validates balance assertions in transactions.

Balance assertions use the syntax AMOUNT = ASSERTION_AMOUNT to verify that the running balance of an account equals a specific value after the posting is applied.

Options

  • :skip_assertions - if true, skips validation and returns :ok

Examples

iex> input = """
...> 2024/01/01 Opening
...>     Assets:Cash  100 CHF
...>     Equity:Opening  -100 CHF
...>
...> 2024/01/15 Balance Check
...>     Assets:Cash  0 = 100 CHF
...>     Equity:Adjustments  0
...> """
iex> {:ok, transactions, _} = ExLedger.parse_ledger(input)
iex> ExLedger.validate_balance_assertions(transactions)
:ok

validate_transaction(transaction)

Validates that a transaction is balanced.

Examples

iex> transaction = %{postings: [
...>   %{account: "Assets:Cash", amount: %{value: 10.0, currency: "$"}},
...>   %{account: "Equity:Opening", amount: %{value: -10.0, currency: "$"}}
...> ]}
iex> ExLedger.validate_transaction(transaction)
:ok