pantrycalc

    MoonBit-native recipe scaling, allergen-safe substitution and pantry shopping planner

    recipe
    pantry
    nutrition
    allergen
    shopping
    Download zip
    Author
    Version
    0.1.4
    License
    MIT
    Last updated
    2 months ago
    Downloads
    57

    No dependencies

    #PantryCalc

    PantryCalc 是一个 MoonBit 原生的食谱与家庭库存规划库。它把食谱份数缩放、单位换算、过敏原替代、库存缺口、购物清单、营养/成本估算和临期库存轮转放在同一个可测试模型里,适合做食谱 App、家庭备餐工具、餐饮小店配料表、教学示例或 MoonBit 生态中的领域库。

    #重要边界

    本次申报项目为 PantryCalc,Mooncakes 包名为 WB-ai-nb/pantrycalc。它不是评审反馈解析工具,不生成初审补正证明,不做新颖性矩阵,不审查 README/CI,也不替代报名材料审核。

    PantryCalc 的功能边界固定在“食谱/库存/购物规划”赛道:核心数据结构、算法、示例、测试和文档均围绕烹饪库存规划展开。公开仓库、Mooncakes 包、README、API 和示例统一使用 WB-ai-nb/pantrycalc。

    #差异化亮点

    • 把份数缩放、单位换算、过敏原替代、库存缺口和购物清单放在同一套领域模型里,避免用户在多个工具之间手动拼结果。
    • 不只判断“能不能做这道菜”,还会根据开封、临期、过敏原安全、购物成本和蛋白质目标给出零浪费备餐排序。
    • 通过 rotation_plan 与 zero_waste_meal_prep 把家庭库存中的临期食材转化为可测试的优先级,而不是只输出静态食谱。
    • 内置 40 条手写样例食材和 1440 条结构化扩展食材记录,用来验证库在较大数据量下的筛选、汇总和导出路径。

    #功能

    • 食谱份数缩放:按目标份数缩放食材数量、营养和成本。
    • 单位换算:支持 g/kg、ml/l、tsp/tbsp/cup、piece/slice/pinch,并阻止质量、体积、计数混算。
    • 过敏原约束:根据 DietProfile 检测 sesame、soy、milk、egg、peanut 等过敏原。
    • 替代食材:把芝麻酱替换为 sunflower butter,把酱油替换为 coconut aminos 等,保持口感意图并移除风险过敏原。
    • 库存缺口:比较食谱需求和 PantryItem 库存,输出需要购买的数量和成本估算。
    • 营养/成本汇总:输出 kcal、protein、cost 和可读摘要。
    • 临期库存轮转:根据保质期、开封状态和偏好标记,给出优先使用和额外提醒。
    • 零浪费周备餐:在多个候选食谱之间按临期库存利用率、购物成本、过敏原安全和蛋白质目标排序,推荐更少浪费的一餐。
    • Markdown/CSV 导出:生成食谱卡片、计划报告和购物清单 CSV。
    • 扩展食材库:内置 40 条人工样例和 1440 条结构化扩展食材记录,便于测试筛选和示例生成。

    #安装

    moon add WB-ai-nb/pantrycalc

    Mooncakes 目标包:

    WB-ai-nb/pantrycalc@0.1.4

    #最小示例

    test {
    let plan = @pantrycalc.example_plan()
    inspect(plan.verdict, content="shopping-needed")
    inspect(plan.allergen_hit_count, content="0")
    }

    #运行示例

    moon run src/cmd/main moon run src/examples/basic

    moon run src/cmd/main 的输出会包含:

    PantryCalc ready package=WB-ai-nb/pantrycalc pantrycalc=shopping-needed rotation-lines= mealprep=zero-waste-priority

    #核心 API

    • Quantity / MeasureUnit / MeasureKind:计量单位模型。
    • canonical_amount / convert_quantity / quantity_missing:单位换算和缺口计算。
    • Nutrition / Ingredient / Recipe:食谱、食材和营养模型。
    • scale_recipe / total_cost / total_nutrition:份数缩放和汇总。
    • DietProfile / Substitution:饮食约束和替代规则。
    • adapt_recipe / allergen_hit_count:过敏原规避。
    • shopping_list / shopping_cost:库存缺口和购物清单。
    • plan_recipe / PantryPlan::summary / PantryPlan::to_markdown:一站式计划生成和报告。
    • rotation_plan / rotation_summary / expiring_not_in_recipe:临期库存轮转。
    • zero_waste_meal_prep / meal_prep_rankings / MealPrepPlan::summary:零浪费周备餐推荐。
    • food_catalog / extended_food_catalog:可复用食材数据。
    • recipe_card / shopping_csv:文档和 CSV 输出。

    #本地验证

    moon version --all moon check --deny-warn moon check --target all --deny-warn moon build moon test --deny-warn moon fmt --check moon info moon doc moon run src/cmd/main moon run src/examples/basic moon package --list

    当前本地验证结果:

    • moon check --deny-warn:通过
    • moon check --target all --deny-warn:通过
    • moon test --deny-warn:18 passed, 0 failed
    • moon doc:通过
    • 有效 MoonBit 代码行数:4541
    • MoonBit 为主要实现语言
    • CI 配置:.github/workflows/ci.yml
    • 开源许可证:MIT

    #项目结构

    • src/model.mbt:核心数据结构。
    • src/quantity.mbt:单位换算、缩放、营养和成本汇总。
    • src/planner.mbt:过敏原检测、替代食材、购物清单和计划评分。
    • src/rotation.mbt:临期库存轮转和未使用临期库存提醒。
    • src/mealprep.mbt:零浪费周备餐排序和推荐。
    • src/report.mbt:Markdown、CSV 和摘要导出。
    • src/sample.mbt:样例食谱、库存、替代规则和基础食材库。
    • src/extended_catalog.mbt:1440 条结构化扩展食材数据。
    • src/pantrycalc_test.mbt / src/pantrycalc_wbtest.mbt:黑盒和白盒测试。
    • src/cmd/main:CLI smoke 示例。
    • src/examples/basic:完整运行示例。
    • docs/:API、设计、测试、发布和非重合说明。

    #开源与第三方说明

    PantryCalc 使用 MIT License。仓库未引入第三方源码文件、图片、音频、字体、私有数据或来源不明素材。食材样例和扩展食材数据为项目内原创的结构化测试数据,仅用于软件示例和算法验证,不构成医学、营养或食品安全建议。

    DietProfile

    pub(all) struct DietProfile {
    avoid_allergens : Array[String]
    prefer_tags : Array[String]
    dislike_tags : Array[String]
    max_total_cost_cents : Int
    max_total_kcal : Int
    min_total_protein_g : Int
    } derive(Eq,
    Debug
    )

    DietProfile::DietProfile

    fn DietProfile::DietProfile(avoid_allergens? : Array[String], prefer_tags? : Array[String], dislike_tags? : Array[String], max_total_cost_cents? : Int, max_total_kcal? : Int, min_total_protein_g? : Int) -> DietProfile

    Ingredient

    pub(all) struct Ingredient {
    name : String
    group : String
    quantity : Quantity
    nutrition : Nutrition
    cost_cents : Int
    allergens : Array[String]
    tags : Array[String]
    } derive(Eq,
    Debug
    )

    Ingredient::Ingredient

    fn Ingredient::Ingredient(name? : String, group? : String, quantity? : Quantity, nutrition? : Nutrition, cost_cents? : Int, allergens? : Array[String], tags? : Array[String]) -> Ingredient

    MealPrepOption

    pub(all) struct MealPrepOption {
    recipe_title : String
    servings : Int
    verdict : String
    plan_score : Int
    rotation_score : Int
    expiring_used_count : Int
    expiring_unused_count : Int
    shopping_cost_cents : Int
    total_protein_g : Int
    score : Int
    reason : String
    } derive(Eq,
    Debug
    )

    MealPrepOption::MealPrepOption

    fn MealPrepOption::MealPrepOption(recipe_title? : String, servings? : Int, verdict? : String, plan_score? : Int, rotation_score? : Int, expiring_used_count? : Int, expiring_unused_count? : Int, shopping_cost_cents? : Int, total_protein_g? : Int, score? : Int, reason? : String) -> MealPrepOption

    MealPrepPlan

    pub(all) struct MealPrepPlan {
    days : Int
    window_days : Int
    options : Array[MealPrepOption]
    best : MealPrepOption
    verdict : String
    score : Int
    } derive(Eq,
    Debug
    )

    MealPrepPlan::MealPrepPlan

    fn MealPrepPlan::MealPrepPlan(days? : Int, window_days? : Int, options? : Array[MealPrepOption], best? : MealPrepOption, verdict? : String, score? : Int) -> MealPrepPlan

    MealPrepPlan::summary

    fn MealPrepPlan::summary(self : MealPrepPlan) -> String

    MealPrepPlan::to_markdown

    fn MealPrepPlan::to_markdown(self : MealPrepPlan) -> String

    MeasureKind

    pub(all) enum MeasureKind {
    Mass
    Volume
    Count
    } derive(Eq,
    Debug
    )

    MeasureUnit

    pub(all) enum MeasureUnit {
    Gram
    Kilogram
    Milliliter
    Liter
    Teaspoon
    Tablespoon
    Cup
    Piece
    Slice
    Pinch
    } derive(Eq,
    Debug
    )

    Nutrition

    pub(all) struct Nutrition {
    kcal : Int
    protein_g : Int
    fat_g : Int
    carb_g : Int
    fiber_g : Int
    sodium_mg : Int
    } derive(Eq,
    Debug
    )

    Nutrition::Nutrition

    fn Nutrition::Nutrition(kcal? : Int, protein_g? : Int, fat_g? : Int, carb_g? : Int, fiber_g? : Int, sodium_mg? : Int) -> Nutrition

    PantryItem

    pub(all) struct PantryItem {
    name : String
    quantity : Quantity
    expires_in_days : Int
    opened : Bool
    preferred : Bool
    } derive(Eq,
    Debug
    )

    PantryItem::PantryItem

    fn PantryItem::PantryItem(name? : String, quantity? : Quantity, expires_in_days? : Int, opened? : Bool, preferred? : Bool) -> PantryItem

    PantryPlan

    pub(all) struct PantryPlan {
    verdict : String
    recipe_title : String
    servings : Int
    total_cost_cents : Int
    total_kcal : Int
    total_protein_g : Int
    allergen_hit_count : Int
    missing_count : Int
    substitution_count : Int
    shopping : Array[ShoppingLine]
    substitutions : Array[Substitution]
    warnings : Array[PantryWarning]
    } derive(Eq,
    Debug
    )

    PantryPlan::PantryPlan

    fn PantryPlan::PantryPlan(verdict? : String, recipe_title? : String, servings? : Int, total_cost_cents? : Int, total_kcal? : Int, total_protein_g? : Int, allergen_hit_count? : Int, missing_count? : Int, substitution_count? : Int, shopping? : Array[ShoppingLine], substitutions? : Array[Substitution], warnings? : Array[PantryWarning]) -> PantryPlan

    PantryPlan::summary

    fn PantryPlan::summary(self : PantryPlan) -> String

    PantryPlan::to_markdown

    fn PantryPlan::to_markdown(self : PantryPlan) -> String

    PantryWarning

    pub(all) struct PantryWarning {
    code : String
    message : String
    severity : String
    } derive(Eq,
    Debug
    )

    PantryWarning::PantryWarning

    fn PantryWarning::PantryWarning(code? : String, message? : String, severity? : String) -> PantryWarning

    Quantity

    pub(all) struct Quantity {
    amount : Int
    unit : MeasureUnit
    } derive(Eq,
    Debug
    )

    Quantity::Quantity

    fn Quantity::Quantity(amount? : Int, unit? : MeasureUnit) -> Quantity

    Recipe

    pub(all) struct Recipe {
    title : String
    servings : Int
    ingredients : Array[Ingredient]
    steps : Array[String]
    notes : Array[String]
    } derive(Eq,
    Debug
    )

    Recipe::Recipe

    fn Recipe::Recipe(title? : String, servings? : Int, ingredients? : Array[Ingredient], steps? : Array[String], notes? : Array[String]) -> Recipe

    RotationLine

    pub(all) struct RotationLine {
    name : String
    required : Quantity
    available : Quantity
    use_first : Quantity
    buy : Quantity
    priority : Int
    reason : String
    } derive(Eq,
    Debug
    )

    RotationLine::RotationLine

    fn RotationLine::RotationLine(name? : String, required? : Quantity, available? : Quantity, use_first? : Quantity, buy? : Quantity, priority? : Int, reason? : String) -> RotationLine

    ShoppingLine

    pub(all) struct ShoppingLine {
    name : String
    needed : Quantity
    available : Quantity
    buy : Quantity
    estimated_cost_cents : Int
    } derive(Eq,
    Debug
    )

    ShoppingLine::ShoppingLine

    fn ShoppingLine::ShoppingLine(name? : String, needed? : Quantity, available? : Quantity, buy? : Quantity, estimated_cost_cents? : Int) -> ShoppingLine

    Substitution

    pub(all) struct Substitution {
    source : String
    target : Ingredient
    ratio_percent : Int
    reason : String
    removes_allergens : Array[String]
    adds_tags : Array[String]
    } derive(Eq,
    Debug
    )

    Substitution::Substitution

    fn Substitution::Substitution(source? : String, target? : Ingredient, ratio_percent? : Int, reason? : String, removes_allergens? : Array[String], adds_tags? : Array[String]) -> Substitution

    adapt_recipe

    fn adapt_recipe(recipe : Recipe, profile : DietProfile, substitutions : Array[Substitution]) -> Recipe

    add_quantity

    fn add_quantity(left : Quantity, right : Quantity) -> Quantity

    allergen_hit_count

    fn allergen_hit_count(recipe : Recipe, profile : DietProfile) -> Int

    best_meal_prep_option

    fn best_meal_prep_option(options : Array[MealPrepOption]) -> MealPrepOption

    canonical_amount

    fn canonical_amount(quantity : Quantity) -> Int

    convert_quantity

    fn convert_quantity(quantity : Quantity, unit : MeasureUnit) -> Quantity

    default_substitutions

    fn default_substitutions() -> Array[Substitution]

    disliked_tag_count

    fn disliked_tag_count(recipe : Recipe, profile : DietProfile) -> Int

    example_pantry

    fn example_pantry() -> Array[PantryItem]

    example_plan

    fn example_plan() -> PantryPlan

    example_profile

    fn example_profile() -> DietProfile

    example_recipe

    fn example_recipe() -> Recipe

    example_zero_waste_meal_prep

    fn example_zero_waste_meal_prep() -> MealPrepPlan

    expiring_not_in_recipe

    fn expiring_not_in_recipe(recipe : Recipe, pantry : Array[PantryItem], window_days : Int) -> Array[PantryWarning]

    extended_food_amounts

    fn extended_food_amounts() -> Array[Int]

    extended_food_carbs

    fn extended_food_carbs() -> Array[Int]

    extended_food_catalog

    fn extended_food_catalog() -> Array[Ingredient]

    extended_food_costs

    fn extended_food_costs() -> Array[Int]

    extended_food_fats

    fn extended_food_fats() -> Array[Int]

    extended_food_groups

    fn extended_food_groups() -> Array[String]

    extended_food_kcals

    fn extended_food_kcals() -> Array[Int]

    extended_food_names

    fn extended_food_names() -> Array[String]

    extended_food_proteins

    fn extended_food_proteins() -> Array[Int]

    extended_food_units

    fn extended_food_units() -> Array[MeasureUnit]

    food_catalog

    fn food_catalog() -> Array[Ingredient]

    has_enough

    fn has_enough(available : Quantity, needed : Quantity) -> Bool

    ingredient

    fn ingredient(name : String, group : String, amount : Int, unit : MeasureUnit, kcal : Int, protein_g : Int, fat_g : Int, carb_g : Int, cost_cents : Int, allergens? : Array[String], tags? : Array[String]) -> Ingredient

    ingredient_has_allergen

    fn ingredient_has_allergen(ingredient : Ingredient, profile : DietProfile) -> Bool

    meal_prep_option

    fn meal_prep_option(recipe : Recipe, servings : Int, profile : DietProfile, pantry : Array[PantryItem], substitutions : Array[Substitution], window_days : Int) -> MealPrepOption

    meal_prep_rankings

    fn meal_prep_rankings(recipes : Array[Recipe], servings : Int, profile : DietProfile, pantry : Array[PantryItem], substitutions : Array[Substitution], window_days : Int) -> Array[MealPrepOption]

    missing_count

    fn missing_count(lines : Array[ShoppingLine]) -> Int

    nutrition_add

    fn nutrition_add(left : Nutrition, right : Nutrition) -> Nutrition

    pantry_rice_bowl

    fn pantry_rice_bowl() -> Recipe

    plan_recipe

    fn plan_recipe(recipe : Recipe, servings : Int, profile : DietProfile, pantry : Array[PantryItem], substitutions : Array[Substitution]) -> PantryPlan

    plan_score

    fn plan_score(plan : PantryPlan) -> Int

    quantity_missing

    fn quantity_missing(needed : Quantity, available : Quantity) -> Quantity

    quantity_text

    fn quantity_text(quantity : Quantity) -> String

    recipe_card

    fn recipe_card(recipe : Recipe) -> String

    rotation_plan

    fn rotation_plan(recipe : Recipe, pantry : Array[PantryItem]) -> Array[RotationLine]

    rotation_score

    fn rotation_score(lines : Array[RotationLine]) -> Int

    rotation_summary

    fn rotation_summary(lines : Array[RotationLine]) -> String

    same_measure_kind

    fn same_measure_kind(left : Quantity, right : Quantity) -> Bool

    scale_ingredient

    fn scale_ingredient(ingredient : Ingredient, from_servings : Int, to_servings : Int) -> Ingredient

    scale_nutrition

    fn scale_nutrition(nutrition : Nutrition, from_servings : Int, to_servings : Int) -> Nutrition

    scale_quantity

    fn scale_quantity(quantity : Quantity, from_servings : Int, to_servings : Int) -> Quantity

    scale_recipe

    fn scale_recipe(recipe : Recipe, servings : Int) -> Recipe

    shopping_cost

    fn shopping_cost(lines : Array[ShoppingLine]) -> Int

    shopping_csv

    fn shopping_csv(lines : Array[ShoppingLine]) -> String

    shopping_list

    fn shopping_list(recipe : Recipe, pantry : Array[PantryItem]) -> Array[ShoppingLine]

    subtract_quantity

    fn subtract_quantity(left : Quantity, right : Quantity) -> Quantity

    total_cost

    fn total_cost(recipe : Recipe) -> Int

    total_nutrition

    fn total_nutrition(recipe : Recipe) -> Nutrition

    unit_kind

    fn unit_kind(unit : MeasureUnit) -> MeasureKind

    unit_text

    fn unit_text(unit : MeasureUnit) -> String

    urgent_rotation_count

    fn urgent_rotation_count(lines : Array[RotationLine]) -> Int

    used_substitutions

    fn used_substitutions(recipe : Recipe, profile : DietProfile, substitutions : Array[Substitution]) -> Array[Substitution]

    zero_waste_meal_prep

    fn zero_waste_meal_prep(recipes : Array[Recipe], servings : Int, profile : DietProfile, pantry : Array[PantryItem], substitutions : Array[Substitution], days : Int, window_days : Int) -> MealPrepPlan