Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date formatting options #26

Merged
merged 9 commits into from
Jun 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ module.exports = {
ecmaVersion: 2018,
},
rules: {
'no-continue': 'off'
},
};
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ Install the module with: `npm install qif2json`

```javascript
var qif2json = require('qif2json');
qif2json.parse(qifData);
qif2json.parse(qifData, options);

// Or to read in a file directly
qif2json.parseFile(filePath, function(err, qifData){
qif2json.parseFile(filePath, options, function(err, qifData){
// done!
});
```

If installed globally, the `qif2json` command can also be used with an input file and the output JSON will be pretty-printed to the console

## Options

* `dateFormat` - The format of dates within the file. The `fetcha` module is used for parsing them into Date objects. See https://www.npmjs.com/package/fecha#formatting-tokens for available formatting tokens. The special format `"us"` will use us-format MM/DD/YYYY dates. Dates are normalised before parsing so `/`, `'` become `-` and spaces are removed. On the commandline you can specify multiple date formats comma-delimited.

## Contributing
Take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using `npm test`.

Expand Down
16 changes: 11 additions & 5 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ const qif2json = require('./lib/qif2json.js');
const args = process.argv.slice(2);
let transactionsOnly;
let file;
let dateFormat;

args.forEach((arg) => {
while (args.length > 0) {
const arg = args.shift();
if (arg.indexOf('-') !== 0) {
file = arg;
return;
continue;
}
switch (arg) {
case '--transactions':
case '-t':
transactionsOnly = true;
break;
case '--date-format':
case '-d':
dateFormat = args.shift().split(',');
break;
default:
break;
}
});
}

