Skip to main content

DisplayContext

Struct DisplayContext 

Source
pub struct DisplayContext { /* private fields */ }
Expand description

Display context for formatting numbers with consistent precision per currency.

Tracks a frequency distribution of decimal places per currency and exposes it via get_precision under the configured Precision policy. Default policy is Precision::MostCommon to match Python bean-query.

Fixed per-currency overrides (from option "display_precision") always win over inferred precision regardless of the policy.

Implementations§

Source§

impl DisplayContext

Source

pub fn new() -> Self

Create a new empty display context.

Source

pub fn update(&mut self, number: Decimal, currency: &str)

Update the display context with a number for a currency.

Records the decimal precision (number of digits after the decimal point) of number against currency’s histogram, so subsequent get_precision calls reflect the new sample under the active Precision policy.

Source

pub fn update_from(&mut self, other: &Self)

Update the display context from another display context.

  • Inferred per-currency distributions: merge histograms (sum counts across both sides). This preserves frequency information so the merged context’s mode reflects the union of samples — strictly more correct than the old “max of maxes” merge, and matches Python display_context.DisplayContext.update_from.
  • Fixed per-currency overrides (option "display_precision"): propagated from other only when self has no fixed override for that currency (so a per-context override stays authoritative).
  • render_commas: enabled if either side has it on (one-way “sticky on” merge — same rationale as before).
  • precision policy: NOT propagated. The policy is a property of the consumer (e.g. BQL renderer vs price-display formatter), not the data, so it stays as set on self.

The fixed-precision and render_commas merging matters when a column context inherits from a ledger context for Value::Number rendering: without it, the ledger’s display options would silently fail to apply to naked-decimal columns. See PR #961 follow-up.

Source

pub fn adopt_grouping_from(&mut self, other: &Self)

Adopt other’s grouping policy wholesale: the ledger-wide flag and every per-commodity override.

Distinct from Self::update_from, which merges precision facts inferred from data and is one-way for grouping. Separator rendering is presentation policy for a whole table, so a derived context — the query writer’s per-column contexts, say — must take it entire rather than reconstruct it. Reconstructing it is exactly how SUM(number) came to disagree with SUM(position) in one query (#1892), and how a commodity’s own declaration came to be dropped (#1896).

Source

pub const fn set_precision(&mut self, precision: Precision)

Set the inference policy for Self::get_precision.

Default is Precision::MostCommon to match Python bean-query. Callers that need to preserve the highest-precision sample (e.g. price-display formatters) can opt into Precision::Maximum.

Source

pub const fn precision(&self) -> Precision

Get the active inference policy.

Source

pub fn currencies(&self) -> impl Iterator<Item = &str>

Iterate the currencies that have observed dp samples or fixed overrides, in deterministic-but-unspecified order.

Skips the __default__ sentinel — that bucket is for naked-decimal columns (BQL Value::Number) and isn’t a “real” currency from the user’s perspective.

Source

pub fn resolved_precisions(&self) -> Vec<(String, u32)>

Export every currency’s RESOLVED precision (fixed override if set, else the inferred precision under the active policy) as a wire-friendly list, sorted by currency. This is what crosses the FFI boundary as ledger-options.display-precision (#1766): the same per-currency answer Self::get_precision would give, so an embedder consuming the list renders with this context’s precision decisions without re-deriving inference (rustfava’s loader keeps a Python re-derivation only as a fallback for engines that predate this field). Skips the __default__ naked-decimal bucket (see Self::currencies).

Source

pub fn histogram(&self, currency: &str) -> Vec<(u32, u32)>

Return the dp histogram for currency as ascending (dp, count) pairs. Empty if the currency has no observed samples.

Useful for diagnostic / debugging tooling (e.g. rledger doctor display-context) that wants to show why a particular precision was chosen.

Source

pub fn precision_under(&self, currency: &str, policy: Precision) -> Option<u32>

Look up the precision that would be returned under a specific policy, without mutating self. Same semantics as Self::get_precision but lets a single context be queried under both policies (e.g. for diagnostic output that compares MostCommon vs Maximum).

Source

pub fn has_fixed_precision(&self, currency: &str) -> bool

True if currency has a fixed-precision override (from option "display_precision" or Self::set_fixed_precision).

Source

pub fn for_surface(&self, surface: OutputSurface) -> Cow<'_, Self>

This context, adjusted for the surface it will be written to.

Only render_commas is affected — precision is a property of the data and is identical on every surface. See OutputSurface for why the distinction exists.

Borrows unless the flag actually has to change, so the common cases cost nothing: a ledger where nothing groups (almost all of them) and any human-facing surface both borrow. Only suppressing separators for a machine or ledger-text surface clones, and that clone carries the per-currency histograms — worth avoiding on a REPL’s hot path, and wasted entirely on the JSON writer, which ignores the context.

The borrow test is Self::renders_any_commas, not the ledger-wide flag: a commodity may declare render_commas: TRUE while the ledger default is off, and borrowing on the strength of the global flag alone would leak that commodity’s separators onto a machine surface.

Source

pub const fn set_render_commas(&mut self, render_commas: bool)

Set the render_commas flag.

Source

pub fn set_render_commas_for(&mut self, currency: &str, render: bool)

Declare whether currency renders thousands separators, overriding the ledger-wide Self::set_render_commas.

Source

pub fn render_commas_for(&self, currency: &str) -> bool

Whether currency renders thousands separators: its own declaration if it has one, else the ledger-wide default.

Numerals with no currency in scope — metadata values, custom directive values — have nothing to look up and take the default.

Source

pub fn renders_any_commas(&self) -> bool

Whether ANY currency renders separators.

Lets a caller skip the per-numeral lookup entirely for the overwhelming majority of ledgers, which declare nothing.

Source

pub const fn render_commas(&self) -> bool

Get the render_commas flag.

Source

pub fn set_fixed_precision(&mut self, currency: &str, precision: u32)

Set a fixed precision for a currency (from option "display_precision").

Fixed precision takes precedence over inferred precision.

Source

pub fn from_directives<'a, I>( directives: I, fixed_precisions: impl IntoIterator<Item = (&'a str, u32)>, render_commas: bool, ) -> Self
where I: IntoIterator<Item = &'a Directive>, I::IntoIter: Clone,

