depreciation
Depreciation schedule of an asset with each year's charge, accumulated depreciation and book value: straight-line, declining balance at any factor or rate with an optional switch to straight-line, double-declining, Excel's fixed-rate declining balance (DB), sum-of-the-years'-digits and units-of-production, with salvage value and a partial first year in months. Decimal arithmetic.
Install
/tools install depreciation /tools install depreciation@1.0.0
In a console session, or let the model find it in normal mode: axlr_catalog describes it and axlr_install_tool asks for the same install card. The card reads catalog axlr · depreciation 1.0.0 · verified once the index signature, the manifest digest and every file digest check out. Then the model runs it with axlr_run_tool depreciation; each run is approved as a local_exec.
Arguments
One JSON object on stdin, validated against the schema before the program starts.
| name | type | required | default | meaning |
|---|---|---|---|---|
method | string | yes | one of "straight_line", "declining_balance", "double_declining", "fixed_declining_balance", "sum_of_years_digits", "units_of_production" | |
cost | number | string | yes | ||
salvage | number | string | no | 0 | |
life | integer | no | Useful life in years; all methods but units_of_production. ≥ 1; ≤ 200 | |
first_year_months | integer | no | 12 | Months of use in the first year. ≥ 1; ≤ 12 |
factor | number | string | no | 2 | declining_balance: rate = factor / life |
rate | number | string | no | declining_balance: rate per year instead of factor | |
switch_to_straight_line | boolean | no | false | |
total_units | number | string | no | units_of_production: lifetime units | |
units | array of ['number', 'string'] | no | units_of_production: units of each year. items ≤ 200 | |
start_year | integer | no | 1 | Label of the first row |
Input schema (JSON)
{
"type": "object",
"required": [
"method",
"cost"
],
"additionalProperties": false,
"properties": {
"method": {
"type": "string",
"enum": [
"straight_line",
"declining_balance",
"double_declining",
"fixed_declining_balance",
"sum_of_years_digits",
"units_of_production"
]
},
"cost": {
"type": [
"number",
"string"
]
},
"salvage": {
"type": [
"number",
"string"
],
"default": 0
},
"life": {
"type": "integer",
"minimum": 1,
"maximum": 200,
"description": "Useful life in years; all methods but units_of_production"
},
"first_year_months": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"default": 12,
"description": "Months of use in the first year"
},
"factor": {
"type": [
"number",
"string"
],
"default": 2,
"description": "declining_balance: rate = factor / life"
},
"rate": {
"type": [
"number",
"string"
],
"description": "declining_balance: rate per year instead of factor"
},
"switch_to_straight_line": {
"type": "boolean",
"default": false
},
"total_units": {
"type": [
"number",
"string"
],
"description": "units_of_production: lifetime units"
},
"units": {
"type": "array",
"items": {
"type": [
"number",
"string"
]
},
"maxItems": 200,
"description": "units_of_production: units of each year"
},
"start_year": {
"type": "integer",
"default": 1,
"description": "Label of the first row"
}
}
}
Example
Input
{
"method": "sum_of_years_digits",
"cost": 30000,
"salvage": 7500,
"life": 10
}
Output (stdout)
{
"total_depreciation": "22500.00",
"final_book_value": "7500.00"
}
Files
The command the install card registers is python3 depreciation.py, run in the workspace with PATH and HOME as its only environment. Each file's SHA-256 is in the signed index; the console checks them before showing the card and again before writing.
depreciation.py · sha256 9a11ef2a1ce0fe4257c6e54dbd8f5ae9a93f27aa980c29df3c24a8309cb71737
#!/usr/bin/env python3
"""Depreciation schedules in Decimal: straight-line, declining balance at any factor or rate
(optionally switching to straight-line when that gives more), double-declining, the fixed-rate
declining balance of Excel's DB, sum-of-the-years'-digits and units-of-production, with a
salvage value and a partial first year counted in months. Each row gives the year's charge,
the accumulated depreciation and the book value.
Input (JSON on stdin):
method "straight_line" | "declining_balance" | "double_declining" |
"fixed_declining_balance" | "sum_of_years_digits" | "units_of_production"
cost cost of the asset
salvage residual value at the end of its life (default 0)
life useful life in years (periods), an integer
first_year_months months of use in the first year, 1-12 (default 12); with fewer than 12
the schedule has life + 1 rows
factor declining_balance: rate = factor / life (default 2)
rate declining_balance: a rate per year instead of the factor (0.3 or "30%")
switch_to_straight_line declining_balance, double_declining: take the straight-line charge
on the remaining book value when it is larger (default false)
total_units, units units_of_production: lifetime units and the units of each year
start_year optional label of the first year
"""
import json
import sys
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation, getcontext
getcontext().prec = 40
METHODS = ("straight_line", "declining_balance", "double_declining", "fixed_declining_balance", "sum_of_years_digits", "units_of_production")
CENT = Decimal("0.01")
def fail(message):
print(json.dumps({"error": message}))
sys.exit(2)
def dec(value, field, percent=False):
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
fail(f"{field} must be a number or a decimal string")
text = str(value).strip()
scale = Decimal(1)
if percent and text.endswith("%"):
text, scale = text[:-1], Decimal("0.01")
try:
number = Decimal(text) * scale
except InvalidOperation:
fail(f"{field} must be a number or a decimal string")
if not number.is_finite():
fail(f"{field} must be finite")
return number
def money(x):
return format(x.quantize(CENT, ROUND_HALF_UP), "f")
def fractions_of(life, months):
"""Share of a full year of use in each fiscal year."""
if months == 12:
return [Decimal(1)] * life
first = Decimal(months) / 12
return [first] + [Decimal(1)] * (life - 1) + [1 - first]
def straight_line(cost, salvage, life, months):
annual = (cost - salvage) / life
return [annual * f for f in fractions_of(life, months)]
def sum_of_years(cost, salvage, life, months):
total = Decimal(life * (life + 1) // 2)
full = [(cost - salvage) * (life - k) / total for k in range(life)] # depreciation year k + 1
if months == 12:
return full
first = Decimal(months) / 12
out = [full[0] * first]
for j in range(1, life):
out.append(full[j - 1] * (1 - first) + full[j] * first)
out.append(full[-1] * (1 - first))
return out
def declining(cost, salvage, life, months, rate, switch):
out, book, used = [], cost, Decimal(0)
for fraction in fractions_of(life, months):
charge = book * rate * fraction
remaining = life - used
if switch and remaining > 0:
charge = max(charge, (book - salvage) * fraction / remaining)
charge = max(Decimal(0), min(charge, book - salvage))
out.append(charge)
book -= charge
used += fraction
return out
def fixed_declining(cost, salvage, life, months):
rate = (1 - (salvage / cost) ** (Decimal(1) / life)).quantize(Decimal("0.001"), ROUND_HALF_UP)
out, book = [], cost
for fraction in fractions_of(life, months):
charge = book * rate * fraction
out.append(charge)
book -= charge
return out, rate
def main():
try:
request = json.load(sys.stdin)
except ValueError as error:
fail(f"stdin is not JSON: {error}")
if not isinstance(request, dict):
fail("input must be a JSON object")
known = {"method", "cost", "salvage", "life", "first_year_months", "factor", "rate", "switch_to_straight_line",
"total_units", "units", "start_year"}
extra = sorted(set(request) - known)
if extra:
fail(f"unknown field: {extra[0]}")
method = request.get("method")
if method not in METHODS:
fail("method must be one of " + ", ".join(METHODS))
if "cost" not in request:
fail("cost is required")
cost = dec(request["cost"], "cost")
salvage = dec(request.get("salvage", 0), "salvage")
if cost <= 0:
fail("cost must be positive")
if salvage < 0 or salvage >= cost:
fail("salvage must be from 0 to below cost")
months = request.get("first_year_months", 12)
if isinstance(months, bool) or not isinstance(months, int) or not 1 <= months <= 12:
fail("first_year_months must be an integer from 1 to 12")
start_year = request.get("start_year", 1)
if isinstance(start_year, bool) or not isinstance(start_year, int):
fail("start_year must be an integer")
switch = request.get("switch_to_straight_line", False)
if not isinstance(switch, bool):
fail("switch_to_straight_line must be true or false")
result = {"method": method, "cost": money(cost), "salvage": money(salvage)}
if method == "units_of_production":
if "total_units" not in request or "units" not in request:
fail("units_of_production needs total_units and units (a list, one per year)")
total_units = dec(request["total_units"], "total_units")
if total_units <= 0:
fail("total_units must be positive")
raw = request["units"]
if not isinstance(raw, list) or not 1 <= len(raw) <= 200:
fail("units must be a list of 1 to 200 yearly amounts")
per_unit = (cost - salvage) / total_units
charges, book = [], cost
for i, u in enumerate(raw):
used = dec(u, f"units[{i}]")
if used < 0:
fail(f"units[{i}] must not be negative")
charge = min(used * per_unit, book - salvage)
charges.append(charge)
book -= charge
result["rate_per_unit"] = format(per_unit.quantize(Decimal("0.000001"), ROUND_HALF_UP), "f")
if sum(dec(u, "units") for u in raw) > total_units:
result["note"] = "the units exceed total_units: depreciation stops at the salvage value"
else:
life = request.get("life")
if isinstance(life, bool) or not isinstance(life, int) or not 1 <= life <= 200:
fail("life must be an integer number of years from 1 to 200")
result["life"] = life
result["first_year_months"] = months
if method == "straight_line":
charges = straight_line(cost, salvage, life, months)
elif method == "sum_of_years_digits":
charges = sum_of_years(cost, salvage, life, months)
elif method == "fixed_declining_balance":
if switch:
fail("switch_to_straight_line applies to declining_balance and double_declining")
charges, rate = fixed_declining(cost, salvage, life, months)
result["rate"] = format(rate, "f")
else:
if method == "double_declining":
if "rate" in request or "factor" in request:
fail("double_declining has factor 2; use declining_balance for another factor or rate")
rate = Decimal(2) / life
elif "rate" in request:
if "factor" in request:
fail("give factor or rate, not both")
rate = dec(request["rate"], "rate", percent=True)
else:
factor = dec(request.get("factor", 2), "factor")
if factor <= 0:
fail("factor must be positive")
rate = factor / life
if not 0 < rate <= 1:
fail("the declining rate must be above 0 and at most 100%")
charges = declining(cost, salvage, life, months, rate, switch)
result["rate"] = format(rate.quantize(Decimal("0.000001"), ROUND_HALF_UP).normalize(), "f")
result["switch_to_straight_line"] = switch
rows, accumulated = [], Decimal(0)
for k, charge in enumerate(charges):
start = cost - accumulated
accumulated += charge
rows.append({"year": start_year + k, "book_value_start": money(start), "depreciation": money(charge),
"accumulated": money(accumulated), "book_value_end": money(cost - accumulated)})
result["total_depreciation"] = money(accumulated)
result["final_book_value"] = money(cost - accumulated)
left = cost - accumulated - salvage
if left.quantize(CENT, ROUND_HALF_UP) != 0:
result["undepreciated_above_salvage"] = money(left)
result["schedule"] = rows
print(json.dumps(result))
if __name__ == "__main__":
try:
main()
except ArithmeticError:
fail("the numbers are out of the range this tool can compute")
Where it is served
The registry serves only AXLR clients, so a browser gets 403; fetch with a User-Agent that starts with axlr:
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/depreciation/1.0.0/manifest.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/depreciation/1.0.0/depreciation.py