///|
let quantity_limit : Int64 = 1000000000000L
///|
fn valid_quantity(q : Int64) -> Bool {
q >= 0L && q <= quantity_limit
}
///|
fn lot_valid(lot : LotRule) -> Bool {
match lot {
LotForLot => true
Multiple(m) => m > 0L && m <= 1000000000L
MinimumMultiple(minimum, m) =>
minimum > 0L && minimum <= 1000000000L && m > 0L && m <= 1000000000L
}
}
///|
/// Exact integer lot rounding; input and intermediate values are bounded.
pub fn order_quantity(
net : Int64,
lot : LotRule,
) -> Result[Int64, PlanningError] {
if !valid_quantity(net) || !lot_valid(lot) {
return Err(InvalidItem("lot or net quantity"))
}
if net == 0L {
return Ok(0L)
}
let (minimum, multiple) = match lot {
LotForLot => (0L, 1L)
Multiple(m) => (0L, m)
MinimumMultiple(n, m) => (n, m)
}
let target = if net > minimum { net } else { minimum }
let count = target / multiple +
(if target % multiple == 0L { 0L } else { 1L })
let rounded = count * multiple
if rounded > quantity_limit {
Err(QuantityLimit("lot rounding"))
} else {
Ok(rounded)
}
}
///|
fn validate_movements(
movements : Array[Movement],
index : Map[String, Int],
horizon : Int,
) -> Result[Unit, PlanningError] {
let ids : Map[String, Bool] = Map([])
for m in movements {
if m.id == "" ||
m.bucket < 0 ||
m.bucket >= horizon ||
m.quantity <= 0L ||
!valid_quantity(m.quantity) {
return Err(InvalidMovement(m.id))
}
if !index.contains(m.item) {
return Err(UnknownItem(m.item))
}
if ids.contains(m.id) {
return Err(DuplicateMovement(m.id))
}
ids[m.id] = true
}
Ok(())
}
///| Validate the full graph, net parents before children, and peg each gross
///| requirement. A release before bucket zero rejects the entire calculation;
///|
/// no partial feasible plan is returned and no input is mutated.
pub fn plan(request : Request) -> Result[Plan, PlanningError] {
let h = request.horizon
if h < 1 || h > 128 {
return Err(InvalidHorizon)
}
let n = request.items.length()
if n == 0 ||
n > 64 ||
request.bom.length() > 4096 ||
request.demands.length() > 10000 ||
request.receipts.length() > 10000 {
return Err(SizeLimit)
}
let items = request.items.copy()
items.sort_by(fn(a, b) { a.sku.lexical_compare(b.sku) })
let index : Map[String, Int] = Map([])
for i, item in items {
if item.sku == "" ||
!valid_quantity(item.on_hand) ||
!valid_quantity(item.safety_stock) ||
item.lead_time < 0 ||
item.lead_time > 127 ||
!lot_valid(item.lot) {
return Err(InvalidItem(item.sku))
}
if index.contains(item.sku) {
return Err(DuplicateItem(item.sku))
}
index[item.sku] = i
}
match validate_movements(request.demands, index, h) {
Err(e) => return Err(e)
Ok(_) => ()
}
match validate_movements(request.receipts, index, h) {
Err(e) => return Err(e)
Ok(_) => ()
}
let bom = request.bom.copy()
bom.sort_by(fn(a, b) {
if a.parent == b.parent {
a.child.lexical_compare(b.child)
} else {
a.parent.lexical_compare(b.parent)
}
})
let indegree = Array::make(n, 0)
let children : Array[Array[(Int, Int64)]] = Array::makei(n, fn(_) { [] })
for i, edge in bom {
if !index.contains(edge.parent) {
return Err(UnknownItem(edge.parent))
}
if !index.contains(edge.child) {
return Err(UnknownItem(edge.child))
}
if edge.parent == edge.child ||
edge.units_per <= 0L ||
edge.units_per > 1000000000L {
return Err(InvalidComponent(edge.parent, edge.child))
}
if i > 0 &&
bom[i - 1].parent == edge.parent &&
bom[i - 1].child == edge.child {
return Err(DuplicateComponent(edge.parent, edge.child))
}
let p = index[edge.parent]
let c = index[edge.child]
children[p].push((c, edge.units_per))
indegree[c] += 1
}
let visited = Array::make(n, false)
let order : Array[Int] = []
for _ in 0.. quantity_limit {
return Err(QuantityLimit(d.item))
}
gross[i][d.bucket] = total
pegs.push({
item: d.item,
bucket: d.bucket,
quantity: d.quantity,
source: Demand(d.id),
})
}
for r in request.receipts {
let i = index[r.item]
let total = scheduled[i][r.bucket] + r.quantity
if total > quantity_limit {
return Err(QuantityLimit(r.item))
}
scheduled[i][r.bucket] = total
}
let plans : Array[ItemPlan] = []
let orders : Array[Order] = []
for i in order {
let item = items[i]
let releases = Array::make(h, 0L)
let buckets : Array[Bucket] = []
let mut available = item.on_hand
for t in 0.. quantity_limit || needed > quantity_limit {
return Err(QuantityLimit(item.sku))
}
let net = if needed > available { needed - available } else { 0L }
let replenishment = match order_quantity(net, item.lot) {
Ok(q) => q
Err(_) => return Err(QuantityLimit(item.sku))
}
if replenishment > 0L {
let release = t - item.lead_time
if release < 0 {
return Err(BeforeHorizon(item.sku, t, release))
}
releases[release] = replenishment
orders.push({
item: item.sku,
receipt_bucket: t,
release_bucket: release,
quantity: replenishment,
})
for (child, units) in children[i] {
if replenishment > quantity_limit / units {
return Err(QuantityLimit(items[child].sku))
}
let q = replenishment * units
let total = gross[child][release] + q
if total > quantity_limit {
return Err(QuantityLimit(items[child].sku))
}
gross[child][release] = total
pegs.push({
item: items[child].sku,
bucket: release,
quantity: q,
source: ParentOrder(item.sku, t),
})
}
}
// available+replenishment can be at most 2*quantity_limit, well within Int64.
available = available + replenishment - gross[i][t]
if available > quantity_limit {
return Err(QuantityLimit(item.sku))
}
buckets.push({
index: t,
gross: gross[i][t],
scheduled: scheduled[i][t],
net,
planned_receipt: replenishment,
planned_release: 0L,
projected: available,
})
}
let final_buckets = buckets.map(fn(b) {
{ ..b, planned_release: releases[b.index], }
})
plans.push({ item: item.sku, buckets: final_buckets, })
}
plans.sort_by(fn(a, b) { a.item.lexical_compare(b.item) })
orders.sort_by(fn(a, b) {
if a.item == b.item {
a.receipt_bucket.compare(b.receipt_bucket)
} else {
a.item.lexical_compare(b.item)
}
})
pegs.sort_by(fn(a, b) {
if a.item != b.item {
a.item.lexical_compare(b.item)
} else if a.bucket != b.bucket {
a.bucket.compare(b.bucket)
} else {
match (a.source, b.source) {
(Demand(x), Demand(y)) => x.lexical_compare(y)
(Demand(_), ParentOrder(_, _)) => -1
(ParentOrder(_, _), Demand(_)) => 1
(ParentOrder(x, tx), ParentOrder(y, ty)) =>
if x == y {
tx.compare(ty)
} else {
x.lexical_compare(y)
}
}
}
})
Ok({ horizon: h, items: plans, orders, pegs, })
}