diff --git a/README.md b/README.md index 35ea489..ae6e0b4 100644 --- a/README.md +++ b/README.md @@ -82,28 +82,36 @@ package.jsonをテキストエディタで開くと以下のような記載を 3: hoge2 select board number: 2 ``` - +Trelloからのデータのダウンロードが開始します。 しばらく待つと次に以下が表示されます。 ``` -0: Show only header -1: All -2: Specify condition by js -Select output data: +1: set group by +2: input filter and show data +current group by: +select: ``` -出力したいデータの種類を左側の番号で選択します。 +実施したいことを左側の番号で選択します。 + +1を入力すると、group byを設定できます。 +group byは集計する際にまとめる変数であり、例えば`member`と入力するとメンバーの種類毎にPointの合計と、timeの合計がcsvで出力されるようになります。 +group byの指定の際に何も表示せずにリターンすると、group byが未設定となります。 +``` +select: 1 +specify variable name, empty string means no group by + variable names: cardId,number,title,point,listName,inDate,outDate,time,labelPink,labelGreen,member +group by: member +``` -0を入力すると、CSVのヘッダのみを標準出力に出力します。 -1を入力すると、全データをCSV形式で標準出力に出力します。 -2を入力すると、JavaScriptの条件文により出力するデータを選択できるようになります。 +2を入力すると、JavaScriptの条件文を入力できます。 +条件文を入力すると、各行をその条件文で評価し、trueとなった行のみを出力・集計の対象とします。 ``` -0: Show only header -1: All -2: Specify condition by js -Select output data: 2 -input condition by js: listName === "Doing" +select: 2 +specify filter by javascript condition, the following variables are available, "true" means showing all data + variable names: cardId,number,title,point,listName,inDate,outDate,time,labelPink,labelGreen,member +input condition: inDate > new Date("2020/2/22") && listName === "Doing" ``` 条件文の例を示します。 diff --git a/index.js b/index.js index b90146e..5d3ec2b 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const readLine = require('readline') const fetch = require('node-fetch') const moment = require('moment-timezone') const Iconv = require('iconv').Iconv +const clc = require('cli-color') if (process.argv.length !== 5) { console.error( @@ -31,6 +32,11 @@ const isLinux = process.platform === 'linux' // 文字コード変換の準備 const iconv = new Iconv('UTF-8', 'SHIFT_JIS//IGNORE') +// color設定 +const inf = clc.xterm(83) +const st = clc.xterm(50) +const er = clc.xterm(204) + const readUserInput = (question, initialInput) => { const rl = readLine.createInterface({ input: process.stdin, @@ -51,10 +57,10 @@ const readUserInput = (question, initialInput) => { const inputText = async (questionText, re, initialInput) => { while (true) { let a = await readUserInput(questionText, initialInput) - if (a && a.match(re)) { + if (a.match(re)) { return a } else { - console.log('error: invalid input') + console.log(er('error: invalid input')) } } } @@ -157,7 +163,7 @@ const parseData = (actionMap, cardsMap) => { return list } -const writeList = list => { +const writeList = (list, showiingData) => { console.log( 'cardId,number,title,point,listName,inDate,outDate,time,labelPink,labelGreen,member' ) @@ -176,6 +182,40 @@ const writeList = list => { console.log(`total point: ${totalPoint}`) } +const writeListGroup = (list, groupby) => { + console.log(`${groupby},totalPoint,totalTime`) + let total = 0 + let timesByGroup = {} + let relatedCardIds = {} + let pointsByGroup = {} + let cardPointMap = {} + for (let d of list) { + total += new Date(d.outDate).getTime() - new Date(d.inDate).getTime() + cardPointMap[d.cardId] = d.point + timesByGroup[d[groupby]] = timesByGroup[d[groupby]] + ? timesByGroup[d[groupby]] + d.time + : d.time + if (!relatedCardIds[d[groupby]] || !relatedCardIds[d[groupby]][d.cardId]) { + pointsByGroup[d[groupby]] = pointsByGroup[d[groupby]] + ? pointsByGroup[d[groupby]] + d.point + : d.point + if (!relatedCardIds[d[groupby]]) { + relatedCardIds[d[groupby]] = {} + } + relatedCardIds[d[groupby]][d.cardId] = d.cardId + } + } + let totalPoint = 0 + for (let k in cardPointMap) { + totalPoint += cardPointMap[k] + } + for (let k in timesByGroup) { + console.log(`"${k}","${pointsByGroup[k]}","${timesByGroup[k]}"`) + } + console.log(`total: ${total / 1000 / 60 / 60} hrs`) + console.log(`total point: ${totalPoint}`) +} + const main = async () => { try { // Board一覧取得 @@ -184,10 +224,10 @@ const main = async () => { // Board一覧表示 let num = 1 for (const item of boardList) { - console.log(`${num++}: ${item.name}`) + console.log(inf(`${num++}: ${item.name}`)) } // Board選択 - let i = await inputText('select board number: ', `^[1-${num - 1}]$`) + let i = await inputText('select board by number: ', `^[1-${num - 1}]$`) const selectedBoard = boardList[Number(i - 1)] // User取得 let memberMap = {} @@ -239,30 +279,70 @@ const main = async () => { let list = parseData(actionMap, cardsMap) // 出力内容選択 let initialCondition = '' + let groupby = '' + let showingList = [ + 'cardId', + 'number', + 'title', + 'point', + 'listName', + 'inDate', + 'outDate', + 'time', + 'labelPink', + 'labelGreen', + 'member' + ] while (true) { - console.log('----------') - console.log('0: Show only header') - console.log('1: All') - console.log('2: Specify condition by js') - const type = await inputText('Select output data: ', `^[0-2]$`) - if (type === '0') { - console.log('----------') + console.log(inf('----------')) + console.log(inf('1: set group by')) + console.log(inf('2: input filter and show data')) + console.log(st(`current group by: ${groupby}`)) + const type = await inputText('select: ', `^[1-2]$`) + if (type === '1') { console.log( - 'cardId,number,title,point,listName,inDate,outDate,time,labelPink,labelGreen,member' + inf('specify variable name, empty string means no group by') ) - } else if (type === '1') { - console.log('----------') - writeList(list) + console.log(inf(` variable names: ${showingList.toString()}`)) + groupbyText = await inputText('group by: ', `.*`, groupby) + try { + const cardId = 'cardId', + number = 'number', + title = 'title', + point = 'point', + listName = 'listName', + inDate = 'inDate', + outDate = 'outDate', + time = 'time', + labelPink = 'labelPink', + labelGreen = 'labelGreen', + member = 'member', + totalTime = 'totalTime' + if (groupbyText === '') { + groupbyText = '""' + } + groupby = eval(groupbyText) + } catch (err) { + console.error(er(err)) + } } else if (type === '2') { + console.log( + inf( + 'specify filter by javascript condition, the following variables are available, "true" means showing all data' + ) + ) + console.log(inf(` variable names: ${showingList.toString()}`)) const condition = await inputText( - 'input condition by js: ', + 'input condition: ', `.+`, initialCondition ) - console.log('----------') + console.log(inf('----------')) let newList = [] let total = 0 for (let d of list) { + const all = true + const header = false let { cardId, number, @@ -283,16 +363,20 @@ const main = async () => { newList.push(d) } } catch (err) { - console.error(err) + console.error(er(err)) break } } - writeList(newList) + if (groupby !== '') { + writeListGroup(newList, groupby) + } else { + writeList(newList, showingList) + } initialCondition = condition } } } catch (err) { - console.error(err) + console.error(er(err)) } } diff --git a/package-lock.json b/package-lock.json index bbe2075..43fab9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,97 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "cli-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.0.tgz", + "integrity": "sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A==", + "requires": { + "ansi-regex": "^2.1.1", + "d": "^1.0.1", + "es5-ext": "^0.10.51", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.14", + "timers-ext": "^0.1.7" + } + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==" + } + } + }, "iconv": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/iconv/-/iconv-2.3.5.tgz", @@ -13,6 +104,34 @@ "safer-buffer": "^2.1.2" } }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "requires": { + "es5-ext": "~0.10.2" + } + }, + "memoizee": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", + "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==", + "requires": { + "d": "1", + "es5-ext": "^0.10.45", + "es6-weak-map": "^2.0.2", + "event-emitter": "^0.3.5", + "is-promise": "^2.1", + "lru-queue": "0.1", + "next-tick": "1", + "timers-ext": "^0.1.5" + } + }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", @@ -31,6 +150,11 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -40,6 +164,20 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "requires": { + "es5-ext": "~0.10.46", + "next-tick": "1" + } + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" } } } diff --git a/package.json b/package.json index a3a5dbb..925ec41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trello-csv", - "version": "0.0.2", + "version": "0.0.3", "description": "", "main": "index.js", "scripts": { @@ -10,6 +10,7 @@ "author": "hidetak", "license": "ISC", "dependencies": { + "cli-color": "^2.0.0", "iconv": "^2.3.5", "moment": "^2.24.0", "moment-timezone": "^0.5.28",