planter_core/task.rs
1use crate::{duration::PositiveDuration, resources::Resource};
2use anyhow::Context;
3use chrono::{DateTime, Utc};
4
5#[derive(Debug, Clone, Default, PartialEq, Eq)]
6/// A task is a unit of work that can be completed by a person or a group of people.
7/// It can be assigned resources and can have a start, finish, and duration.
8pub struct Task {
9 /// The name of the task.
10 name: String,
11 /// The description of the task.
12 description: String,
13 /// Whether the task is completed.
14 completed: bool,
15 /// The start time of the task.
16 start: Option<DateTime<Utc>>,
17 /// The finish time of the task.
18 finish: Option<DateTime<Utc>>,
19 /// The duration of the task.
20 duration: Option<PositiveDuration>,
21 /// The resources assigned to the task.
22 resources: Vec<Resource>,
23}
24
25impl Task {
26 /// Creates a new task with the given name.
27 ///
28 /// # Arguments
29 ///
30 /// * `name` - The name of the task.
31 ///
32 /// # Returns
33 ///
34 /// A new task with the given name.
35 ///
36 /// # Example
37 ///
38 /// ```
39 /// use planter_core::task::Task;
40 ///
41 /// let task = Task::new("Become world leader");
42 /// assert_eq!(task.name(), "Become world leader");
43 /// ```
44 pub fn new(name: impl Into<String>) -> Self {
45 Task {
46 name: name.into(),
47 description: String::new(),
48 completed: false,
49 start: None,
50 finish: None,
51 duration: None,
52 resources: Vec::new(),
53 }
54 }
55
56 /// Edits the start time of the task.
57 /// If a duration is already set, the finish time will be updated accordingly.
58 /// If there is a finish time set, but not a duration, the duration will be updated accordingly.
59 /// The finish time will be pushed ahead if the start time is after the finish time.
60 ///
61 /// # Arguments
62 ///
63 /// * `start` - The new start time of the task.
64 ///
65 /// # Errors
66 ///
67 /// Returns an error if the task has a finish date and the start date passed
68 /// as parameter is too far from that.
69 ///
70 /// # Example
71 ///
72 /// ```
73 /// use chrono::{Utc, Duration};
74 /// use planter_core::task::Task;
75 ///
76 /// let mut task = Task::new("Become world leader");
77 /// let start_time = Utc::now();
78 /// task.edit_start(start_time);
79 /// assert_eq!(task.start().unwrap(), start_time);
80 /// ```
81 #[allow(clippy::expect_used)]
82 pub fn edit_start(&mut self, start: DateTime<Utc>) -> anyhow::Result<()> {
83 self.start = Some(start);
84
85 if let Some(duration) = self.duration {
86 self.finish = Some(start + *duration);
87 }
88
89 if let Some(finish) = self.finish {
90 if finish < start {
91 self.finish = Some(start);
92 }
93 if self.duration().is_none() {
94 let duration = finish - start;
95 self.duration = Some(
96 duration
97 .try_into()
98 .context("Start time and finish time were too far apart")?,
99 );
100 }
101 }
102 Ok(())
103 }
104
105 /// Returns the start time of the task. It's None by default.
106 ///
107 /// # Example
108 ///
109 /// ```
110 /// use chrono::{Utc};
111 /// use planter_core::task::Task;
112 ///
113 /// let mut task = Task::new("Become world leader");
114 /// assert!(task.start().is_none());
115 ///
116 /// let start_time = Utc::now();
117 /// task.edit_start(start_time);
118 /// assert_eq!(task.start().unwrap(), start_time);
119 /// ```
120 pub fn start(&self) -> Option<DateTime<Utc>> {
121 self.start
122 }
123
124 /// Edits the finish time of the task.
125 /// If there is a start time already set, duration will be updated accordingly.
126 /// Start time will be pushed back if it's after the finish time.
127 ///
128 /// # Arguments
129 ///
130 /// * `finish` - The new finish time of the task.
131 ///
132 /// # Errors
133 ///
134 /// Returns an error if the task has a start date and the finish date passed
135 /// as parameter is too far from that.
136 ///
137 ///
138 /// # Example
139 ///
140 /// ```
141 /// use chrono::{Utc};
142 /// use planter_core::task::Task;
143 ///
144 /// let mut task = Task::new("Become world leader");
145 /// assert!(task.start().is_none());
146 ///
147 /// let mut finish_time = Utc::now();
148 /// task.edit_finish(finish_time).unwrap();
149 /// assert_eq!(task.finish().unwrap(), finish_time);
150 /// ```
151 #[allow(clippy::expect_used)]
152 pub fn edit_finish(&mut self, finish: DateTime<Utc>) -> anyhow::Result<()> {
153 self.finish = Some(finish);
154
155 if let Some(start) = self.start() {
156 let start = if finish < start {
157 self.start = Some(finish);
158 finish
159 } else {
160 start
161 };
162 let duration = finish - start;
163 self.duration = Some(
164 duration
165 .try_into()
166 .context("Start time and finish time were too far apart")?,
167 );
168 }
169 Ok(())
170 }
171
172 /// Returns the finish time of the task. It's None by default.
173 ///
174 /// # Example
175 ///
176 /// ```
177 /// use chrono::{Utc};
178 /// use planter_core::task::Task;
179 ///
180 /// let mut task = Task::new("Become world leader");
181 /// assert!(task.finish().is_none());
182 /// let finish_time = Utc::now();
183 /// task.edit_finish(finish_time);
184 /// assert_eq!(task.finish().unwrap(), finish_time);
185 /// ```
186 pub fn finish(&self) -> Option<DateTime<Utc>> {
187 self.finish
188 }
189
190 /// Edits the duration of the task. If the task has a start time, finish time will be updated accordingly.
191 ///
192 /// # Arguments
193 ///
194 /// * `duration` - The new duration of the task.
195 ///
196 /// # Example
197 ///
198 /// ```
199 /// use chrono::{Utc, Duration};
200 /// use planter_core::{task::Task, duration::PositiveDuration};
201 ///
202 /// let mut task = Task::new("Become world leader");
203 /// task.edit_duration(Duration::minutes(30).try_into().unwrap());
204 /// assert!(task.duration().is_some());
205 /// assert_eq!(task.duration().unwrap(), Duration::minutes(30).try_into().unwrap());
206 /// ```
207 pub fn edit_duration(&mut self, duration: PositiveDuration) {
208 self.duration = Some(duration);
209
210 if let Some(start) = self.start() {
211 let finish = start + *duration;
212 self.finish = Some(finish);
213 }
214 }
215
216 /// Adds a [`Resource`] to the task.
217 ///
218 /// # Arguments
219 ///
220 /// * `resource` - The resource to add to the task.
221 ///
222 /// # Example
223 ///
224 /// ```
225 /// use planter_core::{resources::{Resource, Material, NonConsumable}, task::Task};
226 ///
227 /// let mut task = Task::new("Become world leader");
228 /// let resource = Resource::Material(Material::NonConsumable(
229 /// NonConsumable::new("Crowbar"),
230 /// ));
231 /// task.add_resource(resource);
232 ///
233 /// assert_eq!(task.resources().len(), 1);
234 /// ```
235 pub fn add_resource(&mut self, resource: Resource) {
236 self.resources.push(resource);
237 }
238
239 /// Returns the list of [`Resource`] assigned to the task.
240 ///
241 /// # Example
242 ///
243 /// ```
244 /// use planter_core::task::Task;
245 /// use planter_core::resources::{Resource, Material, NonConsumable};
246 ///
247 /// let mut task = Task::new("Become world leader");
248 /// assert!(task.resources().is_empty());
249 /// let resource = Resource::Material(Material::NonConsumable(
250 /// NonConsumable::new("Crowbar"),
251 /// ));
252 /// task.add_resource(resource);
253 /// assert_eq!(task.resources().len(), 1);
254 /// ```
255 pub fn resources(&self) -> &[Resource] {
256 &self.resources
257 }
258
259 /// Edits the name of the task.
260 ///
261 /// # Arguments
262 ///
263 /// * `name` - The new name of the task.
264 ///
265 /// # Example
266 ///
267 /// ```
268 /// use planter_core::task::Task;
269 ///
270 /// let mut task = Task::new("Become world leader");
271 /// task.edit_name("Become world boss");
272 /// assert_eq!(task.name(), "Become world boss");
273 /// ```
274 pub fn edit_name(&mut self, name: impl Into<String>) {
275 self.name = name.into();
276 }
277
278 /// Returns the name of the task.
279 ///
280 /// # Example
281 ///
282 /// ```
283 /// use planter_core::task::Task;
284 ///
285 /// let mut task = Task::new("Become world leader");
286 /// assert_eq!(task.name(), "Become world leader");
287 /// ```
288 pub fn name(&self) -> &str {
289 &self.name
290 }
291
292 /// Edits the description of the task.
293 ///
294 /// # Arguments
295 ///
296 /// * `description` - The new description of the task.
297 ///
298 /// # Example
299 ///
300 /// ```
301 /// use planter_core::task::Task;
302 ///
303 /// let mut task = Task::new("Become world leader");
304 /// task.edit_description("Description");
305 /// assert_eq!(task.description(), "Description");
306 /// ```
307 pub fn edit_description(&mut self, description: impl Into<String>) {
308 self.description = description.into();
309 }
310
311 /// Returns the description of the task.
312 ///
313 /// # Example
314 ///
315 /// ```
316 /// use planter_core::task::Task;
317 ///
318 /// let mut task = Task::new("Become world leader");
319 /// task.edit_description("Description");
320 /// assert_eq!(task.description(), "Description");
321 /// ```
322 pub fn description(&self) -> &str {
323 &self.description
324 }
325
326 /// Whether the task is completed. It's false by default.
327 ///
328 /// # Example
329 ///
330 /// ```
331 /// use planter_core::task::Task;
332 ///
333 /// let mut task = Task::new("Become world leader");
334 /// assert!(!task.completed());
335 /// task.toggle_completed();
336 /// assert!(task.completed());
337 /// ```
338 pub fn completed(&self) -> bool {
339 self.completed
340 }
341
342 /// Marks the task as completed.
343 ///
344 /// # Example
345 ///
346 /// ```
347 /// use planter_core::task::Task;
348 ///
349 /// let mut task = Task::new("Become world leader");
350 /// assert!(!task.completed());
351 /// task.toggle_completed();
352 /// assert!(task.completed());
353 /// task.toggle_completed();
354 /// assert!(!task.completed());
355 /// ```
356 pub fn toggle_completed(&mut self) {
357 self.completed = !self.completed;
358 }
359
360 /// Returns the duration of the task. It's None by default.
361 ///
362 /// # Example
363 ///
364 /// ```
365 /// use chrono::{Utc, Duration};
366 /// use planter_core::task::Task;
367 ///
368 /// let mut task = Task::new("Become world leader");
369 /// assert!(task.duration().is_none());
370 ///
371 /// task.edit_duration(Duration::hours(1).try_into().unwrap());
372 /// assert!(task.duration().unwrap() == Duration::hours(1).try_into().unwrap());
373 /// ```
374 pub fn duration(&self) -> Option<PositiveDuration> {
375 self.duration
376 }
377}
378
379#[cfg(test)]
380/// Utilities to test Tasks.
381pub mod test_utils {
382 use proptest::prelude::*;
383
384 use super::Task;
385
386 /// Generates an empty task with a random name.
387 pub fn task_strategy() -> impl Strategy<Value = Task> {
388 ".*".prop_map(Task::new)
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use chrono::Duration;
395 use proptest::prelude::*;
396
397 use crate::duration::MAX_DURATION;
398
399 use super::*;
400
401 proptest! {
402 #[test]
403 fn duration_is_properly_set_when_adding_start_and_finish_time(milliseconds in 0..MAX_DURATION) {
404 let start = Utc::now();
405 let finish = start + Duration::milliseconds(milliseconds);
406 let mut task = Task::new("World domination");
407
408 task.edit_start(start).unwrap();
409 task.edit_finish(finish).unwrap();
410
411 assert!(task.duration().unwrap() == Duration::milliseconds(milliseconds).try_into().unwrap());
412 }
413
414 #[test]
415 fn task_times_stay_none_when_adding_duration(milliseconds in 0..MAX_DURATION) {
416 let mut task = Task::new("World domination");
417
418 let duration = Duration::milliseconds(milliseconds).try_into().unwrap();
419 task.edit_duration(duration);
420 assert!(task.finish().is_none());
421 assert!(task.start().is_none());
422 }
423
424 #[test]
425 fn finish_time_is_properly_set_when_adding_duration(milliseconds in 0..MAX_DURATION) {
426 let start = Utc::now();
427 let mut task = Task::new("World domination");
428
429 task.edit_start(start).unwrap();
430 let duration = Duration::milliseconds(milliseconds).try_into().unwrap();
431 task.edit_duration(duration);
432 assert!(task.finish().unwrap() == start + *duration);
433 }
434
435 #[test]
436 fn finish_time_is_properly_pushed_ahead_when_adding_duration(milliseconds in 0..MAX_DURATION) {
437 let start = Utc::now();
438 let finish = start + Duration::milliseconds(milliseconds);
439 let mut task = Task::new("World domination");
440
441 task.edit_start(start).unwrap();
442 task.edit_finish(finish).unwrap();
443
444 let duration = Duration::milliseconds(milliseconds + 1).try_into().unwrap();
445 task.edit_duration(duration);
446 assert!(task.finish().unwrap() == start + *duration);
447 }
448
449
450 #[test]
451 fn start_time_is_properly_pushed_back_when_adding_earlier_finish_time(milliseconds in 0..MAX_DURATION) {
452 let start = Utc::now();
453 let finish = start - Duration::milliseconds(milliseconds);
454 let mut task = Task::new("World domination");
455
456 task.edit_start(start).unwrap();
457 task.edit_finish(finish).unwrap();
458
459 assert!(task.start().unwrap() == task.finish().unwrap());
460 }
461 }
462
463 #[test]
464 fn edit_start_returns_error_when_too_far_apart() {
465 let milliseconds = MAX_DURATION + 1;
466 let finish = Utc::now();
467 let start = finish - Duration::milliseconds(milliseconds);
468 let mut task = Task::new("World domination");
469
470 task.edit_finish(finish).unwrap();
471
472 assert!(task.edit_start(start).is_err());
473 }
474
475 #[test]
476 fn edit_finish_returns_error_when_too_far_apart() {
477 let milliseconds = MAX_DURATION + 1;
478 let start = Utc::now();
479 let finish = start + Duration::milliseconds(milliseconds);
480 let mut task = Task::new("World domination");
481
482 task.edit_start(start).unwrap();
483
484 assert!(task.edit_finish(finish).is_err());
485 }
486}