Skip to main content

Inventory

Struct Inventory 

Source
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

Source

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 lots
  • method - The booking method to use
§Errors

Exactly the errors Self::reduce would return for the same input.

Source

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).

Source

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

Source

pub fn new() -> Self

Create an empty inventory.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub const fn undo_is_open(&self) -> bool

Whether an undo log is currently open.

Source

pub fn commit_undo(&mut self)

Discard the log — the transaction committed.

Source

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.

Source

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.

Source

pub fn new_shared() -> Self

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.

Source

pub fn is_empty(&self) -> bool

Check if inventory is empty.

Source

pub fn len(&self) -> usize

Get the number of positions (including empty ones).

Source

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.

Source

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).

Source

pub fn currencies(&self) -> Vec<&str>

Get all currencies in this inventory.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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 lots
  • method - 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 (else InsufficientUnits error)

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.

Source

pub fn compact(&mut self)

Remove all empty positions.

Source

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.

Source

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.

Source

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

Source

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 Clone for Inventory

Source§

fn clone(&self) -> Inventory

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Inventory

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Inventory

Source§

fn default() -> Inventory

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for Inventory

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Display for Inventory

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Eq for Inventory

Source§

impl PartialEq for Inventory

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for Inventory

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> ArchivePointee for T

Source§

type ArchivedMetadata = ()

The archived version of the pointer metadata for this type.
Source§

fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata

Converts some archived metadata to the pointer metadata for itself.
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> LayoutRaw for T

Source§

fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>

Returns the layout of the type.
Source§

impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
where T: SharedNiching<N1, N2>, N1: Niching<T>, N2: Niching<T>,

Source§

unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool

Returns whether the given value has been niched. Read more
Source§

fn resolve_niched(out: Place<NichedOption<T, N1>>)

Writes data to out indicating that a T is niched.
Source§

impl<T> Pointee for T

Source§

type Metadata = ()

The metadata type for pointers and references to this type.
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.