imag/lib/domain/libimaghabit/src/habit.rs

418 lines
15 KiB
Rust
Raw Normal View History

2017-09-01 18:41:09 +00:00
//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015-2019 Matthias Beyer <mail@beyermatthias.de> and contributors
2017-09-01 18:41:09 +00:00
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; version
// 2.1 of the License.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
use toml::Value;
use toml_query::read::TomlValueReadTypeExt;
2017-09-01 18:41:09 +00:00
use toml_query::insert::TomlValueInsertExt;
use chrono::NaiveDateTime;
use chrono::Local;
use chrono::NaiveDate;
use failure::Error;
use failure::Fallible as Result;
use failure::ResultExt;
use failure::err_msg;
2017-09-01 18:41:09 +00:00
use crate::iter::HabitInstanceStoreIdIterator;
use crate::util::IsHabitCheck;
use crate::util::get_string_header_from_entry;
use crate::instance::IsHabitInstance;
2017-09-01 18:41:09 +00:00
use libimagentrylink::linker::InternalLinker;
2017-09-01 18:41:09 +00:00
use libimagstore::store::Store;
use libimagstore::store::FileLockEntry;
use libimagstore::store::Entry;
use libimagstore::storeid::StoreId;
use libimagstore::storeid::StoreIdIterator;
use libimagentryutil::isa::Is;
use libimagentryutil::isa::IsKindHeaderPathProvider;
use libimagutil::date::date_to_string;
2017-09-01 18:41:09 +00:00
/// A HabitTemplate is a "template" of a habit. A user may define a habit "Eat vegetable".
/// If the user ate a vegetable, she should create a HabitInstance from the Habit with the
/// appropriate date (and optionally a comment) set.
pub trait HabitTemplate : Sized {
/// Create an instance from this habit template
///
/// By default creates an instance with the name of the template, the current time and the
/// current date and copies the comment from the template to the instance.
///
/// It uses `Store::retrieve()` underneath. So if there is already an instance for the day
/// passed, this will simply return the instance.
fn create_instance_with_date<'a>(&mut self, store: &'a Store, date: &NaiveDate)
-> Result<FileLockEntry<'a>>;
/// Shortcut for calling `Self::create_instance_with_date()` with an instance of
/// `::chrono::Local::today().naive_local()`.
fn create_instance_today<'a>(&mut self, store: &'a Store) -> Result<FileLockEntry<'a>>;
2017-09-01 18:41:09 +00:00
/// Same as `HabitTemplate::create_instance_with_date()` but uses `Store::retrieve` internally.
fn retrieve_instance_with_date<'a>(&mut self, store: &'a Store, date: &NaiveDate)
-> Result<FileLockEntry<'a>>;
/// Same as `HabitTemplate::create_instance_today()` but uses `Store::retrieve` internally.
fn retrieve_instance_today<'a>(&mut self, store: &'a Store) -> Result<FileLockEntry<'a>>;
/// Get instances for this template
fn linked_instances(&self) -> Result<HabitInstanceStoreIdIterator>;
/// Get the date of the next date when the habit should be done
2017-12-22 09:59:27 +00:00
fn next_instance_date_after(&self, base: &NaiveDateTime) -> Result<Option<NaiveDate>>;
2017-12-05 19:27:33 +00:00
/// Get the date of the next date when the habit should be done
2017-12-22 09:59:27 +00:00
fn next_instance_date(&self) -> Result<Option<NaiveDate>>;
2017-12-05 19:27:33 +00:00
2017-09-01 18:41:09 +00:00
/// Check whether the instance is a habit by checking its headers for the habit data
fn is_habit_template(&self) -> Result<bool>;
fn habit_name(&self) -> Result<String>;
2017-10-22 10:31:48 +00:00
fn habit_basedate(&self) -> Result<String>;
fn habit_recur_spec(&self) -> Result<String>;
2017-09-01 18:41:09 +00:00
fn habit_comment(&self) -> Result<String>;
2017-12-22 12:20:11 +00:00
fn habit_until_date(&self) -> Result<Option<String>>;
2017-09-01 18:41:09 +00:00
fn instance_exists_for_date(&self, date: &NaiveDate) -> Result<bool>;
/// Create a StoreId for a habit name and a date the habit should be instantiated for
fn instance_id_for(habit_name: &String, habit_date: &NaiveDate) -> Result<StoreId>;
2017-09-01 18:41:09 +00:00
}
provide_kindflag_path!(pub IsHabitTemplate, "habit.template.is_habit_template");
2017-09-01 18:41:09 +00:00
impl HabitTemplate for Entry {
fn create_instance_with_date<'a>(&mut self, store: &'a Store, date: &NaiveDate) -> Result<FileLockEntry<'a>> {
let name = self.habit_name()?;
2017-09-01 18:41:09 +00:00
let comment = self.habit_comment()?;
let date = date_to_string(date);
let id = instance_id_for_name_and_datestr(&name, &date)?;
2017-09-01 18:41:09 +00:00
store.create(id)
2017-09-01 18:41:09 +00:00
.map_err(From::from)
.and_then(|entry| postprocess_instance(entry, name, date, comment, self))
2017-09-01 18:41:09 +00:00
}
fn create_instance_today<'a>(&mut self, store: &'a Store) -> Result<FileLockEntry<'a>> {
self.create_instance_with_date(store, &Local::today().naive_local())
}
fn retrieve_instance_with_date<'a>(&mut self, store: &'a Store, date: &NaiveDate) -> Result<FileLockEntry<'a>> {
let name = self.habit_name()?;
let comment = self.habit_comment()?;
let date = date_to_string(date);
let id = instance_id_for_name_and_datestr(&name, &date)?;
2018-03-22 20:33:50 +00:00
store.retrieve(id)
.map_err(From::from)
.and_then(|entry| postprocess_instance(entry, name, date, comment, self))
}
fn retrieve_instance_today<'a>(&mut self, store: &'a Store) -> Result<FileLockEntry<'a>> {
self.retrieve_instance_with_date(store, &Local::today().naive_local())
}
fn linked_instances(&self) -> Result<HabitInstanceStoreIdIterator> {
let iter = self
.get_internal_links()?
.map(|link| link.get_store_id().clone())
.filter(IsHabitCheck::is_habit_instance)
.map(Ok);
let sidi = StoreIdIterator::new(Box::new(iter));
Ok(HabitInstanceStoreIdIterator::new(sidi))
}
2017-12-22 09:59:27 +00:00
fn next_instance_date_after(&self, base: &NaiveDateTime) -> Result<Option<NaiveDate>> {
2017-12-05 19:27:33 +00:00
use kairos::timetype::TimeType;
use kairos::parser::parse;
use kairos::parser::Parsed;
use kairos::iter::extensions::Every;
let date_from_s = |r: String| -> Result<TimeType> {
match parse(&r)? {
Parsed::TimeType(tt) => Ok(tt),
Parsed::Iterator(_) => {
Err(format_err!("'{}' yields an iterator. Cannot use.", r))
2017-12-05 19:27:33 +00:00
},
}
};
debug!("Base is {:?}", base);
2017-12-05 19:27:33 +00:00
let basedate = date_from_s(self.habit_basedate()?)?;
debug!("Basedate is {:?}", basedate);
2017-12-05 19:27:33 +00:00
let increment = date_from_s(self.habit_recur_spec()?)?;
debug!("Increment is {:?}", increment);
2017-12-05 19:27:33 +00:00
2017-12-22 12:20:11 +00:00
let until = self.habit_until_date()?.map(|s| -> Result<_> {
r#try!(date_from_s(s))
2017-12-22 12:20:11 +00:00
.calculate()?
.get_moment()
.map(Clone::clone)
.ok_or_else(|| Error::from(err_msg("until-date seems to have non-date value")))
2017-12-22 12:20:11 +00:00
});
2017-12-22 09:59:27 +00:00
debug!("Until-Date is {:?}", basedate);
2017-12-05 19:27:33 +00:00
for element in basedate.every(increment)? {
debug!("Calculating: {:?}", element);
let element = element?.calculate()?;
debug!(" = {:?}", element);
2017-12-05 19:27:33 +00:00
if let Some(ndt) = element.get_moment() {
if ndt >= base {
debug!("-> {:?} >= {:?}", ndt, base);
2017-12-22 12:20:11 +00:00
if let Some(u) = until {
if ndt > &(u?) {
return Ok(None);
} else {
return Ok(Some(ndt.date()));
}
2017-12-22 09:59:27 +00:00
} else {
return Ok(Some(ndt.date()));
}
2017-12-05 19:27:33 +00:00
}
} else {
return Err(err_msg("Iterator seems to return bogus values."));
2017-12-05 19:27:33 +00:00
}
}
unreachable!() // until we have habit-end-date support
}
/// Get the date of the next date when the habit should be done
2017-12-22 09:59:27 +00:00
fn next_instance_date(&self) -> Result<Option<NaiveDate>> {
use kairos::timetype::TimeType;
let today = TimeType::today();
let today = today.get_moment().unwrap(); // we know this is safe.
debug!("Today is {:?}", today);
self.next_instance_date_after(&today.date().and_hms(0, 0, 0))
}
2017-09-01 18:41:09 +00:00
/// Check whether the instance is a habit by checking its headers for the habit data
fn is_habit_template(&self) -> Result<bool> {
self.is::<IsHabitTemplate>().map_err(From::from)
2017-09-01 18:41:09 +00:00
}
fn habit_name(&self) -> Result<String> {
get_string_header_from_entry(self, "habit.template.name")
2017-09-01 18:41:09 +00:00
}
2017-10-22 10:31:48 +00:00
fn habit_basedate(&self) -> Result<String> {
get_string_header_from_entry(self, "habit.template.basedate")
2017-10-22 10:31:48 +00:00
}
fn habit_recur_spec(&self) -> Result<String> {
get_string_header_from_entry(self, "habit.template.recurspec")
2017-09-01 18:41:09 +00:00
}
fn habit_comment(&self) -> Result<String> {
get_string_header_from_entry(self, "habit.template.comment")
2017-09-01 18:41:09 +00:00
}
2017-12-22 12:20:11 +00:00
fn habit_until_date(&self) -> Result<Option<String>> {
self.get_header()
.read_string("habit.template.until")
.map_err(From::from)
.map(|os| os.map(String::from))
2017-12-22 09:59:27 +00:00
}
fn instance_exists_for_date(&self, date: &NaiveDate) -> Result<bool> {
let name = self.habit_name()?;
let date = date_to_string(date);
for link in self.get_internal_links()? {
let sid = link.get_store_id();
let instance_id = instance_id_for_name_and_datestr(&name, &date)?;
if sid.is_habit_instance() && instance_id == *sid {
return Ok(true);
}
}
return Ok(false);
}
fn instance_id_for(habit_name: &String, habit_date: &NaiveDate) -> Result<StoreId> {
instance_id_for_name_and_datestr(habit_name, &date_to_string(habit_date))
}
}
fn instance_id_for_name_and_datestr(habit_name: &String, habit_date: &String) -> Result<StoreId> {
crate::module_path::new_id(format!("instance/{}-{}", habit_name, habit_date))
.context(format_err!("Failed building ID for instance: habit name = {}, habit date = {}", habit_name, habit_date))
.map_err(Error::from)
2017-09-01 18:41:09 +00:00
}
pub mod builder {
use toml::Value;
use toml_query::insert::TomlValueInsertExt;
use chrono::NaiveDate;
use libimagstore::store::Store;
use libimagstore::storeid::StoreId;
use libimagstore::store::FileLockEntry;
use libimagentryutil::isa::Is;
2018-03-22 12:59:01 +00:00
use libimagutil::debug_result::DebugResult;
2017-09-01 18:41:09 +00:00
use failure::Error;
use failure::Fallible as Result;
use failure::err_msg;
use libimagutil::date::date_to_string;
use crate::habit::IsHabitTemplate;
2017-09-01 18:41:09 +00:00
2018-02-08 13:22:57 +00:00
#[derive(Debug)]
2017-09-01 18:41:09 +00:00
pub struct HabitBuilder {
name: Option<String>,
comment: Option<String>,
2017-10-22 10:31:48 +00:00
basedate: Option<NaiveDate>,
recurspec: Option<String>,
2017-12-22 09:59:27 +00:00
untildate: Option<NaiveDate>,
2017-09-01 18:41:09 +00:00
}
impl HabitBuilder {
2017-12-03 17:41:21 +00:00
pub fn with_name(mut self, name: String) -> Self {
2017-09-01 18:41:09 +00:00
self.name = Some(name);
self
}
2017-12-03 17:41:21 +00:00
pub fn with_comment(mut self, comment: String) -> Self {
2017-09-01 18:41:09 +00:00
self.comment = Some(comment);
self
}
2017-12-03 17:41:21 +00:00
pub fn with_basedate(mut self, date: NaiveDate) -> Self {
2017-10-22 10:31:48 +00:00
self.basedate = Some(date);
self
}
2017-12-03 17:41:21 +00:00
pub fn with_recurspec(mut self, spec: String) -> Self {
2017-10-22 10:31:48 +00:00
self.recurspec = Some(spec);
2017-09-01 18:41:09 +00:00
self
}
2017-12-22 09:59:27 +00:00
pub fn with_until(mut self, date: NaiveDate) -> Self {
self.untildate = Some(date);
self
}
2017-09-01 18:41:09 +00:00
pub fn build<'a>(self, store: &'a Store) -> Result<FileLockEntry<'a>> {
#[inline]
fn mkerr(s: &'static str) -> Error {
Error::from(format_err!("Habit builder missing: {}", s))
2017-09-01 18:41:09 +00:00
}
2018-03-22 12:59:01 +00:00
let name = self.name
.ok_or_else(|| mkerr("name"))
.map_dbg_str("Success: Name present")?;
2017-12-03 17:44:35 +00:00
2018-03-22 12:59:01 +00:00
let dateobj = self.basedate
.ok_or_else(|| mkerr("date"))
.map_dbg_str("Success: Date present")?;
2017-12-03 17:44:35 +00:00
let recur : String = self.recurspec
2018-03-22 12:59:01 +00:00
.ok_or_else(|| mkerr("recurspec"))
.map_dbg_str("Success: Recurr spec present")?;
2017-12-03 17:44:35 +00:00
2017-12-22 12:20:11 +00:00
if let Some(until) = self.untildate {
debug!("Success: Until-Date present");
if dateobj > until {
let e = Error::from(err_msg("Habit builder logic error: until-date before start date"));
2017-12-22 12:20:11 +00:00
return Err(e);
}
2017-12-22 09:59:27 +00:00
}
if let Err(e) = ::kairos::parser::parse(&recur).map_err(Error::from) {
debug!("Kairos failed: {:?}", e);
return Err(e)
2017-10-22 10:31:48 +00:00
}
2017-09-01 18:41:09 +00:00
let date = date_to_string(&dateobj);
2017-12-03 17:44:35 +00:00
debug!("Success: Date valid");
2017-09-01 18:41:09 +00:00
let comment = self.comment.unwrap_or_else(|| String::new());
let sid = r#try!(build_habit_template_sid(&name));
2017-12-03 17:44:35 +00:00
debug!("Creating entry in store for: {:?}", sid);
let mut entry = r#try!(store.create(sid));
2017-09-01 18:41:09 +00:00
let _ = entry.set_isflag::<IsHabitTemplate>()?;
{
let h = entry.get_header_mut();
let _ = h.insert("habit.template.name", Value::String(name))?;
let _ = h.insert("habit.template.basedate", Value::String(date))?;
let _ = h.insert("habit.template.recurspec", Value::String(recur))?;
let _ = h.insert("habit.template.comment", Value::String(comment))?;
}
2017-12-22 12:20:11 +00:00
if let Some(until) = self.untildate {
let until = date_to_string(&until);
r#try!(entry.get_header_mut().insert("habit.template.until", Value::String(until)));
2017-12-22 12:20:11 +00:00
}
2017-09-01 18:41:09 +00:00
2017-12-03 17:44:35 +00:00
debug!("Success: Created entry in store and set headers");
2017-09-01 18:41:09 +00:00
Ok(entry)
}
}
impl Default for HabitBuilder {
fn default() -> Self {
HabitBuilder {
name: None,
comment: None,
2017-10-22 10:31:48 +00:00
basedate: None,
recurspec: None,
2017-12-22 09:59:27 +00:00
untildate: None,
2017-09-01 18:41:09 +00:00
}
}
}
/// Buld a StoreId for a Habit from a date object and a name of a habit
fn build_habit_template_sid(name: &String) -> Result<StoreId> {
crate::module_path::new_id(format!("template/{}", name)).map_err(From::from)
2017-09-01 18:41:09 +00:00
}
}
2018-03-22 12:54:15 +00:00
fn postprocess_instance<'a>(mut entry: FileLockEntry<'a>,
name: String,
date: String,
comment: String,
template: &mut Entry)
2018-03-22 12:54:15 +00:00
-> Result<FileLockEntry<'a>>
{
{
let _ = entry.set_isflag::<IsHabitInstance>()?;
let hdr = entry.get_header_mut();
let _ = hdr.insert("habit.instance.name", Value::String(name))?;
let _ = hdr.insert("habit.instance.date", Value::String(date))?;
let _ = hdr.insert("habit.instance.comment", Value::String(comment))?;
}
entry.add_internal_link(template)?;
2018-03-22 12:54:15 +00:00
Ok(entry)
}