From a846f7184d27ac1c9e65cf83608ae8e89fe815d7 Mon Sep 17 00:00:00 2001 From: SheetJS Date: Wed, 22 Mar 2017 03:50:11 -0400 Subject: [PATCH] utility improvements - sheet_to_csv strip option (fixes #182 h/t @davidworkman9) - sheet_to_json dateNF option (fixes #134 h/t @rotemtam) - file type detection expanded to 4 byte magic number --- README.md | 8 ++++++-- bits/22_xmlutils.js | 2 +- bits/87_read.js | 21 ++++++++++++--------- bits/90_utils.js | 13 ++++++++----- docbits/80_parseopts.md | 3 +-- docbits/82_util.md | 5 +++++ docbits/99_badges.md | 2 +- test.js | 25 ++++++++++++++++++------- xlsx.flow.js | 36 +++++++++++++++++++++--------------- xlsx.js | 34 ++++++++++++++++++++-------------- 10 files changed, 93 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 857cb1e..f229ec5 100644 --- a/README.md +++ b/README.md @@ -677,7 +677,7 @@ The exported `read` and `readFile` functions accept an options argument: | cellHTML | true | Parse rich text and save HTML to the .h field | | cellNF | false | Save number format string to the .z field | | cellStyles | false | Save style/theme info to the .s field | -| cellDates | false | Store dates as type `d` (default is `n`) ** | +| cellDates | false | Store dates as type `d` (default is `n`) | | sheetStubs | false | Create cell objects of type `z` for stub cells | | sheetRows | 0 | If >0, read the first `sheetRows` rows ** | | bookDeps | false | If true, parse calculation chains | @@ -701,7 +701,6 @@ The exported `read` and `readFile` functions accept an options argument: - `sheetRows-1` rows will be generated when looking at the JSON object output (since the header row is counted as a row when parsing the data) - `bookVBA` merely exposes the raw vba object. It does not parse the data. -- `cellDates` currently does not convert numerical dates to JS dates. - Currently only XOR encryption is supported. Unsupported error will be thrown for files employing other encryption methods. - WTF is mainly for development. By default, the parser will suppress read @@ -826,6 +825,10 @@ produces CSV output. The function takes an options argument: | :---------- | :------: | :-------------------------------------------------- | | FS | `","` | "Field Separator" delimiter between fields | | RS | `"\n"` | "Record Separator" delimiter between rows | +| dateNF | fmt 14 | Use specified date format in string output | +| strip | false | Remove trailing field separators in each record ** | + +- `strip` will remove trailing commas from each line under default `FS/RS` For the example sheet: @@ -852,6 +855,7 @@ generate different types of JS objects. The function takes an options argument: | raw | `false` | Use raw values (true) or formatted strings (false) | | range | from WS | Override Range (see table below) | | header | | Control output format (see table below) | +| dateNF | fmt 14 | Use specified date format in string output | - `raw` only affects cells which have a format code (`.z`) field or a formatted text (`.w`) field. diff --git a/bits/22_xmlutils.js b/bits/22_xmlutils.js index 02d2d01..de09b13 100644 --- a/bits/22_xmlutils.js +++ b/bits/22_xmlutils.js @@ -2,7 +2,7 @@ var attregexg=/([^\s?>\/]+)=((?:")([^"]*)(?:")|(?:')([^']*)(?:'))/g; var tagregex=/<[^>]*>/g; var nsregex=/<\w*:/, nsregex2 = /<(\/?)\w+:/; function parsexmltag(tag/*:string*/, skip_root/*:?boolean*/)/*:any*/ { - var z = ([]/*:any*/); + var z = ({}/*:any*/); var eq = 0, c = 0; for(; eq !== tag.length; ++eq) if((c = tag.charCodeAt(eq)) === 32 || c === 10 || c === 13) break; if(!skip_root) z[0] = tag.substr(0, eq); diff --git a/bits/87_read.js b/bits/87_read.js index f72f70f..bf57537 100644 --- a/bits/87_read.js +++ b/bits/87_read.js @@ -1,11 +1,13 @@ -function firstbyte(f/*:RawData*/,o/*:?TypeOpts*/)/*:number*/ { +function firstbyte(f/*:RawData*/,o/*:?TypeOpts*/)/*:Array*/ { + var x = ""; switch((o||{}).type || "base64") { - case 'buffer': return f[0]; - case 'base64': return Base64.decode(f.substr(0,12)).charCodeAt(0); - case 'binary': return f.charCodeAt(0); - case 'array': return f[0]; + case 'buffer': return [f[0], f[1], f[2], f[3]]; + case 'base64': x = Base64.decode(f.substr(0,24)); break; + case 'binary': x = f; break; + case 'array': return [f[0], f[1], f[2], f[3]]; default: throw new Error("Unrecognized type " + (o ? o.type : "undefined")); } + return [x.charCodeAt(0), x.charCodeAt(1), x.charCodeAt(2), x.charCodeAt(3)]; } function read_zip(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { @@ -23,18 +25,19 @@ function read_zip(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { } function readSync(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { - var zip, d = data, n=0; + var zip, d = data, n=[0]; var o = opts||{}; if(!o.type) o.type = (has_buf && Buffer.isBuffer(data)) ? "buffer" : "base64"; if(o.type == "file") { o.type = "buffer"; d = _fs.readFileSync(data); } - switch((n = firstbyte(d, o))) { + switch((n = firstbyte(d, o))[0]) { case 0xD0: return parse_xlscfb(CFB.read(d, o), o); case 0x09: return parse_xlscfb(s2a(o.type === 'base64' ? Base64.decode(d) : d), o); case 0x3C: return parse_xlml(d, o); - case 0x50: return read_zip(d, o); + case 0x50: if(n[1] == 0x4B && n[2] < 0x20 && n[3] < 0x20) return read_zip(d, o); break; case 0xEF: return parse_xlml(d, o); - default: throw new Error("Unsupported file " + n); + default: throw new Error("Unsupported file " + n.join("|")); } + throw new Error("Unsupported file format " + n.join("|")); } function readFileSync(filename/*:string*/, opts/*:?ParseOpts*/)/*:Workbook*/ { diff --git a/bits/90_utils.js b/bits/90_utils.js index 1c12129..8665387 100644 --- a/bits/90_utils.js +++ b/bits/90_utils.js @@ -60,15 +60,16 @@ function safe_decode_range(range/*:string*/)/*:Range*/ { } function safe_format_cell(cell/*:Cell*/, v/*:any*/) { - if(cell.z !== undefined) try { return (cell.w = SSF.format(cell.z, v)); } catch(e) { } - if(!cell.XF) return v; - try { return (cell.w = SSF.format(cell.XF.ifmt||0, v)); } catch(e) { return ''+v; } + var q = (cell.t == 'd' && v instanceof Date); + if(cell.z != null) try { return (cell.w = SSF.format(cell.z, q ? datenum(v) : v)); } catch(e) { } + try { return (cell.w = SSF.format((cell.XF||{}).ifmt||(q ? 14 : 0), q ? datenum(v) : v)); } catch(e) { return ''+v; } } -function format_cell(cell/*:Cell*/, v/*:any*/) { +function format_cell(cell/*:Cell*/, v/*:any*/, o/*:any*/) { if(cell == null || cell.t == null || cell.t == 'z') return ""; if(cell.w !== undefined) return cell.w; - if(v === undefined) return safe_format_cell(cell, cell.v); + if(cell.t == 'd' && !cell.z && o && o.dateNF) cell.z = o.dateNF; + if(v == undefined) return safe_format_cell(cell, cell.v); return safe_format_cell(cell, v); } @@ -146,6 +147,7 @@ function sheet_to_csv(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/) { var r = safe_decode_range(sheet["!ref"]); var FS = o.FS !== undefined ? o.FS : ",", fs = FS.charCodeAt(0); var RS = o.RS !== undefined ? o.RS : "\n", rs = RS.charCodeAt(0); + var endregex = new RegExp(FS+"+$"); var row = "", rr = "", cols = []; var i = 0, cc = 0, val; var R = 0, C = 0; @@ -166,6 +168,7 @@ function sheet_to_csv(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/) { /* NOTE: Excel CSV does not support array formulae */ row += (C === r.s.c ? "" : FS) + txt; } + if(o.strip) row = row.replace(endregex,""); out += row + RS; } return out; diff --git a/docbits/80_parseopts.md b/docbits/80_parseopts.md index a4ef4f9..439cb9d 100644 --- a/docbits/80_parseopts.md +++ b/docbits/80_parseopts.md @@ -9,7 +9,7 @@ The exported `read` and `readFile` functions accept an options argument: | cellHTML | true | Parse rich text and save HTML to the .h field | | cellNF | false | Save number format string to the .z field | | cellStyles | false | Save style/theme info to the .s field | -| cellDates | false | Store dates as type `d` (default is `n`) ** | +| cellDates | false | Store dates as type `d` (default is `n`) | | sheetStubs | false | Create cell objects of type `z` for stub cells | | sheetRows | 0 | If >0, read the first `sheetRows` rows ** | | bookDeps | false | If true, parse calculation chains | @@ -33,7 +33,6 @@ The exported `read` and `readFile` functions accept an options argument: - `sheetRows-1` rows will be generated when looking at the JSON object output (since the header row is counted as a row when parsing the data) - `bookVBA` merely exposes the raw vba object. It does not parse the data. -- `cellDates` currently does not convert numerical dates to JS dates. - Currently only XOR encryption is supported. Unsupported error will be thrown for files employing other encryption methods. - WTF is mainly for development. By default, the parser will suppress read diff --git a/docbits/82_util.md b/docbits/82_util.md index 2a26720..474046d 100644 --- a/docbits/82_util.md +++ b/docbits/82_util.md @@ -34,6 +34,10 @@ produces CSV output. The function takes an options argument: | :---------- | :------: | :-------------------------------------------------- | | FS | `","` | "Field Separator" delimiter between fields | | RS | `"\n"` | "Record Separator" delimiter between rows | +| dateNF | fmt 14 | Use specified date format in string output | +| strip | false | Remove trailing field separators in each record ** | + +- `strip` will remove trailing commas from each line under default `FS/RS` For the example sheet: @@ -60,6 +64,7 @@ generate different types of JS objects. The function takes an options argument: | raw | `false` | Use raw values (true) or formatted strings (false) | | range | from WS | Override Range (see table below) | | header | | Control output format (see table below) | +| dateNF | fmt 14 | Use specified date format in string output | - `raw` only affects cells which have a format code (`.z`) field or a formatted text (`.w`) field. diff --git a/docbits/99_badges.md b/docbits/99_badges.md index 615917c..f8d2fb1 100644 --- a/docbits/99_badges.md +++ b/docbits/99_badges.md @@ -1,6 +1,6 @@ ## Badges -[![Build Status](https://saucelabs.com/browser-matrix/xlsx.svg)](https://saucelabs.com/u/xlsx) +[![Build Status](https://saucelabs.com/browser-matrix/sheetjs.svg)](https://saucelabs.com/u/sheetjs) [![Build Status](https://travis-ci.org/SheetJS/js-xlsx.svg?branch=master)](https://travis-ci.org/SheetJS/js-xlsx) diff --git a/test.js b/test.js index 5bde88a..cac1f91 100644 --- a/test.js +++ b/test.js @@ -1056,8 +1056,9 @@ function sheet_from_array_of_arrays(data, opts) { if(typeof cell.v === 'number') cell.t = 'n'; else if(typeof cell.v === 'boolean') cell.t = 'b'; else if(cell.v instanceof Date) { - cell.t = 'n'; cell.z = X.SSF._table[14]; - cell.v = datenum(cell.v); + cell.z = X.SSF._table[14]; + if(opts && opts.cellDates) cell.t = 'd'; + else { cell.t = 'n'; cell.v = datenum(cell.v); } } else cell.t = 's'; ws[cell_ref] = cell; @@ -1090,7 +1091,7 @@ describe('json output', function() { it('should use first-row headers and full sheet by default', function() { var json = X.utils.sheet_to_json(ws); assert.equal(json.length, data.length - 1); - assert.equal(json[0][1], true); + assert.equal(json[0][1], "TRUE"); assert.equal(json[1][2], "bar"); assert.equal(json[2][3], "qux"); assert.doesNotThrow(function() { seeker(json, [1,2,3], "sheetjs"); }); @@ -1099,7 +1100,7 @@ describe('json output', function() { it('should create array of arrays if header == 1', function() { var json = X.utils.sheet_to_json(ws, {header:1}); assert.equal(json.length, data.length); - assert.equal(json[1][0], true); + assert.equal(json[1][0], "TRUE"); assert.equal(json[2][1], "bar"); assert.equal(json[3][2], "qux"); assert.doesNotThrow(function() { seeker(json, [0,1,2], "sheetjs"); }); @@ -1109,7 +1110,7 @@ describe('json output', function() { it('should use column names if header == "A"', function() { var json = X.utils.sheet_to_json(ws, {header:'A'}); assert.equal(json.length, data.length); - assert.equal(json[1].A, true); + assert.equal(json[1].A, "TRUE"); assert.equal(json[2].B, "bar"); assert.equal(json[3].C, "qux"); assert.doesNotThrow(function() { seeker(json, "ABC", "sheetjs"); }); @@ -1119,7 +1120,7 @@ describe('json output', function() { it('should use column labels if specified', function() { var json = X.utils.sheet_to_json(ws, {header:["O","D","I","N"]}); assert.equal(json.length, data.length); - assert.equal(json[1].O, true); + assert.equal(json[1].O, "TRUE"); assert.equal(json[2].D, "bar"); assert.equal(json[3].I, "qux"); assert.doesNotThrow(function() { seeker(json, "ODI", "sheetjs"); }); @@ -1130,7 +1131,7 @@ describe('json output', function() { it('should accept custom ' + w[0] + ' range', function() { var json = X.utils.sheet_to_json(ws, {header:1, range:w[1]}); assert.equal(json.length, 3); - assert.equal(json[0][0], true); + assert.equal(json[0][0], "TRUE"); assert.equal(json[1][1], "bar"); assert.equal(json[2][2], "qux"); assert.doesNotThrow(function() { seeker(json, [0,1,2], "sheetjs"); }); @@ -1152,6 +1153,16 @@ describe('json output', function() { assert.equal(json[i].S_1, 7 + i); } }); + it('should handle raw data if requested', function() { + var _ws = sheet_from_array_of_arrays(data, {cellDates:true}); + var json = X.utils.sheet_to_json(_ws, {header:1, raw:true}); + console.log(json, typeof json[2][2]); + assert.equal(json.length, data.length); + assert.equal(json[1][0], true); + assert.equal(json[2][1], "bar"); + assert.equal(json[2][2].getTime(), new Date("2014-02-19T14:30Z").getTime()); + assert.equal(json[3][2], "qux"); + }); }); describe('js -> file -> js', function() { diff --git a/xlsx.flow.js b/xlsx.flow.js index f7d1455..5f783c4 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -1516,7 +1516,7 @@ var attregexg=/([^\s?>\/]+)=((?:")([^"]*)(?:")|(?:')([^']*)(?:'))/g; var tagregex=/<[^>]*>/g; var nsregex=/<\w*:/, nsregex2 = /<(\/?)\w+:/; function parsexmltag(tag/*:string*/, skip_root/*:?boolean*/)/*:any*/ { - var z = ([]/*:any*/); + var z = ({}/*:any*/); var eq = 0, c = 0; for(; eq !== tag.length; ++eq) if((c = tag.charCodeAt(eq)) === 32 || c === 10 || c === 13) break; if(!skip_root) z[0] = tag.substr(0, eq); @@ -13860,14 +13860,16 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { zip.file('xl/_rels/workbook.' + wbext + '.rels', write_rels(opts.wbrels)); return zip; } -function firstbyte(f/*:RawData*/,o/*:?TypeOpts*/)/*:number*/ { +function firstbyte(f/*:RawData*/,o/*:?TypeOpts*/)/*:Array*/ { + var x = ""; switch((o||{}).type || "base64") { - case 'buffer': return f[0]; - case 'base64': return Base64.decode(f.substr(0,12)).charCodeAt(0); - case 'binary': return f.charCodeAt(0); - case 'array': return f[0]; + case 'buffer': return [f[0], f[1], f[2], f[3]]; + case 'base64': x = Base64.decode(f.substr(0,24)); break; + case 'binary': x = f; break; + case 'array': return [f[0], f[1], f[2], f[3]]; default: throw new Error("Unrecognized type " + (o ? o.type : "undefined")); } + return [x.charCodeAt(0), x.charCodeAt(1), x.charCodeAt(2), x.charCodeAt(3)]; } function read_zip(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { @@ -13885,18 +13887,19 @@ function read_zip(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { } function readSync(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { - var zip, d = data, n=0; + var zip, d = data, n=[0]; var o = opts||{}; if(!o.type) o.type = (has_buf && Buffer.isBuffer(data)) ? "buffer" : "base64"; if(o.type == "file") { o.type = "buffer"; d = _fs.readFileSync(data); } - switch((n = firstbyte(d, o))) { + switch((n = firstbyte(d, o))[0]) { case 0xD0: return parse_xlscfb(CFB.read(d, o), o); case 0x09: return parse_xlscfb(s2a(o.type === 'base64' ? Base64.decode(d) : d), o); case 0x3C: return parse_xlml(d, o); - case 0x50: return read_zip(d, o); + case 0x50: if(n[1] == 0x4B && n[2] < 0x20 && n[3] < 0x20) return read_zip(d, o); break; case 0xEF: return parse_xlml(d, o); - default: throw new Error("Unsupported file " + n); + default: throw new Error("Unsupported file " + n.join("|")); } + throw new Error("Unsupported file format " + n.join("|")); } function readFileSync(filename/*:string*/, opts/*:?ParseOpts*/)/*:Workbook*/ { @@ -14044,15 +14047,16 @@ function safe_decode_range(range/*:string*/)/*:Range*/ { } function safe_format_cell(cell/*:Cell*/, v/*:any*/) { - if(cell.z !== undefined) try { return (cell.w = SSF.format(cell.z, v)); } catch(e) { } - if(!cell.XF) return v; - try { return (cell.w = SSF.format(cell.XF.ifmt||0, v)); } catch(e) { return ''+v; } + var q = (cell.t == 'd' && v instanceof Date); + if(cell.z != null) try { return (cell.w = SSF.format(cell.z, q ? datenum(v) : v)); } catch(e) { } + try { return (cell.w = SSF.format((cell.XF||{}).ifmt||(q ? 14 : 0), q ? datenum(v) : v)); } catch(e) { return ''+v; } } -function format_cell(cell/*:Cell*/, v/*:any*/) { +function format_cell(cell/*:Cell*/, v/*:any*/, o/*:any*/) { if(cell == null || cell.t == null || cell.t == 'z') return ""; if(cell.w !== undefined) return cell.w; - if(v === undefined) return safe_format_cell(cell, cell.v); + if(cell.t == 'd' && !cell.z && o && o.dateNF) cell.z = o.dateNF; + if(v == undefined) return safe_format_cell(cell, cell.v); return safe_format_cell(cell, v); } @@ -14130,6 +14134,7 @@ function sheet_to_csv(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/) { var r = safe_decode_range(sheet["!ref"]); var FS = o.FS !== undefined ? o.FS : ",", fs = FS.charCodeAt(0); var RS = o.RS !== undefined ? o.RS : "\n", rs = RS.charCodeAt(0); + var endregex = new RegExp(FS+"+$"); var row = "", rr = "", cols = []; var i = 0, cc = 0, val; var R = 0, C = 0; @@ -14150,6 +14155,7 @@ function sheet_to_csv(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/) { /* NOTE: Excel CSV does not support array formulae */ row += (C === r.s.c ? "" : FS) + txt; } + if(o.strip) row = row.replace(endregex,""); out += row + RS; } return out; diff --git a/xlsx.js b/xlsx.js index 668c870..08011e9 100644 --- a/xlsx.js +++ b/xlsx.js @@ -1467,7 +1467,7 @@ var attregexg=/([^\s?>\/]+)=((?:")([^"]*)(?:")|(?:')([^']*)(?:'))/g; var tagregex=/<[^>]*>/g; var nsregex=/<\w*:/, nsregex2 = /<(\/?)\w+:/; function parsexmltag(tag, skip_root) { - var z = ([]); + var z = ({}); var eq = 0, c = 0; for(; eq !== tag.length; ++eq) if((c = tag.charCodeAt(eq)) === 32 || c === 10 || c === 13) break; if(!skip_root) z[0] = tag.substr(0, eq); @@ -13799,13 +13799,15 @@ f = "docProps/app.xml"; return zip; } function firstbyte(f,o) { + var x = ""; switch((o||{}).type || "base64") { - case 'buffer': return f[0]; - case 'base64': return Base64.decode(f.substr(0,12)).charCodeAt(0); - case 'binary': return f.charCodeAt(0); - case 'array': return f[0]; + case 'buffer': return [f[0], f[1], f[2], f[3]]; + case 'base64': x = Base64.decode(f.substr(0,24)); break; + case 'binary': x = f; break; + case 'array': return [f[0], f[1], f[2], f[3]]; default: throw new Error("Unrecognized type " + (o ? o.type : "undefined")); } + return [x.charCodeAt(0), x.charCodeAt(1), x.charCodeAt(2), x.charCodeAt(3)]; } function read_zip(data, opts) { @@ -13822,18 +13824,19 @@ var zip, d = data; } function readSync(data, opts) { - var zip, d = data, n=0; + var zip, d = data, n=[0]; var o = opts||{}; if(!o.type) o.type = (has_buf && Buffer.isBuffer(data)) ? "buffer" : "base64"; if(o.type == "file") { o.type = "buffer"; d = _fs.readFileSync(data); } - switch((n = firstbyte(d, o))) { + switch((n = firstbyte(d, o))[0]) { case 0xD0: return parse_xlscfb(CFB.read(d, o), o); case 0x09: return parse_xlscfb(s2a(o.type === 'base64' ? Base64.decode(d) : d), o); case 0x3C: return parse_xlml(d, o); - case 0x50: return read_zip(d, o); + case 0x50: if(n[1] == 0x4B && n[2] < 0x20 && n[3] < 0x20) return read_zip(d, o); break; case 0xEF: return parse_xlml(d, o); - default: throw new Error("Unsupported file " + n); + default: throw new Error("Unsupported file " + n.join("|")); } + throw new Error("Unsupported file format " + n.join("|")); } function readFileSync(filename, opts) { @@ -13977,15 +13980,16 @@ function safe_decode_range(range) { } function safe_format_cell(cell, v) { - if(cell.z !== undefined) try { return (cell.w = SSF.format(cell.z, v)); } catch(e) { } - if(!cell.XF) return v; - try { return (cell.w = SSF.format(cell.XF.ifmt||0, v)); } catch(e) { return ''+v; } + var q = (cell.t == 'd' && v instanceof Date); + if(cell.z != null) try { return (cell.w = SSF.format(cell.z, q ? datenum(v) : v)); } catch(e) { } + try { return (cell.w = SSF.format((cell.XF||{}).ifmt||(q ? 14 : 0), q ? datenum(v) : v)); } catch(e) { return ''+v; } } -function format_cell(cell, v) { +function format_cell(cell, v, o) { if(cell == null || cell.t == null || cell.t == 'z') return ""; if(cell.w !== undefined) return cell.w; - if(v === undefined) return safe_format_cell(cell, cell.v); + if(cell.t == 'd' && !cell.z && o && o.dateNF) cell.z = o.dateNF; + if(v == undefined) return safe_format_cell(cell, cell.v); return safe_format_cell(cell, v); } @@ -14063,6 +14067,7 @@ function sheet_to_csv(sheet, opts) { var r = safe_decode_range(sheet["!ref"]); var FS = o.FS !== undefined ? o.FS : ",", fs = FS.charCodeAt(0); var RS = o.RS !== undefined ? o.RS : "\n", rs = RS.charCodeAt(0); + var endregex = new RegExp(FS+"+$"); var row = "", rr = "", cols = []; var i = 0, cc = 0, val; var R = 0, C = 0; @@ -14083,6 +14088,7 @@ function sheet_to_csv(sheet, opts) { /* NOTE: Excel CSV does not support array formulae */ row += (C === r.s.c ? "" : FS) + txt; } + if(o.strip) row = row.replace(endregex,""); out += row + RS; } return out;