summaryrefslogtreecommitdiff
path: root/python/demo.py
diff options
context:
space:
mode:
authorJohn Wiegley <johnw@newartisans.com>2009-11-20 05:56:24 -0500
committerJohn Wiegley <johnw@newartisans.com>2009-11-20 05:56:24 -0500
commitb00e7ac19a096a7b736863dced616d552843ed6e (patch)
treef2edf023d2994e95ae26eaf26dd9097003d9f7e2 /python/demo.py
parent73c3aa324b5d7ed850ded57a83a6a1e2aa33c185 (diff)
downloadfork-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.py285
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')