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
impl DisplayContext
Sourcepub fn update(&mut self, number: Decimal, currency: &str)
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.
Sourcepub fn update_from(&mut self, other: &Self)
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 fromotheronly whenselfhas 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).precisionpolicy: 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 onself.
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.
Sourcepub fn adopt_grouping_from(&mut self, other: &Self)
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).
Sourcepub const fn set_precision(&mut self, precision: Precision)
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.
Sourcepub fn currencies(&self) -> impl Iterator<Item = &str>
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.
Sourcepub fn resolved_precisions(&self) -> Vec<(String, u32)>
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).
Sourcepub fn histogram(&self, currency: &str) -> Vec<(u32, u32)>
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.
Sourcepub fn precision_under(&self, currency: &str, policy: Precision) -> Option<u32>
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).
Sourcepub fn has_fixed_precision(&self, currency: &str) -> bool
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).
Sourcepub fn for_surface(&self, surface: OutputSurface) -> Cow<'_, Self>
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.
Sourcepub const fn set_render_commas(&mut self, render_commas: bool)
pub const fn set_render_commas(&mut self, render_commas: bool)
Set the render_commas flag.
Sourcepub fn set_render_commas_for(&mut self, currency: &str, render: bool)
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.
Sourcepub fn render_commas_for(&self, currency: &str) -> bool
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.
Sourcepub fn renders_any_commas(&self) -> bool
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.
Sourcepub const fn render_commas(&self) -> bool
pub const fn render_commas(&self) -> bool
Get the render_commas flag.
Sourcepub fn set_fixed_precision(&mut self, currency: &str, precision: u32)
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.
Sourcepub fn from_directives<'a, I>(
directives: I,
fixed_precisions: impl IntoIterator<Item = (&'a str, u32)>,
render_commas: bool,
) -> Self
pub fn from_directives<'a, I>( directives: I, fixed_precisions: impl IntoIterator<Item = (&'a str, u32)>, render_commas: bool, ) -> Self
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):
-
Scan every directive’s amounts to infer per-currency dp distributions (posting units, cost specs, price annotations, balance amounts + tolerances, price directives).
-
Apply
fixed_precisions(fromoption "display_precision"). -
Apply per-commodity
precision: Nmetadata (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-validatesurfaces them asInvalidPrecisionMetadatawarnings (E5003) so users see the problem without breaking loading. -
Apply
render_commas, the ledger-wide grouping flag, plus the per-commodityrender_commas: TRUE|FALSEdeclarations picked up in the same walk as stage 3. Grouping resolves by the same tiers as precision — seeSelf::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.
Sourcepub fn get_precision(&self, currency: &str) -> Option<u32>
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.
Sourcepub fn default_precision(&self) -> u32
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):
__default__bucket — if any naked-decimal observations have been recorded viaupdate(n, DEFAULT_CURRENCY), the bucket’s effective precision wins. This is what BQL populates forValue::Numbercolumns (matches Pythonbean-query’s per-columnDecimalRenderer).- 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.00for a USD-only file). - 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.
Sourcepub fn quantize(&self, number: Decimal, currency: &str) -> Decimal
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.
Sourcepub fn format(&self, number: Decimal, currency: &str) -> String
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
decimalcarries scale through arithmetic and bean-query preserves it, so aSUM(number) GROUP BY currencythat aggregates a-805.50896row and a-396.50000row 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 USD→7.50). Preserves the #954 fix that stopsSUM(0.00) = 0rendering as plain0. - 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).
Sourcepub fn format_plain(&self, number: Decimal, currency: &str) -> String
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).
Sourcepub fn format_amount(&self, number: Decimal, currency: &str) -> String
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:
DecimalRendererfor naked decimals (preserves scale, since Pythondecimalcarries scale through arithmetic).AmountRendererfor 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.
Sourcepub fn format_amount_number(&self, number: Decimal, currency: &str) -> String
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>.
Sourcepub fn format_default(&self, number: Decimal) -> String
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
impl Clone for DisplayContext
Source§fn clone(&self) -> DisplayContext
fn clone(&self) -> DisplayContext
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for DisplayContext
impl Debug for DisplayContext
Source§impl Default for DisplayContext
impl Default for DisplayContext
Source§fn default() -> DisplayContext
fn default() -> DisplayContext
Auto Trait Implementations§
impl Freeze for DisplayContext
impl RefUnwindSafe for DisplayContext
impl Send for DisplayContext
impl Sync for DisplayContext
impl Unpin for DisplayContext
impl UnsafeUnpin for DisplayContext
impl UnwindSafe for DisplayContext
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,
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.