add Recipe::unit_cost()

This commit is contained in:
Sebastian Kuzminsky 2025-03-18 11:10:15 -06:00
parent 6677f41af2
commit 06e6002327
2 changed files with 53 additions and 16 deletions

View file

@ -8,6 +8,16 @@ pub enum RecipeLoadError {
TomlDeserializeError(#[from] toml::de::Error), TomlDeserializeError(#[from] toml::de::Error),
} }
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum VitaminError {
#[error("not a vitamin")]
NotAVitamin,
#[error("vitamin has bogus inputs")]
InputsError,
#[error("vitamon has no output")]
OutputError,
}
#[derive(Debug, serde::Deserialize, PartialEq)] #[derive(Debug, serde::Deserialize, PartialEq)]
pub struct Input { pub struct Input {
// Quantity defaults to "amount=1" if omitted. // Quantity defaults to "amount=1" if omitted.
@ -85,6 +95,45 @@ impl Recipe {
Ok(recipe) Ok(recipe)
} }
/// Compute the cost (of quantity 1). Currently only work on vitamins.
pub fn unit_cost(&self) -> Result<f32, VitaminError> {
if !self.is_vitamin() {
return Err(VitaminError::NotAVitamin);
}
// Vitamins must have exactly one Input, and it must be Capital.
if self.inputs.len() != 1 {
return Err(VitaminError::InputsError);
}
let capital = match self.inputs.get("capital") {
Some(capital) => capital,
None => return Err(VitaminError::InputsError),
};
if capital.quantity.unit != Some(crate::quantity::Unit::USDollar) {
return Err(VitaminError::InputsError);
}
let total_cost = capital.quantity.amount;
let outputs = match &self.outputs {
Some(outputs) => outputs,
None => return Err(VitaminError::OutputError),
};
// FIXME: For now Vitamins must produce exactly one output.
if outputs.len() != 1 {
return Err(VitaminError::OutputError);
}
let (_output_name, output_info) = outputs.iter().next().unwrap();
let output_quantity = output_info.quantity;
// compute the "unit cost" of this input
return Ok(total_cost / output_quantity.amount);
}
// A "Vitamin" is a recipe whose only input is "capital". // A "Vitamin" is a recipe whose only input is "capital".
pub fn is_vitamin(&self) -> bool { pub fn is_vitamin(&self) -> bool {
if self.inputs.len() != 1 { if self.inputs.len() != 1 {

View file

@ -52,6 +52,8 @@ pub enum RecipeCompileError {
VitaminCapitalHasWrongUnit(String), VitaminCapitalHasWrongUnit(String),
#[error("input produces no output: {0}")] #[error("input produces no output: {0}")]
InputProducesNoOutput(String), InputProducesNoOutput(String),
#[error(transparent)]
VitaminError(#[from] crate::recipe::VitaminError),
} }
impl Repos { impl Repos {
@ -130,17 +132,8 @@ impl Repos {
true => { true => {
let amount_needed = input_info.quantity * quantity; let amount_needed = input_info.quantity * quantity;
// FIXME: for now Vitamins must have exactly one Input, and it must be Capital. let cost_each = input_recipe.unit_cost()?;
assert_eq!(input_recipe.inputs.len(), 1); let total_cost = amount_needed.amount * cost_each;
let input_capital = input_recipe.inputs.get("capital").ok_or_else(|| {
RecipeCompileError::VitaminLacksCapital(input_name.into())
})?;
if input_capital.quantity.unit != Some(crate::quantity::Unit::USDollar) {
return Err(RecipeCompileError::VitaminCapitalHasWrongUnit(
input_name.into(),
));
}
let cost = input_capital.quantity.amount;
let outputs = match &input_recipe.outputs { let outputs = match &input_recipe.outputs {
None => { None => {
@ -155,11 +148,6 @@ impl Repos {
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
let (_output_name, output_info) = outputs.iter().next().unwrap(); let (_output_name, output_info) = outputs.iter().next().unwrap();
let output_quantity = output_info.quantity;
// compute the "unit cost" of this input
let cost_each = cost / output_quantity.amount;
let total_cost = amount_needed.amount * cost_each;
// FIXME: Try to coerce the units here - if we buy // FIXME: Try to coerce the units here - if we buy
// hose in rolls measured in feet, but we use lengths // hose in rolls measured in feet, but we use lengths