Skip to content

Commit dc25b5e

Browse files
author
Schmidely Stéphane
authored
Merge pull request #9 from stephlm2dev/feature/process-data
Extract raw data (without data aggregation)
2 parents 1625ce5 + 8764c0e commit dc25b5e

8 files changed

Lines changed: 231 additions & 11 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"@types/inquirer": "^6.0.3",
1616
"@types/shelljs": "^0.8.5",
1717
"inquirer": "^6.3.1",
18+
"json2csv": "^4.5.1",
1819
"moment": "^2.24.0",
20+
"moment-range": "^4.0.2",
1921
"shelljs": "^0.8.3"
2022
},
2123
"devDependencies": {

src/commands/export.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import { Command } from '@oclif/command'
22
import { EOL } from 'os'
33

44
// Our files - FIXME path import
5-
import { FLAGS, FORMATS, PROJECTS } from '../utils/commands/export/constants'
5+
import {
6+
FLAGS, FORMATS, PROJECTS, TIMETRACKING
7+
} from '../utils/commands/export/constants'
68
import ExportUi from '../utils/commands/export/ui'
79
import ExportUtils from '../utils/commands/export/utils'
8-
import ExportValidator from '../utils/commands/export/validators'
10+
import ExportValidator, {
11+
TimetrackingValidator
12+
} from '../utils/commands/export/validators'
913

1014
export default class Export extends Command {
1115
static description = 'export data for a specific date / project'
@@ -51,10 +55,12 @@ export default class Export extends Command {
5155
const { interactive, ...params } = flags
5256
this.checkParams(tools, params, availableFlagsValues)
5357

54-
// (FIXME) Step 4 - Extract data
55-
// (FIXME) Step 5 - Filter data
56-
// (FIXME) Step 6 - Aggregate data
57-
// (FIXME) Step 7 - Save data
58+
// Step 4 - Filter data
59+
const data = this.filterData(tools, params, TIMETRACKING)
60+
61+
// (FIXME) Step 5 - Aggregate data
62+
// Step 6 - Save data
63+
await tools.utils.saveFile(data, params)
5864
}
5965

6066
/**
@@ -103,7 +109,9 @@ export default class Export extends Command {
103109
* Check validity of every params
104110
*/
105111
private checkParams(tools: any, params: any, availableFlagsValues: any) {
106-
const invalidParams = tools.validator.checkParams(params, availableFlagsValues)
112+
const invalidParams = tools.validator.checkParams(
113+
params, availableFlagsValues
114+
)
107115
if (invalidParams.length !== 0) {
108116
this.error(
109117
tools.utils.errorMessage('invalid param', invalidParams),
@@ -112,4 +120,19 @@ export default class Export extends Command {
112120
}
113121
return true
114122
}
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+
}
115138
}

src/utils/commands/export/constants.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ class Constants {
66
private _flags: any
77
private _formats: Array<string>
88
private _projects: Array<string>
9+
private _timetracking: Array<any>
910

1011
constructor() {
1112
this._flags = this.setFlags()
1213
this._formats = this.setFormats()
1314
this._projects = this.setProjects()
15+
this._timetracking = this.setTimetracking()
1416
}
1517

1618
get flags() {
@@ -25,6 +27,10 @@ class Constants {
2527
return this._projects
2628
}
2729

30+
get timetracking() {
31+
return this._timetracking
32+
}
33+
2834
private setFlags() {
2935
return {
3036
// -h, --help
@@ -86,15 +92,31 @@ class Constants {
8692
*/
8793
return stdout.trim().split(EOL)
8894
}
95+
96+
/**
97+
* Export data from Timewarrior
98+
*/
99+
private setTimetracking() {
100+
const command = 'timew export'
101+
const { stdout, stderr, code } = exec(command, { silent: true })
102+
/* FIXME handle error case
103+
if (code !== 0) {
104+
this.error(stderr.trim(), { exit: code })
105+
}
106+
*/
107+
return JSON.parse(stdout)
108+
}
89109
}
90110

91111
const constants = new Constants()
92112
const FLAGS = constants.flags
93113
const FORMATS = constants.formats
94114
const PROJECTS = constants.projects
115+
const TIMETRACKING = constants.timetracking
95116

96117
export {
97118
FLAGS,
98119
FORMATS,
99-
PROJECTS
120+
PROJECTS,
121+
TIMETRACKING
100122
}

src/utils/commands/export/utils.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import * as fs from 'fs'
2+
import { parse } from 'json2csv'
3+
import * as moment from 'moment'
14
import { EOL } from 'os'
25

36
export default class ExportUtils {
@@ -13,6 +16,56 @@ export default class ExportUtils {
1316
})
1417
}
1518

19+
/**
20+
* Format timetracking to standart format
21+
*/
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'
32+
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+
}
44+
}
45+
46+
/**
47+
* Save file on disk
48+
*/
49+
public saveFile(data: Array<any>, params: any) {
50+
// #neurodecisions$2019-01-12$2019-12-12.csv
51+
const fromDate = params.from.replace(/-/g, '')
52+
const toDate = params.to.replace(/-/g, '')
53+
const project = params.project.replace(/[^a-zA-Z ]/g, '')
54+
const filename = `${project}-${fromDate}-${toDate}.${params.format}`
55+
let writeData = null
56+
if (params.format === 'json') {
57+
writeData = this.formatAsJson(data)
58+
} else if (params.format === 'csv') {
59+
writeData = this.formatAsCsv(data)
60+
}
61+
try {
62+
fs.writeFileSync(filename, writeData)
63+
return true
64+
} catch (err) {
65+
// FIXME handle error (err)
66+
}
67+
}
68+
1669
/**
1770
* Format error message
1871
*/
@@ -24,4 +77,23 @@ export default class ExportUtils {
2477
}).join(EOL)
2578
return `${title}${EOL}${details}`
2679
}
80+
81+
/**
82+
* Stringify data
83+
*/
84+
private formatAsJson(data) {
85+
return JSON.stringify(data)
86+
}
87+
88+
/**
89+
* Convert JSON as CSV
90+
*/
91+
private formatAsCsv(data) {
92+
const headers = ['start', 'end', 'duration', 'project', 'description']
93+
try {
94+
return parse(data, { fields: headers })
95+
} catch (err) {
96+
// FIXME handle error (err)
97+
}
98+
}
2799
}

