pub struct Inventory { /* private fields */ }Expand description
An inventory is a collection of positions.
It tracks all positions for an account and supports booking operations for adding and reducing positions.
§Examples
use rustledger_core::{Inventory, Position, Amount, Cost, BookingMethod};
use rust_decimal_macros::dec;
let mut inv = Inventory::new();
// Add a simple position
inv.add(Position::simple(Amount::new(dec!(100), "USD")));
assert_eq!(inv.units("USD"), dec!(100));
// Add a position with cost
let cost = Cost::new(dec!(150.00), "USD");
inv.add(Position::with_cost(Amount::new(dec!(10), "AAPL"), cost));
assert_eq!(inv.units("AAPL"), dec!(10));Implementations§
Source§impl Inventory
impl Inventory
Sourcepub fn try_reduce(
&self,
units: &Amount,
cost_spec: Option<&CostSpec>,
method: BookingMethod,
) -> Result<BookingResult, BookingError>
pub fn try_reduce( &self, units: &Amount, cost_spec: Option<&CostSpec>, method: BookingMethod, ) -> Result<BookingResult, BookingError>
Try reducing positions without modifying the inventory.
The read-only preview of Self::reduce: returns exactly what
reduce would return — the same matched lots and cost basis on
success, the same error otherwise — without mutating self.
Implemented as reduce on a clone, so it is equivalent BY
CONSTRUCTION. It previously re-implemented every booking method’s
selection logic in a parallel try_* tree, which drifted from the
mutating path in three places (STRICT ambiguity, NONE shorting, {*}
merge dispatch) — the recurring one-logic-two-paths class (#1648,
#1663, #1686). The clone is cheap: positions is an
imbl::Vector, so cloning is O(1) structural sharing and reduce’s
copy-on-write rebuild touches only the clone. The
try_reduce_predicts_reduce property test pins the equivalence.
§Arguments
units- The units to reduce (negative for selling)cost_spec- Optional cost specification for matching lotsmethod- The booking method to use
§Errors
Exactly the errors Self::reduce would return for the same input.
Sourcepub fn merge_average(&mut self) -> Result<(), OverflowError>
pub fn merge_average(&mut self) -> Result<(), OverflowError>
Collapse every cost-bearing lot of each currency into a single weighted-average-cost lot. Cost-less (cash) positions are left untouched.
This realizes the balance of an AVERAGE-booked account, where all lots of a commodity share one running cost. The journal keeps the real per-lot costs; only this realized view merges them (matching hledger’s pool model). A currency whose lots net to zero is removed; a currency whose lots have mismatched cost currencies is left untouched.
§Errors
OverflowError when a currency’s lots sum outside rust_decimal’s
range. The merged view is a realized balance, so a clamped total would
be rendered as an exact position (#1863).
Sourcepub fn merged_pool_cost(
&self,
units: &Amount,
) -> Result<Option<Amount>, BookingError>
pub fn merged_pool_cost( &self, units: &Amount, ) -> Result<Option<Amount>, BookingError>
The per-unit pool cost {*} would produce here, without producing it.
Ok(None) means there is no pool cost to compare against — cost-less
lots, where {*} degrades to AVERAGE. Errors are the ones the reduction
itself would raise (no matching lots, insufficient units); a caller
checking a precondition should let the reduction report them rather than
pre-empting it, so that one function keeps owning the message.
Exists so BookingEngine::apply can verify a carried {*} against the
cost booking recorded BEFORE the merge mutates anything (#2068).
Source§impl Inventory
impl Inventory
Sourcepub fn positions(&self) -> impl Iterator<Item = &Position> + '_
pub fn positions(&self) -> impl Iterator<Item = &Position> + '_
Iterate over all positions.
Previously returned &[Position]; now returns an iterator
because the underlying storage is a tree-based persistent
vector (imbl::Vector) that doesn’t expose a contiguous slice.
Most callers already iterate — for callers that need
random-access / indexed / .len() slice semantics, see
Self::position_list.
Sourcepub fn position_list(&self) -> Vec<&Position>
pub fn position_list(&self) -> Vec<&Position>
Materialize all positions as a Vec<&Position> for slice-style
access (indexing, .len(), .first(), .is_empty()).
Allocates O(N) pointers per call. Callers that only iterate
once should use Self::positions instead — this is for code
paths that need slice semantics.
Sourcepub fn compact_if_sparse(&mut self)
pub fn compact_if_sparse(&mut self)
Drop tombstones once they outnumber live lots, so slots stay within 2x the real position count and iteration cannot degrade toward “every lot this account ever held”.
Renumbers slots, so it must not run while an undo log is open or while any caller holds a slot index. The engine calls it after a transaction commits, which is the one moment both hold.
Amortized: each compaction is O(slots) but halves them, so the cost per removed lot is constant.
§Panics
Panics in debug builds if an undo log is open.
Sourcepub fn begin_undo(&mut self)
pub fn begin_undo(&mut self)
Begin recording an undo log so a failed transaction can be reverted without having copied this inventory.
apply used to snapshot every touched account with Inventory::clone.
That was written when the backing was imbl::Vector and the clone was
O(1); since #2056 booking’s backing is owned, so it became O(lots) per
touched account per transaction — the largest superlinear term left in
the pipeline, worth 56% of a 20,000-transaction investment run.
A reduction touches one or two lots. Recording those is proportional to what changed instead of to what the account holds.
§Panics
Panics in debug builds if a log is already open — nesting would make “restore to the start” ambiguous.
Sourcepub const fn undo_is_open(&self) -> bool
pub const fn undo_is_open(&self) -> bool
Whether an undo log is currently open.
Sourcepub fn commit_undo(&mut self)
pub fn commit_undo(&mut self)
Discard the log — the transaction committed.
Sourcepub fn rollback_undo(&mut self)
pub fn rollback_undo(&mut self)
Restore this inventory to its state at Self::begin_undo.
Rebuilds the derived caches wholesale rather than unwinding them: this is the failure path, so being obviously right beats being fast.
§Panics
Panics in debug builds if the result differs from a witness copy taken
at begin_undo — that means a mutation path bypassed the log.
Sourcepub fn modify_positions(&mut self, f: impl FnOnce(&mut Vec<Position>))
pub fn modify_positions(&mut self, f: impl FnOnce(&mut Vec<Position>))
Rewrite the positions wholesale, then rebuild every derived cache.
Replaces the old positions_mut, which handed out &mut Vec<Position>
directly. That is no longer possible — the backing is sparse, so the
vector holds Option<Position> alongside a live count, and a caller
writing through it could desync that count with no way to notice.
It also left units_cache and simple_index describing the OLD
contents, which this rebuilds for you.
Pre-1.0 break: the closure sees a dense Vec<Position> with tombstones
already dropped, and whatever it leaves becomes the inventory.
An inventory whose positions are structurally SHARED.
For accumulators that are cloned far more often than they are mutated — BQL’s JOURNAL running balance, which emits one snapshot per output row. Cloning is O(1) and successive snapshots share structure, so N rows cost O(base + sum of deltas) instead of O(N x base) (#1086).
Everything else should use Inventory::new: the default contiguous
backing is what makes booking’s reduce cheap, and reduce converts
to it anyway.
Sourcepub fn units(&self, currency: &str) -> Decimal
pub fn units(&self, currency: &str) -> Decimal
Get total units of a currency (ignoring cost lots).
This sums all positions of the given currency regardless of cost basis. Uses an internal cache for O(1) lookups.
Sourcepub fn add_headroom_for(&self, currency: &str, needed: Decimal) -> bool
pub fn add_headroom_for(&self, currency: &str, needed: Decimal) -> bool
Whether every add of currency totaling at most needed in absolute
value is guaranteed not to overflow.
add overflows at exactly two checked_adds: the per-currency running
total, and — for a cost-less position — the single merged lot that
simple_index points at. Both operands are bounded here against
needed, so a true answer means no sequence of adds whose magnitudes
sum to needed can overflow either, at any intermediate step: every
partial sum is bounded by the total.
Conservative by construction — false only ever means “cannot prove
it”, never “will overflow”. Callers use it to skip work that exists
solely to recover from overflow (#1897).
Sourcepub fn currencies(&self) -> Vec<&str>
pub fn currencies(&self) -> Vec<&str>
Get all currencies in this inventory.
Sourcepub fn is_reduced_by(&self, units: &Amount, scope: ReductionScope) -> bool
pub fn is_reduced_by(&self, units: &Amount, scope: ReductionScope) -> bool
Check if the given units would reduce (not augment) this inventory.
Returns true if there’s a position with the same currency but opposite
sign, meaning these units would reduce the inventory rather than add to it.
When has_cost_spec is true, only positions with a cost basis are
considered for reduction matching. Simple (no-cost) positions are ignored
because they live in a different “cost layer” — a sell-without-cost-spec
that left a negative simple position should not cause a subsequent
cost-bearing augmentation to be misclassified as a reduction.
See: issue #875, beancount#889.
This is used to determine whether a posting is a sale/reduction or a purchase/augmentation.
Sourcepub fn is_booking_reduction(
&self,
units: &Amount,
cost: Option<&CostSpec>,
method: BookingMethod,
) -> bool
pub fn is_booking_reduction( &self, units: &Amount, cost: Option<&CostSpec>, method: BookingMethod, ) -> bool
Whether a posting of units carrying cost would REDUCE this inventory
under method — the single source for the reduction-vs-augmentation
decision shared by the booking engine (BookingEngine::apply) and the
Late validator’s inventory pass.
A posting reduces only when it carries a cost spec (cost.is_some() —
presence of the spec, which includes an empty/unresolved one like {}),
the booking method isn’t NONE (issue #1182 — NONE accumulates every
posting as an augmentation, with no lot matching), and the inventory holds
a cost-bearing position of the opposite sign in the same currency
(Self::is_reduced_by with ReductionScope::CostBearingOnly). This
gate was previously written byte-for-byte in both crates and the #1182 fix
had to be applied twice.
Sourcepub fn book_value(
&self,
units_currency: &str,
) -> Result<FxHashMap<Currency, Decimal>, OverflowError>
pub fn book_value( &self, units_currency: &str, ) -> Result<FxHashMap<Currency, Decimal>, OverflowError>
Get the total book value (cost basis) for a currency.
Returns the sum of all cost bases for positions of the given currency.
§Errors
OverflowError when a position’s book value, or the running
per-currency total, leaves rust_decimal’s range.
Sourcepub fn add(&mut self, position: Position) -> Result<(), OverflowError>
pub fn add(&mut self, position: Position) -> Result<(), OverflowError>
Add a position to the inventory.
For positions without cost, this merges with existing positions
of the same currency using O(1) HashMap lookup.
For positions with cost, this adds as a new lot (O(1)). Lot aggregation for display purposes is handled separately at output time (e.g., in the query result formatter).
§TLA+ Specification
Implements AddAmount action from Conservation.tla:
- Invariant:
inventory + totalReduced = totalAdded - After add:
totalAdded' = totalAdded + amount
See: spec/tla/Conservation.tla
§Errors
OverflowError when the running total for this currency leaves
rust_decimal’s ~±7.9e28 range. The inventory is left UNCHANGED — the
units cache is only committed once the merge is known to fit, so a
caller that reports the error and moves on does not carry a
half-applied position (#1863).
Sourcepub fn reduce(
&mut self,
units: &Amount,
cost_spec: Option<&CostSpec>,
method: BookingMethod,
) -> Result<BookingResult, BookingError>
pub fn reduce( &mut self, units: &Amount, cost_spec: Option<&CostSpec>, method: BookingMethod, ) -> Result<BookingResult, BookingError>
Reduce positions from the inventory using the specified booking method.
§Arguments
units- The units to reduce (negative for selling)cost_spec- Optional cost specification for matching lotsmethod- The booking method to use
§Returns
Returns a BookingResult with the matched positions and cost basis,
or a BookingError if the reduction cannot be performed.
§TLA+ Specification
Implements ReduceAmount action from Conservation.tla:
- Invariant:
inventory + totalReduced = totalAdded - After reduce:
totalReduced' = totalReduced + amount - Precondition:
amount <= inventory(elseInsufficientUnitserror)
Lot selection follows these TLA+ specs based on method:
Fifo:FIFOCorrect.tla- Oldest lots first (selected_date <= all other dates)Lifo:LIFOCorrect.tla- Newest lots first (selected_date >= all other dates)Hifo:HIFOCorrect.tla- Highest cost first (selected_cost >= all other costs)
See: spec/tla/Conservation.tla, spec/tla/FIFOCorrect.tla, etc.
Sourcepub fn merge(&mut self, other: &Self) -> Result<(), OverflowError>
pub fn merge(&mut self, other: &Self) -> Result<(), OverflowError>
Merge this inventory with another.
§Errors
OverflowError when a merged running total leaves rust_decimal’s
range. self keeps the positions merged before the failure.
Sourcepub fn at_cost(&self) -> Result<Self, OverflowError>
pub fn at_cost(&self) -> Result<Self, OverflowError>
Convert inventory to cost basis.
Returns a new inventory where all positions are converted to their cost basis. Positions without cost are returned as-is.
§Errors
OverflowError when a units × cost product, or the running total
of those products, leaves rust_decimal’s range. Note this can fire on
inputs far below the ceiling — the product overflows when neither
operand does.
Sourcepub fn at_units(&self) -> Result<Self, OverflowError>
pub fn at_units(&self) -> Result<Self, OverflowError>
Convert inventory to units only.
Returns a new inventory where all positions have their cost removed, effectively aggregating by currency only.
§Errors
OverflowError when stripping costs merges lots whose combined units
leave rust_decimal’s range.
Source§impl Inventory
impl Inventory
Sourcepub fn try_from_positions<I>(iter: I) -> Result<Self, OverflowError>where
I: IntoIterator<Item = Position>,
pub fn try_from_positions<I>(iter: I) -> Result<Self, OverflowError>where
I: IntoIterator<Item = Position>,
Build an inventory from positions.
Replaces the former FromIterator<Position> impl, which was removed
deliberately: from_iter cannot report failure, so it had to swallow
the overflow from Self::add and hand back an inventory holding a
wrong total with nothing to indicate it (#1863). A collect() that can
silently lie is worse than no collect().
§Errors
OverflowError when a running total leaves rust_decimal’s range.
Trait Implementations§
Source§impl<'de> Deserialize<'de> for Inventory
impl<'de> Deserialize<'de> for Inventory
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for Inventory
Auto Trait Implementations§
impl Freeze for Inventory
impl RefUnwindSafe for Inventory
impl Send for Inventory
impl Sync for Inventory
impl Unpin for Inventory
impl UnsafeUnpin for Inventory
impl UnwindSafe for Inventory
Blanket Implementations§
Source§impl<T> ArchivePointee for T
impl<T> ArchivePointee for T
Source§type ArchivedMetadata = ()
type ArchivedMetadata = ()
Source§fn pointer_metadata(
_: &<T as ArchivePointee>::ArchivedMetadata,
) -> <T as Pointee>::Metadata
fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.Source§impl<T> LayoutRaw for T
impl<T> LayoutRaw for T
Source§fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>
fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>
Source§impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
Source§unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool
unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool
Source§fn resolve_niched(out: Place<NichedOption<T, N1>>)
fn resolve_niched(out: Place<NichedOption<T, N1>>)
out indicating that a T is niched.