Added more documentation to python/demo.py

This commit is contained in:
John Wiegley 2009-11-20 05:56:24 -05:00
parent 73c3aa324b
commit b00e7ac19a
2 changed files with 239 additions and 60 deletions

285
python/demo.py Normal file → Executable file
View file

@ -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
#
# 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':
pool = ledger.commodity_pool
comms = ledger.commodities
usd = pool.find_or_create('$')
eur = pool.find_or_create('EUR')
xcd = pool.find_or_create('XCD')
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)
assert not pool.find('CAD')
assert not pool.has_key('CAD')
assert not 'CAD' in pool
# 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()))
for symbol in pool.iterkeys(): pass
for commodity in pool.itervalues(): pass
# jww (2009-11-19): Not working: missing conversion from std::pair
#for symbol, commodity in pool.iteritems(): pass
#for symbol, commodity in pool: pass
# This creates a price exchange entry, trading EUR for $0.77 each at the
# current time.
pool.exchange(eur, ledger.Amount('$0.77'))
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:
# 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)
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')
# 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(5, mini.precision)
assertEqual(5, mini.display_precision) # display_precision == 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
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 EUR') / 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()
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')

View file

@ -224,6 +224,14 @@ namespace {
return comm.strip_annotations(keep);
}
boost::optional<amount_t> py_price(annotation_t& ann) {
return ann.price;
}
boost::optional<amount_t> py_set_price(annotation_t& ann,
const boost::optional<amount_t>& price) {
return ann.price = price;
}
} // unnamed namespace
void export_commodity()
@ -295,7 +303,7 @@ void export_commodity()
map_value_type_converter<commodity_pool_t::commodities_map>();
scope().attr("commodity_pool") = commodity_pool_t::current_pool;
scope().attr("commodities") = commodity_pool_t::current_pool;
scope().attr("COMMODITY_STYLE_DEFAULTS") = COMMODITY_STYLE_DEFAULTS;
scope().attr("COMMODITY_STYLE_SUFFIXED") = COMMODITY_STYLE_SUFFIXED;
@ -375,9 +383,7 @@ void export_commodity()
.def("drop_flags", &supports_flags<>::drop_flags)
#endif
.add_property("price",
make_getter(&annotation_t::price),
make_setter(&annotation_t::price))
.add_property("price", py_price, py_set_price)
.add_property("date",
make_getter(&annotation_t::date),
make_setter(&annotation_t::date))