Skip to content

Commit d301240

Browse files
authored
Merge pull request #47 from PicoJr/dev
Prepare release 2.1.0
2 parents d9dc0ec + 04e280c commit d301240

File tree

9 files changed

+154
-19
lines changed

9 files changed

+154
-19
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.1.0](https://crates.io/crates/rtw/2.1.0) Dec 8, 2020
9+
10+
* Add `--report` option to summary command.
11+
812
## [2.0.1](https://crates.io/crates/rtw/2.0.1) Nov 3, 2020
913

1014
* Fix CLI output for Windows 10 cf [#43](https://github.com/PicoJr/rtw/pull/43) thanks [ythri](https://github.com/ythri)

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rtw"
3-
version = "2.0.1"
3+
version = "2.1.0"
44
authors = ["PicoJr <picojr_dev@gmx.com>"]
55
edition = "2018"
66
repository = "https://github.com/PicoJr/rtw"

commands.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* [Display finished activities summary for last week](#display-finished-activities-summary-for-last-week)
1818
* [Display finished activities summary for range](#display-finished-activities-summary-for-range)
1919
* [Display finished activities id](#display-finished-activities-id)
20+
* [Display a report (sum same activities)](#display-a-report-sum-same-activities)
2021
* [Display a timeline](#display-a-timeline)
2122
* [For the day](#for-the-day)
2223
* [For the week](#for-the-week)
@@ -208,6 +209,22 @@ Example output:
208209

209210
> id 0 = last finished activity
210211
212+
### Display a report (sum same activities)
213+
214+
Example:
215+
```
216+
rtw track 8 - 9 foo
217+
rtw track 9 - 10 foo
218+
rtw track 10 - 11 bar
219+
rtw summary --report
220+
```
221+
222+
Example output:
223+
```
224+
foo 02:00:00 (2 segments)
225+
bar 01:00:00 (1 segments)
226+
```
227+
211228
## Display a timeline
212229

213230
### For the day

src/cli_helper.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ pub fn get_app() -> App<'static, 'static> {
217217
.short("d")
218218
.long("description")
219219
.help("display activities descriptions"),
220+
)
221+
.arg(
222+
Arg::with_name("report")
223+
.short("r")
224+
.long("report")
225+
.help("sum up activities with same tag together"),
220226
),
221227
)
222228
.subcommand(
@@ -363,8 +369,9 @@ pub fn parse_cancel_args(cancel_m: &ArgMatches) -> anyhow::Result<Option<Activit
363369
pub fn parse_summary_args(
364370
summary_m: &ArgMatches,
365371
clock: &dyn Clock,
366-
) -> anyhow::Result<((DateTimeW, DateTimeW), bool, bool)> {
372+
) -> anyhow::Result<((DateTimeW, DateTimeW), bool, bool, bool)> {
367373
let display_id = summary_m.is_present("id");
374+
let report = summary_m.is_present("report");
368375
let display_description = summary_m.is_present("description");
369376
let values_arg = summary_m.values_of("tokens");
370377
if let Some(values) = values_arg {
@@ -374,7 +381,12 @@ pub fn parse_summary_args(
374381
Ok((range_start, range_end)) => {
375382
let range_start = clock.date_time(range_start);
376383
let range_end = clock.date_time(range_end);
377-
Ok(((range_start, range_end), display_id, display_description))
384+
Ok((
385+
(range_start, range_end),
386+
display_id,
387+
display_description,
388+
report,
389+
))
378390
}
379391
Err(e) => Err(anyhow::anyhow!(e)),
380392
};
@@ -390,7 +402,7 @@ pub fn parse_summary_args(
390402
clock.today_range()
391403
}
392404
};
393-
Ok((range, display_id, display_description))
405+
Ok((range, display_id, display_description, report))
394406
}
395407

396408
pub fn parse_timeline_args(

src/rtw_cli.rs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ use crate::rtw_config::RTWConfig;
66
use crate::rtw_core::activity::{Activity, OngoingActivity};
77
use crate::rtw_core::clock::Clock;
88
use crate::rtw_core::datetimew::DateTimeW;
9+
use crate::rtw_core::durationw::DurationW;
910
use crate::rtw_core::service::ActivityService;
1011
use crate::rtw_core::storage::Storage;
1112
use crate::rtw_core::ActivityId;
1213
use crate::rtw_core::{Description, Tags};
1314
use crate::service::Service;
1415
use crate::timeline::render_days;
1516
use clap::ArgMatches;
17+
use itertools::Itertools;
1618

1719
type ActivityWithId = (ActivityId, Activity);
1820

@@ -24,7 +26,7 @@ pub enum RTWAction {
2426
Start(DateTimeW, Tags, Option<Description>),
2527
Track((DateTimeW, DateTimeW), Tags, Option<Description>),
2628
Stop(DateTimeW, Option<ActivityId>),
27-
Summary((DateTimeW, DateTimeW), bool, bool),
29+
Summary((DateTimeW, DateTimeW), bool, bool, bool),
2830
DumpICal((DateTimeW, DateTimeW)),
2931
Continue,
3032
Delete(ActivityId),
@@ -48,6 +50,27 @@ enum OptionalOrAmbiguousOrNotFound {
4850
NotFound(ActivityId),
4951
}
5052

53+
fn merge_same_tags(activities: &[ActivityWithId]) -> Vec<(ActivityId, Activity, DurationW, usize)> {
54+
let uniques: Vec<ActivityWithId> = activities
55+
.iter()
56+
.cloned()
57+
.unique_by(|(_i, activity)| activity.get_title())
58+
.collect();
59+
uniques
60+
.iter()
61+
.cloned()
62+
.map(|(i, activity)| {
63+
let same_tag = activities
64+
.iter()
65+
.filter(|(_i, other)| activity.get_title() == other.get_title());
66+
let durations: Vec<DurationW> = same_tag.map(|(_i, a)| a.get_duration()).collect();
67+
let segments = durations.len();
68+
let duration = durations.into_iter().sum();
69+
(i, activity, duration, segments)
70+
})
71+
.collect()
72+
}
73+
5174
fn get_ongoing_activity<S: Storage>(
5275
id_maybe: Option<ActivityId>,
5376
service: &Service<S>,
@@ -90,12 +113,13 @@ where
90113
Ok(RTWAction::Stop(abs_stop_time, stopped_id_maybe))
91114
}
92115
("summary", Some(sub_m)) => {
93-
let ((range_start, range_end), display_id, display_description) =
116+
let ((range_start, range_end), display_id, display_description, report) =
94117
cli_helper::parse_summary_args(sub_m, clock)?;
95118
Ok(RTWAction::Summary(
96119
(range_start, range_end),
97120
display_id,
98121
display_description,
122+
report,
99123
))
100124
}
101125
("timeline", Some(sub_m)) => {
@@ -128,7 +152,7 @@ where
128152
Ok(RTWAction::Cancel(cancelled_id_maybe))
129153
}
130154
("dump", Some(sub_m)) => {
131-
let ((range_start, range_end), _display_id, _description) =
155+
let ((range_start, range_end), _display_id, _description, _report) =
132156
cli_helper::parse_summary_args(sub_m, clock)?;
133157
Ok(RTWAction::DumpICal((range_start, range_end)))
134158
}
@@ -191,7 +215,7 @@ where
191215
}
192216
}
193217
}
194-
RTWAction::Summary((range_start, range_end), display_id, display_description) => {
218+
RTWAction::Summary((range_start, range_end), display_id, display_description, report) => {
195219
let activities = service.get_finished_activities()?;
196220
let activities: Vec<(ActivityId, Activity)> = activities
197221
.iter()
@@ -207,6 +231,25 @@ where
207231
.unwrap_or_default();
208232
if activities.is_empty() {
209233
println!("No filtered data found.");
234+
} else if report {
235+
let activities_report = merge_same_tags(activities.as_slice());
236+
for (_id, finished, duration, segments) in activities_report {
237+
let singular_or_plural = if segments <= 1 {
238+
String::from("segment")
239+
} else {
240+
// segments > 1
241+
String::from("segments")
242+
};
243+
let output = format!(
244+
"{:width$} {} ({} {})",
245+
finished.get_title(),
246+
duration,
247+
segments,
248+
singular_or_plural,
249+
width = longest_title
250+
);
251+
println!("{}", output)
252+
}
210253
} else {
211254
for (id, finished) in activities {
212255
let output = format!(

src/rtw_core/durationw.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
use chrono::Duration;
33
use std::fmt;
44
use std::fmt::{Error, Formatter};
5+
use std::iter::Sum;
6+
use std::ops::Add;
57

68
/// Newtype on `chrono::Duration`
79
pub struct DurationW(chrono::Duration);
@@ -24,6 +26,12 @@ impl DurationW {
2426
}
2527
}
2628

29+
impl Default for DurationW {
30+
fn default() -> Self {
31+
DurationW::new(Duration::seconds(0))
32+
}
33+
}
34+
2735
impl From<Duration> for DurationW {
2836
fn from(d: Duration) -> Self {
2937
DurationW(d)
@@ -35,3 +43,17 @@ impl Into<Duration> for DurationW {
3543
self.0
3644
}
3745
}
46+
47+
impl Add<DurationW> for DurationW {
48+
type Output = DurationW;
49+
50+
fn add(self, rhs: DurationW) -> Self::Output {
51+
DurationW::new(self.0 + rhs.0)
52+
}
53+
}
54+
55+
impl Sum for DurationW {
56+
fn sum<I: Iterator<Item = DurationW>>(iter: I) -> Self {
57+
iter.fold(DurationW::default(), Add::add)
58+
}
59+
}

src/timeline.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,9 @@ pub(crate) fn render_days(activities: &[Interval], colors: &[RGB]) -> anyhow::Re
199199
.collect();
200200
let day_month = day_activities
201201
.first()
202-
.and_then(|(_, a)| {
202+
.map(|(_, a)| {
203203
let start_time: DateTime<Local> = a.get_start_time().into();
204-
Some(start_time.format("%d/%m").to_string())
204+
start_time.format("%d/%m").to_string()
205205
})
206206
.unwrap_or_else(|| "??/??".to_string());
207207
let total: DurationW = DurationW::from(day_total(day_activities.as_slice()));
@@ -213,22 +213,22 @@ pub(crate) fn render_days(activities: &[Interval], colors: &[RGB]) -> anyhow::Re
213213
.with_length(available_length)
214214
.with_boundaries((min_second, max_second))
215215
.render()
216-
.or_else(|e| match e {
217-
TBLError::NoBoundaries => Err(anyhow!("failed to create timeline")),
218-
TBLError::Intersection(left, right) => Err(anyhow!(
216+
.map_err(|e| match e {
217+
TBLError::NoBoundaries => anyhow!("failed to create timeline"),
218+
TBLError::Intersection(left, right) => anyhow!(
219219
"failed to create timeline: some activities are overlapping: {:?} intersects {:?}", left, right
220-
)),
220+
),
221221
})?;
222222
let legend = Renderer::new(day_activities.as_slice(), &bounds, &legend)
223223
.with_renderer(&render)
224224
.with_length(available_length)
225225
.with_boundaries((min_second, max_second))
226226
.render()
227-
.or_else(|e| match e {
228-
TBLError::NoBoundaries => Err(anyhow!("failed to create timeline")),
229-
TBLError::Intersection(left, right) => Err(anyhow!(
227+
.map_err(|e| match e {
228+
TBLError::NoBoundaries => anyhow!("failed to create timeline"),
229+
TBLError::Intersection(left, right) => anyhow!(
230230
"failed to create timeline: some activities are overlapping: {:?} intersects {:?}", left, right
231-
)),
231+
),
232232
})?;
233233
let timeline = legend.iter().zip(data.iter());
234234
for (i, (legend_timelines, data_timelines)) in timeline.enumerate() {

tests/integration_test.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,43 @@ mod tests {
8686
.success();
8787
}
8888

89+
#[test]
90+
fn summary_something_with_report() {
91+
let test_dir = tempdir().expect("could not create temp directory");
92+
let test_dir_path = test_dir.path().to_str().unwrap();
93+
let mut cmd = Command::cargo_bin("rtw").unwrap();
94+
cmd.arg("-d")
95+
.arg(test_dir_path)
96+
.arg("track")
97+
.arg("09:00")
98+
.arg("-")
99+
.arg("10:00")
100+
.arg("foo")
101+
.assert()
102+
.success();
103+
let mut cmd = Command::cargo_bin("rtw").unwrap();
104+
cmd.arg("-d")
105+
.arg(test_dir_path)
106+
.arg("track")
107+
.arg("10:00")
108+
.arg("-")
109+
.arg("11:00")
110+
.arg("foo")
111+
.assert()
112+
.success();
113+
let mut cmd = Command::cargo_bin("rtw").unwrap();
114+
cmd.arg("-d")
115+
.arg(test_dir_path)
116+
.arg("summary")
117+
.arg("--report")
118+
.arg("08:00")
119+
.arg("-")
120+
.arg("12:00")
121+
.assert()
122+
.success()
123+
.stdout(predicates::str::contains("foo 02:00:00 (2 segments)\n"));
124+
}
125+
89126
#[test]
90127
fn dump_ical_nothing() {
91128
let test_dir = tempdir().expect("could not create temp directory");

0 commit comments

Comments
 (0)