-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* XLSX support with ExcelJS * Prettier * Change range option to nested arrays General code clean up * Tests and bug fixes * Respect header row order when resolving conflicts * Fil/xlsx (#249) * document xlsx (minimalist, we'll work on the notebook first) * fix coverage reporter (avoids a crash on my computer; solution found at tapjs/tapjs#624) * unknown sheet name * simplify rows naming * NN is always called on string (cell specifier such as "AA99") * test name * more range specifiers * Column only range test case * sheetNames is enumerable * One more test to check for empty columns Prettier + use default/base tap reporter * Add Node 16 to the test matrix * Revert reporter to classic for Node 16 * Don't fail matrix quickly in actions * More coverage. * Example of .xlsx in README * Remove Excel from Workbook naming * Fix dates * Fix for sharedFormula * Coerce errors to NaN * Properly escape html * Make sheetNames read-only * Require colons in range specifiers * Include row numbers * Use only string form ranges * Coerce range specifiers to strings * Update README.md Co-authored-by: Mike Bostock <mbostock@gmail.com> * Apply suggestions from code review Co-authored-by: Mike Bostock <mbostock@gmail.com> * Simplify hyperlinks * Prettier * Pass options through * Rename helper functions for clarity, range tests * Simpler * Consistent comment format * Consistent regexes * Fix hyperlinks for certain cases Co-authored-by: Philippe Rivière <fil@rezo.net> Co-authored-by: Mike Bostock <mbostock@gmail.com>
- Loading branch information
1 parent
6cfe135
commit db58b85
Showing
7 changed files
with
412 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
export class Workbook { | ||
constructor(workbook) { | ||
Object.defineProperties(this, { | ||
_: {value: workbook}, | ||
sheetNames: { | ||
value: workbook.worksheets.map((s) => s.name), | ||
enumerable: true, | ||
}, | ||
}); | ||
} | ||
sheet(name, options) { | ||
const sname = | ||
typeof name === "number" | ||
? this.sheetNames[name] | ||
: this.sheetNames.includes((name += "")) | ||
? name | ||
: null; | ||
if (sname == null) throw new Error(`Sheet not found: ${name}`); | ||
const sheet = this._.getWorksheet(sname); | ||
return extract(sheet, options); | ||
} | ||
} | ||
|
||
function extract(sheet, {range, headers = false} = {}) { | ||
let [[c0, r0], [c1, r1]] = parseRange(range, sheet); | ||
const headerRow = headers && sheet._rows[r0++]; | ||
let names = new Set(["#"]); | ||
for (let n = c0; n <= c1; n++) { | ||
let name = (headerRow ? valueOf(headerRow._cells[n]) : null) || toColumn(n); | ||
while (names.has(name)) name += "_"; | ||
names.add(name); | ||
} | ||
names = new Array(c0).concat(Array.from(names)); | ||
|
||
const output = new Array(r1 - r0 + 1); | ||
for (let r = r0; r <= r1; r++) { | ||
const row = (output[r - r0] = Object.defineProperty({}, "#", { | ||
value: r + 1, | ||
})); | ||
const _row = sheet._rows[r]; | ||
if (_row && _row.hasValues) | ||
for (let c = c0; c <= c1; c++) { | ||
const value = valueOf(_row._cells[c]); | ||
if (value != null) row[names[c + 1]] = value; | ||
} | ||
} | ||
|
||
output.columns = names.filter(() => true); // Filter sparse columns | ||
return output; | ||
} | ||
|
||
function valueOf(cell) { | ||
if (!cell) return; | ||
const {value} = cell; | ||
if (value && value instanceof Date) return value; | ||
if (value && typeof value === "object") { | ||
if (value.formula || value.sharedFormula) | ||
return value.result && value.result.error ? NaN : value.result; | ||
if (value.richText) return value.richText.map((d) => d.text).join(""); | ||
if (value.text) { | ||
let {text} = value; | ||
if (text.richText) text = text.richText.map((d) => d.text).join(""); | ||
return value.hyperlink && value.hyperlink !== text | ||
? `${value.hyperlink} ${text}` | ||
: text; | ||
} | ||
return value; | ||
} | ||
return value; | ||
} | ||
|
||
function parseRange(specifier = ":", {columnCount, rowCount}) { | ||
specifier += ""; | ||
if (!specifier.match(/^[A-Z]*\d*:[A-Z]*\d*$/)) | ||
throw new Error("Malformed range specifier"); | ||
const [[c0 = 0, r0 = 0], [c1 = columnCount - 1, r1 = rowCount - 1]] = | ||
specifier.split(":").map(fromCellReference); | ||
return [ | ||
[c0, r0], | ||
[c1, r1], | ||
]; | ||
} | ||
|
||
// Returns the default column name for a zero-based column index. | ||
// For example: 0 -> "A", 1 -> "B", 25 -> "Z", 26 -> "AA", 27 -> "AB". | ||
function toColumn(c) { | ||
let sc = ""; | ||
c++; | ||
do { | ||
sc = String.fromCharCode(64 + (c % 26 || 26)) + sc; | ||
} while ((c = Math.floor((c - 1) / 26))); | ||
return sc; | ||
} | ||
|
||
// Returns the zero-based indexes from a cell reference. | ||
// For example: "A1" -> [0, 0], "B2" -> [1, 1], "AA10" -> [26, 9]. | ||
function fromCellReference(s) { | ||
const [, sc, sr] = s.match(/^([A-Z]*)(\d*)$/); | ||
let c = 0; | ||
if (sc) | ||
for (let i = 0; i < sc.length; i++) | ||
c += Math.pow(26, sc.length - i - 1) * (sc.charCodeAt(i) - 64); | ||
return [c ? c - 1 : undefined, sr ? +sr - 1 : undefined]; | ||
} |
Oops, something went wrong.