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.

lots source pure

Package lots is the cost of goods consumed as a pure function of how much of a lot has been consumed.

Overview

Package lots is the cost of goods consumed as a pure function of how much of a lot has been consumed.

A realm stores receipts of a stock-keeping unit — each a quantity and what was paid for it — and asks this package what the goods it just shipped cost. Nothing here reads the chain, holds coins, or knows what a ledger is, so a realm can test a year of its own inventory costing without a node.

Example
1p, _ := NewPool(FIFO, 64)
2p.Receive(100, 2500)             // 100 units for 2500 in total
3p.Receive(50, 1400)              // a second, dearer receipt
4cogs, _ := p.Consume(120)        // what the shipment cost: DERIVED
5onHand, _ := p.CostOnHand()      // what is left is worth

The amount is never a parameter

This is the thesis the rest of the package serves. A caller says how *many* units left the shelf; it never says what they cost. The cost is computed from the lots the units came out of, so a realm cannot post a cost of goods sold that disagrees with the inventory it holds — not because it is careful, but because there is no argument through which the two could be made to disagree.

[p/.../depreciation] makes the same move for a capital asset, where the charge is a function of periods elapsed. Here it is sharpened: a depreciation schedule is fixed at acquisition and a caller supplies only time, while an inventory pool is mutated by every receipt, and the function still holds.

Why the cost is a difference of a pure function, and not a running average

The obvious implementation keeps a remaining quantity and a remaining cost and, for a shipment of n units, charges

Example
1remainingCost * n / remainingQuantity

It is wrong in a way that is specific and quiet. The division truncates, and what it truncates is retained in the lot rather than charged, so the cost charged for the first k units out of a lot **depends on how those k units were sliced into shipments**. Measured, with the whole lot's life evaluated:

Example
1lot(Qty=7, Cost=10) consumed as [1,1,1]
2    naive       -> [1, 1, 1]   total 3
3    telescoping -> [1, 1, 2]   total 4

Nothing here is an accumulator. Lot.CostAt is a pure function of the cumulative units consumed,

Example
1CostAt(k) == Cost*k / Qty

and Lot.Take is its difference at two points. So charges **telescope** —

Example
1Take(a→b) + Take(b→c) == Take(a→c)

— by construction rather than by careful remainder arithmetic, and there is no stored remaining cost that could drift from the lot at all. A realm that loses track of what it has charged can recompute it from the quantities.

What the naive form actually gets wrong, stated precisely

It **mis-times** cost; it does not lose it. Over a lot's whole life the naive form charges exactly Cost, because the shipment that exhausts the lot has `n == remainingQuantity` and so charges the whole remainder. Saying "it loses money" would be a stronger and false claim, and a reader who checks the totals would correctly conclude the warning was overstated and ignore the real defect. The real defect is that a *period's* cost of goods sold, and the inventory value carried at any moment before exhaustion, both depend on shipment slicing — which is a misstatement of both the income statement and the balance sheet, for a quantity nobody chose.

