hledger and Ledger

hledger was inspired by the app that pioneered plain text accounting: Ledger (https://ledger-cli.org). This page describes differences between them, and a little history.

If you are a Ledger user trying to use hledger with your data, feel free to skip ahead to Interoperating tips. And please let me know your experience in the #hledger or #plaintextaccounting chats. Related: #1962.

Differences

10000 foot view

How is hledger different from Ledger ? First, the high-order differences:

  • hledger is actively maintained (since 2008)
  • hledger focusses strongly on UX, reliability, and real-world practicality
  • hledger is written in Haskell, which helps with correctness and maintainability
  • hledger tries to reimplement Ledger's best parts in more depth, with improved functionality and robustness.

Compared to Ledger, hledger has

  • a complete and accurate manual
  • standard "financial statement" reports
  • multi-column reports
  • an easier query syntax
  • better depth limiting
  • a battle-tested CSV/SSV/TSV import system
  • and comes with multiple officially-supported user interfaces (CLI, TUI, WUI, HTTP-JSON).

Compared to hledger, Ledger has

  • assisted lot tracking for investment transactions
  • more support for embedding small programs in your data to get custom behaviour (value expressions, maybe python expressions ?)
  • smaller executables.

See also:

Feature differences

Over time, features have propagated both ways. These features are common to both hledger and Ledger:

  • command line interface
  • journal, timeclock, csv input formats
  • csv conversion rules
  • text, csv output formats
  • multiple commodities
  • costs and cost reporting
  • market prices and value reporting
  • virtual/unbalanced postings
  • automated postings
  • periodic transactions
  • budget reporting
  • unrealised capital gains reporting
  • report filtering with flags and query arguments
  • basic output format customisation
  • commands: accounts, balance, commodities, payees, prices, print, register, stats, tags

For differences, see the detailed PTA Feature Matrix.

The Features page introduces hledger's features for new users.

Performance differences

Ledger was traditionally faster than hledger with large files, eg above 5k transactions. (Many people record about 1-2k transactions per year.) Ledger's speed came partly from providing fewer guarantees, eg Ledger's balance assertions/assignments are not date-aware.

Since about 2021 the performance gap seems to me to have closed or reversed, at least on my mac, where hledger often runs faster and in less memory than Ledger, especially with very large files.

In 2022, hledger ~1.25 compiled natively on a macbook air m1 processed 25k transactions per second:

$ hledger --version
hledger 1.24.99.2-gba5b0e93f-20220205, mac-aarch64
$ make throughput
date: Tue Feb 8 11:03:50 HST 2022
system: Darwin slate.local 21.3.0 Darwin Kernel Version 21.3.0: Wed Jan 5 21:37:58 PST 2022; root:xnu-8019.80.24~20/RELEASE_ARM64_T8101 arm64
executable: hledger
version: hledger 1.24.99.2-gba5b0e93f-20220205, mac-aarch64
  1000 txns: Run time (throughput)    : 0.07s (15308 txns/s)
  2000 txns: Run time (throughput)    : 0.09s (21121 txns/s)
  3000 txns: Run time (throughput)    : 0.13s (23648 txns/s)
  4000 txns: Run time (throughput)    : 0.17s (23226 txns/s)
  5000 txns: Run time (throughput)    : 0.21s (23647 txns/s)
  6000 txns: Run time (throughput)    : 0.24s (24784 txns/s)
  7000 txns: Run time (throughput)    : 0.29s (24166 txns/s)
  8000 txns: Run time (throughput)    : 0.33s (24450 txns/s)
  9000 txns: Run time (throughput)    : 0.35s (25516 txns/s)
 10000 txns: Run time (throughput)    : 0.41s (24226 txns/s)
100000 txns: Run time (throughput)    : 4.32s (23158 txns/s)
Tue Feb  8 11:03:57 HST 2022

Newer hledger versions are slower than this. hledger 1.29-1.32.2 have a performance bug which can be seen with large files, #2153 (see eg 2153#issuecomment-1912942305 benchmarks).

2024's hledger 1.40 on macbook air m1 runs at roughly 16k txns/s for me:

$ hledger -f examples/100ktxns-1kaccts.journal stats
Main file           : .../100ktxns-1kaccts.journal
Included files      : 0
Txns span           : 2000-01-01 to 2273-10-16 (100000 days)
Last txn            : 2273-10-15 (90965 days from now)
Txns                : 100000 (1.0 per day)
Txns last 30 days   : 31 (1.0 per day)
Txns last 7 days    : 8 (1.1 per day)
Payees/descriptions : 100000
Accounts            : 1000 (depth 10)
Commodities         : 26
Market prices       : 100000
Runtime stats       : 6.23 s elapsed, 16051 txns/s, 258 MB live, 773 MB alloc

More independent benchmarking is needed, help welcome.

Command line differences

  • hledger does not require a space between command-line flags and their values, eg -fFILE works as well as -f FILE

  • hledger uses --ignore-assertions/-I to disable balance assertions. Ledger uses --permissive for that, and uses -I as the short form of --prices.

  • hledger's -x/--explicit flag makes print show all amounts; Ledger's --explicit flag does something else.

  • hledger's and Ledger's -H/--historical flags are unrelated. hledger's -H makes register and balance-like commands include balances from before the report start date, instead of starting at zero:

    hledger register --help:
    -H --historical           show historical running total/balance
                              (includes postings before report start date)
    
    hledger balance --help:
    -H --historical           show historical ending balance in each period
                              (includes postings before report start date)
    

    Whereas Ledger's -H changes the valuation date used by -V/-X:

    ledger --help:
    --historical (-H)
                              Value commodities at the time of their acquisition.
    
  • hledger's query language is a little less powerful than Ledger's, simpler, and easier to remember. It uses google-like prefixes, such as desc:, payee:, amt:, and not:. Multiple query terms are combined using fixed AND/OR rules. More complex query combinations with AND/OR/NOT can be expressed with expr:"BOOLEANEXPR". Some complex queries can also be achieved by using two invocations of hledger in a pipe, eg: hledger print QUERY1 | hledger -f- -I reg QUERY2

  • hledger provides more short flags (-b, -e, -p, -D, -W, -M, -Q, -Y) and the date: query argument for setting report period and interval, and all of these combine nicely.

  • hledger handles the transaction/posting status mark more precisely, fixing a recurring source of confusion: It calls the three statuses "unmarked", "pending", "cleared", and provides corresponding --unmarked/-U, --pending/-P, and --cleared/-C flags, which can be combined. Ledger's -U, which matches things not cleared, is -UP in hledger. hledger's -U matches things with no status mark, which can't easily be done in Ledger.

Journal format differences

hledger's journal format mimics (a subset of) Ledger's quite closely. You can maintain a journal file that works with both hledger and Ledger simultaneously, if you restrict yourself to compatible features, perhaps keeping non-compatible features in separate files.

A typical gnarly old Ledger file will not work with hledger as-is. Here are some of the roadbumps to expect (see also: #1752):

  • Some features are supported only by one or the other (Ledger's (AMOUNTEXPR), ((VALUEEXPR)).. , hledger's ==, =*, ==*..)
  • Some will be accepted but ignored, probably causing transactions not to balance ({LOTCOST}, {=LOTFIXEDCOST}, [LOTDATE], (LOTNOTE)..)
  • Some may be interpreted differently (balance assertions, balance assignments..)
  • Some may have different restrictions (dates, comments..)

You'll find lots of tips for how to handle these and other differences, below.

But first, an overview of Ledger's (extensive) journal format. Here are its features, from the Ledger manual (2022-12), and their supportedness in hledger (1.40, 2024-09): Y (supported), Ignored (accepted but ignored), or N (not accepted).

Supported in hledger ?1.40hledger name, notes
TRANSACTIONS SYNTAX
5.1 Basic formatY
5.2 Eliding amountsY"Inferred / implicit amounts"
5.3 Auxiliary datesY"Secondary dates". A misfeature, best avoided.
5.4 CodesY
5.5 Transaction stateY"Transaction status"
5.6 Transaction notesY"Transaction description (and/or payee)"
5.7 MetadataY"Tags"
5.7.1 Metadata tagsY"Tag names". Format is TAG1:, TAG2: not :TAG1:TAG2:.
5.7.1.1 Payee metadata tagN
5.7.2 Metadata valuesY"Tag values". They are terminated by a comma or newline.
5.7.3 Typed metadataNValues of the date:/date2: tags are checked as dates; all others are strings.
5.8 Virtual postingsY"Virtual postings" AKA "unbalanced postings", and "balanced virtual postings"
5.9 Expression amountsN
4.5.4 Value expressionsN
5.10 Balance verificationY
5.10.1 Balance assertionsY
5.10.1.1 Special assertion value 0N
5.10.2 Balance assignmentsY
5.10.3 Resetting a balanceY
5.10.4 Balancing transactionsY
5.11 Posting costY"Inferred cost"
5.12 Explicit posting costsY"(Explicit) unit cost"
5.12.1 Primary and secondary commoditiesYEssentially yes - in certain cases, order of postings determines which commodity is converted to.
5.13 Posting cost expressionsN
5.14 Total posting costsY"(Explicit) total cost"
5.15 Virtual posting costsIgnoredThe parentheses are ignored (it's treated like a regular cost).
5.16 Commodity pricesIgnoredLot costs are recorded in the transaction, but not carried along with the lot automatically, and {AMT} is ignored. Such Ledger entries probably won't balance.
5.16.1 Total commodity pricesIgnored
5.17 Prices versus costsNIf attached to a posting amount we call it "cost", if declared ambiently with a P directive we call it "price" or "market price". {AMT} is ignored.
5.18 Fixated prices and costsIgnored{=AMT} is ignored.
5.19 Lot datesIgnored
5.20 Lot notesIgnored
5.21 Lot value expressionsN
5.22 Automated TransactionsY"Auto postings"
5.22.1 Amount multipliersYWe use a different syntax (*NUM or *AMT).
5.22.2 Accessing the matching posting’s amountN
5.22.3 Referring to the matching posting’s accountN
5.22.4 Applying metadata to every matched postingIgnoredTags attached to an auto posting rule's "transaction line" are ignored.
5.22.5 Applying metadata to the generated postingYTags attached to an auto posting rule's postings affect the generated postings.
5.22.6 State flagsY"Posting status" of auto posting rule's postings affect the generated postings.
5.22.7 Effective DatesY"Secondary dates", see above.
5.22.8 Periodic TransactionsY
DIRECTIVES doc
P historical (market) pricesY"Market price", "price"
= An automated transaction.Y"Auto posting rule"
~ A periodic transaction.Y"Periodic transaction rule"
; # % * | comment linesY"Comment line". % and | are not supported.
! or @ as a directive prefixYLegacy syntax, best avoided.
account pre-declare account namesY
account subdirectivesIgnored
apply account set a default parent accountY
apply fixed set fixated pricesIgnored
apply tag assign a tag to transactionsIgnored
alias rewrite account namesY"Account alias" (basic or regular expression)
assert error if a value expression failsIgnoredUse hledger check assertions or hledger-check-fancyassertions instead.
bucket/A set a default balancing accountIgnored
capture replace accounts matched by regex with anotherIgnoredCan be emulated with a regular expression account alias.
check warn if a value expression failsIgnoredUse hledger check assertions or hledger-check-fancyassertions.
comment start multi-line commentsY"Multi-line comment", "Comment block"
commodity pre-declare commoditiesY
commodity subdirectivesYformat is supported, other subdirectives are ignored.
define define value expressions for future useIgnored
end/end apply shorthand for ending most recent apply blockN
end apply accountY
end apply fixedIgnored
end apply tagIgnored
end apply yearIgnored
end tagIgnored
eval/expr evaluate a value expressionIgnored
include include another fileY
payee pre-declare payee namesY
payee subdirectivesIgnored
python embed python in journalIgnored
tag pre-declare tag namesY
tag subdirectivesIgnored
test, a synonym for commentN
value EXPR set a default valuation function for all commoditiesIgnored
Y/year/apply year set the year for year-less datesY
N COMM ignore pricing information for a commodityIgnored
D AMT set a default commodity and its formatYA decimal mark is required (followed by 0 or more zeroes).
C AMT1 = AMT2 declare a commodity equivalencyIgnored
I, i, O, o, b, h timeclock entries in journalNTimeclock data must be in a separate timeclock file. (A journal file can include that if needed.)
--command-line-flags in journalIgnored

Decoding errors

hledger, like most Haskell programs, exits with a confusing error message if it sees non-ascii data and the system locale is not configured to decode UTF-8. If your data contains non-ascii characters and hledger gives an error such as "invalid byte sequence", "mkTextEncoding: invalid argument" or similar, you must configure your locale.

Tabs and spaces

In places which normally require two or more spaces (or tabs), eg between account name and amount, ledger will also accept a single tab character. But hledger always requires two or more spaces or tabs (ensuring a visually distinct gap). So you might need to add a space in such cases.

Decimal mark

Ledger parses 1,000 as 1000, but hledger parses it as 1, by default (see hledger > Decimal marks).

To prevent any undetected disagreements, use commodity directives or decimal-mark directives to disambiguate the decimal mark character during parsing.

Balancing precision

Ledger and hledger can occasionally disagree on whether a transaction is balanced. In this journal, $'s precision (number of decimal places) is 2 in txn1, 4 in txn2, and 4 globally:

2022-01-01 txn1
    expenses                                 AAA 989.02 @ $1.123456  ; $1111.12045312
    checking                                  $-1111.12

2022-01-02 txn2
    expenses                                      $0.1234
    checking

Ledger checks transaction balancedness using local precisions only, so it balances with precision 2, and accepts txn1's $-0.00045312 imbalance.

hledger checks transaction balancedness using global precisions, so it balances with precision 4, and rejects txn1's imbalance. To read these entries with hledger, you have to limit $'s global precision, by adding -c '\$0.00' to the command (easiest when piping) or commodity $0.00 to the file (more permanent, when creating a new file).

More: #1964

Balance assertions / assignments

Ledger calculates balance assignments and checks balance assertions simply in the order they were parsed. hledger calculates balance assignments and checks balance assertions in date order and then (for postings on the same date) parse order. This ensures correct, reliable behaviour independent of the ordering of journal entries and files.

hledger correctly handles multiple balance assignments/assertions within a single transaction.

Ledger rejects the following balance assertion, as if (a) and a were different accounts; hledger does not.

2023-01-01
  (a)  1

2023-01-02
  a    1 = 2
  b

In addition to =, hledger supports several other kinds of balance assertion, with syntax ==, =* and ==*. Ledger rejects these.

hledger allows @/@@ cost notation in balance assertion/assignment amounts, ie to the right of the equals sign; Ledger does not.

hledger adds a restriction on balance assignments: it does not allow balance assignments on accounts affected by auto posting rules (since in general this can make balancing the journal impossible).

Directive scope

The region affected by directives, and their behaviour with included files or sibling files, is sometimes different in hledger. This is to ensure robust, predictable behaviour. Here are hledger's Directive effects and Directives and multiple files behaviour. You might need to move directives and/or rearrange your files.

Commodity directives

hledger allows commodity directives with a format subdirective to be written as one line, eg these are equivalent:

commodity JPY
  format 1.00 JPY

commodity 1.00 JPY

hledger's commodity directive currently ignores other subdirectives (eg alias).

hledger's commodity directive always requires a decimal mark in the amount. To display no decimal digits, write the decimal mark at the end:

commodity 1000. JPY

And as mentioned above, hledger assumes a single period or comma is a decimal mark, so when specifying digit group marks, write a decimal mark as well: Eg:

commodity 1,000. JPY

See also: hledger > commodity directive.

Periodic transactions

hledger understands most Ledger periodic transactions, but if you find some variants that are not supported, please report.

When you do specify a custom start date, hledger will start the transactions on that date. Ledger seems to always generate them on the period boundaries.

Amount expressions

hledger does not support value expressions, Ledger's embedded programming language. In particular, parenthesised amount expressions like ($10 / 3) are not supported; these must be converted to explicit amounts. Here are the known ways:

  • Convert each expression manually, eg replace ($10 / 3) with $3.333.

  • Convert each expression with ledger. Eg in emacs, select the parenthesised expression and enter C-u M-| xargs ledger eval (and remove the newline). This might not work in all cases.

  • Convert all expressions with beancount. This is a lossy conversion, but it might be good enough. After installing ledger2beancount, beancount, and beancount2ledger (see #33), try:

    $ ledger2beancount file.ledger > file.beancount
    $ beancount2ledger file.beancount > file.journal
    

Lot notation

hledger currently does not provide automatic lot selection or a --lots report; instead you must track them manually, recording cost basis with @ and using explicit per-lot subaccounts and gain/loss postings (see https://hledger.org/investments.html).

More importantly, hledger ignores Ledger's lot notation, like -5 AAPL {$50.00} [2012/04/10] (Oh my!) @@ $375.00. (Any of {LOTUNITCOST}, {{LOTTOTALCOST}}, {=FIXEDLOTUNITCOST}, {{=FIXEDLOTTOTALCOST}}, [LOTDATE], (LOTNOTE) after a posting amount). This can disrupt transaction balancing, making files unreadable. (#1084) For now the only true workaround is to rewrite such entries to use hledger-style explicit lot notation.

Other differences

  • hledger's input data formats (journal, timeclock, timedot, ...) are separate; you can't mix timeclock entries and journal entries in one file as in Ledger. (Though a journal file can include a timeclock file.) This helps implement more useful error messages.

  • hledger supports international number formats, auto-detecting the decimal mark (comma or period), digit group mark (period, comma, or space) and digit group sizes to use for each commodity. Or, these can be declared explicitly with commodity directives.

  • hledger's default commodity directive (D) sets the commodity to be used for subsequent commodityless amounts, and also sets that commodity's display settings if such an amount is the first seen. Ledger uses D only for commodity display settings and for the entry command.

  • hledger auto postings allow only minimal customisation of the amount (just multiplying the matched amount by a constant), not a full embedded expression language like Ledger. (And we call them "auto" to avoid "automatic" vs "automated" confusion.)

  • In multi-period reports, hledger expands the report start/end dates to span whole periods.

  • hledger's print command always shows both the primary transaction date and any secondary date, in their usual positions. Ledger's print command with --aux-date replaces the primary date with any secondary date.

  • hledger always shows time balances (from timeclock or timedot data) in hours.

  • hledger always splits multi-day time sessions at midnight, to show the per-day amounts. Ledger does this only with the --day-break flag.

  • hledger's CSV/TSV/SSV-reading and import system is more mature and flexible than Ledger's convert command.

  • Ledger can report multiple errors at once; hledger reports only one error at a time.

  • Ledger can also output warnings. hledger does not print warnings; it either succeeds or fails.

  • hledger will complain if transaction or posting comments contain date: or date2: not followed by a valid date tag value.

Interoperating tips

The core of hledger's and Ledger's journal formats is the same, so you can use both tools on the same files, if you are careful to avoid tool-specific features.

When you can't avoid tool-specific syntax, you can put it in separate tool-specific files, and have both of these include a shared common file. (Eg 2023.ledger and 2023.hledger, both including 2023.journal).

A third approach is to do a one-way conversion to a new file, using whatever edits and transformations are necessary, and automate it as much as possible (with sed, perl, Emacs macros, or similar), so you can redo the conversion when needed, perhaps incrementally.

Ledger to hledger

Most Ledger users will have at least some Ledger-specific syntax, so the quickest way to tap into hledger reports may be:

$ ledger print --raw | hledger -f- -I CMD

The print command omits directives. --raw prevents decimal zeroes being added to amounts and disrupting transaction balancing. -I disables checking of balance assertions (if needed). If this works you can do quick reporting like so:

$ ledger print --raw | hledger -f- check       # check for problems
$ ledger print --raw | hledger -f- stats       # show journal statistics
$ ledger print --raw | hledger -f- is -MAS -2  # summarise monthly revenues/expenses
$ ledger print --raw | hledger -f- web         # view journal in hledger-web WUI
$ hledger-ui -f <(ledger print --raw)          # view journal in hledger-ui TUI (works in bash)

Some common problems:

  • hledger does not support Ledger's amount expressions, like ($10 / 3). If you have those, see Amount expressions above.

  • hledger does not support all of Ledger's lot notation, like -5 AAPL {$50.00} [2012/04/10] (Oh my!) @@ $375.00. It can parse it, but will ignore it, so transaction balancing will probably fail. For now the only true workaround is to rewrite such entries to use hledger-style lot notation. See Lot notation above.

See also the other Differences mentioned above.

hledger to Ledger

Currently there's no specific output format for Ledger; use print's standard txt output format.

$ hledger print | ledger --permissive -f - CMD

Ledger requires a space between -f and -. --permissive disables checking of balance assertions (if needed).

Some common problems:

  • hledger's extended balance assertions (=*, ==, ==*) are not supported by Ledger and must be avoided or commented out (eg with sed -E -e 's/(==|=\*)/; \1/').

  • Transactions which hledger considers balanced (using global display precisions) can be considered unbalanced by Ledger (using local display precisions) (see Balancing precision). Try to make those transaction amounts more precise so that they balance in both.

  • hledger print will add a trailing decimal mark to amounts with digit group marks and no decimal digits, to disambiguate them (since 1.31), but Ledger currently does not parse such numbers. You can avoid them by suppressing digit group marks (eg with -c) or by ensuring some decimal digits (eg with --round); see hledger > Trailing decimal marks.

See also the other Differences mentioned above.

History

I (Simon) discovered John Wiegley's Ledger in 2006, and was very happy to find this efficient command-line reporting tool with a transparent data format. Initially, I used it to generate time reports for my job. Before long I wanted some improvements - splitting sessions at day boundaries, reporting in hours, etc.

Meanwhile, John was now busy elsewhere. For a long time the Ledger project remained stalled, with unfixed functionality/documentation bugs and an ever-looming v3 release making life hard for new users and creating friction for community growth. I did what I could to help - reporting bugs, providing support, contributing a domain and website - but I didn't want to invest in learning C++.

I was learning and investing time in Haskell, and I felt Ledger could be perhaps implemented well, and perhaps more effectively in the long run, in this language. I urgently needed a rock solid, hassle-free and enjoyable accounting tool. Also, I wanted a more active project and some way to make progress on the roadbumps and confusion facing other newcomers.

Of course I tried a little shiny-tech salesmanship on John, but couldn't expect him to start over. (At that time he was deeply in the C++ world; nowadays he is a Haskell expert!)

So in 2007 I began experimenting. I built a toy parser in a few different languages, and it was easiest in Haskell. I kept tinkering. Goals included:

  1. to get better at Haskell by building something useful to me
  2. to implement at least the basic core of Ledger, adapted for my needs
  3. to learn how well Haskell could work for real-world applications

And later:

  1. to provide a new highly-compatible implementation of at least the basics of Ledger, useful to others, with a greater focus on ease of use, reliability, documentation and web presence
  2. to experiment with new user interfaces, APIs, etc.

Before too long I had a tool that was useful to me. With Ledger still installed, and by maintaining high compatibility, I now had two implementations which could be compared at times of confusion about functionality or suspected bugs/bookkeeping errors, which was quite valuable.

Later, John returned for a while and finished Ledger version 3, the Ledger project attracted new contributors and maintainers, and incremental improvements resumed. I continued sharing discoveries and design discussions, and we have seen many ideas propagating in both directions. I think having independent but compatible implementations has been quite helpful for troubleshooting, exploring the design space, and growing the community. For a while I ran LedgerTips on twitter.

hledger shared #ledger's IRC channel until 2014, when I created the #hledger channel (now accessible on Libera IRC and Matrix).

In 2016 I set up https://plaintextaccounting.org as a common entry point and information hub.

The further adventures in hledger's development are not yet told, other than in the commit log, issue tracker and mail list, but other contributors joined the project and CREDITS notes some of their work.