1use std::fmt::{self, Debug, Display};
25
26use once_cell::sync::Lazy;
27use regex::Regex;
28
29const STOP_CMD: &str = "stop";
30
31static TIMESTAMP_RE: Lazy<Regex> = Lazy::new(|| {
36 Regex::new(r"\A(\d{4}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01]) (?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-6][0-9])").expect("Date time Regex failed.")
37});
38static LAX_LINE_RE: Lazy<Regex> = Lazy::new(|| {
40 Regex::new(r"\A(\d{4}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01]) (?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-6][0-9])(.)(.+)").expect("Entry line Regex failed.")
41});
42pub static PROJECT_RE: Lazy<Regex> =
44 Lazy::new(|| Regex::new(r"\+(\S+)").expect("Entry project regex failed."));
45static TASKNAME_RE: Lazy<Regex> =
47 Lazy::new(|| Regex::new(r"@(\S+)").expect("Task name Regex failed."));
48static STOP_LINE: Lazy<Regex> = Lazy::new(|| {
50 Regex::new(r"\A(\d{4}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01]) (?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-6][0-9]) stop").expect("Stop line Regex failed")
51});
52static EVENT_LINE: Lazy<Regex> = Lazy::new(|| {
54 Regex::new(r"\A(\d{4}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01]) (?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-6][0-9])\^.+").expect("Event line Regex failed")
55});
56pub static YEAR_RE: Lazy<Regex> =
58 Lazy::new(|| Regex::new(r"^(\d\d\d\d)").expect("Date regex failed"));
59pub static MARKER_RE: Lazy<Regex> =
61 Lazy::new(|| Regex::new(r"^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d(.)").expect("Marker regex failed"));
62
63#[doc(inline)]
64use crate::date::{Date, DateTime};
65
66pub mod error;
67pub mod kind;
68
69pub use error::EntryError;
71pub use kind::EntryKind;
73
74#[derive(Debug, Clone, Eq, PartialEq)]
79#[must_use]
80pub struct Entry {
81 time: DateTime,
83 project: Option<String>,
85 text: String,
87 kind: EntryKind
89}
90
91impl Entry {
93 pub fn task_breakdown(entry_text: &str) -> (Option<String>, Option<String>) {
95 if entry_text.is_empty() {
96 return (None, None);
97 }
98
99 let task = PROJECT_RE.replace(entry_text, "").trim().to_string();
100 if let Some(caps) = TASKNAME_RE.captures(&task) {
101 if let Some(tname) = caps.get(1) {
102 let detail = TASKNAME_RE.replace(&task, "").trim().to_string();
103 let tname = tname.as_str().to_string();
104 return (Some(tname), (!detail.is_empty()).then_some(detail));
105 }
106 }
107 (None, (!task.is_empty()).then_some(task))
108 }
109
110 pub fn is_stop_line(line: &str) -> bool { STOP_LINE.is_match(line) }
112
113 pub fn is_event_line(line: &str) -> bool { EVENT_LINE.is_match(line) }
115
116 pub fn datetime_from_line(line: &str) -> Option<&str> {
118 if line.is_empty() || Self::is_comment_line(line) {
119 return None;
120 }
121
122 if let Some(caps) = LAX_LINE_RE.captures(line) {
123 return caps.get(1).map(|s| s.as_str());
124 }
125 None
126 }
127
128 pub fn date_from_line(line: &str) -> Option<&str> {
130 Self::datetime_from_line(line).and_then(|s| s.split_whitespace().next())
131 }
132
133 pub fn extract_year(line: &str) -> Option<i32> {
135 if Self::is_comment_line(line) {
136 return None;
137 }
138
139 YEAR_RE
140 .captures(line)
141 .and_then(|cap| cap[0].parse::<i32>().ok())
142 }
143
144 pub fn is_comment_line(line: &str) -> bool { line.starts_with('#') }
146}
147
148impl Entry {
150 pub fn new(entry_text: &str, time: DateTime) -> Self {
152 Self::new_marked(entry_text, time, EntryKind::Start)
153 }
154
155 pub fn new_marked(entry_text: &str, time: DateTime, kind: EntryKind) -> Self {
158 let kind = if kind == EntryKind::Start && entry_text == STOP_CMD {
159 EntryKind::Stop
160 }
161 else {
162 kind
163 };
164 let oproject = PROJECT_RE.captures(entry_text)
165 .and_then(|caps| caps.get(1).map(|m| String::from(m.as_str())));
166 Self { time, project: oproject, text: entry_text.into(), kind }
167 }
168
169 pub fn new_stop(time: DateTime) -> Self { Self::new_marked(STOP_CMD, time, EntryKind::Stop) }
171
172 pub fn from_line(line: &str) -> Result<Self, EntryError> {
180 if line.is_empty() {
181 return Err(EntryError::BlankLine);
182 }
183
184 match LAX_LINE_RE.captures(line) {
185 Some(caps) => {
186 let Some(stamp) = caps.get(1) else { return Err(EntryError::InvalidTimeStamp); };
187 let Ok(time) = stamp.as_str().parse::<DateTime>() else {
188 return Err(EntryError::InvalidTimeStamp);
189 };
190 let kind = EntryKind::try_new(caps.get(2).and_then(|m| m.as_str().chars().next()))?;
191 Ok(Entry::new_marked(
192 caps.get(3).map_or("", |m| m.as_str()),
193 time,
194 kind
195 ))
196 }
197 None => Err(if TIMESTAMP_RE.is_match(line) {
198 EntryError::MissingTask
199 }
200 else {
201 EntryError::InvalidTimeStamp
202 })
203 }
204 }
205}
206
207impl Entry {
209 pub fn project(&self) -> Option<&str> { self.project.as_deref() }
211
212 pub fn entry_text(&self) -> &str { &self.text }
214
215 pub fn task(&self) -> Option<String> { Self::task_breakdown(&self.text).0 }
217
218 pub fn detail(&self) -> Option<String> { Self::task_breakdown(&self.text).1 }
220
221 pub fn task_and_detail(&self) -> (Option<String>, Option<String>) {
223 Self::task_breakdown(&self.text)
224 }
225
226 pub fn epoch(&self) -> i64 { self.time.timestamp() }
228
229 pub fn date(&self) -> Date { self.time.date() }
231
232 pub fn date_time(&self) -> DateTime { self.time }
234
235 #[rustfmt::skip]
237 pub fn timestamp(&self) -> String {
238 format!("{} {:02}:{:02}", self.time.date(), self.time.hour(), self.time.minute())
239 }
240
241 pub fn stamp(&self) -> String { self.date().to_string() }
243
244 pub fn is_start(&self) -> bool { self.kind == EntryKind::Start }
246
247 pub fn is_stop(&self) -> bool { self.kind == EntryKind::Stop }
249
250 pub fn is_ignore(&self) -> bool { self.kind == EntryKind::Ignored }
252
253 pub fn ignore(&self) -> Self { Self { kind: EntryKind::Ignored, ..self.clone() } }
255
256 pub fn is_event(&self) -> bool { self.kind == EntryKind::Event }
258}
259
260impl Entry {
262 pub fn change_date_time(&self, date_time: DateTime) -> Self {
265 let mut entry = self.clone();
266 entry.time = date_time;
267 entry
268 }
269
270 pub fn change_text(&self, task: &str) -> Self {
273 if self.is_stop() { return self.clone(); }
274 Self::new_marked(task, self.time, self.kind)
275 }
276
277 pub fn to_day_end(&self) -> Self { Self { time: self.date().day_end(), ..self.clone() } }
279}
280
281impl Display for Entry {
282 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 let mark = match self.kind {
285 EntryKind::Ignored => '!',
286 EntryKind::Event => '^',
287 _ => ' '
288 };
289 write!(f, "{}{mark}{}", self.time, self.text)
290 }
291}
292
293impl PartialOrd for Entry {
294 #[rustfmt::skip]
296 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
297 Some(self.time.cmp(&other.time)
298 .then_with(|| self.text.cmp(&other.text)))
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use assert2::{assert, let_assert};
305 use rstest::rstest;
306
307 use super::*;
308
309 const CANONICAL_LINE: &str = "2013-06-05 10:00:02 +proj1 @do something";
310 const IGNORED_LINE: &str = "2013-06-05 10:00:02!+proj1 @do something";
311 const EVENT_LINE: &str = "2013-06-05 10:00:02^+proj1 @do something";
312 const STOP_LINE: &str = "2013-06-05 10:00:02 stop";
313
314 fn reference_time() -> i64 { DateTime::new((2013, 6, 5), (10, 0, 2)).expect("Hardcoded value").timestamp() }
315
316 #[test]
317 fn from_line_error_if_empty() {
318 assert!(Err(EntryError::BlankLine) == Entry::from_line(""));
319 }
320
321 #[rstest]
322 #[case("# Random comment", "simple comment")]
323 #[case("#2013-06-05 10:00:02 +test @Commented", "commented entry")]
324 fn is_comment_found(#[case]input: &str, #[case]msg: &str) {
325 assert!(Entry::is_comment_line(input), "{msg}");
326 }
327
328 #[rstest]
329 #[case("", "empty line")]
330 #[case("2013-06-05 10:00:02 +test @Commented", "entry line")]
331 fn is_comment_not_found(#[case]input: &str, #[case]msg: &str) {
332 assert!(!Entry::is_comment_line(input), "{msg}");
333 }
334
335 #[rstest]
336 #[case("", "empty line")]
337 #[case("# Random comment", "simple comment")]
338 #[case("#2013-06-05 10:00:02 +test @Commented", "commented entry")]
339 fn test_datetime_not_found(#[case]input: &str, #[case]msg: &str) {
340 assert!(None == Entry::datetime_from_line(input), "{msg}");
341 }
342
343 #[rstest]
344 #[case(CANONICAL_LINE, "2013-06-05 10:00:02", "entry line")]
345 #[case(IGNORED_LINE, "2013-06-05 10:00:02", "ignored line")]
346 #[case(EVENT_LINE, "2013-06-05 10:00:02", "event line")]
347 #[case(STOP_LINE, "2013-06-05 10:00:02", "stop line")]
348 fn test_datetime_from_line(#[case]input: &str, #[case]expected: &str, #[case]msg: &str) {
349 let_assert!(Some(dt) = Entry::datetime_from_line(input));
350 assert!(dt == expected, "{msg}");
351 }
352
353 #[rstest]
354 #[case("", "empty line")]
355 #[case("# Random comment", "simple comment")]
356 #[case("#2013-06-05 10:00:02 +test @Commented", "entry line")]
357 fn test_date_not_found(#[case]input: &str, #[case]msg: &str) {
358 assert!(None == Entry::date_from_line(input), "{msg}");
359 }
360
361 #[rstest]
362 #[case(CANONICAL_LINE, "2013-06-05", "entry line")]
363 #[case(IGNORED_LINE, "2013-06-05", "ignored line")]
364 #[case(EVENT_LINE, "2013-06-05", "event line")]
365 #[case(STOP_LINE, "2013-06-05", "stop line")]
366 fn test_date_from_line(#[case]input: &str, #[case]expected: &str, #[case]msg: &str) {
367 let_assert!(Some(dt) = Entry::date_from_line(input));
368 assert!(dt == expected, "{msg}");
369 }
370
371 #[test]
372 fn from_line_error_if_not_entry() {
373 assert!(Err(EntryError::InvalidTimeStamp) == Entry::from_line("This is not an entry"));
374 }
375
376 #[test]
377 fn from_line_canonical_entry() {
378 let_assert!(Ok(entry) = Entry::from_line(CANONICAL_LINE));
379 assert!(entry.stamp() == String::from("2013-06-05"));
380 assert!(entry.project() == Some("proj1"));
381 assert!(entry.entry_text() == "+proj1 @do something");
382 assert!(entry.task() == Some(String::from("do")));
383 assert!(entry.detail() == Some(String::from("something")));
384 assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
385 assert!(entry.epoch() == reference_time());
386 assert!(entry.to_string().as_str() == CANONICAL_LINE);
387 assert!(!entry.is_stop());
388 assert!(!entry.is_ignore());
389 assert!(!entry.is_event());
390 }
391
392 #[test]
393 fn new_canonical_entry() {
394 let_assert!(Ok(canonical_time) = "2013-06-05 10:00:02".parse::<DateTime>());
395 let entry = Entry::new("+proj1 @do something", canonical_time);
396 assert!(entry.stamp() == String::from("2013-06-05"));
397 assert!(entry.project() == Some("proj1"));
398 assert!(entry.entry_text() == "+proj1 @do something");
399 assert!(entry.task() == Some(String::from("do")));
400 assert!(entry.detail() == Some(String::from("something")));
401 assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
402 assert!(entry.epoch() == reference_time());
403 assert!(entry.to_string().as_str() == CANONICAL_LINE);
404 assert!(!entry.is_stop());
405 assert!(!entry.is_ignore());
406 assert!(!entry.is_event());
407 }
408
409 #[test]
410 fn from_line_no_task_entry() {
411 const LINE: &str = "2013-06-05 10:00:02 +proj1 do something";
412 let_assert!(Ok(entry) = Entry::from_line(LINE));
413 assert!(entry.stamp() == String::from("2013-06-05"));
414 assert!(entry.project() == Some("proj1"));
415 assert!(entry.entry_text() == "+proj1 do something");
416 assert!(entry.task() == None);
417 assert!(entry.detail() == Some(String::from("do something")));
418 assert!(entry.task_and_detail() == (None, Some(String::from("do something"))));
419 assert!(entry.epoch() == reference_time());
420 assert!(entry.to_string().as_str() == LINE);
421 assert!(!entry.is_stop());
422 assert!(!entry.is_ignore());
423 assert!(!entry.is_event());
424 }
425
426 #[test]
427 fn from_line_no_detail_entry() {
428 const LINE: &str = "2013-06-05 10:00:02 +proj1 @something";
429 let_assert!(Ok(entry) = Entry::from_line(LINE));
430 assert!(entry.stamp() == String::from("2013-06-05"));
431 assert!(entry.project() == Some("proj1"));
432 assert!(entry.entry_text() == "+proj1 @something");
433 assert!(entry.task() == Some(String::from("something")));
434 assert!(entry.detail() == None);
435 assert!(entry.task_and_detail() == (Some(String::from("something")), None));
436 assert!(entry.epoch() == reference_time());
437 assert!(entry.to_string().as_str() == LINE);
438 assert!(!entry.is_stop());
439 assert!(!entry.is_ignore());
440 assert!(!entry.is_event());
441 }
442
443 #[test]
444 fn from_line_no_entry_text() {
445 const LINE: &str = "2013-06-05 10:00:02 +proj1";
446 let_assert!(Ok(entry) = Entry::from_line(LINE));
447 assert!(entry.stamp() == String::from("2013-06-05"));
448 assert!(entry.project() == Some("proj1"));
449 assert!(entry.entry_text() == "+proj1");
450 assert!(entry.task() == None);
451 assert!(entry.detail() == None);
452 assert!(entry.task_and_detail() == (None, None));
453 assert!(entry.epoch() == reference_time());
454 assert!(entry.to_string().as_str() == LINE);
455 assert!(!entry.is_stop());
456 assert!(!entry.is_ignore());
457 assert!(!entry.is_event());
458 }
459
460 #[test]
461 fn from_line_stop_entry() {
462 let_assert!(Ok(entry) = Entry::from_line(STOP_LINE));
463 assert!(entry.stamp() == String::from("2013-06-05"));
464 assert!(entry.project() == None);
465 assert!(entry.entry_text() == "stop");
466 assert!(entry.task() == None);
467 assert!(entry.detail() == Some(String::from("stop")));
468 assert!(entry.task_and_detail() == (None, Some(String::from("stop"))));
469 assert!(entry.epoch() == reference_time());
470 assert!(entry.to_string().as_str() == STOP_LINE);
471 assert!(entry.is_stop());
472 assert!(!entry.is_ignore());
473 assert!(!entry.is_event());
474 }
475
476 #[test]
477 fn test_extract_year() {
478 let line = "2018-11-20 12:34:43 +test @Event";
479 assert!(Some(2018) == Entry::extract_year(line));
480 }
481
482 #[test]
483 fn test_extract_year_fail() {
484 let line = "xyzzy 2018-11-20 12:34:43 +test @Event";
485 assert!(Entry::extract_year(line) == None);
486 }
487
488 #[test]
489 fn new_stop_entry() {
490 let_assert!(Ok(canonical_time) = "2013-06-05 10:00:02".parse::<DateTime>());
491 let entry = Entry::new("stop", canonical_time);
492 assert!(entry.stamp() == String::from("2013-06-05"));
493 assert!(entry.project() == None);
494 assert!(entry.entry_text() == "stop");
495 assert!(entry.task() == None);
496 assert!(entry.detail() == Some(String::from("stop")));
497 assert!(entry.task_and_detail() == (None, Some(String::from("stop"))));
498 assert!(entry.epoch() == reference_time());
499 assert!(entry.to_string().as_str() == STOP_LINE);
500 assert!(entry.is_stop());
501 assert!(!entry.is_ignore());
502 assert!(!entry.is_event());
503 }
504
505 #[test]
506 fn from_line_ignored_entry() {
507 let_assert!(Ok(entry) = Entry::from_line(IGNORED_LINE));
508 assert!(entry.stamp() == String::from("2013-06-05"));
509 assert!(entry.project() == Some("proj1"));
510 assert!(entry.entry_text() == "+proj1 @do something");
511 assert!(entry.task() == Some(String::from("do")));
512 assert!(entry.detail() == Some(String::from("something")));
513 assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
514 assert!(entry.epoch() == reference_time());
515 assert!(entry.to_string().as_str() == IGNORED_LINE);
516 assert!(!entry.is_stop());
517 assert!(entry.is_ignore());
518 assert!(!entry.is_event());
519 }
520
521 #[test]
522 fn new_ignored_entry() {
523 let_assert!(Ok(canonical_time) = "2013-06-05 10:00:02".parse::<DateTime>());
524 let entry = Entry::new_marked("+proj1 @do something", canonical_time, EntryKind::Ignored);
525 assert!(entry.stamp() == String::from("2013-06-05"));
526 assert!(entry.project() == Some("proj1"));
527 assert!(entry.entry_text() == "+proj1 @do something");
528 assert!(entry.task() == Some(String::from("do")));
529 assert!(entry.detail() == Some(String::from("something")));
530 assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
531 assert!(entry.epoch() == reference_time());
532 assert!(entry.to_string().as_str() == IGNORED_LINE);
533 assert!(!entry.is_stop());
534 assert!(entry.is_ignore());
535 assert!(!entry.is_event());
536 }
537
538 #[test]
539 fn from_line_event_entry() {
540 let_assert!(Ok(entry) = Entry::from_line(EVENT_LINE));
541 assert!(entry.stamp() == String::from("2013-06-05"));
542 assert!(entry.project() == Some("proj1"));
543 assert!(entry.entry_text() == "+proj1 @do something");
544 assert!(entry.task() == Some(String::from("do")));
545 assert!(entry.detail() == Some(String::from("something")));
546 assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
547 assert!(entry.epoch() == reference_time());
548 assert!(entry.to_string().as_str() == EVENT_LINE);
549 assert!(!entry.is_stop());
550 assert!(!entry.is_ignore());
551 assert!(entry.is_event());
552 }
553
554 #[test]
555 fn new_event_entry() {
556 let_assert!(Ok(canonical_time) = "2013-06-05 10:00:02".parse::<DateTime>());
557 let entry = Entry::new_marked("+proj1 @do something", canonical_time, EntryKind::Event);
558 assert!(entry.stamp() == String::from("2013-06-05"));
559 assert!(entry.project() == Some("proj1"));
560 assert!(entry.entry_text() == "+proj1 @do something");
561 assert!(entry.task() == Some(String::from("do")));
562 assert!(entry.detail() == Some(String::from("something")));
563 assert!(entry.task_and_detail() == (Some(String::from("do")), Some(String::from("something"))));
564 assert!(entry.epoch() == reference_time());
565 assert!(entry.to_string().as_str() == EVENT_LINE);
566 assert!(!entry.is_stop());
567 assert!(!entry.is_ignore());
568 assert!(entry.is_event());
569 }
570
571 #[test]
572 fn compare_entry() {
573 let_assert!(Ok(entry1) = Entry::from_line("2013-06-05 10:00:02 +proj1"));
574 let_assert!(Ok(entry2) = Entry::from_line("2013-06-05 11:00:02 +proj1"));
575 assert!(entry2 > entry1);
576 assert!(entry1 < entry2);
577 assert!(entry1 == entry1);
578 }
579
580 #[test]
581 fn test_change_date_time_start() {
582 let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00 +proj1"));
583 let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
584 let new_entry = entry.change_date_time(dt);
585
586 assert!(new_entry != entry);
587 let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
588 assert!(new_entry.date_time() == dt);
589 assert!(new_entry.entry_text() == entry.entry_text());
590 assert!(new_entry.is_start());
591 }
592
593 #[test]
594 fn test_change_date_time_event() {
595 let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00^+event"));
596 let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
597 let new_entry = entry.change_date_time(dt);
598
599 assert!(new_entry != entry);
600 let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
601 assert!(new_entry.date_time() == dt);
602 assert!(new_entry.entry_text() == entry.entry_text());
603 assert!(new_entry.is_event());
604 }
605
606 #[test]
607 fn test_change_date_time_ignored() {
608 let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00!+proj1"));
609 let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
610 let new_entry = entry.change_date_time(dt);
611
612 assert!(new_entry != entry);
613 let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
614 assert!(new_entry.date_time() == dt);
615 assert!(new_entry.entry_text() == entry.entry_text());
616 assert!(new_entry.is_ignore());
617 }
618
619 #[test]
620 fn test_change_date_time_stop() {
621 let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00 stop"));
622 let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
623 let new_entry = entry.change_date_time(dt);
624
625 assert!(new_entry != entry);
626 let_assert!(Ok(dt) = DateTime::new((2022, 12, 27), (9, 50, 00)));
627 assert!(new_entry.date_time() == dt);
628 assert!(new_entry.entry_text() == entry.entry_text());
629 assert!(new_entry.is_stop());
630 }
631
632 #[test]
633 fn test_change_text_start() {
634 let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00 +proj1"));
635 let new_entry = entry.change_text("+proj2 @Changed");
636
637 assert!(new_entry != entry);
638 assert!(new_entry.date_time() == entry.date_time());
639 assert!(new_entry.entry_text() == "+proj2 @Changed");
640 assert!(new_entry.is_start());
641 }
642
643 #[test]
644 fn test_change_text_event() {
645 let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00^+event"));
646 let new_entry = entry.change_text("+proj2 @Changed");
647
648 assert!(new_entry != entry);
649 assert!(new_entry.date_time() == entry.date_time());
650 assert!(new_entry.entry_text() == "+proj2 @Changed");
651 assert!(new_entry.is_event());
652 }
653
654 #[test]
655 fn test_change_text_ignored() {
656 let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00!+proj1"));
657 let new_entry = entry.change_text("+proj2 @Changed");
658
659 assert!(new_entry != entry);
660 assert!(new_entry.date_time() == entry.date_time());
661 assert!(new_entry.entry_text() == "+proj2 @Changed");
662 assert!(new_entry.is_ignore());
663 }
664
665 #[test]
666 fn test_change_text_stop() {
667 let_assert!(Ok(entry) = Entry::from_line("2022-12-27 10:00:00 stop"));
668 let new_entry = entry.change_text("+proj2 @Changed");
669
670 assert!(new_entry == entry);
671 assert!(new_entry.date_time() == entry.date_time());
672 assert!(new_entry.entry_text() == "stop");
673 assert!(new_entry.is_stop());
674 }
675}