Compare commits

..

7 Commits

Author SHA1 Message Date
artus
5233e44bf4 preps future ideas 2019-01-05 21:56:17 +01:00
artus
8699dfef33 inits importer module 2019-01-05 21:37:19 +01:00
artus
2d544e334d decouples meals generation 2018-12-24 16:03:50 +01:00
artus
8236b8ac9f first step towards weekly planning 2018-12-24 15:44:19 +01:00
artus
f0bc06d9ed cleanups 2018-12-24 14:38:28 +01:00
artus
8bdf7bb3b7 updates solver.rs 2018-12-24 14:17:48 +01:00
artus
c6642268b5 starts basic solver 2018-12-23 21:13:06 +01:00
9 changed files with 271 additions and 11 deletions

4
cookbook/src/importer.rs Normal file
View File

@@ -0,0 +1,4 @@
//! Import recipes from the web
//!

View File

@@ -1,3 +1,16 @@
mod meal;
mod storage;
mod importer;
pub use self::meal::Meal;
pub fn fetch_meals() -> Vec<Meal> {
vec![
Meal::new("Raclette".to_string(), 800),
Meal::new("Soupe".to_string(), 400),
]
}
#[cfg(test)]
mod tests {
#[test]

31
cookbook/src/meal.rs Normal file
View File

@@ -0,0 +1,31 @@
/// An individual ingredient
pub struct Ingredient {
name: String,
}
impl Ingredient {
pub(super) fn new(name: String) -> Ingredient {
Ingredient { name }
}
}
/// An ordered set of dishes
#[derive(Debug,Clone)]
pub struct Meal {
name: String,
nutritional_value: i32,
}
impl Meal {
pub(super) fn new(name: String, nutritional_value: i32) -> Meal {
Meal { name, nutritional_value }
}
pub fn nutritional_value(&self) -> i32 {
self.nutritional_value
}
}

View File

@@ -1,11 +0,0 @@
enum Meal {
FullMeal,
Composed,
}
fn retrieve_all() -> Vec<Meal> {
vec!()
}

20
cookbook/src/storage.rs Normal file
View File

@@ -0,0 +1,20 @@
//! Storage backend for persistent data
//!
use std::collections::HashMap;
/// An entry in the storage
struct Entry<T>(T);
/// A storage container
pub struct Storage<T> {
content: HashMap<String, Entry<T>>,
}
impl<T> Storage<T> {
pub(super) fn insert(&mut self, item: T) -> Result<(), ()> {
Err(())
}
}

44
planner/src/bin/weekly.rs Normal file
View File

@@ -0,0 +1,44 @@
//! The weekly menu planner
//!
use cookbook::{Meal, fetch_meals};
use planner::solver::{Variables, Domain, solve_all};
fn generate_weekly_menu() -> String {
let assignments: Variables<Meal> = [
("LundiMidi".to_string(), None), ("LundiSoir".to_string(), None),
("MardiMidi".to_string(), None), ("MardiSoir".to_string(), None),
("MercrediMidi".to_string(), None), ("MercrediSoir".to_string(), None),
].iter().cloned().collect();
let meals: Domain<Meal> = Domain::new(fetch_meals());
let validator = |vars: &Variables<Meal>| {
let mut result = true;
for day in ["Lundi", "Mardi", "Mercredi"].into_iter() {
let all_day = vars.keys().filter(|k| k.starts_with(day));
let mut nutri_value = 0;
for key in all_day {
nutri_value += vars.get(key)
.expect("no value here !")
.expect("no meal there !")
.nutritional_value()
}
println!("{} -> {}", day, nutri_value);
if nutri_value != 1200 { result = false; };
}
println!("Validator returns {}", result);
result
};
let solutions = solve_all(assignments, &meals, validator);
format!("{:#?}", solutions)
}
fn main() {
println!("{}", generate_weekly_menu());
}
#[cfg(test)]
mod tests {
}

View File

@@ -1,3 +1,7 @@
extern crate cookbook;
pub mod solver;
#[cfg(test)]
mod tests {
#[test]

16
planner/src/rules.rs Normal file
View File

@@ -0,0 +1,16 @@
//! Rules used by the `planner`
//! A rule is a constraint on valid solutions, but also provides insights
//! and eventually inferences to optimize the solving process.
// Nutritional values
// - Per day : according to user profile (man: 2000kcal, woman: 1800kcal)
// - Per meal : some meals should have higher nutrional values than others
// Ingredients
// - Per week : should use most of a limited set of ingredients (excluding
// condiments, ...)
// - To consume : must use a small set of ingredients (leftovers)
//
// Price
// - Per week : should restrict ingredients cost to a given amount

139
planner/src/solver.rs Normal file
View File

@@ -0,0 +1,139 @@
//! # Solver
//!
//! Provides `Variables`, `Domain` structs and `solve_all` function.
use std::fmt;
use std::clone::Clone;
use std::collections::HashMap;
/// An assignments map of variables
pub type Variables<'a, V> = HashMap<String, Option<&'a V>>;
enum Assignment<'a, V> {
Update(String, &'a V),
Clear(String)
}
/// The domain of values that can be assigned to variables
#[derive(Clone)]
pub struct Domain<V> {
values: Vec<V>
}
impl<V> Domain<V> {
pub fn new(values: Vec<V>) -> Domain<V> {
Domain { values }
}
}
impl<V: fmt::Debug> fmt::Debug for Domain<V> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Domain<{:?}>", self.values)
}
}
/// Returns all possible Updates for next assignements, prepended with
/// a Clear to ensure the variable is unset before when leaving the branch.
fn assign_next<'a,'b, V>(assign: &'b Variables<'a, V>, domain: &'a Domain<V>)
-> Option<Vec<Assignment<'a, V>>>
where V: fmt::Debug
{
// Panics on empty domain
// If domain values are filtered, then the branch is a dead end
if domain.values.is_empty() { panic!("No values in domain : {:?}", domain); };
// TODO: should be able to inject a choosing strategy
if let Some((key,_)) = assign.iter().find(|(_, val)| val.is_none()) {
let mut updates = vec![Assignment::Clear(key.clone())];
// TODO: should be able to filter domain values (inference, pertinence)
for value in domain.values.iter() {
updates.push(Assignment::Update(key.clone(), value));
}
Some(updates)
} else { // End of assignements
None
}
}
/// Visit all possible solutions, using a stack.
pub fn solve_all<'a, V>(
mut assign: Variables<'a, V>,
domain: &'a Domain<V>,
is_valid: fn(&Variables<'a,V>) -> bool
) -> Vec<Variables<'a, V>>
where V: Clone + fmt::Debug
{
let mut solutions: Vec<Variables<V>> = vec![];
let mut stack: Vec<Assignment<'a, V>> = vec![];
stack.append(&mut assign_next(&assign,domain).unwrap());
loop {
let node = stack.pop();
if node.is_none() { break; };
match node.unwrap() {
Assignment::Update(key, val) => {
// Assign the variable and open new branches, if any.
*assign.get_mut(&key).unwrap() = Some(val);
// TODO: handle case of empty domain.values
if let Some(mut nodes) = assign_next(&assign, domain) {
stack.append(&mut nodes);
} else {
// Assignements are completed
if is_valid(&assign) {
solutions.push(assign.clone());
};
};
},
Assignment::Clear(key) => {
// We are closing this branch, unset the variable
*assign.get_mut(&key).unwrap() = None;
},
};
};
solutions
}
#[cfg(test)]
mod tests {
#[test]
fn test_solver_find_pairs() {
use super::*;
// Find all pairs of two differents
let assign: Variables<i32> = [
("Left".to_string(), None),
("Right".to_string(), None),
].iter().cloned().collect();
let domain = Domain::new(vec![1,2,3]);
let constraint = |assign: &Variables<i32>| {
assign.get("Left").unwrap() == assign.get("Right").unwrap()
};
let solutions: Vec<Variables<i32>> = vec![
[("Left".to_string(), Some(&3)), ("Right".to_string(), Some(&3)),].iter().cloned().collect(),
[("Left".to_string(), Some(&2)), ("Right".to_string(), Some(&2)),].iter().cloned().collect(),
[("Left".to_string(), Some(&1)), ("Right".to_string(), Some(&1)),].iter().cloned().collect(),
];
assert_eq!(solve_all(assign, &domain, constraint), solutions);
}
#[test]
fn test_solver_find_pairs_with_initial() {
use super::*;
// Find all pairs of two differents
let assign: Variables<i32> = [
("Left".to_string(), None),
("Right".to_string(), Some(&2)),
].iter().cloned().collect();
let domain = Domain::new(vec![1,2,3]);
let constraint = |assign: &Variables<i32>| {
assign.get("Left").unwrap() == assign.get("Right").unwrap()
};
let solutions: Vec<Variables<i32>> = vec![
[("Left".to_string(), Some(&2)), ("Right".to_string(), Some(&2)),].iter().cloned().collect(),
];
assert_eq!(solve_all(assign, &domain, constraint), solutions);
}
}