Skip to content

SuperInstance/ternary-budget

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

11 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ternary-budget

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.

The Insight

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.

Quick Start

[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);

Architecture

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)

API Reference

BudgetItem

BudgetItem::new(id: &str, allocated: f64, spent: f64) -> BudgetItem
item.status(tolerance: f64) -> i8     // +1, 0, or -1
item.remaining() -> f64               // allocated - spent

The 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

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() -> f64
  • overall_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.

Real-World Example: Sprint 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);
}

Rebalancing

rebalance() is the budget's self-healing mechanism:

  1. Sum the remaining (surplus) from all under-budget items
  2. Sum the deficit from all over-budget items
  3. 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

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.

Ecosystem

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

Open Questions

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

Stats

Metric Value
Lines of Rust 156
Tests 8
Public types 2
Public functions 9

License

MIT

About

Ternary budget: resource allocation where items are {-1=over, 0=on-track, +1=under} budget

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages