Skip to content

Commit 01dcf76

Browse files
authored
Merge pull request #14 from stephlm2dev/feature/aggregate-data
Draft data aggregation
2 parents dc25b5e + e76594b commit 01dcf76

9 files changed

Lines changed: 205 additions & 62 deletions

File tree

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v10.15.1
1+
v10.16.0

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4+
5+
## 0.0.0 (2020-01-06)

DEVELOPMENT.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Development
2+
3+
## Commits
4+
5+
This package follows rules of:
6+
7+
- https://github.com/commitizen/cz-cli/
8+
- https://github.com/conventional-changelog/standard-version

README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ task-warrior-export
33

44
Export {task/time}warrior data per month/per project
55

6+
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
67
[![Version](https://img.shields.io/npm/v/task-warrior-export.svg)](https://npmjs.org/package/task-warrior-export)
78
[![CircleCI](https://circleci.com/gh/stephlm2dev/task-warrior-export/tree/master.svg?style=shield)](https://circleci.com/gh/stephlm2dev/task-warrior-export/tree/master)
89
[![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/stephlm2dev/task-warrior-export?branch=master&svg=true)](https://ci.appveyor.com/project/stephlm2dev/task-warrior-export/branch/master)
@@ -35,23 +36,29 @@ USAGE
3536

3637
## `twe hello [FILE]`
3738

38-
describe the command here
39+
export data for a specific date / project
3940

4041
```
4142
USAGE
42-
$ twe hello [FILE]
43+
$ twe export
4344
4445
OPTIONS
45-
-f, --force
46-
-h, --help show CLI help
47-
-n, --name=name name to print
46+
-f, --format=format output format (ie "json" or "csv")
47+
-f, --from=from start date with format "YYYY-MM-DD"
48+
-h, --help display help
49+
-i, --interactive interactive mode
50+
-p, --project=project name of the project
51+
-t, --to=to end date with format "YYYY-MM-DD"
4852
49-
EXAMPLE
50-
$ twe hello
51-
hello world from ./src/hello.ts!
53+
EXAMPLES
54+
$ twe export --interactive
55+
56+
$ twe export --format=json --interactive
57+
58+
$ twe export --format=csv --project=name --from=2019-12-01 --to=2019-12-15
5259
```
5360

54-
_See code: [src/commands/hello.ts](https://github.com/stephlm2dev/task-warrior-export/blob/v0.0.0/src/commands/hello.ts)_
61+
_See code: [src/commands/export.ts](https://github.com/stephlm2dev/task-warrior-export/blob/v0.0.0/src/commands/export.ts)_
5562

5663
## `twe help [COMMAND]`
5764

docs/project-from-to.csv

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
task,start,end,hours_duration,human_duration
2+
<task-description>,<start-datetime>,<end-datetime>,<hours_duration>,<human_duration>
3+
,<start-datetime>,<end-datetime>,<hours_duration>,<human_duration>
4+
,<start-datetime>,<end-datetime>,<hours_duration>,<human_duration>
5+
, , ,<total_hours_duration>,<total_human_duration>
6+
7+
<count-tasks> tasks, , ,<total-duration-in-hours>,<total-duration-in-human-hours>

docs/project-from-to.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[
2+
{
3+
"task": "<task-description>",
4+
"events": [
5+
{
6+
"start": "<start-datetime>",
7+
"end": "<end-datetime>",
8+
"hours_duration": "<duration-in-hours>"
9+
"human_duration": "<duration-in-human-hours>"
10+
}
11+
],
12+
"total": {
13+
"hours_duration": "<total-duration-in-hours>"
14+
"human_duration": "<total-duration-in-human-hours>"
15+
}
16+
},
17+
{
18+
"tasks": "<count-tasks> tasks",
19+
"hours_duration": "<total-duration-in-hours>"
20+
"human_duration": "<total-duration-in-human-hours>"
21+
}
22+
]

package.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@
2828
"@types/mocha": "5",
2929
"@types/node": "9",
3030
"chai": "4",
31+
"commitizen": "^4.0.3",
32+
"cz-conventional-changelog": "^3.0.2",
3133
"globby": "8",
3234
"mocha": "5",
3335
"nyc": "11",
36+
"standard-version": "^7.0.1",
3437
"ts-node": "5",
3538
"tslib": "1",
3639
"tslint": "5",
@@ -63,7 +66,15 @@
6366
"posttest": "tsc -p test --noEmit && tslint -p test -t stylish",
6467
"prepack": "rm -rf lib && tsc && oclif-dev manifest && oclif-dev readme",
6568
"test": "nyc mocha --forbid-only \"test/**/*.test.ts\"",
66-
"version": "oclif-dev readme && git add README.md"
69+
"version": "oclif-dev readme && git add README.md",
70+
"commit": "git-cz",
71+
"release": "standard-version -s",
72+
"release:beta": "standard-version -s -p beta"
6773
},
68-
"types": "lib/index.d.ts"
74+
"types": "lib/index.d.ts",
75+
"config": {
76+
"commitizen": {
77+
"path": "./node_modules/cz-conventional-changelog"
78+
}
79+
}
6980
}

src/commands/export.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import {
77
} from '../utils/commands/export/constants'
88
import ExportUi from '../utils/commands/export/ui'
99
import ExportUtils from '../utils/commands/export/utils'
10-
import ExportValidator, {
11-
TimetrackingValidator
12-
} from '../utils/commands/export/validators'
10+
import ExportValidator from '../utils/commands/export/validators'
1311

1412
export default class Export extends Command {
1513
static description = 'export data for a specific date / project'
@@ -56,11 +54,14 @@ export default class Export extends Command {
5654
this.checkParams(tools, params, availableFlagsValues)
5755

5856
// Step 4 - Filter data
59-
const data = this.filterData(tools, params, TIMETRACKING)
57+
const filteredData = tools.utils.filterData(tools, params, TIMETRACKING)
58+
59+
// Step 5 - Aggregate data
60+
const aggregatedData = tools.utils.aggregateData(tools, filteredData)
6061

61-
// (FIXME) Step 5 - Aggregate data
6262
// Step 6 - Save data
63-
await tools.utils.saveFile(data, params)
63+
await tools.utils.saveFile(filteredData, params, 'raw')
64+
await tools.utils.saveFile(aggregatedData, params, 'aggregated')
6465
}
6566

6667
/**
@@ -120,19 +121,4 @@ export default class Export extends Command {
120121
}
121122
return true
122123
}
123-
124-
/**
125-
* Filter timetracking data from params
126-
*/
127-
private filterData(tools, params: any, timetracking: Array<any>) {
128-
return timetracking.reduce((acc: Array<any>, tracking: any) => {
129-
let valid: boolean | string = new TimetrackingValidator().isValid(
130-
tracking, params
131-
)
132-
if (valid) {
133-
tracking = tools.utils.formatTimetracking(tracking)
134-
}
135-
return valid ? acc.concat([tracking]) : acc
136-
}, [])
137-
}
138124
}

src/utils/commands/export/utils.ts

Lines changed: 126 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { parse } from 'json2csv'
33
import * as moment from 'moment'
44
import { EOL } from 'os'
55

6+
import { TimetrackingValidator } from './validators'
7+
68
export default class ExportUtils {
79
constructor() {}
810

@@ -17,46 +19,56 @@ export default class ExportUtils {
1719
}
1820

1921
/**
20-
* Format timetracking to standart format
22+
* Filter timetracking data from params
2123
*/
22-
public formatTimetracking(tracking: any) {
23-
// {
24-
// "start":"20190612T150556Z",
25-
// "end":"20190612T170000Z",
26-
// "tags":["#sideproject","Task/time warrior export"]
27-
// }
28-
const startDate = moment(tracking.start)
29-
const endDate = moment(tracking.end)
30-
const duration = moment.duration(endDate.diff(startDate))
31-
const formatDatetime = 'DD/MM/YYYY HH:mm'
24+
public filterData(tools: any, params: any, timetracking: Array<any>) {
25+
return timetracking.reduce((acc: Array<any>, tracking: any) => {
26+
let valid: boolean | string = new TimetrackingValidator().isValid(
27+
tracking, params
28+
)
29+
if (valid) {
30+
tracking = this.formatTimetracking(tracking)
31+
}
32+
return valid ? acc.concat([tracking]) : acc
33+
}, [])
34+
}
3235

33-
const [project, description, ...tags] = tracking.tags
34-
return {
35-
start: startDate.format(formatDatetime),
36-
end: endDate.format(formatDatetime),
37-
duration: moment({
38-
hour: duration.hours(),
39-
minute: duration.minutes()
40-
}).format('HH:mm'),
41-
project,
42-
description
43-
}
36+
/**
37+
* Aggregate time tracking for stats
38+
*/
39+
public aggregateData(tools: any, filteredData: Array<any>) {
40+
const totalDurations = filteredData.reduce(this.aggregatePerDay, {})
41+
const totalDurationsAsArray = Object.keys(totalDurations).map(
42+
trackingDate => {
43+
const [hours, minutes] = totalDurations[trackingDate].split(':')
44+
return {
45+
date: trackingDate,
46+
total: `${parseInt(hours, 10)}h ${minutes}min`,
47+
human: this.convertToManHours(totalDurations[trackingDate])
48+
}
49+
}
50+
)
51+
const total = totalDurationsAsArray.reduce(this.totalAggregate, {
52+
total: 0, human: 0
53+
})
54+
return totalDurationsAsArray.concat([total])
4455
}
4556

4657
/**
4758
* Save file on disk
4859
*/
49-
public saveFile(data: Array<any>, params: any) {
60+
public saveFile(data: Array<any>, params: any, prefix: string) {
5061
// #neurodecisions$2019-01-12$2019-12-12.csv
62+
const format = params.format
5163
const fromDate = params.from.replace(/-/g, '')
5264
const toDate = params.to.replace(/-/g, '')
5365
const project = params.project.replace(/[^a-zA-Z ]/g, '')
54-
const filename = `${project}-${fromDate}-${toDate}.${params.format}`
66+
const filename = `${project}-${fromDate}-${toDate}-${prefix}.${format}`
5567
let writeData = null
56-
if (params.format === 'json') {
68+
if (format === 'json') {
5769
writeData = this.formatAsJson(data)
58-
} else if (params.format === 'csv') {
59-
writeData = this.formatAsCsv(data)
70+
} else if (format === 'csv') {
71+
writeData = this.formatAsCsv(data, prefix)
6072
}
6173
try {
6274
fs.writeFileSync(filename, writeData)
@@ -78,6 +90,32 @@ export default class ExportUtils {
7890
return `${title}${EOL}${details}`
7991
}
8092

93+
/**
94+
* Format timetracking to standart format
95+
*/
96+
private formatTimetracking(tracking: any) {
97+
// {
98+
// "start":"20190612T150556Z",
99+
// "end":"20190612T170000Z",
100+
// "tags":["#sideproject","Task/time warrior export"]
101+
// }
102+
const startDate = moment(tracking.start)
103+
const endDate = moment(tracking.end)
104+
const duration = moment.duration(endDate.diff(startDate))
105+
const formatDatetime = 'DD/MM/YYYY HH:mm'
106+
107+
const [project, description, ...tags] = tracking.tags
108+
return {
109+
description,
110+
start: startDate.format(formatDatetime),
111+
end: endDate.format(formatDatetime),
112+
duration: moment({
113+
hour: duration.hours(),
114+
minute: duration.minutes()
115+
}).format('HH:mm')
116+
}
117+
}
118+
81119
/**
82120
* Stringify data
83121
*/
@@ -88,12 +126,71 @@ export default class ExportUtils {
88126
/**
89127
* Convert JSON as CSV
90128
*/
91-
private formatAsCsv(data) {
92-
const headers = ['start', 'end', 'duration', 'project', 'description']
129+
private formatAsCsv(data, prefix) {
130+
let headers = null
131+
if (prefix === 'raw') {
132+
headers = ['description', 'start', 'end', 'duration']
133+
} else {
134+
headers = ['date', 'total', 'human']
135+
}
93136
try {
94137
return parse(data, { fields: headers })
95138
} catch (err) {
96139
// FIXME handle error (err)
97140
}
98141
}
142+
143+
/**
144+
* Aggregate time tracking per day
145+
*/
146+
private aggregatePerDay(acc: any, tracking: any) {
147+
const startDate = moment(tracking.start, 'DD/MM/YYYY HH:mm')
148+
const formattedStartDate = startDate.format('DD/MM/YYYY')
149+
if (formattedStartDate in acc) {
150+
const duration = moment.duration(acc[formattedStartDate]).add(
151+
moment.duration(tracking.duration)
152+
)
153+
acc[formattedStartDate] = moment({
154+
hour: duration.hours(),
155+
minute: duration.minutes()
156+
}).format('HH:mm')
157+
} else {
158+
acc[formattedStartDate] = tracking.duration
159+
}
160+
return acc
161+
}
162+
163+
/**
164+
* Convert duration in hours to man hours
165+
* 0,25 -> 2h
166+
* 0,50 -> 4h (half day)
167+
* 0,75 -> 6h
168+
* 1,00 -> 8h (full day)
169+
*/
170+
private convertToManHours(duration: string) {
171+
const decimalHours = moment.duration(duration).asHours()
172+
const rawManHours = (decimalHours * 0.25) / 2
173+
let roundedManHours = 0
174+
if (rawManHours <= 0.25) {
175+
roundedManHours = 0.25
176+
} else if (rawManHours > 0.25 && rawManHours <= 0.5) {
177+
roundedManHours = 0.5
178+
} else if (rawManHours > 0.5 && rawManHours <= 0.75) {
179+
roundedManHours = 0.75
180+
} else if (rawManHours > 0.75) {
181+
roundedManHours = 1
182+
}
183+
return roundedManHours
184+
}
185+
186+
/**
187+
* Total aggregate of all time tracking
188+
*/
189+
private totalAggregate(acc: any, trackingStats: any) {
190+
// FIXME issue with total
191+
return {
192+
total: acc.total,
193+
human: acc.human + trackingStats.human
194+
}
195+
}
99196
}

0 commit comments

Comments
 (0)