summaryrefslogtreecommitdiff
path: root/python/demo.py
blob: b3fe1d0438292e42ed96e04e058304884ae3e05d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
#!/usr/bin/env python

import sys
from datetime import datetime

# The following literate program will demonstrate, by example, how to use the
# Ledger Python module to access your data and build custom reports using the
# magic of Python.

import ledger

print("Welcome to the Ledger.Python demo!")

# Some quick helper functions to help us assert various types of truth
# throughout the script.

def assertEqual(pat, candidate):
    if pat != candidate:
        raise Exception("FAILED: %s != %s" % (pat, candidate))
        sys.exit(1)

###############################################################################
#
# COMMODITIES
#
# Every amount in Ledger has a commodity, even if it is the "null commodity".
# What's special about commodities are not just their symbol, but how they
# alter the way amounts are displayed.
#
# For example, internally Ledger uses infinite precision rational numbers,
# which have no decimal point.  So how does it know that $1.00 / $0.75 should
# be displayed as $1.33, and not with an infinitely repeating decimal?  It
# does it by consulting the commodity.
#
# Whenever an amount is encountered in your data file, Ledger observes how you
# specified it:
#   - How many digits of precision did you use?
#   - Was the commodity name before or after the amount?
#   - Was the commodity separated from the amount by a space?
#   - Did you use thousands markers (1,000)?
#   - Did you use European-style numbers (1.000,00)?
#
# By tracking this information for each commodity, Ledger knows how you want
# to see the amount in your reports.  This way, dollars can be output as
# $123.56, while stock options could be output as 10.113 AAPL.
#
# Your program can access the known set of commodities using the global
# `ledger.commodities'.  This object behaves like a dict, and support all of
# the non-modifying dict protocol methods.  If you wish to create a new
# commodity without parsing an amount, you can use the method
# `find_or_create':

comms = ledger.commodities

usd = comms.find_or_create('$')
xcd = comms.find_or_create('XCD')

# Tests currency symbols encoded using UCS. For details see #2132.
eur = comms.find_or_create('€') # UCS-2 / UCS-4
xxx = comms.find_or_create('¤') # UCS-1

assert not comms.find('CAD')
assert not comms.has_key('CAD')
assert not 'CAD' in comms

# The above mentioned commodity display attributes can be set using commodity
# display flags.  This is not something you will usually be doing, however, as
# these flags can be inferred correctly from a large enough set of sample
# amounts, such as those found in your data file.  If you live in Europe and
# want all amounts to default to the European-style, set the static variable
# `european_by_default'.

eur.add_flags(ledger.COMMODITY_STYLE_DECIMAL_COMMA)
assert eur.has_flags(ledger.COMMODITY_STYLE_DECIMAL_COMMA)
assert not eur.has_flags(ledger.COMMODITY_STYLE_THOUSANDS)

comms.european_by_default = True

# There are a few built-in commodities: null, %, h, m and s.  Normally you
# don't need to worry about them, but they'll show up if you examine all the
# keys in the commodities dict.

assertEqual([u'', u'$', u'%', u'XCD', u'h', u'm', u's', u'¤', u'€'],
            sorted(comms.keys()))

# All the styles of dict iteration are supported:

for symbol in comms.iterkeys():
    pass
for commodity in comms.itervalues():
    pass
#for symbol, commodity in comms.iteritems():
#    pass
#for symbol, commodity in comms:
#    pass

# Another important thing about commodities is that they remember if they've
# been exchanged for another commodity, and what the conversion rate was on
# that date.  You can record specific conversion rates for any date using the
# `exchange' method.

comms.exchange(eur, ledger.Amount('$0.77')) # Trade 1 € for $0.77
comms.exchange(eur, ledger.Amount('$0.66'), datetime.now())

# For the most part, however, you won't be interacting with commodities
# directly, except maybe to look at their `symbol'.

assertEqual('$', usd.symbol)
assertEqual('$', comms['$'].symbol)

###############################################################################
#
# AMOUNTS & BALANCES
#
# Ledger deals with two basic numerical values: Amount and Balance objects.
# An Amount is an infinite-precision rational with an associated commodity
# (even if it is the null commodity, which is called an "uncommoditized
# amount").  A Balance is a collection of Amounts of differing commodities.
#
# Amounts support all the math operations you might expect of an integer,
# except it carries a commodity.  Let's take dollars for example:

zero  = ledger.Amount("$0")
one   = ledger.Amount("$1")
oneb  = ledger.Amount("$1")
two   = ledger.Amount("$2")
three = ledger.Amount("3")      # uncommoditized

assert one == oneb              # numeric equality, not identity
assert one != two
assert not zero                 # tests if it would *display* as a zero
assert one < two
assert one > zero

# For addition and subtraction, only amounts of the same commodity may be
# used, unless one of the amounts has no commodity at all -- in which case the
# result uses the commodity of the other value.  Adding $10 to 10 €, for
# example, causes an ArithmeticError exception, but adding 10 to $10 gives
# $20.

four = ledger.Amount(two)       # make a copy
four += two
assertEqual(four, two + two)
assertEqual(zero, one - one)

