From 8c8132aedfd9c579c9530652e50f40334d0d155b Mon Sep 17 00:00:00 2001 From: David Worms Date: Tue, 13 Nov 2018 16:13:47 +0100 Subject: [PATCH] info: new option --- CHANGELOG.md | 2 +- lib/es5/index.js | 83 +++++++++++++++++++++++++++------------- lib/index.d.ts | 5 +++ lib/index.js | 63 ++++++++++++++++++++++-------- test/api.types.ts | 7 +++- test/options.info.coffee | 65 +++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 test/options.info.coffee diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a4eaa..123ce97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ This is a complete rewrite based with a Buffer implementation. There are no majo New features: -* new options from_line and to_line +* new options info, from_line and to_line * trim: respect `ltrim` and `rtrim` when defined * delimiter: may be a Buffer * delimiter: handle multiple bytes/characters diff --git a/lib/es5/index.js b/lib/es5/index.js index ac37228..4c797d3 100644 --- a/lib/es5/index.js +++ b/lib/es5/index.js @@ -162,6 +162,13 @@ function (_Transform) { throw new Error("Invalid Option Length: escape must be one character, got ".concat(options.escape.length)); } else { options.escape = options.escape[0]; + } // Normalize option `info` + + + if (options.info === undefined || options.info === null || options.info === false) { + options.info = false; + } else if (options.info !== true) { + throw new Error("Invalid Option: info must be true, got ".concat(JSON.stringify(options.info))); } // Normalize option `quote` @@ -217,6 +224,13 @@ function (_Transform) { options.rtrim = false; } + _this.info = { + comment_lines: 0, + empty_lines: 0, + invalid_field_length: 0, + lines: 1, + records: 0 + }; _this.options = options; _this.state = { castField: fnCastField, @@ -227,6 +241,7 @@ function (_Transform) { expectedRecordLength: options.columns === null ? 0 : options.columns.length, field: new ResizeableBuffer(20), firstLineToHeaders: fnFirstLineToHeaders, + info: Object.assign({}, _this.info), previousBuf: undefined, quoting: false, stop: false, @@ -241,13 +256,6 @@ function (_Transform) { wasQuoting: false, wasRowDelimiter: false }; - _this.info = { - comment_lines: 0, - empty_lines: 0, - invalid_field_length: 0, - lines: 1, - records: 0 - }; return _this; } @@ -285,6 +293,7 @@ function (_Transform) { escape = _this$options.escape, from = _this$options.from, from_line = _this$options.from_line, + info = _this$options.info, ltrim = _this$options.ltrim, max_record_size = _this$options.max_record_size, quote = _this$options.quote, @@ -322,6 +331,12 @@ function (_Transform) { if (this.state.wasRowDelimiter === true) { this.info.lines++; + console.log(this.info.records, this.state.info.records); + + if (info === true && this.state.record.length === 0 && this.state.field.length === 0 && this.state.wasQuoting === false) { + this.state.info = Object.assign({}, this.info); + } + this.state.wasRowDelimiter = false; } @@ -348,9 +363,7 @@ function (_Transform) { if ((chr === cr || chr === nl) && this.state.wasRowDelimiter === false) { this.state.wasRowDelimiter = true; - } - - var recordDelimiterLength = this.__isRecordDelimiter(chr, buf, pos); // Previous char was a valid escape char + } // Previous char was a valid escape char // treat the current char as a regular char @@ -420,6 +433,8 @@ function (_Transform) { } if (this.state.quoting === false) { + var recordDelimiterLength = this.__isRecordDelimiter(chr, buf, pos); + if (recordDelimiterLength !== 0) { // Do not emit comments which take a full line var skipCommentLine = this.state.commenting && this.state.record.length === 0 && this.state.field.length === 0; @@ -434,7 +449,7 @@ function (_Transform) { continue; } - if (this.state.enabled === false && this.info.lines + (this.state.wasRowDelimiter ? 1 : 0) >= from_line) { + if (this.state.enabled === false && this.info.lines + (this.state.wasRowDelimiter === true ? 1 : 0) >= from_line) { this.state.enabled = true; this.__resetField(); @@ -491,8 +506,8 @@ function (_Transform) { } } - if (this.state.commenting === false && max_record_size !== 0) { - if (this.state.record_length + this.state.field.length > max_record_size) { + if (this.state.commenting === false) { + if (max_record_size !== 0 && this.state.record_length + this.state.field.length > max_record_size) { var _err2 = this.__error("Max Record Size: record exceed the maximum number of tolerated bytes of ".concat(max_record_size, " on line ").concat(this.info.lines)); if (_err2 !== undefined) return _err2; @@ -550,6 +565,7 @@ function (_Transform) { value: function __onRow() { var _this$options2 = this.options, columns = _this$options2.columns, + info = _this$options2.info, from = _this$options2.from, relax_column_count = _this$options2.relax_column_count, raw = _this$options2.raw, @@ -609,7 +625,7 @@ function (_Transform) { if (from === 1 || this.info.records >= from) { if (columns !== false) { - var obj = {}; + var obj = {}; // Transform record array to an object for (var i in record) { if (columns[i].disabled) continue; @@ -619,30 +635,39 @@ function (_Transform) { var objname = this.options.objname; if (objname === undefined) { - if (raw === true) { - this.push({ - raw: this.state.rawBuffer.toString(), + if (raw === true || info === true) { + this.push(Object.assign({ record: obj - }); + }, raw === true ? { + raw: this.state.rawBuffer.toString() + } : {}, info === true ? { + info: this.state.info + } : {})); } else { this.push(obj); } } else { - if (raw === true) { - this.push({ - raw: this.state.rawBuffer.toString(), + if (raw === true || info === true) { + this.push(Object.assign({ record: [obj[objname], obj] - }); + }, raw === true ? { + raw: this.state.rawBuffer.toString() + } : {}, info === true ? { + info: this.state.info + } : {})); } else { this.push([obj[objname], obj]); } } } else { - if (raw === true) { - this.push({ - raw: this.state.rawBuffer.toString(), + if (raw === true || info === true) { + this.push(Object.assign({ record: record - }); + }, raw === true ? { + raw: this.state.rawBuffer.toString() + } : {}, info === true ? { + info: this.state.info + } : {})); } else { this.push(record); } @@ -675,12 +700,16 @@ function (_Transform) { }, { key: "__resetRow", value: function __resetRow() { + var info = this.options.info; + if (this.options.raw === true) { this.state.rawBuffer.reset(); } this.state.record = []; - this.state.record_length = 0; + this.state.record_length = 0; // if(info === true){ + // this.state.info = null + // } } }, { key: "__onField", diff --git a/lib/index.d.ts b/lib/index.d.ts index 5aaff61..19da80c 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -101,6 +101,11 @@ declare namespace parse { * Start handling records from the requested line number. */ from_line?: number; + + /** + * Generate two properties `info` and `record` where `info` is a snapshot of the info object at the time the record was created and `record` is the parsed array or object. + */ + info?: boolean; /** * If true, ignore whitespace immediately following the delimiter (i.e. left-trim all fields), defaults to false. diff --git a/lib/index.js b/lib/index.js index 2489132..ecbe849 100644 --- a/lib/index.js +++ b/lib/index.js @@ -105,6 +105,12 @@ class Parser extends Transform { }else{ options.escape = options.escape[0] } + // Normalize option `info` + if(options.info === undefined || options.info === null || options.info === false){ + options.info = false + }else if(options.info !== true){ + throw new Error(`Invalid Option: info must be true, got ${JSON.stringify(options.info)}`) + } // Normalize option `quote` if(options.quote === null || options.quote === false || options.quote === ''){ options.quote = null @@ -151,6 +157,13 @@ class Parser extends Transform { }else if(options.rtrim !== true){ options.rtrim = false } + this.info = { + comment_lines: 0, + empty_lines: 0, + invalid_field_length: 0, + lines: 1, + records: 0 + } this.options = options this.state = { castField: fnCastField, @@ -161,6 +174,7 @@ class Parser extends Transform { expectedRecordLength: options.columns === null ? 0 : options.columns.length, field: new ResizeableBuffer(20), firstLineToHeaders: fnFirstLineToHeaders, + info: Object.assign({}, this.info), previousBuf: undefined, quoting: false, stop: false, @@ -173,13 +187,6 @@ class Parser extends Transform { wasQuoting: false, wasRowDelimiter: false } - this.info = { - comment_lines: 0, - empty_lines: 0, - invalid_field_length: 0, - lines: 1, - records: 0 - } } _transform(buf, encoding, callback){ if(this.state.stop === true){ @@ -199,7 +206,7 @@ class Parser extends Transform { callback(err) } __parse(nextBuf, end){ - const {comment, escape, from, from_line, ltrim, max_record_size, quote, raw, relax, rtrim, skip_empty_lines, to, to_line} = this.options + const {comment, escape, from, from_line, info, ltrim, max_record_size, quote, raw, relax, rtrim, skip_empty_lines, to, to_line} = this.options let {record_delimiter} = this.options const {previousBuf, rawBuffer, escapeIsQuote, trimChars} = this.state let buf @@ -221,6 +228,10 @@ class Parser extends Transform { } if(this.state.wasRowDelimiter === true){ this.info.lines++ + console.log(this.info.records, this.state.info.records) + if(info === true && this.state.record.length === 0 && this.state.field.length === 0 && this.state.wasQuoting === false){ + this.state.info = Object.assign({}, this.info) + } this.state.wasRowDelimiter = false } if(to_line !== -1 && this.info.lines > to_line){ @@ -356,12 +367,13 @@ class Parser extends Transform { } } } - if(this.state.commenting === false && max_record_size !== 0){ - if(this.state.record_length + this.state.field.length > max_record_size){ + if(this.state.commenting === false){ + if(max_record_size !== 0 && this.state.record_length + this.state.field.length > max_record_size){ const err = this.__error(`Max Record Size: record exceed the maximum number of tolerated bytes of ${max_record_size} on line ${this.info.lines}`) if(err !== undefined) return err } } + const lappend = ltrim === false || this.state.quoting === true || this.state.field.length !== 0 || !this.__isCharTrimable(chr) // rtrim in non quoting is handle in __onField const rappend = rtrim === false || this.state.wasQuoting === false @@ -399,7 +411,7 @@ class Parser extends Transform { return chr === space || chr === cr || chr === nl } __onRow(){ - const {columns, from, relax_column_count, raw, skip_lines_with_empty_values} = this.options + const {columns, info, from, relax_column_count, raw, skip_lines_with_empty_values} = this.options const {enabled, record} = this.state // Validate column length if(columns === true && this.state.firstLineToHeaders){ @@ -441,27 +453,40 @@ class Parser extends Transform { if(from === 1 || this.info.records >= from){ if(columns !== false){ const obj = {} + // Transform record array to an object for(let i in record){ if(columns[i].disabled) continue obj[columns[i].name] = record[i] } const {objname} = this.options if(objname === undefined){ - if(raw === true){ - this.push({raw: this.state.rawBuffer.toString(), record: obj}) + if(raw === true || info === true){ + this.push(Object.assign( + {record: obj}, + (raw === true ? {raw: this.state.rawBuffer.toString()}: {}), + (info === true ? {info: this.state.info}: {}) + )) }else{ this.push(obj) } }else{ - if(raw === true){ - this.push({raw: this.state.rawBuffer.toString(), record: [obj[objname], obj]}) + if(raw === true || info === true){ + this.push(Object.assign( + {record: [obj[objname], obj]}, + raw === true ? {raw: this.state.rawBuffer.toString()}: {}, + info === true ? {info: this.state.info}: {} + )) }else{ this.push([obj[objname], obj]) } } }else{ - if(raw === true){ - this.push({raw: this.state.rawBuffer.toString(), record: record}) + if(raw === true || info === true){ + this.push(Object.assign( + {record: record}, + raw === true ? {raw: this.state.rawBuffer.toString()}: {}, + info === true ? {info: this.state.info}: {} + )) }else{ this.push(record) } @@ -485,11 +510,15 @@ class Parser extends Transform { } } __resetRow(){ + const {info} = this.options if(this.options.raw === true){ this.state.rawBuffer.reset() } this.state.record = [] this.state.record_length = 0 + // if(info === true){ + // this.state.info = null + // } } __onField(){ const {cast, rtrim} = this.options diff --git a/test/api.types.ts b/test/api.types.ts index bcffef6..82d16be 100644 --- a/test/api.types.ts +++ b/test/api.types.ts @@ -12,7 +12,7 @@ describe('API Types', () => { const keys: any = Object.keys(options) keys.sort().should.eql([ 'cast', 'cast_date', 'columns', 'comment', 'delimiter', - 'escape', 'from', 'from_line', 'ltrim', 'max_record_size', + 'escape', 'from', 'from_line', 'info', 'ltrim', 'max_record_size', 'objname', 'quote', 'raw', 'readableObjectMode', 'record_delimiter', 'relax', 'relax_column_count', 'rtrim', 'skip_empty_lines', 'skip_lines_with_empty_values', @@ -127,6 +127,11 @@ describe('API Types', () => { options.from_line = 10 }) + it('info', () => { + const options: Options = {} + options.info = true + }) + it('ltrim', () => { const options: Options = {} options.ltrim = true diff --git a/test/options.info.coffee b/test/options.info.coffee new file mode 100644 index 0000000..9a2b287 --- /dev/null +++ b/test/options.info.coffee @@ -0,0 +1,65 @@ + +parse = require '../lib' + +describe 'options info', -> + + describe 'validation', -> + + it 'check the columns value', -> + (-> + parse "", info: 'ok', (->) + ).should.throw 'Invalid Option: info must be true, got "ok"' + + describe 'true', -> + + it 'return info and records', (next) -> + parse ''' + a,b,c + d,e,f + g,h,i + ''', info: true, (err, records) -> + records.map( + ({info}) -> info.lines + ).should.eql [1, 2, 3] unless err + next err + + it 'with skip_empty_lines', (next) -> + parse ''' + + a,b,c + + d,e,f + + g,h,i + ''', info: true, skip_empty_lines: true, (err, records) -> + records.map( + ({info}) -> info.lines + ).should.eql [2, 4, 6] unless err + next err + + it 'with comment', (next) -> + parse ''' + # line 1 + a,b,c + # line 2 + d,e,f + # line 3 + g,h,i + ''', info: true, comment: '#', (err, records) -> + records.map( + ({info}) -> info.lines + ).should.eql [2, 4, 6] unless err + next err + + it 'with multiline records', (next) -> + parse ''' + a,b,c + d,"e + ",f + g,h,i + ''', info: true, (err, records) -> + records.map( + ({info}) -> info.lines + ).should.eql [1, 2, 4] unless err + next err +