Resource allocation where every line item is under budget, on track, or over budget β nothing else.
Traditional budget tracking gives you continuous ratios: 73% spent, 127% spent, 99.8% spent. Your eyes glaze over. You have to squint at numbers to know if things are okay. This crate collapses every line item to one of three states: +1 (under budget), 0 (on track), or -1 (over budget). The tolerance is configurable, and the overall budget health reduces to a single ternary value.
A budget with 47 line items, each showing exact dollar amounts, requires conscious effort to assess. You scan, compare, calculate. A budget where each item shows π’ / π‘ / π΄ can be understood in a glance. Ternary budget tracking is that second thing β structured so you can see the whole picture instantly.
The ternary classification uses a configurable tolerance. With 10% tolerance, an item that's spent 95% of its allocation is "on track" (0). An item at 50% is "under" (+1). An item at 120% is "over" (-1). The distribution() method tells you how many items fall into each bucket β if most are at 0 with a few at +1 and -1, you're healthy.
[dependencies]
ternary-budget = "0.1.0"use ternary_budget::*;
// Track a project budget
let mut budget = BudgetManager::new(500_000.0);
budget.add_item(BudgetItem::new("engineering", 200_000.0, 180_000.0)); // under
budget.add_item(BudgetItem::new("infrastructure", 150_000.0, 148_000.0)); // on track
budget.add_item(BudgetItem::new("marketing", 100_000.0, 115_000.0)); // over
budget.add_item(BudgetItem::new("licensing", 50_000.0, 20_000.0)); // under
// Check individual items
assert_eq!(budget.items["engineering"].status(0.1), 1); // under
assert_eq!(budget.items["infrastructure"].status(0.1), 0); // on track
assert_eq!(budget.items["marketing"].status(0.1), -1); // over
// Overall budget health
assert_eq!(budget.overall_status(0.1), 1); // total spent < total budget
// Distribution: (under, on_track, over)
let (under, on_track, over) = budget.distribution(0.1);
assert_eq!((under, on_track, over), (2, 1, 1));
// Auto-rebalance: transfer surplus to deficit items
let transferred = budget.rebalance();
println!("Reallocated ${:.0} from surplus to deficit", transferred);BudgetManager
βββ total_budget: f64
βββ items: HashMap<String, BudgetItem>
βββ BudgetItem
βββ id: String
βββ allocated: f64
βββ spent: f64
β
βββ status(tolerance) β i8 β {-1, 0, +1}
βββ remaining() β f64
Manager methods:
βββ overall_status(tolerance) β i8
βββ distribution(tolerance) β (under, on_track, over)
βββ rebalance() β f64 (amount transferred)
βββ conservation_error() β f64 (Ξ£allocated - total_budget)
BudgetItem::new(id: &str, allocated: f64, spent: f64) -> BudgetItem
item.status(tolerance: f64) -> i8 // +1, 0, or -1
item.remaining() -> f64 // allocated - spentThe status method computes spent / allocated and classifies:
- +1 if ratio < 1 - tolerance (under budget)
- 0 if ratio is within tolerance of 1.0 (on track)
- -1 if ratio > 1 + tolerance (over budget)
If allocated is 0, the ratio defaults to 1.0 (on track, neither under nor over).
BudgetManager::new(total_budget: f64) -> BudgetManager
manager.add_item(item: BudgetItem)
manager.overall_status(tolerance: f64) -> i8
manager.rebalance() -> f64
manager.distribution(tolerance: f64) -> (usize, usize, usize)
manager.conservation_error() -> f64overall_statusβ same ternary classification, but on the aggregate: total spent / total budget.distributionβ counts items in each ternary state:(under, on_track, over).rebalanceβ calculates total surplus (from under-budget items) and total deficit (from over-budget items). Transfers the minimum of surplus and deficit, distributed equally among deficit items. Returns the amount transferred.conservation_errorβ|Ξ£ allocated_i - total_budget|. Should be 0 if all budget is allocated. Nonzero means there's unallocated or over-allocated budget.
use ternary_budget::*;
// Track story points across a sprint
let mut sprint = BudgetManager::new(80.0); // 80 story points committed
sprint.add_item(BudgetItem::new("auth-service", 13.0, 8.0));
sprint.add_item(BudgetItem::new("api-gateway", 21.0, 20.0));
sprint.add_item(BudgetItem::new("dashboard", 21.0, 25.0)); // over
sprint.add_item(BudgetItem::new("testing", 8.0, 2.0));
sprint.add_item(BudgetItem::new("devops", 13.0, 10.0));
sprint.add_item(BudgetItem::new("bug-fixes", 4.0, 7.0)); // over
// At 10% tolerance, what's the sprint health?
let (under, on_track, over) = sprint.distribution(0.1);
println!("Sprint status: {} on track, {} under, {} over", on_track, under, over);
if sprint.overall_status(0.1) == -1 {
println!("β Sprint is over budget!");
// Rebalance: move points from under-budget to over-budget items
let moved = sprint.rebalance();
println!("Redistributed {:.0} story points", moved);
}rebalance() is the budget's self-healing mechanism:
- Sum the remaining (surplus) from all under-budget items
- Sum the deficit from all over-budget items
- Transfer
min(surplus, deficit)equally to deficit items
This is a simple proportional reallocation. It doesn't account for:
- Priority: Some items shouldn't give up budget (critical path)
- Burn rate: An item at 50% spent with 50% time remaining is on track; an item at 50% spent with 10% time remaining is under
- Dependencies: Shifting budget from one item may block another
These are modeling decisions that belong at a higher layer.
conservation_error() checks whether the sum of all item allocations equals the total budget. In a well-formed budget, this should be exactly 0.0. A positive error means items are over-allocated (Ξ£ > total). A negative error means there's unallocated budget.
This is the ternary analog of a conservation law: what you allocate must equal what you have. The error term tells you how far you've drifted.
Part of the ternary fleet β 200+ crates, 4,300+ tests, one pattern:
- ternary-core β shared traits and Zβ arithmetic
- ternary-grid β spatial grid with ternary cells
- ternary-chemistry β reaction networks with ternary concentrations
- ternary-automata β three-state cellular automata
- Time-aware budgeting: Track burn rate (spent/time_elapsed) not just absolute spent. An item at 90% budget with 90% time remaining is healthy; at 10% time remaining it's critical.
- Forecasting: Project current burn rates forward to predict which items will be over budget by end of period.
- Multi-period budgets: Carry forward surplus/deficit across budget periods.
- Hierarchical budgets: Departments contain projects contain line items. Roll up ternary status through the hierarchy.
| Metric | Value |
|---|---|
| Lines of Rust | 156 |
| Tests | 8 |
| Public types | 2 |
| Public functions | 9 |
MIT