function output(err, data) {
let finalData = data;
Expand All @@ -36,8 +42,8 @@ function output(err, data) {
}

if (!file) {
qif2json.parseStream(process.stdin, output);
qif2json.parseStream(process.stdin, { dateFormat }, output);
process.stdin.resume();
} else {
qif2json.parseFile(file, output);
qif2json.parseFile(file, { dateFormat }, output);
}
45 changes: 29 additions & 16 deletions lib/qif2json.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,38 @@
const fs = require('fs');
const jschardet = require('jschardet');
const Iconv = require('iconv-lite');
const fecha = require('fecha');
const debug = require('debug')('qif2json');

function parseDate(str, format) {
const date = str.replace(' ', '').split(/[^0-9]/);
const US_DATE_FORMATS = ['MM-DD-YYYYHH:mm:ss', 'MM-DD-YYYY', 'MM-DD-YY'];
const UK_DATE_FORMATS = ['DD-MM-YYYYHH:mm:ss', 'DD-MM-YYYY', 'DD-MM-YY'];

if (date[0].length < 2) {
date[0] = `0${date[0]}`;
function parseDate(dateStr, formats) {
if (formats === 'us') {
formats = US_DATE_FORMATS;
}
if (date[1].length < 2) {
date[1] = `0${date[1]}`;
if (!formats) {
formats = UK_DATE_FORMATS;
}
if (date[2].length <= 2) {
date[2] = 2000 + parseInt(date[2], 10);
formats = [].concat(formats);

if (date[2] > new Date().getFullYear()) {
date[2] -= 100;
let str = dateStr.replace(/ /g, '');
str = str.replace(/\//g, '-');
str = str.replace(/'/g, '-');
str = str.replace(/(^|[^0-9])([0-9])([^0-9]|$)/g, '$10$2$3');
debug(`input date ${dateStr} became ${str}`);

while (formats.length) {
const format = formats.shift();
const formatted = fecha.parse(str, format);

if (formatted) {
debug(`input date ${str} parses correctly with ${format}`);
return fecha.format(formatted, 'YYYY-MM-DDTHH:mm:ss');
}
}

if (format === 'us') {
return `${date[2]}-${date[0]}-${date[1]}`;
}
return `${date[2]}-${date[1]}-${date[0]}`;
return `<invalid date:"${dateStr}">`;
}

exports.parse = function parse(qif, options) {
Expand Down Expand Up @@ -67,6 +77,9 @@ exports.parse = function parse(qif, options) {
case 'T':
transaction.amount = parseFloat(line.substring(1).replace(',', ''));
break;
case 'U':
// Looks like a legacy repeat of 'T'
break;
case 'N':
transaction.number = line.substring(1);
break;
Expand Down Expand Up @@ -121,7 +134,7 @@ exports.parse = function parse(qif, options) {
return data;
};

exports.parseInput = function paeeInput(qifData, options, callback) {
exports.parseInput = function parseInput(qifData, options, callback) {
const { encoding } = jschardet.detect(qifData);
let err;

Expand All @@ -131,7 +144,7 @@ exports.parseInput = function paeeInput(qifData, options, callback) {
}

if (encoding.toUpperCase() !== 'UTF-8' && encoding.toUpperCase() !== 'ASCII') {
qifData = Iconv.decode(qifData, encoding);
qifData = Iconv.decode(Buffer.from(qifData), encoding);
} else {
qifData = qifData.toString('utf8');
}
Expand Down
26 changes: 20 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
},
"keywords": [],
"dependencies": {
"debug": "^4.1.1",
"fecha": "^3.0.3",
"iconv-lite": "^0.4.24",
"jschardet": "^2.1.0"
}
Expand Down
6 changes: 3 additions & 3 deletions test/normalTransactions.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('normalTransactions', () => {

expect(qifData.type).toEqual('Bank');
expect(qifData.transactions.length).toEqual(3);
expect(qifData.transactions[0].date).toEqual('1994-06-01');
expect(qifData.transactions[0].date).toEqual('1994-06-01T00:00:00');
expect(qifData.transactions[0].amount).toEqual(-1000);
expect(qifData.transactions[0].number).toEqual('1005');
expect(qifData.transactions[0].payee).toEqual('Bank Of Mortgage');
Expand All @@ -22,11 +22,11 @@ describe('normalTransactions', () => {
expect(qifData.transactions[0].division[1].category).toEqual('Mort Int');
expect(qifData.transactions[0].division[1].amount).toEqual(-746.36);

expect(qifData.transactions[1].date).toEqual('1994-06-02');
expect(qifData.transactions[1].date).toEqual('1994-06-02T00:00:00');
expect(qifData.transactions[1].amount).toEqual(75);
expect(qifData.transactions[1].payee).toEqual('Deposit');

expect(qifData.transactions[2].date).toEqual('1994-06-03');
expect(qifData.transactions[2].date).toEqual('1994-06-03T00:00:00');
expect(qifData.transactions[2].amount).toEqual(-10);
expect(qifData.transactions[2].payee).toEqual('JoBob Biggs');
expect(qifData.transactions[2].memo).toEqual('J.B. gets bucks');
Expand Down
78 changes: 64 additions & 14 deletions test/qif2json.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ describe('qif2json', () => {
expect(data.type).toEqual('Bank');
expect(data.transactions.length).toEqual(3);

expect(data.transactions[0].date).toEqual('2010-03-03');
expect(data.transactions[0].date).toEqual('2010-03-03T00:00:00');
expect(data.transactions[0].amount).toEqual(-379);
expect(data.transactions[0].payee).toEqual('CITY OF SPRINGFIELD');

expect(data.transactions[1].date).toEqual('2010-04-03');
expect(data.transactions[1].date).toEqual('2010-04-03T00:00:00');
expect(data.transactions[1].amount).toEqual(-20.28);
expect(data.transactions[1].payee).toEqual('YOUR LOCAL SUPERMARKET');

expect(data.transactions[2].date).toEqual('2010-03-03');
expect(data.transactions[2].date).toEqual('2010-03-03T00:00:00');
expect(data.transactions[2].amount).toEqual(-421.35);
expect(data.transactions[2].payee).toEqual('SPRINGFIELD WATER UTILITY');
});
Expand All @@ -43,7 +43,7 @@ describe('qif2json', () => {
expect(data.type).toEqual('Bank');
expect(data.transactions.length).toEqual(1);

expect(data.transactions[0].date).toEqual('2010-03-03');
expect(data.transactions[0].date).toEqual('2010-03-03T00:00:00');
expect(data.transactions[0].amount).toEqual(-379);
expect(data.transactions[0].payee).toEqual('CITY OF SPRINGFIELD');
});
Expand All @@ -57,7 +57,7 @@ describe('qif2json', () => {
expect(data.type).toEqual('Bank');
expect(data.transactions.length).toEqual(1);

expect(data.transactions[0].date).toEqual('2010-03-03');
expect(data.transactions[0].date).toEqual('2010-03-03T00:00:00');
expect(data.transactions[0].amount).toEqual(-379);
expect(data.transactions[0].payee).toEqual('CITY OF SPRINGFIELD');
});
Expand Down Expand Up @@ -123,11 +123,11 @@ describe('qif2json', () => {
'T1,337.00',
'CX',
'POpening Balance',
'L[AccountName]'].join('\r\n'));
'L[AccountName]'].join('\r\n'), { dateFormat: 'us' });

expect(data.type).toEqual('AccounType');
expect(data.transactions[0].category).toEqual('[AccountName]');
expect(data.transactions[0].date).toEqual('2014-26-10');
expect(data.transactions[0].date).toEqual('2014-10-26T00:00:00');
expect(data.transactions[0].amount).toEqual(1337.00);
expect(data.transactions[0].clearedStatus).toEqual('X');
});
Expand All @@ -143,12 +143,12 @@ describe('qif2json', () => {
'SFood:Meat',
'$-16.00',
'SFood:Dispensary',
'$-6.00\r\n'].join('\r\n'));
'$-6.00\r\n'].join('\r\n'), { dateFormat: 'us' });

expect(data.type).toEqual('Cardname');
expect(data.transactions.length).toEqual(1);

expect(data.transactions[0].date).toEqual('2014-28-10');
expect(data.transactions[0].date).toEqual('2014-10-28T00:00:00');
expect(data.transactions[0].amount).toEqual(-67);
expect(data.transactions[0].payee).toEqual('Wallmart');

Expand All @@ -164,7 +164,7 @@ describe('qif2json', () => {
'POpening Balance'].join('\r\n'), { dateFormat: 'us' });

expect(data.type).toEqual('Bank');
expect(data.transactions[0].date).toEqual('2016-10-09');
expect(data.transactions[0].date).toEqual('2016-10-09T00:00:00');
expect(data.transactions[0].amount).toEqual(1);
expect(data.transactions[0].payee).toEqual('Opening Balance');
});
Expand All @@ -176,7 +176,7 @@ describe('qif2json', () => {
'POpening Balance'].join('\r\n'), { dateFormat: 'us' });

expect(data.type).toEqual('Bank');
expect(data.transactions[0].date).toEqual('2009-11-10');
expect(data.transactions[0].date).toEqual('2009-11-10T00:00:00');
expect(data.transactions[0].amount).toEqual(1);
expect(data.transactions[0].payee).toEqual('Opening Balance');
});
Expand All @@ -188,7 +188,7 @@ describe('qif2json', () => {
'POpening Balance'].join('\r\n'), { dateFormat: 'us' });

expect(data.type).toEqual('Bank');
expect(data.transactions[0].date).toEqual('1999-11-10');
expect(data.transactions[0].date).toEqual('1999-11-10T00:00:00');
expect(data.transactions[0].amount).toEqual(1);
expect(data.transactions[0].payee).toEqual('Opening Balance');
});
Expand All @@ -205,12 +205,12 @@ describe('qif2json', () => {
'EPork belly',
'$-16.00',
'SFood:Dispensary',
'$-6.00\r\n'].join('\r\n'));
'$-6.00\r\n'].join('\r\n'), { dateFormat: 'us' });

expect(data.type).toEqual('Cardname');
expect(data.transactions.length).toEqual(1);

expect(data.transactions[0].date).toEqual('2014-28-10');
expect(data.transactions[0].date).toEqual('2014-10-28T00:00:00');
expect(data.transactions[0].amount).toEqual(-67);
expect(data.transactions[0].payee).toEqual('Wallmart');

Expand All @@ -219,4 +219,54 @@ describe('qif2json', () => {
expect(data.transactions[0].division[2].amount).toEqual(-6);
expect(data.transactions[0].division[1].description).toEqual('Pork belly');
});

it('Can parse timestamp example', () => {
const data = qif2json.parse(['!Type:Bank',
'D26-06-2019 00:00:00',
'T-379.00',
'PCITY OF SPRINGFIELD',
'^',
'D27-06-2019 00:00:00',
'T-20.28',
'PYOUR LOCAL SUPERMARKET',
'^',
'D28-06-2019 00:00:00',
'T-421.35',
'PSPRINGFIELD WATER UTILITY',
'^',
].join('\r\n'));

expect(data.type).toEqual('Bank');
expect(data.transactions.length).toEqual(3);

expect(data.transactions[0].date).toEqual('2019-06-26T00:00:00');
expect(data.transactions[0].amount).toEqual(-379);
expect(data.transactions[0].payee).toEqual('CITY OF SPRINGFIELD');
});

it('can parse mixed millenium dates', () => {
const data = qif2json.parse(['!Type:Bank',
'D11/10/99',
'T1',
'POpening Balance',
'^',
"D3/ 1' 0",
'T-1',
'PCITY OF SPRINGFIELD\r\n'].join('\r\n'), { dateFormat: 'us' });

expect(data.type).toEqual('Bank');
expect(data.transactions[0].date).toEqual('1999-11-10T00:00:00');
expect(data.transactions[1].date).toEqual('2000-03-01T00:00:00');
});

it('ignores repeated T column instead of crashing on U', () => {
const data = qif2json.parse(['!Type:Bank',
'D11/10/99',
'U1',
'T1',
'POpening Balance'].join('\r\n'), { dateFormat: 'us' });

expect(data.type).toEqual('Bank');
expect(data.transactions[0].amount).toEqual(1);
});
});