src/utils/commands/export/validators.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import DateValidator from './validators/date-validator'
22
import FormatValidator from './validators/format-validator'
33
import ProjectValidator from './validators/project-validator'
44
import SystemRequirementValidator from './validators/system-requirement-validator'
5+
import TimetrackingValidator from './validators/timetracking-validator'
56

67
export default class ExportValidator {
78
private executables = [
@@ -51,5 +52,6 @@ export {
5152
DateValidator,
5253
FormatValidator,
5354
ProjectValidator,
54-
SystemRequirementValidator
55+
SystemRequirementValidator,
56+
TimetrackingValidator
5557
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as Moment from 'moment'
2+
import { extendMoment } from 'moment-range'
3+
4+
import { Validator } from './validator'
5+
6+
/**
7+
* Validation for timetracking
8+
*/
9+
export default class TimetrackingValidator implements Validator {
10+
public isValid(tracking: any, filters: any) {
11+
// {
12+
// "start":"20190612T150556Z",
13+
// "end":"20190612T170000Z",
14+
// "tags":["#sideproject","Task/time warrior export"]
15+
// }
16+
let inInterval = false
17+
const [project, description, ...tags] = tracking.tags
18+
const isSameProject = (project === filters.project)
19+
20+
if (isSameProject) {
21+
const moment = extendMoment(Moment)
22+
const trackingRange = moment.range(
23+
moment(tracking.start), moment(tracking.end)
24+
)
25+
const filtersRange = moment.range(
26+
moment(filters.from), moment(filters.to)
27+
)
28+
29+
inInterval = trackingRange.overlaps(filtersRange)
30+
}
31+
return isSameProject && inInterval
32+
}
33+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export interface Validator {
2-
isValid(value: string | any, arrayOfValues: Array<any> | null): boolean | string | Array<string>
2+
isValid(
3+
value: string | any, arrayOfValues: Array<any> | any | null
4+
): boolean | string | Array<string>
35
}

yarn.lock

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,7 @@ commander@2.15.1:
765765
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
766766
integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==
767767

768-
commander@^2.12.1, commander@~2.20.0:
768+
commander@^2.12.1, commander@^2.15.1, commander@~2.20.0:
769769
version "2.20.0"
770770
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
771771
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
@@ -840,6 +840,13 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
840840
shebang-command "^1.2.0"
841841
which "^1.2.9"
842842

843+
d@1:
844+
version "1.0.0"
845+
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
846+
integrity sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=
847+
dependencies:
848+
es5-ext "^0.10.9"
849+
843850
debug-log@^1.0.1:
844851
version "1.0.1"
845852
resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f"
@@ -976,6 +983,32 @@ error-ex@^1.2.0, error-ex@^1.3.1:
976983
dependencies:
977984
is-arrayish "^0.2.1"
978985

986+
es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
987+
version "0.10.50"
988+
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778"
989+
integrity sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==
990+
dependencies:
991+
es6-iterator "~2.0.3"
992+
es6-symbol "~3.1.1"
993+
next-tick "^1.0.0"
994+
995+
es6-iterator@~2.0.3:
996+
version "2.0.3"
997+
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
998+
integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
999+
dependencies:
1000+
d "1"
1001+
es5-ext "^0.10.35"
1002+
es6-symbol "^3.1.1"
1003+
1004+
es6-symbol@^3.1.0, es6-symbol@^3.1.1, es6-symbol@~3.1.1:
1005+
version "3.1.1"
1006+
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
1007+
integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=
1008+
dependencies:
1009+
d "1"
1010+
es5-ext "~0.10.14"
1011+
9791012
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
9801013
version "1.0.5"
9811014
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -1831,13 +1864,27 @@ json-parse-better-errors@^1.0.1:
18311864
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
18321865
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
18331866

1867+
json2csv@^4.5.1:
1868+
version "4.5.1"
1869+
resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-4.5.1.tgz#d16d1fa7bea3a8cdddeb500a4eca5d7d2888ecc1"
1870+
integrity sha512-o90Xa1ziGk3i7AJEO79Jac4+7SEUk58/DxS5mDPW6GF7poX0y7Y0pm1FbWrkz9VzKE4MpUW9aKBOCpJ0U1Ua8A==
1871+
dependencies:
1872+
commander "^2.15.1"
1873+
jsonparse "^1.3.1"
1874+
lodash.get "^4.4.2"
1875+
18341876
jsonfile@^4.0.0:
18351877
version "4.0.0"
18361878
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
18371879
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
18381880
optionalDependencies:
18391881
graceful-fs "^4.1.6"
18401882

1883+
jsonparse@^1.3.1:
1884+
version "1.3.1"
1885+
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
1886+
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
1887+
18411888
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
18421889
version "3.2.2"
18431890
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -1904,6 +1951,11 @@ lodash._reinterpolate@~3.0.0:
19041951
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
19051952
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
19061953

1954+
lodash.get@^4.4.2:
1955+
version "4.4.2"
1956+
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
1957+
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
1958+
19071959
lodash.template@^4.4.0:
19081960
version "4.4.0"
19091961
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0"
@@ -2101,6 +2153,13 @@ mock-stdin@^0.3.1:
21012153
resolved "https://registry.yarnpkg.com/mock-stdin/-/mock-stdin-0.3.1.tgz#c657d9642d90786435c64ca5e99bbd4d09bd7dd3"
21022154
integrity sha1-xlfZZC2QeGQ1xkyl6Zu9TQm9fdM=
21032155

2156+
moment-range@^4.0.2:
2157+
version "4.0.2"
2158+
resolved "https://registry.yarnpkg.com/moment-range/-/moment-range-4.0.2.tgz#f7c3863df2a1ed7fd1822ba5a7bcf53a78701be9"
2159+
integrity sha512-n8sceWwSTjmz++nFHzeNEUsYtDqjgXgcOBzsHi+BoXQU2FW+eU92LUaK8gqOiSu5PG57Q9sYj1Fz4LRDj4FtKA==
2160+
dependencies:
2161+
es6-symbol "^3.1.0"
2162+
21042163
moment@^2.22.1, moment@^2.24.0:
21052164
version "2.24.0"
21062165
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -2148,6 +2207,11 @@ neo-async@^2.6.0:
21482207
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
21492208
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
21502209

2210+
next-tick@^1.0.0:
2211+
version "1.0.0"
2212+
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
2213+
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
2214+
21512215
nice-try@^1.0.4:
21522216
version "1.0.5"
21532217
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"

0 commit comments

Comments
 (0)