Skip to content

Commit ee0dfbf

Browse files
authored
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
1 parent d8904d0 commit ee0dfbf

File tree

4 files changed

+72
-21
lines changed

4 files changed

+72
-21
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ Returns a promise to the file loads as a [SQLite database client](https://observ
379379
const db = await FileAttachment("chinook.db").sqlite();
380380
```
381381

382+
<a href="#attachment_xlsx" name="attachment_xlsx">#</a> *attachment*.<b>xlsx</b>() [<>](https://github.com/observablehq/stdlib/blob/master/src/xlsx.js "Source")
383+
384+
Returns a promise to the file loaded as an [ExcelWorkbook](https://observablehq.com/@observablehq/excelworkbook).
385+
382386
<a href="#FileAttachments" name="FileAttachments">#</a> <b>FileAttachments</b>(<i>resolve</i>) [<>](https://github.com/observablehq/stdlib/blob/master/src/fileAttachment.js "Source")
383387

384388
*Note: this function is not part of the Observable standard library (in notebooks), but is provided by this module as a means for defining custom file attachment implementations when working directly with the Observable runtime.*

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"url": "https://github.com/observablehq/stdlib.git"
1414
},
1515
"scripts": {
16-
"test": "tap 'test/**/*-test.js'",
16+
"test": "tap 'test/**/*-test.js' --reporter classic",
1717
"prepublishOnly": "rollup -c",
1818
"postpublish": "git push && git push --tags"
1919
},

src/xlsx.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,24 @@ export class ExcelWorkbook {
66
return this._.worksheets.map((sheet) => sheet.name);
77
}
88
sheet(name, {range, headers = false} = {}) {
9-
const sheet = this._.getWorksheet(
10-
typeof name === "number" ? this.sheetNames()[name] : name + ""
11-
);
12-
if (!sheet) throw new Error(`Sheet not found: ${name}`);
9+
const names = this.sheetNames();
10+
const sname = typeof name === "number" ? names[name] : names.includes(name + "") ? name + "" : null;
11+
if (sname == null) throw new Error(`Sheet not found: ${name}`);
12+
const sheet = this._.getWorksheet(sname);
1313
return extract(sheet, {range, headers});
1414
}
1515
}
1616

1717
function extract(sheet, {range, headers}) {
1818
let [[c0, r0], [c1, r1]] = parseRange(range, sheet);
19-
const seen = new Set();
20-
const names = [];
2119
const headerRow = headers && sheet._rows[r0++];
22-
function name(n) {
23-
if (!names[n]) {
24-
let name = (headerRow ? valueOf(headerRow._cells[n]) : AA(n)) || AA(n);
25-
while (seen.has(name)) name += "_";
26-
seen.add((names[n] = name));
27-
}
28-
return names[n];
20+
let names = new Set();
21+
for (let n = c0; n <= c1; n++) {
22+
let name = (headerRow ? valueOf(headerRow._cells[n]) : null) || AA(n);
23+
while (names.has(name)) name += "_";
24+
names.add(name);
2925
}
30-
if (headerRow) for (let c = c0; c <= c1; c++) name(c);
26+
names = new Array(c0).concat(Array.from(names));
3127

3228
const output = new Array(r1 - r0 + 1).fill({});
3329
for (let r = r0; r <= r1; r++) {
@@ -36,7 +32,7 @@ function extract(sheet, {range, headers}) {
3632
const row = (output[r - r0] = {});
3733
for (let c = c0; c <= c1; c++) {
3834
const value = valueOf(_row._cells[c]);
39-
if (value !== null && value !== undefined) row[name(c)] = value;
35+
if (value != null) row[names[c]] = value;
4036
}
4137
}
4238

@@ -75,6 +71,8 @@ function parseRange(specifier = [], {columnCount, rowCount}) {
7571
[c0, r0],
7672
[c1, r1],
7773
];
74+
} else {
75+
throw new Error(`Unknown range specifier`);
7876
}
7977
}
8078

@@ -87,13 +85,13 @@ function AA(c) {
8785
return sc;
8886
}
8987

90-
function NN(s = "") {
91-
const [, sc, sr] = s.match(/^([a-zA-Z]+)?(\d+)?$/);
88+
function NN(s) {
89+
const [, sc, sr] = s.match(/^([A-Z]*)(\d*)$/i);
9290
let c = undefined;
9391
if (sc) {
9492
c = 0;
9593
for (let i = 0; i < sc.length; i++)
9694
c += Math.pow(26, sc.length - i - 1) * (sc.charCodeAt(i) - 64);
9795
}
98-
return [c && c - 1, sr && +sr - 1];
96+
return [c ? c - 1 : undefined, sr ? +sr - 1 : undefined];
9997
}

test/xlsx-test.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ test("FileAttachment.xlsx reads sheet names", (t) => {
2424
t.end();
2525
});
2626

27+
test("FileAttachment.xlsx sheet(name) throws on unknown sheet name", (t) => {
28+
const workbook = new ExcelWorkbook(mockWorkbook({Sheet1: []}));
29+
t.throws(() => workbook.sheet("bad"));
30+
t.end();
31+
});
32+
2733
test("FileAttachment.xlsx reads sheets", (t) => {
2834
const workbook = new ExcelWorkbook(
2935
mockWorkbook({
@@ -37,25 +43,31 @@ test("FileAttachment.xlsx reads sheets", (t) => {
3743
{A: "one", B: "two", C: "three"},
3844
{A: 1, B: 2, C: 3},
3945
]);
46+
t.same(workbook.sheet("Sheet1"), [
47+
{A: "one", B: "two", C: "three"},
48+
{A: 1, B: 2, C: 3},
49+
]);
4050
t.end();
4151
});
4252

4353
test("FileAttachment.xlsx reads sheets with different types", (t) => {
4454
const workbook = new ExcelWorkbook(
4555
mockWorkbook({
4656
Sheet1: [
47-
["one", {richText: [{text: "two"}, {text: "three"}]}],
57+
["one", null, {richText: [{text: "two"}, {text: "three"}]}, undefined],
4858
[
4959
{text: "link", hyperlink: "https://example.com"},
5060
2,
5161
{formula: "=B2*5", result: 10},
5262
],
63+
[],
5364
],
5465
})
5566
);
5667
t.same(workbook.sheet(0), [
57-
{A: "one", B: "twothree"},
68+
{A: "one", C: "twothree"},
5869
{A: `<a href="https://example.com">link</a>`, B: 2, C: 10},
70+
{},
5971
]);
6072
t.end();
6173
});
@@ -152,5 +164,42 @@ test("FileAttachment.xlsx reads sheet ranges", (t) => {
152164
t.same(workbook.sheet(0, {range: "2"}), entireSheet.slice(1));
153165
t.same(workbook.sheet(0, {range: [[undefined, 1]]}), entireSheet.slice(1));
154166

167+
// ":I"
168+
// [,[1,]]
169+
const sheetJ = [
170+
{ I: 8, J: 9 },
171+
{ I: 18, J: 19 },
172+
{ I: 28, J: 29 },
173+
{ I: 38, J: 39 }
174+
];
175+
t.same(workbook.sheet(0, {range: "I"}), sheetJ);
176+
t.same(workbook.sheet(0, {range: [[8, undefined], undefined]}), sheetJ);
177+
t.end();
178+
});
179+
180+
test("FileAttachment.xlsx throws on unknown range specifier", (t) => {
181+
const workbook = new ExcelWorkbook(mockWorkbook({ Sheet1: [] }));
182+
t.throws(() => workbook.sheet(0, {range: 0}));
183+
t.end();
184+
});
185+
186+
test("FileAttachment.xlsx derives column names such as A AA AAA…", (t) => {
187+
const l0 = 26 * 26 * 26 + 26 * 26 + 26;
188+
const workbook = new ExcelWorkbook(
189+
mockWorkbook({
190+
Sheet1: [
191+
Array.from({length: l0}).fill(1),
192+
],
193+
})
194+
);
195+
t.same(workbook.sheet(0, {headers: false}).columns.filter(d => d.match(/^A*$/)), ["A", "AA", "AAA"]);
196+
const workbook1 = new ExcelWorkbook(
197+
mockWorkbook({
198+
Sheet1: [
199+
Array.from({length: l0 + 1}).fill(1),
200+
],
201+
})
202+
);
203+
t.same(workbook1.sheet(0, {headers: false}).columns.filter(d => d.match(/^A*$/)), ["A", "AA", "AAA", "AAAA"]);
155204
t.end();
156205
});

0 commit comments

Comments
 (0)