Measured by differential sweep against exact arithmetic, over quantities 1 to 12 and costs 1 to 50, evaluating **every ordered composition** of the quantity (so every way of slicing a lot's whole life into shipments):

Example
1compositions evaluated           204750
2naive path-dependent             108320   over 260 of 600 (Qty,Cost) pairs
3telescoping path-dependent            0
4naive conservation failures           0
5telescoping conservation failures     0
6worst naive mis-timing            3 units of cost, at Qty=12 Cost=6

"Path-dependent" has a definition, because the count is worthless without one: a composition is path-dependent if the cumulative cost charged after some prefix of it differs from the cumulative cost the same number of units would have carried had they shipped one at a time. The two zeroes on the conservation lines are the evidence for the paragraph above — the naive form reaches the right total every time and gets there wrongly — and the worst case is half of a small lot's cost, not a rounding crumb.

The weighted-average method re-anchors at every receipt, and must

Under WAVG a receipt merges into the single live lot: the merged lot's quantity is what remained plus what arrived, its cost is what remained *was worth* plus what arrived cost, and its consumed count resets to zero. The reset is not bookkeeping convenience and is not a defect — it is forced. CostAt is a function of the lot's own Qty and Cost, and a receipt changes both, so a consumed count measured against the old pair means nothing against the new one. Telescoping therefore holds **within an anchor epoch**, and across the re-anchor what holds instead is exact cost neutrality: the merged lot's cost is the outgoing lot's remaining cost plus the receipt's, with nothing dropped and nothing invented. Pool.CrossFootCost is what proves it, and it is asserted over receipt/consume interleavings in the suite.

The asymmetry this creates is worth knowing before choosing a method: WAVG can **refuse** a receipt that FIFO and LIFO accept, because the merged lot must satisfy the overflow precondition `Cost <= maxInt64/Qty` that two separate lots would each satisfy on their own. See Pool.Receive.

An error return does not revert a Gno transaction

Only a panic reverts. So every mutating method here stages its whole effect — a copy of the lots it will touch, and the total it will charge — and commits only once the last step has succeeded. A method that returned an error after mutating half the pool would leave a realm holding inventory whose cost had been partly charged, with the realm's own error handling unable to undo it, and the realm would have no way to detect it short of a cross-foot. This package does not panic on caller error; it returns errors and keeps the pool untouched when it does.

What this package does not claim

  • **No novelty for FIFO, LIFO or weighted average.** They are textbook and older than software. What was not found in the searched sources is an on-chain integer implementation with a path-independent cost function; see DISCOVERY.md.
  • **Nothing about money.** A lot cost is a number in a record. This package never sees a coin, never holds value, and a realm using it must not infer custody from it.
  • **No specific identification.** The four methods a textbook lists are three here. Specific identification — the shipper naming which lot a unit came from — would make the consumed amount depend on a caller-supplied choice, which is the one thing the thesis above rules out. A realm that needs it should hold one pool per identified lot.
  • **No units of measure, no currency, no SKU identity.** A quantity is an integer and a cost is an integer. Deciding that they are kilograms and thousandths of a ugnot, and that this pool is the one for SKU "A-1", is the realm's job.
  • **No negative inventory, no write-offs, no revaluation.** Consuming more than is on hand is ErrInsufficient, never a negative lot. Shrinkage and lower-of-cost-or-market are not here; a realm that needs them needs a fourth and fifth account and should say so in its own chart.

[p/.../depreciation]: /p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/depreciation

Constants 2

const MaxLotsLimit

1const MaxLotsLimit = 1024
source

MaxLotsLimit bounds the live lots a single pool may hold, and so bounds every walk in this package: Pool.CostOnHand, Pool.QtyOnHand, Pool.Consume and both cross-foots are O(live lots).

What the bound costs is stated rather than waved at. A pool at the limit makes Consume scan up to 1024 lots and the cross-foots walk them all, and a realm that renders many pools in one query multiplies that. The limit is the ceiling a caller may ask for, not a recommendation: a caller passes its own smaller cap to NewPool and should, because exhausted lots are dropped but live ones are not, and a pool fed many small receipts and consumed in large shipments is the shape that fills up.

const FIFO, LIFO, WAVG

 1const (
 2	// FIFO consumes the earliest-received live lot first.
 3	FIFO Method = 1
 4
 5	// LIFO consumes the latest-received live lot first.
 6	LIFO Method = 2
 7
 8	// WAVG keeps one lot, merging every receipt into it at a weighted
 9	// average. See the package doc on why the merge re-anchors.
10	WAVG Method = 3
11)
source

Variables 1

var ErrBadQty, ErrBadCost, ErrBadConsumed, ErrBadSeq, ErrBadMethod, ErrBadMaxLots, ErrPoolFull, ErrInsufficient, ErrSeqExhausted, ErrOverflow

 1var (
 2	// ErrBadQty is returned when a quantity is not positive. A zero-quantity
 3	// receipt is not a lot and a zero-quantity shipment is not an event;
 4	// accepting either would have a realm post an entry for nothing.
 5	ErrBadQty = errors.New("lots: quantity must be positive")
 6
 7	// ErrBadCost is returned when a cost is negative. Zero is allowed: goods
 8	// received at no cost are ordinary — donated stock, samples, a
 9	// promotional allowance that zeroed the invoice — and they consume
10	// exactly zero cost for their whole life, which is the right answer and
11	// not a degenerate one.
12	ErrBadCost = errors.New("lots: cost must not be negative")
13
14	// ErrBadConsumed is returned when a lot's consumed count is negative or
15	// exceeds its quantity. It is reachable only on a lot a caller built by
16	// hand; lots this package produces always satisfy it.
17	ErrBadConsumed = errors.New("lots: consumed must be in [0, qty]")
18
19	// ErrBadSeq is returned when a lot's receipt sequence is negative.
20	ErrBadSeq = errors.New("lots: seq must not be negative")
21
22	// ErrBadMethod is returned for a method outside the declared set.
23	ErrBadMethod = errors.New("lots: unknown method")
24
25	// ErrBadMaxLots is returned when a pool's lot cap is not positive or
26	// exceeds MaxLotsLimit.
27	ErrBadMaxLots = errors.New("lots: maxLots must be in [1, MaxLotsLimit]")
28
29	// ErrPoolFull is returned when a receipt would exceed the pool's own lot
30	// cap. It is fail-closed: the receipt is refused and the pool is
31	// untouched, so a realm sees a refused receipt rather than silently
32	// merged or dropped stock.
33	ErrPoolFull = errors.New("lots: lot cap reached")
34
35	// ErrInsufficient is returned when a shipment exceeds the quantity on
36	// hand. There is no partial fill and no negative inventory: a realm that
37	// wants to ship what it can must ask QtyOnHand first and decide.
38	ErrInsufficient = errors.New("lots: quantity exceeds quantity on hand")
39
40	// ErrSeqExhausted is returned when the receipt sequence space is spent.
41	// Unreachable in practice and checked anyway, because the alternative is
42	// two lots sharing a sequence and a silently non-deterministic order.
43	ErrSeqExhausted = errors.New("lots: receipt sequence space exhausted")
44
45	// ErrOverflow is returned when the arithmetic would wrap. Most overflow
46	// is excluded by a precondition established when a lot is constructed;
47	// see Lot.check and Pool.Receive.
48	ErrOverflow = errors.New("lots: int64 overflow")
49)
source

Functions 3

func NewLot

1func NewLot(qty, cost int64) (Lot, error)
source

NewLot validates and returns a lot of qty units costing cost in total. Seq is zero; Pool.Receive assigns it.

func MustNewPool

1func MustNewPool(method Method, maxLots int) *Pool
source

MustNewPool is NewPool, panicking on an invalid configuration. It is for a realm's init, where a configuration error is a deployment bug and should abort the transaction rather than be handled.

func NewPool

1func NewPool(method Method, maxLots int) (*Pool, error)
source

NewPool validates and returns an empty pool consuming lots in the given order and holding at most maxLots live lots.

Types 3

type Lot

struct
 1type Lot struct {
 2	// Qty is the quantity received. It never changes: consumption moves
 3	// Consumed, not Qty, because CostAt is a function of the original pair
 4	// and reducing Qty would redefine the function under its own callers.
 5	Qty int64
 6
 7	// Cost is what the whole of Qty cost. It never changes either, for the
 8	// same reason — except under [WAVG], where a receipt replaces the lot
 9	// with a freshly anchored one rather than editing this field.
10	Cost int64
11
12	// Consumed is the cumulative quantity consumed from this lot, in
13	// [0, Qty]. It is the sole argument of the cost function.
14	Consumed int64
15
16	// Seq is the receipt order within a pool, assigned by [Pool.Receive].
17	// It orders lots for FIFO and LIFO and **never enters a cost
18	// computation**: two lots with the same Qty and Cost cost the same to
19	// consume whatever their sequence, which is why the ordering decision
20	// and the costing arithmetic can be reviewed separately.
21	Seq int64
22}
source

Lot is one receipt of stock: a quantity, what was paid for all of it, and how much of it has since been consumed. It is a value, is safe to copy, and every method validates its receiver, so a hand-built or zero Lot fails rather than returning a plausible number.

Construct with NewLot so the overflow precondition is checked once rather than on every call. The fields are exported because a realm must be able to render a lot and persist it, and because Pool.Lots hands out copies.

Methods on Lot

func ConsumedCost

method on Lot
1func (l Lot) ConsumedCost() (int64, error)
source

ConsumedCost returns the cost consumed from this lot so far: CostAt(Consumed).

func CostAt

method on Lot
1func (l Lot) CostAt(k int64) (int64, error)
source

CostAt returns the cumulative cost consumed from this lot once k units in total have been consumed from it:

Example
1CostAt(k) == Cost*k / Qty

It is the one function in this package, and everything else is defined from it. It is monotone non-decreasing in k, zero at k == 0, and **exactly Cost at k == Qty** — that endpoint is not an approximation, because Cost*Qty/Qty is exact in integers, and it is what makes a lot's whole life charge exactly what the lot cost and makes dropping an exhausted lot cost-neutral.

k outside [0, Qty] is ErrBadConsumed rather than a clamp: a clamp would let a caller's off-by-one read as a valid cost at the boundary.

func Exhausted

method on Lot
1func (l Lot) Exhausted() bool
source

Exhausted reports whether a well-formed lot has nothing left. An invalid lot is not exhausted — it is invalid, and conflating the two is how a bad lot gets silently dropped by Pool.Consume's housekeeping.

func Remaining

method on Lot
1func (l Lot) Remaining() int64
source

Remaining is Qty - Consumed: the quantity still on the shelf from this lot.

It does not validate, and it is the only method here that does not. On an ill-formed lot it returns an arbitrary number — Lot{Qty: 1, Consumed: minInt64} wraps. A zero result means "exhausted, or invalid", never "exhausted" alone; use Lot.Exhausted, which does validate. Guard a caller-supplied lot with Lot.Valid before quoting Remaining from it.

func RemainingCost

method on Lot
1func (l Lot) RemainingCost() (int64, error)
source

RemainingCost returns what the units still on the shelf are carried at: Cost - ConsumedCost. It is defined in terms of CostAt so the two can never disagree, which is the same reason Lot.Take is. It is zero exactly when the lot is exhausted.

func Take

method on Lot
1func (l Lot) Take(n int64) (Lot, int64, error)
source

Take consumes n units from the lot, returning the advanced lot and the cost of those units. The receiver is a value, so a failed Take cannot have mutated anything the caller holds — the atomicity the package doc describes is structural at this level and has to be built by hand one level up, in Pool.Consume.

The charge is the difference of Lot.CostAt at the two consumed counts, which makes charges telescope exactly:

Example
1Take(a→b) + Take(b→c) == Take(a→c)   for a <= b <= c

So how a realm slices its shipments cannot change what a lot charges in total, nor what it has charged by any point in its life. The charge is never negative; n must be positive and within Lot.Remaining.

func Valid

method on Lot
1func (l Lot) Valid() bool
source

Valid reports whether the lot is well-formed. A zero Lot is not.

type Method

ident
1type Method uint8
source

Method is the order in which lots are consumed.

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 Pool

struct
 1type Pool struct {
 2	method  Method
 3	maxLots int
 4	lots    []Lot
 5	nextSeq int64
 6
 7	// The counters. Each is monotone non-decreasing and each is maintained
 8	// independently of the lots, which is the whole point: a cross-foot
 9	// compares a counter against a walk of the lots, so a bug in either one
10	// is visible rather than cancelling.
11	recvQty  int64
12	recvCost int64
13	consQty  int64
14	chgCost  int64
15}
source

Pool is the live lots of one stock-keeping unit under one consumption method, plus the four cumulative counters the cross-foots compare against.

It is a pointer type with unexported state because the counters and the lots must move together — a realm holding a mutable view of one without the other could leave the pool stating a cost on hand its own receipts contradict. Pool.Lots therefore returns copies.

Methods on Pool

func ChargedCost

method on Pool
1func (p *Pool) ChargedCost() int64
source

ChargedCost is the cumulative cost charged out by Pool.Consume. This is the figure a realm posts as cost of goods sold, and it is derived: no caller has ever supplied it.

func Consume

method on Pool
1func (p *Pool) Consume(qty int64) (int64, error)
source

Consume ships qty units and returns what they cost.

The cost is derived from the lots the units came out of, in the pool's declared order, and is never a parameter. Lots that the shipment exhausts are dropped, which is cost-neutral because Lot.CostAt is exact at Qty.

It is all-or-nothing in both senses. A shipment larger than Pool.QtyOnHand is ErrInsufficient with the pool untouched — there is no partial fill — and the whole effect is staged on a copy and committed only after the last lot has been taken from successfully, because an error return does not revert a Gno transaction. A pool left half-consumed with its counters unmoved would state a cost on hand its lots contradict, detectable only by a cross-foot.

func ConsumedQty

method on Pool
1func (p *Pool) ConsumedQty() int64
source

ConsumedQty is the cumulative quantity consumed.

func CostOnHand

method on Pool
1func (p *Pool) CostOnHand() (int64, error)
source

CostOnHand walks the live lots and sums what they are carried at: the inventory value a realm reports on its balance sheet. Derived, for the same reason as Pool.QtyOnHand.

It returns an error because a lot that failed validation makes the sum meaningless, and because the running total is overflow-checked. That check is believed unreachable — the sum is bounded by recvCost, whose own accumulation Receive guards — and it is written anyway, because "unreachable" is an argument about code that may be edited and the cost of being wrong here is a negative inventory value.

func CrossFootCost

method on Pool
1func (p *Pool) CrossFootCost() (received, charged, onHand int64, foots bool)
source

CrossFootCost reports the pool's cost identity:

Example
1cost received == cost charged out + cost on hand

The left side and the first right-hand term are cumulative counters; the second is a walk of the live lots. So this compares two independent derivations, and it is the strong one of the pair: it is what proves the WAVG re-anchor loses nothing, that dropping exhausted lots is cost-neutral, and that no shipment charged cost that was never received.

foots is false if the identity fails **or if the walk failed**, and onHand is zero in the second case. A caller that wants to tell those apart calls Pool.CostOnHand directly; a caller that only wants to know whether the sub-ledger is sound does not need to, because neither is sound.

func CrossFootQty

method on Pool
1func (p *Pool) CrossFootQty() (received, consumed, onHand int64, foots bool)
source

CrossFootQty reports the pool's quantity identity:

Example
1quantity received == quantity consumed + quantity on hand

It is the weaker of the pair and is **not implied** by Pool.CrossFootCost, which is why both exist. Costs are derived from quantities, so a quantity that went missing in a way that preserved its own lot's cost arithmetic — a lot dropped while still holding units it had charged nothing for, say — foots perfectly on cost and fails here. A realm's invariant should assert both.

func Lot

method on Pool
1func (p *Pool) Lot(i int) (Lot, bool)
source

Lot returns the i'th live lot in receipt order, and whether i was in range.

func LotCount

method on Pool
1func (p *Pool) LotCount() int
source

LotCount is the number of live lots. Exhausted lots are dropped by Pool.Consume, so this is not the number of receipts; that is Pool.ReceiptCount.

func Lots

method on Pool
1func (p *Pool) Lots() []Lot
source

Lots returns the live lots in receipt order, as copies. Mutating the result does not affect the pool.

func MaxLots

method on Pool
1func (p *Pool) MaxLots() int
source

MaxLots is the pool's own live-lot cap.

func Method

method on Pool
1func (p *Pool) Method() Method
source

Method is the consumption order, fixed at construction. It is deliberately immutable: changing it mid-life would recost nothing already charged but would change what the next shipment costs, which is a restatement a sub-ledger must not be able to make silently. A realm that must switch methods opens a new pool and discloses it.

func QtyOnHand

method on Pool
1func (p *Pool) QtyOnHand() int64
source

QtyOnHand walks the live lots and sums what remains. It is derived from the lots rather than read from a counter, which is what makes Pool.CrossFootQty a comparison of two things rather than a tautology.

It cannot overflow: every lot's remaining quantity is non-negative and their sum is bounded by recvQty, which Pool.Receive refused to let overflow.

func ReceiptCount

method on Pool
1func (p *Pool) ReceiptCount() int64
source

ReceiptCount is how many receipts the pool has taken, including those whose lots are exhausted and dropped, and including WAVG merges.

The clamp is not defensive noise. nextSeq is 1 on a constructed pool, so a zero-value Pool — one a realm declared rather than built with NewPool — would otherwise report **minus one receipt**, an affirmatively wrong number rendered as fact. That is the opposite of the choice Method.String makes twenty lines up, where the zero value deliberately reads "invalid" so a caller who skipped the check prints something that reads as wrong rather than as missing. A count has no such word, so the honest answer for a pool that has taken no receipts is zero. Transacting on that pool is still refused: Pool.Receive and Pool.Consume both return ErrBadMethod.

func Receive

method on Pool
1func (p *Pool) Receive(qty, cost int64) error
source

Receive records a receipt of qty units costing cost in total.

Under FIFO and LIFO it appends a lot, refusing with ErrPoolFull at the pool's cap. Under WAVG it merges into the single live lot, re-anchoring it: the merged lot carries the remaining quantity plus qty, the remaining cost plus cost, and a consumed count of zero. See the package doc on why the re-anchor is forced rather than chosen.

The merge is where WAVG can refuse what the other two methods accept. A merged lot must satisfy the same precondition every lot does, `Cost <= maxInt64/Qty`, and two separately-held lots each satisfying it do not imply their merge does. That returns ErrOverflow and leaves the pool exactly as it was, so the receipt is refused rather than partly applied — but a realm must handle it, because under WAVG it is a property of the *history* and not only of the arguments.

Nothing is committed until every check has passed: on any error the pool is untouched, including its counters.

func ReceivedCost

method on Pool
1func (p *Pool) ReceivedCost() int64
source

ReceivedCost is the cumulative cost received.

func ReceivedQty

method on Pool
1func (p *Pool) ReceivedQty() int64
source

ReceivedQty is the cumulative quantity received.

Dependencies

Imports 1

/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/lots

Imported by indexer

Source Files 2