Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

depreciation source pure

Package depreciation is the carrying amount of a capital asset as a pure function of how many whole periods it has be...

Overview

Package depreciation is the carrying amount of a capital asset as a pure function of how many whole periods it has been in service.

A realm stores the plan and the date the asset was placed in service, and asks this package what it is worth now and what to charge for the span since it last asked. Nothing here reads the chain, holds state, or knows what a ledger is, so a realm can test a decade of its own depreciation without a node.

Example
1s, err := New(120000, 0, 120)                 // 120000 over 120 months
2n, _ := Periods(placedAt, now, Monthly)
3charge, _ := s.Charge(booked, n)              // what to post
4carrying, _ := s.CarryingAt(n)                // what it is worth

Why this is a function of n and not an accumulator

The obvious implementation stores the carrying amount, subtracts a per-period charge, and advances an anchor to now. It is wrong in a way that is specific and quiet: the per-period charge is an integer division, so each call truncates, and the part-period the call lands in is forfeited. Run twice as often and you forfeit twice as much — which for an asset register means *running the schedule more often depreciates less*. That is a misstatement of asset value, not a rounding preference.

p/moul/x/games/accrual names this bug and fixes it by advancing the anchor only by the whole periods it actually paid for, leaving the remainder on the clock, and it states the invariant that rules the bug out:

Example
1Advance(s, a, c) == Advance(Advance(s, a, b), b, c)   for a <= b <= c

That invariant is adopted here, and the way it is obtained is different and stronger. Nothing in this package is an accumulator: Schedule.CarryingAt is a pure function of the period count, and Schedule.Charge is the difference of it at two points. So charges **telescope** —

Example
1Charge(a, b) + Charge(b, c) == Charge(a, c)

— by construction rather than by careful anchor arithmetic, and there is no stored carrying amount that could drift from the schedule at all. A realm that loses track of what it has booked can recompute it from the dates. [TestChargesTelescope] asserts it over pseudo-random partitions, because an implementation that truncates per call passes every round span and fails only on the ones that land mid-period.

The read path cannot disagree with the write path for the same reason: there is one function, and `Charge` is defined in terms of it.

Why a bounded loop is the right answer here and O(1) is not available

accrual must be closed form because elapsed time is unbounded: the player who returns after a month is exactly the one whose Render times out. Depreciation has no such exposure, and declining balance has no integer closed form — period n is the rate applied n times, with the floor taken each period, and taking the floor once at the end is a different and wrong number.

What bounds the loop instead is the asset's own **declared useful life**: Schedule.CarryingAt clamps at `Life` periods, `Life` is validated against MaxLife at construction, and the loop exits early once the salvage floor is reached. So the cost of a call is bounded by a number the caller declared and this package checked, not by how long ago the asset was bought. That is a different safety argument from closed-form and it is the one that makes declining balance implementable at all. Straight line *is* closed form and takes no loop.

What this package does not claim

  • **No novelty for the straight-line arithmetic.** A quantity moving at a fixed rate per period toward a bound is shipped elsewhere in this ecosystem more than once; see DISCOVERY.md § 1. What was not found in the searched sources is the domain model — a salvage floor, a carrying amount, a declining-balance schedule over integers.
  • **Nothing about money.** A carrying amount is a number in a plan. This package never sees a coin, and a realm using it must not infer custody from it.
  • **No period convention beyond whole periods.** An asset placed in service mid-period depreciates from its placed-in-service date, not from the start of the calendar period it fell in. Half-year and mid-month conventions are a caller's job: shift `placedAt`. See § 4 of AUDIT.md.
  • **No switch to straight line.** Textbook declining balance often crosses over to straight line once the latter charges more. This does not; see Schedule.CarryingAt on what happens at `Life` instead.

p/moul/x/games/accrual: /p/moul/x/games/accrual/v0

Constants 3

const Daily, Monthly, Yearly

1const (
2	Daily   int64 = 86400
3	Monthly int64 = 30 * Daily
4	Yearly  int64 = 365 * Daily
5)
source

Period lengths in seconds, for a caller that keeps time in unix seconds. They are conveniences and not a calendar: Monthly is 30 days and Yearly is 365, so neither tracks month lengths or leap years. A caller that needs a real calendar must compute its own period count and pass it to the CarryingAt/Charge pair directly, which take period counts and not times.

const MaxLife

1const MaxLife = 1200
source

MaxLife bounds the declared useful life in whole periods, and so bounds the declining-balance loop in Schedule.CarryingAt. 1200 is a century of monthly periods, which is past the point where any real schedule stops being meaningful.

What the bound costs is stated here rather than waved at, because the earlier wording claimed the loop "is not a gas hazard even in a Render" and no gas was ever measured. The figures that were: CarryingAt runs the loop n times with n < Life, so at most MaxLife-1 = 1199 iterations per call, and Schedule.Charge evaluates CarryingAt twice, so one Charge can run 2398. The early exit at the salvage floor is an optimisation and not part of the bound — a schedule whose per-period charge truncates to zero (Cost*RateNum < RateDen) never moves off Cost, never reaches the floor, and so runs the full 1199 on every call for the asset's whole life. A realm that walks many assets in one transaction, or renders them, must size that against the gas it has. This package bounds the work; it does not price it.

