Skip to content
This repository has been archived by the owner on Jul 5, 2022. It is now read-only.

Commit

Permalink
Implements support for XLS and XLSX files
Browse files Browse the repository at this point in the history
  • Loading branch information
adamvoss committed Aug 26, 2017
1 parent 1992695 commit 40cd588
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 25 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ These formats are conceptually different and only work on a limited basis. With

Tabular formats:
- csv
- xls
- xlsx

## Usage

Expand All @@ -90,6 +92,13 @@ any-json can be used to convert (almost) anything to JSON.
This version supports:
cson, csv, hjson, ini, json, json5, toml, yaml
This version has is beta support for:
xls, xlsx
The beta formats should work, but since they are new,
behavior may change in later releases in response to feedback
without requiring a major version update.
command:
convert convert between formats (default when none specified)
combine combine multiple documents
Expand Down
60 changes: 60 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,43 @@ interface FormatConversion {
decode(text: string, reviver?: (key: any, value: any) => any): Promise<any>
}

abstract class AbstractWorkbookConverter implements FormatConversion {
abstract readonly name: string;
abstract readonly bookType: XLSX.BookType;

public async encode(value: any) {
const book = XLSX.utils.book_new();

if (Array.isArray(value)) {
const sheet = XLSX.utils.json_to_sheet(value);
XLSX.utils.book_append_sheet(book, sheet);
}
else {
for (let sheetName of Object.getOwnPropertyNames(value)) {
const sheet = XLSX.utils.json_to_sheet(value[sheetName]);
XLSX.utils.book_append_sheet(book, sheet, sheetName);
}
}

return XLSX.write(book, { WTF: true, bookType: this.bookType, type: "binary" });
}

public async decode(text: string, reviver?: (key: any, value: any) => any): Promise<any> {
const book = XLSX.read(text, { type: "binary" });
if (book.SheetNames.length === 1) {
return XLSX.utils.sheet_to_json(book.Sheets[book.SheetNames[0]], {raw: true, defval: null});
}

const result: any = {};

for (let sheet of book.SheetNames) {
result[sheet] = XLSX.utils.sheet_to_json(book.Sheets[sheet], {raw: true, defval: null })
}

return result;
}
}

class CsonConverter implements FormatConversion {
readonly name: string = 'cson'

Expand Down Expand Up @@ -150,6 +187,16 @@ class TomlConverter implements FormatConversion {
}
}

class XlsxConverter extends AbstractWorkbookConverter {
readonly name = "xlsx"
readonly bookType = "xlsx";
}

class XlsConverter extends AbstractWorkbookConverter {
readonly name = "xls"
readonly bookType = "xlml";
}

class XmlConverter implements FormatConversion {
readonly name: string = 'xml'

Expand Down Expand Up @@ -183,6 +230,8 @@ const codecs = new Map([
new JsonConverter(),
new Json5Converter(),
new TomlConverter(),
new XlsConverter(),
new XlsxConverter(),
new XmlConverter(),
new YamlConverter()
].map(c => [c.name, c] as [string, FormatConversion]))
Expand All @@ -205,4 +254,15 @@ export async function encode(value: any, format: string): Promise<string | Buffe
}

throw new Error("Unknown format " + format + "!");
}

/**
* Gets the anticipated string encoding for the given format.
*/
export function getEncoding(format: string) {
switch (format) {
case "xlsx": return "binary";
case "xls": return "binary";
default: return "utf8";
}
}
33 changes: 15 additions & 18 deletions lib/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@ function removeLeadingDot(formatOrExtension: string) {
else return formatOrExtension;
}

function getEncoding(format: string) {
switch (format) {
case "xlsx": return "binary";
case "xls": return "binary";
default: return "utf8";
}
}

function legacyOptionsParsing(argv: string[]) {
const options: any = {}
var unnamedArgs = [] as string[];
Expand Down Expand Up @@ -131,6 +123,13 @@ any-json can be used to convert (almost) anything to JSON.
This version supports:
cson, csv, hjson, ini, json, json5, toml, yaml
This version has is beta support for:
xls, xlsx
The beta formats should work, but since they are new,
behavior may change in later releases in response to feedback
without requiring a major version update.
command:
convert convert between formats (default when none specified)
combine combine multiple documents
Expand Down Expand Up @@ -180,7 +179,7 @@ for help use 'any-json -?`;
}

const stdin = process.stdin;
const encoding = getEncoding(options.format);
const encoding = anyjson.getEncoding(options.format);

stdin.resume();
if (encoding !== "binary") {
Expand All @@ -198,18 +197,19 @@ for help use 'any-json -?`;
}

const format = options.format || getFormatFromFileName(unnamedArgs[0]);
const input = await anyjson.decode(await readFile(unnamedArgs[0], 'utf8'), format);
const input = await anyjson.decode(await readFile(unnamedArgs[0], anyjson.getEncoding(format)), format);

return await legacyEncode(options, input);
}
// Not legacy parsing continue as usual
}

