diff options
author | John Wiegley <johnw@newartisans.com> | 2009-11-20 05:56:24 -0500 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2009-11-20 05:56:24 -0500 |
commit | b00e7ac19a096a7b736863dced616d552843ed6e (patch) | |
tree | f2edf023d2994e95ae26eaf26dd9097003d9f7e2 /python/demo.py | |
parent | 73c3aa324b5d7ed850ded57a83a6a1e2aa33c185 (diff) | |
download | fork-ledger-b00e7ac19a096a7b736863dced616d552843ed6e.tar.gz fork-ledger-b00e7ac19a096a7b736863dced616d552843ed6e.tar.bz2 fork-ledger-b00e7ac19a096a7b736863dced616d552843ed6e.zip |
Added more documentation to python/demo.py
Diffstat (limited to 'python/demo.py')
-rwxr-xr-x[-rw-r--r--] | python/demo.py | 285 |
1 files changed, 229 insertions, 56 deletions
diff --git a/python/demo.py b/python/demo.py index 788610d7..7b4003f3 100644..100755 --- a/python/demo.py +++ b/python/demo.py @@ -1,91 +1,264 @@ +#!/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: - print "FAILED: %s != %s" % (pat, candidate) + raise Exception("FAILED: %s != %s" % (pat, candidate)) sys.exit(1) +############################################################################### +# # COMMODITIES - -pool = ledger.commodity_pool - -usd = pool.find_or_create('$') -eur = pool.find_or_create('EUR') -xcd = pool.find_or_create('XCD') +# +# 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('$') +eur = comms.find_or_create('EUR') +xcd = comms.find_or_create('XCD') + +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_EUROPEAN) +assert eur.has_flags(ledger.COMMODITY_STYLE_EUROPEAN) +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'EUR', u'XCD', u'h', u'm', u's'], + 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 EUR 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('$', pool['$'].symbol) +assertEqual('$', comms['$'].symbol) -assert not pool.find('CAD') -assert not pool.has_key('CAD') -assert not 'CAD' in pool +############################################################################### +# +# 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 EUR, 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 EUR") + 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') -# There are a few built-in commodities: null, %, h, m and s -assertEqual([u'', u'$', u'%', u'EUR', u'XCD', - u'h', u'm', u's'], sorted(pool.keys())) +assert not amt.keep_precision -for symbol in pool.iterkeys(): pass -for commodity in pool.itervalues(): pass +assertEqual(5, mini.precision) +assertEqual(5, mini.display_precision) # display_precision == precision +assertEqual(2, amt.precision) +assertEqual(2, amt.display_precision) -# jww (2009-11-19): Not working: missing conversion from std::pair -#for symbol, commodity in pool.iteritems(): pass -#for symbol, commodity in pool: pass +mini *= mini +amt *= amt -# This creates a price exchange entry, trading EUR for $0.77 each at the -# current time. -pool.exchange(eur, ledger.Amount('$0.77')) +assertEqual(10, mini.precision) +assertEqual(10, mini.display_precision) +assertEqual(4, amt.precision) +assertEqual(2, amt.display_precision) -# AMOUNTS & BALANCES +# There are several other supported math operations: -# When two amounts are multipied or divided, the result carries the commodity -# of the first term. So, 1 EUR / $0.77 == roughly 1.2987 EUR amt = ledger.Amount('$100.12') market = ((ledger.Amount('1 EUR') / ledger.Amount('$0.77')) * amt) -# An amount's "precision" is a notional thing only. Since Ledger uses -# rational numbers throughout, and only renders to decimal form for printing -# to the user, the meaning of amt.precision should not be relied on as -# meaningful. It only controls how much precision unrounded numbers (those -# for which keep_precision is True, and thus that ignore display_precision) -# are rendered into strings. This is the case, btw, for all uncommoditized -# amounts. -assert not amt.keep_precision -assertEqual(2, amt.precision) -assertEqual(2, amt.display_precision) - -assertEqual('$-100.12', str(amt.negated())) # negate the amount -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(market, amt.value(eur)) # find present market value -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 +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() +assert amt.fits_in_long() # there is no `fits_in_double' assertEqual(100, amt.to_long()) -amt2 = ledger.Amount('$100.12 {140 EUR}') +# 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 EUR. 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 EUR} [2009/10/01]') assert amt2.has_annotation() -assertEqual(amt, amt2.strip_annotations(ledger.KeepDetails())) +assertEqual(amt, amt2.strip_annotations()) -# jww (2009-11-19): Not working: missing conversion from optional<amount_t> -#assertEqual(ledger.Amount('20 EUR'), amt.annotation.price) +assertEqual(ledger.Amount('140 EUR'), amt2.annotation.price) +assertEqual(ledger.Amount('14016,8 EUR'), 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') |