const StraightLine, DecliningBalance

1const (
2	// StraightLine charges the basis evenly across the life. Closed form.
3	StraightLine Method = 1
4
5	// DecliningBalance charges a fixed fraction of the remaining carrying
6	// amount each period, so early periods charge more. Not closed form
7	// over integers; see the package doc on why the loop is bounded.
8	DecliningBalance Method = 2
9)
source

Variables 1

var ErrBadCost, ErrBadSalvage, ErrBadLife, ErrBadMethod, ErrBadRate, ErrBadPeriod, ErrBackwards, ErrBadSpan, ErrOverflow

 1var (
 2	// ErrBadCost is returned when cost is not positive. A zero-cost asset
 3	// has nothing to depreciate and makes Basis degenerate.
 4	ErrBadCost = errors.New("depreciation: cost must be positive")
 5
 6	// ErrBadSalvage is returned when salvage is negative, or is not less
 7	// than cost. Salvage equal to cost is a non-depreciating asset, which
 8	// is legitimate bookkeeping but is not a schedule: there is no charge
 9	// to post, ever, and silently accepting it would have a realm open
10	// accounts and post nothing.
11	ErrBadSalvage = errors.New("depreciation: salvage must be in [0, cost)")
12
13	// ErrBadLife is returned when the useful life is not positive or
14	// exceeds MaxLife.
15	ErrBadLife = errors.New("depreciation: life must be in [1, MaxLife]")
16
17	// ErrBadMethod is returned for a method outside the declared set.
18	ErrBadMethod = errors.New("depreciation: unknown method")
19
20	// ErrBadRate is returned when a declining-balance factor is not a
21	// proper fraction in (0, 1). A rate of 1 writes the asset off in one
22	// period and a rate of 0 never charges anything; both are better said
23	// another way, so neither is accepted here.
24	ErrBadRate = errors.New("depreciation: rate must satisfy 0 < num < den")
25
26	// ErrBadPeriod is returned when a period length is not positive.
27	ErrBadPeriod = errors.New("depreciation: period must be positive")
28
29	// ErrBackwards is returned when now falls before the placed-in-service
30	// time. Time moving backwards is a caller bug — a stored timestamp from
31	// another clock, a test that rewound — and treating it as zero elapsed
32	// would hide it.
33	ErrBackwards = errors.New("depreciation: now falls before placedAt")
34
35	// ErrBadSpan is returned by Charge when the span runs backwards, which
36	// would make a depreciation charge negative and credit an expense.
37	ErrBadSpan = errors.New("depreciation: charge span must not run backwards")
38
39	// ErrOverflow is returned when the arithmetic would wrap. Most
40	// overflow is excluded at construction instead; see New.
41	ErrOverflow = errors.New("depreciation: int64 overflow")
42)
source

Functions 3

func Periods

1func Periods(placedAt, now, period int64) (int32, error)
source

Periods returns the whole periods elapsed between placedAt and now.

A partial period in progress is not counted and is not lost: it is counted by a later call, once it completes. That is what makes the telescoping property above reach the right total however the caller slices time.

The span is measured in uint64 on purpose, and the reason is the whole correctness argument. `now >= placedAt` is already enforced above, so the true elapsed span is non-negative; its largest value is maxInt64 - minInt64 = 2^64 - 1, which does not fit in an int64 and fits *exactly* in a uint64. int64 subtraction wraps two's-complement, so the bit pattern of now - placedAt is already the low 64 bits of the true difference, and reading that pattern as a uint64 recovers the difference with no loss, no clamp and no reachable wrap. ErrOverflow therefore means exactly one thing here — the quotient does not fit in an int32 — and the subtraction needs no guard at all.

The guard it replaces, `placedAt < 0 && now > 0 && elapsed < 0`, was wrong in *both* directions, measured by exhaustive differential sweep against exact arithmetic rather than reasoned about: it returned (0, nil) for Periods(minInt64, 0, 1) and even a negative count for Periods(minInt64, 0, 2^62), and it rejected representable answers such as Periods(-1, maxInt64, maxInt64), whose count is 1. See § 5 of AUDIT.md.

func New

1func New(cost, salvage int64, life int32) (Schedule, error)
source

New validates and returns a straight-line schedule.

func NewDeclining

1func NewDeclining(cost, salvage int64, life int32, num, den int64) (Schedule, error)
source

NewDeclining validates and returns a declining-balance schedule charging num/den of the remaining carrying amount each period. Double-declining over a 10-period life is num=2, den=10.

Types 2

type Method

ident
1type Method uint8
source

Method is how the basis is spread across the useful life.

Methods on Method

func String