try:
    two += ledger.Amount("20 €")
    assert False
except ArithmeticError:
    pass

# Use `number' to get the uncommoditized version of an Amount

assertEqual(three, (two + one).number())

# Multiplication and division does supports Amounts of different commodities,
# however:
#   - If either amount is uncommoditized, the result carries the commodity of
#     the other amount.
#   - Otherwise, the result always carries the commodity of the first amount.

five = ledger.Amount("5 CAD")

assertEqual(one, two / two)
assertEqual(five, (five * ledger.Amount("$2")) - ledger.Amount("5"))

# An amount's commodity determines the decimal precision it's displayed with.
# However, this "precision" is a notional thing only.  You can tell an amount
# to ignore its display precision by setting `keep_precision' to True.
# (Uncommoditized amounts ignore the value of `keep_precision', and assume it
# is always True).  In this case, Ledger does its best to maintain maximal
# precision by watching how the Amount is used.  That is, 1.01 * 1.01 yields a
# precision of 4.  This tracking is just a best estimate, however, since
# internally Ledger never uses floating-point values.

amt  = ledger.Amount('$100.12')
mini = ledger.Amount('0.00045')

assert not amt.keep_precision

assertEqual(5, mini.precision)
assertEqual(5, mini.display_precision) # display_precision == precision
assertEqual(2, amt.precision)
assertEqual(2, amt.display_precision)

mini *= mini
amt  *= amt

assertEqual(10, mini.precision)
assertEqual(10, mini.display_precision)
assertEqual(4, amt.precision)
assertEqual(2, amt.display_precision)

# There are several other supported math operations:

amt    = ledger.Amount('$100.12')
market = ((ledger.Amount('1 €') / ledger.Amount('$0.77')) * amt)

assertEqual(market, amt.value(eur))            # find present market value

assertEqual('$-100.12', str(amt.negated()))    # negate the amount
assertEqual('$-100.12', str(- amt))            # negate it more simply
assertEqual('$0.01',    str(amt.inverted()))   # reverse NUM/DEM
assertEqual('$100.12',  str(amt.rounded()))    # round it to display precision
assertEqual('$100.12',  str(amt.truncated()))  # truncate to display precision
assertEqual('$100.00',  str(amt.floored()))    # floor it to nearest integral
assertEqual('$100.12',  str(abs(amt)))         # absolute value
assertEqual('$100.12',  str(amt))              # render to a string
assertEqual('100.12',   amt.quantity_string()) # render quantity to a string
assertEqual('100.12',   str(amt.number()))     # strip away commodity
assertEqual(1,          amt.sign())            # -1, 0 or 1
assert amt.is_nonzero()                        # True if display amount nonzero
assert not amt.is_zero()                       # True if display amount is zero
assert not amt.is_realzero()                   # True only if value is 0/0
assert not amt.is_null()                       # True if uninitialized

# Amounts can also be converted the standard floats and integers, although
# this is not recommend since it can lose precision.

assertEqual(100.12, amt.to_double())
assert amt.fits_in_long()       # there is no `fits_in_double'
assertEqual(100, amt.to_long())

# Finally, amounts can be annotated to provide additional information about
# "lots" of a given commodity.  This example shows $100.12 that was purchased
# on 2009/10/01 for 140 €.  Lot information can be accessed through via the
# Amount's `annotation' property.  You can also strip away lot details to get
# the underlying amount.  If you want the total price of any Amount, by
# multiplying by its per-unit lot price, call the `Amount.price' method
# instead of the `Annotation.price' property.

amt2 = ledger.Amount('$100.12 {140 €} [2009/10/01]')

assert amt2.has_annotation()
assertEqual(amt, amt2.strip_annotations())

assertEqual(ledger.Amount('140 €'), amt2.annotation.price)
assertEqual(ledger.Amount('14016,8 €'), amt2.price()) # european amount!

###############################################################################
#
# VALUES
#
# As common as Amounts and Balances are, there is a more prevalent numeric
# type you will encounter when generating reports: Value objects.  A Value is
# a variadic type that can be any of the following types:
#   - Amount
#   - Balance
#   - boolean
#   - integer
#   - datetime
#   - date
#   - string
#   - regex
#   - sequence
#
# The reason for the variadic type is that it supports dynamic self-promotion.
# For example, it is illegal to add two Amounts of different commodities, but
# it is not illegal to add two Value amounts of different commodities.  In the
# former case an exception in raised, but in the latter the Value simply
# promotes itself to a Balance object to make the addition valid.
#
# Values are not used by any of Ledger's data objects (Journal, Transaction,
# Posting or Account), but they are used extensively by value expressions.

val = ledger.Value('$100.00')

assert val.is_amount()
assertEqual('$', val.to_amount().commodity.symbol)

# JOURNALS

#journal.find_account('')
#journal.find_or_create_account('')

# ACCOUNTS

#account.name
#account.fullname()
#account.amount
#account.total

# TRANSACTIONS

#txn.payee

# POSTINGS

#post.account

# REPORTING

#journal.collect('-M food')
#journal.collect_accounts('^assets ^liab ^equity')

print('Demo completed successfully.')