///|
pub(all) struct RotationLine {
name : String
required : Quantity
available : Quantity
use_first : Quantity
buy : Quantity
priority : Int
reason : String
} derive(Eq, Debug)
///|
pub fn RotationLine::RotationLine(
name? : String = "",
required? : Quantity = Quantity(),
available? : Quantity = Quantity(),
use_first? : Quantity = Quantity(),
buy? : Quantity = Quantity(),
priority? : Int = 0,
reason? : String = "",
) -> RotationLine {
{ name, required, available, use_first, buy, priority, reason }
}
///|
fn min_quantity(left : Quantity, right : Quantity) -> Quantity {
if !same_measure_kind(left, right) {
return left
}
let left_base = canonical_amount(left)
let right_base = canonical_amount(right)
if left_base <= right_base {
left
} else {
convert_quantity(right, left.unit)
}
}
///|
fn rotation_priority(item : PantryItem) -> Int {
let mut score = 0
if item.expires_in_days <= 0 {
score += 100
} else if item.expires_in_days <= 2 {
score += 70
} else if item.expires_in_days <= 7 {
score += 40
} else if item.expires_in_days <= 14 {
score += 20
}
if item.opened {
score += 15
}
if item.preferred {
score += 5
}
score
}
///|
fn rotation_reason(item : PantryItem) -> String {
if item.expires_in_days <= 0 {
return "expired stock should be removed before cooking"
}
if item.expires_in_days <= 2 {
return "use immediately because it expires within two days"
}
if item.expires_in_days <= 7 && item.opened {
return "opened item expires this week"
}
if item.expires_in_days <= 7 {
return "expires this week"
}
if item.opened {
return "opened pantry item should be rotated before sealed stock"
}
"normal pantry stock"
}
///|
fn recipe_contains(recipe : Recipe, item : PantryItem) -> Bool {
for ingredient in recipe.ingredients {
if same_name(ingredient.name, item.name) {
return true
}
}
false
}
///|
fn matching_pantry_item(
pantry : Array[PantryItem],
ingredient : Ingredient,
) -> PantryItem? {
let mut best : PantryItem? = None
let mut best_score = -1
for item in pantry {
if same_name(item.name, ingredient.name) &&
same_measure_kind(item.quantity, ingredient.quantity) {
let score = rotation_priority(item)
if score > best_score {
best = Some(item)
best_score = score
}
}
}
best
}
///|
pub fn rotation_plan(
recipe : Recipe,
pantry : Array[PantryItem],
) -> Array[RotationLine] {
let lines = Array::new(capacity=recipe.ingredients.length())
for ingredient in recipe.ingredients {
match matching_pantry_item(pantry, ingredient) {
Some(item) => {
let use_first = min_quantity(ingredient.quantity, item.quantity)
let buy = quantity_missing(ingredient.quantity, item.quantity)
lines.push(
RotationLine(
name=ingredient.name,
required=ingredient.quantity,
available=item.quantity,
use_first~,
buy~,
priority=rotation_priority(item),
reason=rotation_reason(item),
),
)
}
None =>
lines.push(
RotationLine(
name=ingredient.name,
required=ingredient.quantity,
available=Quantity(amount=0, unit=ingredient.quantity.unit),
use_first=Quantity(amount=0, unit=ingredient.quantity.unit),
buy=ingredient.quantity,
priority=0,
reason="not available in pantry",
),
)
}
}
lines
}
///|
pub fn rotation_score(lines : Array[RotationLine]) -> Int {
let mut score = 0
for line in lines {
score += line.priority
if line.buy.amount > 0 {
score += 10
}
}
score
}
///|
pub fn urgent_rotation_count(lines : Array[RotationLine]) -> Int {
let mut count = 0
for line in lines {
if line.priority >= 70 {
count += 1
}
}
count
}
///|
pub fn expiring_not_in_recipe(
recipe : Recipe,
pantry : Array[PantryItem],
window_days : Int,
) -> Array[PantryWarning] {
let warnings = Array::new()
for item in pantry {
if item.expires_in_days <= window_days && !recipe_contains(recipe, item) {
warnings.push(
PantryWarning(
code="unused-expiring",
message=item.name + " expires soon but is not used by this recipe.",
severity="info",
),
)
}
}
warnings
}
///|
pub fn rotation_summary(lines : Array[RotationLine]) -> String {
let out = StringBuilder()
out.write_string("rotation-lines=" + lines.length().to_string())
out.write_string(" urgent=" + urgent_rotation_count(lines).to_string())
out.write_string(" score=" + rotation_score(lines).to_string())
for line in lines {
if line.priority >= 70 || line.buy.amount > 0 {
out.write_string(" | ")
out.write_string(line.name)
out.write_string(": use ")
out.write_string(quantity_text(line.use_first))
out.write_string(", buy ")
out.write_string(quantity_text(line.buy))
out.write_string(", ")
out.write_string(line.reason)
}
}
out.to_string()
}