method on Method
1func (m Method) String() string
source

String names the method. The zero Method is "invalid" rather than empty, so a caller that renders it without checking Valid prints something that reads as wrong instead of something that reads as missing.

func Valid

method on Method
1func (m Method) Valid() bool
source

Valid reports whether m is a method this package implements.

type Schedule

struct
 1type Schedule struct {
 2	// Cost is the depreciable basis at placed-in-service.
 3	Cost int64
 4
 5	// Salvage is the floor. The carrying amount never falls below it and
 6	// reaches it exactly at Life.
 7	Salvage int64
 8
 9	// Life is the useful life in whole periods.
10	Life int32
11
12	// Method is how the basis is spread.
13	Method Method
14
15	// RateNum and RateDen are the declining-balance factor as a proper
16	// fraction. Both are zero for StraightLine.
17	RateNum int64
18	RateDen int64
19}
source

Schedule is the depreciation plan for one asset. It is a value, carries no state about what has been booked, and is safe to copy. Construct with New or NewDeclining so the invariants — including the overflow preconditions — are checked once rather than on every call.

Methods on Schedule

func AccumulatedAt

method on Schedule
1func (s Schedule) AccumulatedAt(n int32) (int64, error)
source

AccumulatedAt returns accumulated depreciation after n whole periods: the contra-asset balance, Cost - CarryingAt(n). It is defined in terms of CarryingAt so the two can never disagree, which is the same reason Charge is.

func Basis

method on Schedule
1func (s Schedule) Basis() int64
source

Basis is Cost - Salvage: the total that will ever be charged across the whole life, under either method.

It does not validate, and it is the only exported method here that does not. That is forced rather than chosen: check() calls Basis to establish one of its own overflow preconditions, so Basis cannot call check(). It is therefore a bare subtraction over whatever fields it was handed, and on an ill-formed schedule it returns an arbitrary number — Schedule{Cost: maxInt64, Salvage: -1} wraps to minInt64. A zero result means "invalid, or zero cost", never "fully depreciated"; depreciation booked to date is Schedule.AccumulatedAt, which does validate. Guard a caller-supplied schedule with Schedule.Valid before quoting Basis from it.

func CarryingAt

method on Schedule
1func (s Schedule) CarryingAt(n int32) (int64, error)
source

CarryingAt returns the carrying amount after n whole periods in service.

It is monotone non-increasing in n, equals Cost at n <= 0, and equals Salvage exactly at every n >= Life. That last clamp is a declared convention and is doing real work under DecliningBalance, which approaches the floor asymptotically and would otherwise never reach it: the final period absorbs whatever remains. The consequence is worth stating because it is visible in the numbers — a declining-balance schedule's last charge can be much larger than the one before it. Textbooks usually avoid that by crossing over to straight line partway; this package does not, because the crossover rule is a policy choice that belongs to the caller and burying it here would make the schedule unexplainable from its own fields.

A declining-balance schedule whose per-period charge truncates to zero never depreciates at all. The charge is (c*RateNum)/RateDen in integers, so whenever Cost*RateNum < RateDen it is 0, c never moves off Cost, and CarryingAt returns Cost at every n below Life and Salvage at Life — the whole of the basis falls in the last period. That is integer truncation behaving correctly and it is accepted rather than guarded, because the alternative is a minimum-charge rule, which is a policy a caller may reasonably want either way and cannot be inferred from the fields. It is documented because it is invisible in the ordinary numbers and is the one shape that also defeats the loop's early exit; see MaxLife on the cost.

Under StraightLine the result is closed form. Under DecliningBalance it costs O(min(n, Life)) iterations, bounded by MaxLife and exiting early at the floor; see the package doc.

func Charge

method on Schedule
1func (s Schedule) Charge(from, to int32) (int64, error)
source

Charge returns the depreciation expense for moving a schedule from `from` whole periods booked to `to`.

It is the difference of CarryingAt at the two points, which makes charges telescope exactly:

Example
1Charge(a, b) + Charge(b, c) == Charge(a, c)   for a <= b <= c

So how often a realm runs the schedule cannot change the total it books, and a realm that has booked through period `from` can post Charge(from, now) with no stored carrying amount to drift. The result is never negative; a span that runs backwards is ErrBadSpan rather than a credit to an expense account.

func FullyDepreciatedAt

method on Schedule
1func (s Schedule) FullyDepreciatedAt(placedAt, period int64) (int64, error)
source

FullyDepreciatedAt returns the time at which the asset first carries only its salvage value, for a realm that wants to tell a reader when the charges stop. It is placedAt + Life*period.

Under DecliningBalance the carrying amount may sit very close to salvage well before this time, and under both methods it is not at salvage until this time, because Life is what the clamp in CarryingAt uses.

func Valid

method on Schedule
1func (s Schedule) Valid() bool
source

Valid reports whether the schedule is well-formed. A zero Schedule is not.

Dependencies

Imports 1

/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/depreciation

Imported by indexer

Source Files 2