async function convert(value: any, options: any, outputFileName?: string) {
const result = await anyjson.encode(value, options.output_format || getFormatFromFileName(outputFileName) || "json");
const output_format = options.output_format || getFormatFromFileName(outputFileName) || "json";
const result = await anyjson.encode(value, output_format);

if (outputFileName) {
await util.promisify(fs.writeFile)(outputFileName, result, "utf8")
await util.promisify(fs.writeFile)(outputFileName, result, anyjson.getEncoding(output_format))
return "";
}
return result;
Expand All @@ -227,8 +227,7 @@ for help use 'any-json -?`;
const fileName = options._args[0] as string;
const format = options.input_format || getFormatFromFileName(fileName);

// TODO: Will need to check for binary files (see `getEncoding`)
const fileContents = await readFile(fileName, "utf8")
const fileContents = await readFile(fileName, anyjson.getEncoding(format))

const parsed = await anyjson.decode(fileContents, format)

Expand All @@ -242,9 +241,8 @@ for help use 'any-json -?`;

const items = await Promise.all(
options._args.map(async fileName => {
// TODO: Will need to check for binary files (see `getEncoding`)
const format = options.input_format || getFormatFromFileName(fileName);
const fileContents = await readFile(fileName, 'utf8') as string
const fileContents = await readFile(fileName, anyjson.getEncoding(format)) as string
return await anyjson.decode(fileContents, format)
})
)
Expand All @@ -263,7 +261,6 @@ for help use 'any-json -?`;

const format = options.input_format || getFormatFromFileName(fileName);

// TODO: Will need to check for binary files (see `getEncoding`)
const fileContents = await readFile(fileName, "utf8")

const array = await anyjson.decode(fileContents, format) as any[]
Expand All @@ -289,7 +286,7 @@ for help use 'any-json -?`;

if (require.main === module) {
main(process.argv)
.then(s => console.log(s))
.then(s => process.stdout.write(s as string, 'binary'))
.catch(error => {
console.error(error);
process.exit(2);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"typescript": "^2.4.2",
"util.promisify": "^1.0.0",
"xlsjs": "^0.7.5",
"xlsx": "^0.8.1",
"xlsx": "^0.11.3",
"xml2js": "0.4.17"
},
"devDependencies": {
Expand Down
22 changes: 22 additions & 0 deletions test/fixtures/in/multi-sheet-workbook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"Quantity": [
{
"Name": "Widget 1",
"Quantity": 5
},
{
"Name": "Widget 2",
"Quantity": 3
}
],
"Prices": [
{
"Name": "Widget A",
"Price": 3
},
{
"Name": "Widget B",
"Price": 0.26
}
]
}
18 changes: 18 additions & 0 deletions test/fixtures/in/product-set.xls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"id": 2,
"name": "An ice sculpture",
"price": 12.5,
"tags": null,
"dimensions": null,
"warehouseLocation": null
},
{
"id": 3,
"name": "A blue mouse",
"price": 25.5,
"tags": null,
"dimensions": null,
"warehouseLocation": null
}
]
18 changes: 18 additions & 0 deletions test/fixtures/in/product-set.xlsx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"id": 2,
"name": "An ice sculpture",
"price": 12.5,
"tags": null,
"dimensions": null,
"warehouseLocation": null
},
{
"id": 3,
"name": "A blue mouse",
"price": 25.5,
"tags": null,
"dimensions": null,
"warehouseLocation": null
}
]
Binary file added test/fixtures/out/multi-sheet-workbook.xls
Binary file not shown.
Binary file added test/fixtures/out/multi-sheet-workbook.xlsx
Binary file not shown.
Binary file added test/fixtures/out/product-set.xls
Binary file not shown.
Binary file added test/fixtures/out/product-set.xlsx
Binary file not shown.
37 changes: 31 additions & 6 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,43 @@ suite('problematic-formats', () => {
})
});

suite('tabular-formats', () => {

const tabular_formats = [
"csv",
// "xls",
// "xlsx"
]
suite('tabular-formats', () => {
const tabular_formats = ["csv"];

testEncode(tabular_formats);
testDecode(tabular_formats);
});

suite('multi-sheet workbooks', () => {

const multisheet_formats = [
"xls",
"xlsx"
];

testDecode(multisheet_formats);

for (const format of multisheet_formats) {
test(`decode ${format}`, async () => {
const expected = JSON.parse(await readInputFixture(`multi-sheet-workbook.json`));
const contents = await readOutputFixture(`multi-sheet-workbook.${format}`, anyjson.getEncoding(format))
const actual = await anyjson.decode(contents, format);
return assert.deepEqual(actual, expected)
});

test(`encode ${format}`, async () => {
const input = JSON.parse(await readInputFixture(`multi-sheet-workbook.json`));
const actual = await anyjson.encode(input, format)
const expected = await readOutputFixture(`multi-sheet-workbook.${format}`, anyjson.getEncoding(format))

return assert.deepEqual(
await anyjson.decode(actual as string, format),
await anyjson.decode(expected, format));
});
}
});

function testEncode(formats: string[]) {
suite('encode', function () {
suite('product-set', () => {
Expand Down

0 comments on commit 40cd588

Please sign in to comment.