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:
- PTA: Getting started FAQ: Which PTA app should I choose ?
- https://www.libhunt.com/compare-hledger-vs-ledger
- https://www.reddit.com/r/plaintextaccounting/comments/ztdk0o/ledger_vs_hledger/
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:
, andnot:
. Multiple query terms are combined using fixed AND/OR rules. More complex query combinations with AND/OR/NOT can be expressed withexpr:"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 thedate:
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.40 | hledger name, notes |
---|---|---|
TRANSACTIONS SYNTAX | ||
5.1 Basic format | Y | |
5.2 Eliding amounts | Y | "Inferred / implicit amounts" |
5.3 Auxiliary dates | Y | "Secondary dates". A misfeature, best avoided. |
5.4 Codes | Y | |
5.5 Transaction state | Y | "Transaction status" |
5.6 Transaction notes | Y | "Transaction description (and/or payee)" |
5.7 Metadata | Y | "Tags" |
5.7.1 Metadata tags | Y | "Tag names". Format is TAG1:, TAG2: not :TAG1:TAG2: . |
5.7.1.1 Payee metadata tag | N | |
5.7.2 Metadata values | Y | "Tag values". They are terminated by a comma or newline. |
5.7.3 Typed metadata | N | Values of the date: /date2: tags are checked as dates; all others are strings. |
5.8 Virtual postings | Y | "Virtual postings" AKA "unbalanced postings", and "balanced virtual postings" |
5.9 Expression amounts | N | |
4.5.4 Value expressions | N | |
5.10 Balance verification | Y | |
5.10.1 Balance assertions | Y | |
5.10.1.1 Special assertion value 0 | N | |
5.10.2 Balance assignments | Y | |
5.10.3 Resetting a balance | Y | |
5.10.4 Balancing transactions | Y | |
5.11 Posting cost | Y | "Inferred cost" |
5.12 Explicit posting costs | Y | "(Explicit) unit cost" |
5.12.1 Primary and secondary commodities | Y | Essentially yes - in certain cases, order of postings determines which commodity is converted to. |
5.13 Posting cost expressions | N | |
5.14 Total posting costs | Y | "(Explicit) total cost" |
5.15 Virtual posting costs | Ignored | The parentheses are ignored (it's treated like a regular cost). |
5.16 Commodity prices | Ignored | Lot 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 prices | Ignored | |
5.17 Prices versus costs | N | If 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 costs | Ignored | {=AMT} is ignored. |
5.19 Lot dates | Ignored | |
5.20 Lot notes | Ignored | |
5.21 Lot value expressions | N | |
5.22 Automated Transactions | Y | "Auto postings" |
5.22.1 Amount multipliers | Y | We use a different syntax (*NUM or *AMT ). |
5.22.2 Accessing the matching posting’s amount | N | |
5.22.3 Referring to the matching posting’s account | N | |
5.22.4 Applying metadata to every matched posting | Ignored | Tags attached to an auto posting rule's "transaction line" are ignored. |
5.22.5 Applying metadata to the generated posting | Y | Tags attached to an auto posting rule's postings affect the generated postings. |
5.22.6 State flags | Y | "Posting status" of auto posting rule's postings affect the generated postings. |
5.22.7 Effective Dates | Y | "Secondary dates", see above. |
5.22.8 Periodic Transactions | Y | |
DIRECTIVES doc | ||
P historical (market) prices | Y | "Market price", "price" |
= An automated transaction. | Y | "Auto posting rule" |
~ A periodic transaction. | Y | "Periodic transaction rule" |
; # % * | comment lines | Y | "Comment line". % and | are not supported. |
! or @ as a directive prefix | Y | Legacy syntax, best avoided. |
account pre-declare account names | Y | |
account subdirectives | Ignored | |
apply account set a default parent account | Y | |
apply fixed set fixated prices | Ignored | |
apply tag assign a tag to transactions | Ignored | |
alias rewrite account names | Y | "Account alias" (basic or regular expression) |
assert error if a value expression fails | Ignored | Use hledger check assertions or hledger-check-fancyassertions instead. |
bucket /A set a default balancing account | Ignored | |
capture replace accounts matched by regex with another | Ignored | Can be emulated with a regular expression account alias. |
check warn if a value expression fails | Ignored | Use hledger check assertions or hledger-check-fancyassertions . |
comment start multi-line comments | Y | "Multi-line comment", "Comment block" |
commodity pre-declare commodities | Y | |
commodity subdirectives | Y | format is supported, other subdirectives are ignored. |
define define value expressions for future use | Ignored | |
end /end apply shorthand for ending most recent apply block | N | |
end apply account | Y | |
end apply fixed | Ignored | |
end apply tag | Ignored | |
end apply year | Ignored | |
end tag | Ignored | |
eval /expr evaluate a value expression | Ignored | |
include include another file | Y | |
payee pre-declare payee names | Y | |
payee subdirectives | Ignored | |
python embed python in journal | Ignored | |
tag pre-declare tag names | Y | |
tag subdirectives | Ignored | |
test , a synonym for comment | N | |
value EXPR set a default valuation function for all commodities | Ignored | |
Y /year /apply year set the year for year-less dates | Y | |
N COMM ignore pricing information for a commodity | Ignored | |
D AMT set a default commodity and its format | Y | A decimal mark is required (followed by 0 or more zeroes). |
C AMT1 = AMT2 declare a commodity equivalency | Ignored | |
I, i, O, o, b, h timeclock entries in journal | N | Timeclock data must be in a separate timeclock file. (A journal file can include that if needed.) |
--command-line-flags in journal | Ignored |
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:
ordate2:
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 withsed -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:
- to get better at Haskell by building something useful to me
- to implement at least the basic core of Ledger, adapted for my needs
- to learn how well Haskell could work for real-world applications
And later:
- 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
- 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.