Build a display context from a set of directives plus fixed per-currency overrides. This is THE canonical builder — the loader calls it for every load, and the FFI component’s session.format calls it over the held entries (#1766) — so the sampling rules below stay in one place.

Four stages, in precedence order (later wins):

  1. Scan every directive’s amounts to infer per-currency dp distributions (posting units, cost specs, price annotations, balance amounts + tolerances, price directives).

  2. Apply fixed_precisions (from option "display_precision").

  3. Apply per-commodity precision: N metadata (issue #991), AFTER the options so a commodity-level declaration wins over the global option. Multi-declaration of the same currency is last-wins (matches typical option-stacking semantics). Invalid values are silently skipped here — rustledger-validate surfaces them as InvalidPrecisionMetadata warnings (E5003) so users see the problem without breaking loading.

  4. Apply render_commas, the ledger-wide grouping flag, plus the per-commodity render_commas: TRUE|FALSE declarations picked up in the same walk as stage 3. Grouping resolves by the same tiers as precision — see Self::render_commas_for.

The iterator must be cheaply cloneable (e.g. a slice iter or a map over one) because the directives are walked twice (amount scan, then commodity metadata).

render_commas is a PARAMETER rather than something callers apply afterwards with Self::set_render_commas. It used to be the latter, and both production callers carried their own copy of from_directives(..) + set_render_commas(..); deleting the second line from the FFI copy passed the entire test suite. Requiring it here makes that omission a compile error (#1902 Phase 2).

Self::set_render_commas remains for contexts built some other way — a derived or per-column context, which is not a ledger load.

Source

pub fn get_precision(&self, currency: &str) -> Option<u32>

Get the precision for a currency.

Returns the fixed precision if set; otherwise looks up the inferred precision under the active Precision policy (MostCommon by default — the mode of the observed distribution; or Maximum — the highest observed dp). Returns None if the currency has never been seen.

Source

pub fn default_precision(&self) -> u32

Get the default precision used when formatting a Decimal that has no associated currency (e.g. the result of SUM(number) in BQL).

Resolution order (matches the BQL renderer’s expectations after PR #986):

  1. __default__ bucket — if any naked-decimal observations have been recorded via update(n, DEFAULT_CURRENCY), the bucket’s effective precision wins. This is what BQL populates for Value::Number columns (matches Python bean-query’s per-column DecimalRenderer).
  2. Max effective precision across every other currency — fallback when no naked-decimal observations exist. Covers issue #954: a column of Value::Number(0) that came from an aggregate collapsing to literal zero still renders with the column’s expected dp (e.g. 0.00 for a USD-only file).
  3. Returns 0 if no currencies have been recorded at all.

“Effective” precision means per-currency fixed overrides inferred (same rule as Self::get_precision) and respects the active Precision policy, so a fixed display_precision of 2 for USD won’t be overridden by an inferred 4-digit value.

Source

pub fn quantize(&self, number: Decimal, currency: &str) -> Decimal

Quantize a number to the tracked precision for a currency.

Mirrors Python’s Decimal.quantize: the result has exactly the target scale — rounding when the input has more dp, padding with trailing zeros when the input has fewer. This matches what bean-query’s AmountRenderer does: it quantizes via the ledger dctx before populating the column dctx, so the column dctx sees uniformly-padded values.

If the currency has no tracked precision, returns the number unchanged.

Pre-fix this used round_dp(dp), which only ROUNDS down — it never PADS up. That meant a 2dp input under a 4dp target stayed 2dp, the column dctx saw dp=2, and the output rendered 2dp instead of bean-query’s 4dp.

Source

pub fn format(&self, number: Decimal, currency: &str) -> String

Format a decimal number for a currency using the tracked precision.

Render rules (matching bean-query’s AmountRenderer.format):

  • If the value’s intrinsic scale exceeds the currency’s tracked precision, render at the value’s scale. Python’s decimal carries scale through arithmetic and bean-query preserves it, so a SUM(number) GROUP BY currency that aggregates a -805.50896 row and a -396.50000 row renders as -1202.00896 (scale=5), not -1202.01 (rounded to USD’s 2dp).
  • If the value’s scale is less than the tracked precision, pad with trailing zeros (7.5 USD7.50). Preserves the #954 fix that stops SUM(0.00) = 0 rendering as plain 0.
  • If the currency has no tracked precision, fall through to the value’s natural rendering with trailing zeros stripped.

The previous implementation always quantized to the tracked precision via round_dp(dp). That was correct for under-scale padding but wrong for over-scale truncation — it lost arithmetic precision that bean-query preserved (closes #1103).

Source

pub fn format_plain(&self, number: Decimal, currency: &str) -> String

Ledger-text variant of Self::format (#1766): pads a TRACKED currency’s value to the tracked precision (never rounding an over-precise value away), and returns an UNTRACKED currency’s value at its own scale, byte-faithful — no normalize() trailing-zero stripping, which would silently widen a balance assertion’s implicit tolerance. Never emits thousands separators regardless of render_commas: canonical ledger text carries none (separators stay a report/query display concern).

Source

pub fn format_amount(&self, number: Decimal, currency: &str) -> String

Format an amount (number + currency) using the tracked precision.

Unlike Self::format (which preserves over-scale arithmetic precision to match Python bean-query’s DecimalRenderer for scalar Value::Number results), this method always quantizes to the currency’s tracked dp — matching bean-query’s AmountRenderer for Amounts, Positions, and Inventory entries.

Python uses two distinct renderers for the two semantic kinds of output:

  • DecimalRenderer for naked decimals (preserves scale, since Python decimal carries scale through arithmetic).
  • AmountRenderer for amount-typed values (uses the ledger’s display context per-currency dp, which is the user-facing “how many decimal places does this currency render at” setting).

Rust used to conflate the two through a single format call, which is why #1103’s fix (preserving scale in format) inadvertently regressed the BQL compat suite by ~7pp on queries that produce Value::Inventory — the position amounts inside the inventory now render with raw arithmetic scale instead of the currency’s display dp. See #1112 for the regression analysis.

Source

pub fn format_amount_number(&self, number: Decimal, currency: &str) -> String

Format the number portion of an Amount/Position (no currency suffix), quantized to the tracked dp.

Used by the BQL numberify rendering path that strips the currency from positions/inventories — same semantics as Self::format_amount but without the trailing <CURRENCY>.

Source

pub fn format_default(&self, number: Decimal) -> String

Format a Decimal that has no associated currency.

Used by the BQL query renderer for Value::Number results — bare Decimals produced by aggregates like SUM(number) or columns like cost_number.

Matches Python bean-query’s DecimalRenderer.format, which uses the value’s natural string representation (preserving the scale baked into the Decimal) without imposing uniform precision across rows. So Value::Number(Decimal('0.00')) renders 0.00 (scale survives — covers issue #954) while Value::Number(0) renders 0 (no artificial padding).

When the value has scale 0 (no fractional part) but the context has a __default__-bucket precision, we DO pad up to that precision — this is the issue #954 path: an aggregate that collapsed to literal zero (scale lost) still gets rendered with the column’s expected dp.

Trait Implementations§

Source§

impl Clone for DisplayContext

Source§

fn clone(&self) -> DisplayContext

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 DisplayContext

Source§

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

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

impl Default for DisplayContext

Source§

fn default() -> DisplayContext

Returns the “default value” for a type. 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> 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, 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.