ledger
======

[![CRAN Status Badge](https://www.r-pkg.org/badges/version/ledger)](https://cran.r-project.org/package=ledger)
[![R-CMD-check](https://github.com/trevorld/r-ledger/actions/workflows/R-CMD-check.yaml/badge.svg?branch=master)](https://github.com/trevorld/r-ledger/actions)
[![Coverage Status](https://codecov.io/github/trevorld/r-ledger/branch/master/graph/badge.svg)](https://app.codecov.io/github/trevorld/r-ledger?branch=master)
[![RStudio CRAN mirror downloads](https://cranlogs.r-pkg.org/badges/ledger)](https://cran.r-project.org/package=ledger)

`ledger` is an R package to import data from [plain text
accounting](https://plaintextaccounting.org/) software like
[Ledger](https://ledger-cli.org/), [HLedger](https://hledger.org/), and
[Beancount](https://github.com/beancount/beancount) into an R data frame
for convenient analysis, plotting, and export.

Right now it supports reading in the register from `ledger`, `hledger`,
and `beancount` files.

Installation
------------

To install the last version released to CRAN use the following command
in R:

    install.packages("ledger")

To install the development version of the `ledger` package (and its R
package dependencies) use the `install_github` function from the
`remotes` package in R:

    install.packages("remotes")
    remotes::install_github("trevorld/r-ledger")

This package also has some system dependencies that need to be installed
depending on which plaintext accounting files you wish to read to be
able to read in:

ledger  
[ledger](https://ledger-cli.org/) (&gt;= 3.1)

hledger  
[hledger](https://hledger.org/) (&gt;= 1.4)

beancount  
[beanquery](https://github.com/beancount/beanquery)

To install hledger run the following in your shell:

    stack update && stack install --resolver=lts-14.3 hledger-lib-1.15.2 hledger-1.15.2 hledger-web-1.15 hledger-ui-1.15 --verbosity=error

To install <span class="title-ref">bean-query</span> (to read <span
class="title-ref">beancount</span> files) run the following in your
shell:

    pip3 install beanquery

[Several pre-compiled Ledger binaries are
available](https://ledger-cli.org/download.html) (often found in several
open source repos).

Examples
--------

### API

The main function of this package is `register()` which reads in the
register of a plaintext accounting file. This package also registers S3
methods so one can use `rio::import()` to read in a register, a
`net_worth()` convenience function, and a `prune_coa()` convenience
function.

#### `register()`

Here are some examples of very basic files stored within the package:

    library("ledger")
    ledger_file <- system.file("extdata", "example.ledger", package = "ledger")
    register(ledger_file)

    ## # A tibble: 42 × 8
    ##    date       mark  payee       description     account amount commodity comment
    ##    <date>     <chr> <chr>       <chr>           <chr>    <dbl> <chr>     <chr>  
    ##  1 2015-12-31 *     <NA>        Opening Balanc… Assets…  5000  USD       ""     
    ##  2 2015-12-31 *     <NA>        Opening Balanc… Equity… -5000  USD       ""     
    ##  3 2016-01-01 *     Landlord    Rent            Assets… -1500  USD       ""     
    ##  4 2016-01-01 *     Landlord    Rent            Expens…  1500  USD       ""     
    ##  5 2016-01-01 *     Brokerage   Buy Stock       Assets… -1000  USD       ""     
    ##  6 2016-01-01 *     Brokerage   Buy Stock       Equity…  1000  USD       ""     
    ##  7 2016-01-01 *     Brokerage   Buy Stock       Assets…     4  SP        ""     
    ##  8 2016-01-01 *     Brokerage   Buy Stock       Equity… -1000  USD       ""     
    ##  9 2016-01-01 *     Supermarket Grocery store   Expens…   501. USD       "Link:…
    ## 10 2016-01-01 *     Supermarket Grocery store   Liabil…  -501. USD       "Link:…
    ## # ℹ 32 more rows

    hledger_file <- system.file("extdata", "example.hledger", package = "ledger")
    register(hledger_file)

    ## # A tibble: 42 × 12
    ##    date       mark  payee   description account amount commodity historical_cost
    ##    <date>     <chr> <chr>   <chr>       <chr>    <dbl> <chr>               <dbl>
    ##  1 2015-12-31 *     <NA>    Opening Ba… Assets…  5000  USD                 5000 
    ##  2 2015-12-31 *     <NA>    Opening Ba… Equity… -5000  USD                -5000 
    ##  3 2016-01-01 *     Landlo… Rent        Assets… -1500  USD                -1500 
    ##  4 2016-01-01 *     Landlo… Rent        Expens…  1500  USD                 1500 
    ##  5 2016-01-01 *     Broker… Buy Stock   Assets… -1000  USD                -1000 
    ##  6 2016-01-01 *     Broker… Buy Stock   Equity…  1000  USD                 1000 
    ##  7 2016-01-01 *     Broker… Buy Stock   Assets…     4  SP                  1000 
    ##  8 2016-01-01 *     Broker… Buy Stock   Equity… -1000  USD                -1000 
    ##  9 2016-01-01 *     Superm… Grocery st… Expens…   501. USD                  501.
    ## 10 2016-01-01 *     Superm… Grocery st… Liabil…  -501. USD                 -501.
    ## # ℹ 32 more rows
    ## # ℹ 4 more variables: hc_commodity <chr>, market_value <dbl>,
    ## #   mv_commodity <chr>, id <chr>

    beancount_file <- system.file("extdata", "example.beancount", package = "ledger")
    register(beancount_file)

    ## # A tibble: 42 × 13
    ##    date       mark  payee   description account amount commodity historical_cost
    ##    <date>     <chr> <chr>   <chr>       <chr>    <dbl> <chr>               <dbl>
    ##  1 2015-12-31 *     ""      Opening Ba… Assets…  5000  USD                 5000 
    ##  2 2015-12-31 *     ""      Opening Ba… Equity… -5000  USD                -5000 
    ##  3 2016-01-01 *     "Landl… Rent        Assets… -1500  USD                -1500 
    ##  4 2016-01-01 *     "Landl… Rent        Expens…  1500  USD                 1500 
    ##  5 2016-01-01 *     "Broke… Buy Stock   Assets… -1000  USD                -1000 
    ##  6 2016-01-01 *     "Broke… Buy Stock   Equity…  1000  USD                 1000 
    ##  7 2016-01-01 *     "Broke… Buy Stock   Assets…     4  SP                  1000 
    ##  8 2016-01-01 *     "Broke… Buy Stock   Equity… -1000  USD                -1000 
    ##  9 2016-01-01 *     "Super… Grocery st… Expens…   501. USD                  501.
    ## 10 2016-01-01 *     "Super… Grocery st… Liabil…  -501. USD                 -501.
    ## # ℹ 32 more rows
    ## # ℹ 5 more variables: hc_commodity <chr>, market_value <dbl>,
    ## #   mv_commodity <chr>, tags <chr>, id <chr>

Here is an example reading in a beancount file generated by
`bean-example`:

    bean_example_file <- tempfile(fileext = ".beancount")
    system(paste("bean-example -o", bean_example_file), ignore.stderr=TRUE)
    df <- register(bean_example_file)
    print(df)

    ## # A tibble: 2,674 × 13
    ##    date       mark  payee  description account  amount commodity historical_cost
    ##    <date>     <chr> <chr>  <chr>       <chr>     <dbl> <chr>               <dbl>
    ##  1 2024-01-01 *     ""     Opening Ba… Assets…  3.04e3 USD                3041. 
    ##  2 2024-01-01 *     ""     Opening Ba… Equity… -3.04e3 USD               -3041. 
    ##  3 2024-01-01 *     ""     Allowed co… Income… -1.85e4 IRAUSD           -18500  
    ##  4 2024-01-01 *     ""     Allowed co… Assets…  1.85e4 IRAUSD            18500  
    ##  5 2024-01-03 *     "Uncl… Eating out… Liabil… -2.58e1 USD                 -25.8
    ##  6 2024-01-03 *     "Uncl… Eating out… Expens…  2.58e1 USD                  25.8
    ##  7 2024-01-04 *     "BANK… Monthly ba… Assets… -4   e0 USD                  -4  
    ##  8 2024-01-04 *     "BANK… Monthly ba… Expens…  4   e0 USD                   4  
    ##  9 2024-01-04 *     "Hool… Payroll     Assets…  1.35e3 USD                1351. 
    ## 10 2024-01-04 *     "Hool… Payroll     Assets…  1.2 e3 USD                1200  
    ## # ℹ 2,664 more rows
    ## # ℹ 5 more variables: hc_commodity <chr>, market_value <dbl>,
    ## #   mv_commodity <chr>, tags <chr>, id <chr>

    suppressPackageStartupMessages(library("dplyr"))
    dplyr::filter(df, grepl("Expenses", account), grepl("trip", tags)) %>%
        group_by(trip = tags, account) %>%
        summarize(trip_total = sum(amount), .groups = "drop")

    ## # A tibble: 6 × 3
    ##   trip                  account                  trip_total
    ##   <chr>                 <chr>                         <dbl>
    ## 1 trip-boston-2024      Expenses:Food:Restaurant     245.  
    ## 2 trip-los-angeles-2025 Expenses:Food:Alcohol          6.76
    ## 3 trip-los-angeles-2025 Expenses:Food:Coffee          29.6 
    ## 4 trip-los-angeles-2025 Expenses:Food:Restaurant     443.  
    ## 5 trip-new-york-2025    Expenses:Food:Coffee          52.1 
    ## 6 trip-new-york-2025    Expenses:Food:Restaurant     936.

#### Using `rio::import()` and `rio::convert()`

If one has loaded in the `ledger` package one can also use
`rio::import()` to read in the register:

    df2 <- rio::import(bean_example_file)
    all.equal(df, tibble::as_tibble(df2))

    ## [1] TRUE

The main advantage of this is that it allows one to use `rio::convert`
to easily convert plaintext accounting files to several other file
formats such as a csv file. Here is a shell example:

    bean-example -o example.beancount
    Rscript --default-packages=ledger,rio -e 'convert("example.beancount", "example.csv")'

#### `net_worth()`

Some examples of using the `net_worth` function using the example files
from the `register` examples:

    dates <- seq(as.Date("2016-01-01"), as.Date("2018-01-01"), by="years")
    net_worth(ledger_file, dates)

    ## # A tibble: 3 × 6
    ##   date       commodity net_worth assets liabilities revalued
    ##   <date>     <chr>         <dbl>  <dbl>       <dbl>    <dbl>
    ## 1 2016-01-01 USD           5000    5000          0         0
    ## 2 2017-01-01 USD           4361.   4882       -521.        0
    ## 3 2018-01-01 USD           6743.   6264       -521.     1000

    net_worth(hledger_file, dates)

    ## # A tibble: 3 × 5
    ##   date       commodity net_worth assets liabilities
    ##   <date>     <chr>         <dbl>  <dbl>       <dbl>
    ## 1 2016-01-01 USD           5000    5000          0 
    ## 2 2017-01-01 USD           4361.   4882       -521.
    ## 3 2018-01-01 USD           6743.   7264       -521.

    net_worth(beancount_file, dates)

    ## # A tibble: 3 × 5
    ##   date       commodity net_worth assets liabilities
    ##   <date>     <chr>         <dbl>  <dbl>       <dbl>
    ## 1 2016-01-01 USD           5000    5000          0 
    ## 2 2017-01-01 USD           4361.   4882       -521.
    ## 3 2018-01-01 USD           6743.   7264       -521.

    dates <- seq(min(as.Date(df$date)), max(as.Date(df$date)), by="years")
    net_worth(bean_example_file, dates)

    ## # A tibble: 6 × 5
    ##   date       commodity net_worth assets liabilities
    ##   <date>     <chr>         <dbl>  <dbl>       <dbl>
    ## 1 2025-01-01 IRAUSD           0      0           0 
    ## 2 2025-01-01 USD          42788. 43595.       -807.
    ## 3 2025-01-01 VACHR           66     66           0 
    ## 4 2026-01-01 IRAUSD           0      0           0 
    ## 5 2026-01-01 USD          86967. 88872.      -1905.
    ## 6 2026-01-01 VACHR           60     60           0

#### `prune_coa()`

Some examples using the `prune_coa` function to simplify the "Chart of
Account" names to a given maximum depth:

    suppressPackageStartupMessages(library("dplyr"))
    df <- register(bean_example_file) %>% dplyr::filter(!is.na(commodity))
    df %>% prune_coa() %>%
        group_by(account, mv_commodity) %>%
        summarize(market_value = sum(market_value), .groups = "drop")

    ## # A tibble: 11 × 3
    ##    account     mv_commodity market_value
    ##    <chr>       <chr>               <dbl>
    ##  1 Assets      IRAUSD             10100 
    ##  2 Assets      USD               113230.
    ##  3 Assets      VACHR                 23 
    ##  4 Equity      USD                -3041.
    ##  5 Expenses    IRAUSD             45400 
    ##  6 Expenses    USD               210495.
    ##  7 Expenses    VACHR                272 
    ##  8 Income      IRAUSD            -55500 
    ##  9 Income      USD              -297361.
    ## 10 Income      VACHR               -295 
    ## 11 Liabilities USD                -2721.

    df %>% prune_coa(2) %>%
        group_by(account, mv_commodity) %>%
        summarize(market_value = sum(market_value), .groups = "drop")

    ## # A tibble: 17 × 3
    ##    account                     mv_commodity market_value
    ##    <chr>                       <chr>               <dbl>
    ##  1 Assets:US                   IRAUSD           1.01e+ 4
    ##  2 Assets:US                   USD              1.13e+ 5
    ##  3 Assets:US                   VACHR            2.3 e+ 1
    ##  4 Equity:Opening-Balances     USD             -3.04e+ 3
    ##  5 Expenses:Financial          USD              3.59e+ 2
    ##  6 Expenses:Food               USD              1.50e+ 4
    ##  7 Expenses:Health             USD              5.72e+ 3
    ##  8 Expenses:Home               USD              6.77e+ 4
    ##  9 Expenses:Taxes              IRAUSD           4.54e+ 4
    ## 10 Expenses:Taxes              USD              1.18e+ 5
    ## 11 Expenses:Transport          USD              3.24e+ 3
    ## 12 Expenses:Vacation           VACHR            2.72e+ 2
    ## 13 Income:US                   IRAUSD          -5.55e+ 4
    ## 14 Income:US                   USD             -2.97e+ 5
    ## 15 Income:US                   VACHR           -2.95e+ 2
    ## 16 Liabilities:AccountsPayable USD              5.68e-14
    ## 17 Liabilities:US              USD             -2.72e+ 3

### Basic personal accounting reports

Here is some examples using the functions in the package to help
generate various personal accounting reports of the beancount example
generated by `bean-example`.

First we load the (mainly tidyverse) libraries we'll be using and
adjusting terminal output:

    library("ledger")
    library("dplyr")
    filter <- dplyr::filter
    library("ggplot2")
    library("scales")
    library("tidyr")
    library("zoo")
    filename <- tempfile(fileext = ".beancount")
    system(paste("bean-example -o", filename), ignore.stderr=TRUE)
    df <- register(filename) %>% mutate(yearmon = zoo::as.yearmon(date)) %>%
          filter(commodity=="USD")
    nw <- net_worth(filename)

Then we'll write some convenience functions we'll use over and over
again:

    print_tibble_rows <- function(df) {
        print(df, n=nrow(df))
    }
    count_beans <- function(df, filter_str = "", ...,
                            amount = "amount",
                            commodity="commodity",
                            cutoff=1e-3) {
        commodity <- sym(commodity)
        amount_var <- sym(amount)
        filter(df, grepl(filter_str, account)) %>%
            group_by(account, !!commodity, ...) %>%
            summarize(!!amount := sum(!!amount_var), .groups = "drop") %>%
            filter(abs(!!amount_var) > cutoff & !is.na(!!amount_var)) %>%
            arrange(desc(abs(!!amount_var)))
    }

#### Basic balance sheets

Here is some basic balance sheets (using the market value of our
assets):

    print_balance_sheet <- function(df) {
        assets <- count_beans(df, "^Assets",
                     amount="market_value", commodity="mv_commodity")
        print_tibble_rows(assets)
        liabilities <- count_beans(df, "^Liabilities",
                           amount="market_value", commodity="mv_commodity")
        print_tibble_rows(liabilities)
    }
    print(nw)

    ## # A tibble: 3 × 5
    ##   date       commodity net_worth assets liabilities
    ##   <date>     <chr>         <dbl>  <dbl>       <dbl>
    ## 1 2026-03-29 IRAUSD       10100  10100           0 
    ## 2 2026-03-29 USD          96917. 99893.      -2975.
    ## 3 2026-03-29 VACHR          -41    -41           0

    print_balance_sheet(prune_coa(df, 2))

    ## # A tibble: 1 × 3
    ##   account   mv_commodity market_value
    ##   <chr>     <chr>               <dbl>
    ## 1 Assets:US USD                 4023.
    ## # A tibble: 1 × 3
    ##   account        mv_commodity market_value
    ##   <chr>          <chr>               <dbl>
    ## 1 Liabilities:US USD                -2975.

    print_balance_sheet(df)

    ## # A tibble: 3 × 3
    ##   account                 mv_commodity market_value
    ##   <chr>                   <chr>               <dbl>
    ## 1 Assets:US:BofA:Checking USD              3576.   
    ## 2 Assets:US:ETrade:Cash   USD               447.   
    ## 3 Assets:US:Vanguard:Cash USD                -0.140
    ## # A tibble: 1 × 3
    ##   account                    mv_commodity market_value
    ##   <chr>                      <chr>               <dbl>
    ## 1 Liabilities:US:Chase:Slate USD                -2975.

#### Basic net worth chart

Here is a basic chart of one's net worth from the beginning of the
plaintext accounting file to today by month:

    next_month <- function(date) {
        zoo::as.Date(zoo::as.yearmon(date) + 1/12)
    }
    nw_dates <- seq(next_month(min(df$date)), next_month(Sys.Date()), by="months")
    df_nw <- net_worth(filename, nw_dates) %>% filter(commodity=="USD")
    ggplot(df_nw, aes(x=date, y=net_worth, colour=commodity, group=commodity)) +
      geom_line() + scale_y_continuous(labels=scales::dollar)

![Basic net worth chart](man/figures/README-net_worth_chart-1.png)

#### Basic income sheets

    month_cutoff <- zoo::as.yearmon(Sys.Date()) - 2/12
    compute_income <- function(df) {
        count_beans(df, "^Income", yearmon) %>%
            mutate(income = -amount) %>%
            select(-amount) %>% ungroup()
    }
    print_income <- function(df) {
        compute_income(df) %>%
            filter(yearmon >= month_cutoff) %>%
            spread(yearmon, income, fill=0) %>%
            print_tibble_rows()
    }
    compute_expenses <- function(df) {
        count_beans(df, "^Expenses", yearmon) %>%
            mutate(expenses = amount) %>%
            select(-amount) %>% ungroup()
    }
    print_expenses <- function(df) {
        compute_expenses(df) %>%
            filter(yearmon >= month_cutoff) %>%
            spread(yearmon, expenses, fill=0) %>%
            print_tibble_rows()
    }
    compute_total <- function(df) {
    full_join(compute_income(prune_coa(df)) %>% select(-account),
              compute_expenses(prune_coa(df)) %>% select(-account),
              by=c("yearmon", "commodity")) %>%
        mutate(income = ifelse(is.na(income), 0, income),
               expenses = ifelse(is.na(expenses), 0, expenses),
               net = income - expenses) %>%
        gather(type, amount, -yearmon, -commodity)
    }
    print_total <- function(df) {
        compute_total(df) %>%
            filter(yearmon >= month_cutoff) %>%
            spread(yearmon, amount, fill=0) %>%
            print_tibble_rows()
    }
    print_total(df)

    ## # A tibble: 3 × 5
    ##   commodity type     `Jan 2026` `Feb 2026` `Mar 2026`
    ##   <chr>     <chr>         <dbl>      <dbl>      <dbl>
    ## 1 USD       expenses      9568.      7352.      5667.
    ## 2 USD       income       15719.     10479.     10587.
    ## 3 USD       net           6151.      3128.      4920.

    print_income(prune_coa(df, 2))

    ## # A tibble: 1 × 5
    ##   account   commodity `Jan 2026` `Feb 2026` `Mar 2026`
    ##   <chr>     <chr>          <dbl>      <dbl>      <dbl>
    ## 1 Income:US USD           15719.     10479.     10587.

    print_expenses(prune_coa(df, 2))

    ## # A tibble: 6 × 5
    ##   account            commodity `Jan 2026` `Feb 2026` `Mar 2026`
    ##   <chr>              <chr>          <dbl>      <dbl>      <dbl>
    ## 1 Expenses:Financial USD               4          4        13.0
    ## 2 Expenses:Food      USD             577.       449.      551. 
    ## 3 Expenses:Health    USD             291.       194.      194. 
    ## 4 Expenses:Home      USD            2600.      2600.        0  
    ## 5 Expenses:Taxes     USD            5977.      3984.     4789. 
    ## 6 Expenses:Transport USD             120        120       120

    print_income(df)

    ## # A tibble: 4 × 5
    ##   account                       commodity `Jan 2026` `Feb 2026` `Mar 2026`
    ##   <chr>                         <chr>          <dbl>      <dbl>      <dbl>
    ## 1 Income:US:ETrade:GLD:Dividend USD              0          0        108. 
    ## 2 Income:US:Hooli:GroupTermLife USD             73.0       48.6       48.6
    ## 3 Income:US:Hooli:Match401k     USD           1800       1200       1200  
    ## 4 Income:US:Hooli:Salary        USD          13846.      9231.      9231.

    print_expenses(df)

    ## # A tibble: 21 × 5
    ##    account                            commodity `Jan 2026` `Feb 2026` `Mar 2026`
    ##    <chr>                              <chr>          <dbl>      <dbl>      <dbl>
    ##  1 Expenses:Financial:Commissions     USD             0          0          8.95
    ##  2 Expenses:Financial:Fees            USD             4          4          4   
    ##  3 Expenses:Food:Groceries            USD           144.        94.4      242.  
    ##  4 Expenses:Food:Restaurant           USD           432.       355.       309.  
    ##  5 Expenses:Health:Dental:Insurance   USD             8.7        5.8        5.8 
    ##  6 Expenses:Health:Life:GroupTermLife USD            73.0       48.6       48.6 
    ##  7 Expenses:Health:Medical:Insurance  USD            82.1       54.8       54.8 
    ##  8 Expenses:Health:Vision:Insurance   USD           127.        84.6       84.6 
    ##  9 Expenses:Home:Electricity          USD            65         65          0   
    ## 10 Expenses:Home:Internet             USD            79.9       80.0        0   
    ## 11 Expenses:Home:Phone                USD            54.8       55.3        0   
    ## 12 Expenses:Home:Rent                 USD          2400       2400          0   
    ## 13 Expenses:Taxes:Y2025:US:Federal    USD             0          0        539.  
    ## 14 Expenses:Taxes:Y2025:US:State      USD             0          0        265.  
    ## 15 Expenses:Taxes:Y2026:US:CityNYC    USD           525.       350.       350.  
    ## 16 Expenses:Taxes:Y2026:US:Federal    USD          3189.      2126.      2126.  
    ## 17 Expenses:Taxes:Y2026:US:Medicare   USD           320.       213.       213.  
    ## 18 Expenses:Taxes:Y2026:US:SDI        USD             3.36       2.24       2.24
    ## 19 Expenses:Taxes:Y2026:US:SocSec     USD           845.       563.       563.  
    ## 20 Expenses:Taxes:Y2026:US:State      USD          1095.       730.       730.  
    ## 21 Expenses:Transport:Tram            USD           120        120        120

And here is a plot of income, expenses, and net income over time:

    ggplot(compute_total(df), aes(x=yearmon, y=amount, group=commodity, colour=commodity)) +
      facet_grid(type ~ .) +
      geom_line() + geom_hline(yintercept=0, linetype="dashed") +
      scale_x_continuous() + scale_y_continuous(labels=scales::comma)

![Monthly income chart](man/figures/README-income_chart-1.png)
