From 69f0bd079fd824dc9e929ccbbaa6bcaab0c38a7c Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Sun, 18 Jun 2023 17:57:18 +0200 Subject: feat(new-tool): json to csv converter --- src/tools/json-to-csv/index.ts | 12 +++ src/tools/json-to-csv/json-to-csv.e2e.spec.ts | 29 ++++++++ src/tools/json-to-csv/json-to-csv.service.test.ts | 89 +++++++++++++++++++++++ src/tools/json-to-csv/json-to-csv.service.ts | 35 +++++++++ src/tools/json-to-csv/json-to-csv.vue | 32 ++++++++ 5 files changed, 197 insertions(+) create mode 100644 src/tools/json-to-csv/index.ts create mode 100644 src/tools/json-to-csv/json-to-csv.e2e.spec.ts create mode 100644 src/tools/json-to-csv/json-to-csv.service.test.ts create mode 100644 src/tools/json-to-csv/json-to-csv.service.ts create mode 100644 src/tools/json-to-csv/json-to-csv.vue (limited to 'src/tools/json-to-csv') diff --git a/src/tools/json-to-csv/index.ts b/src/tools/json-to-csv/index.ts new file mode 100644 index 0000000..acfef02 --- /dev/null +++ b/src/tools/json-to-csv/index.ts @@ -0,0 +1,12 @@ +import { List } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'JSON to CSV', + path: '/json-to-csv', + description: 'Convert JSON to CSV with automatic header detection.', + keywords: ['json', 'to', 'csv', 'convert'], + component: () => import('./json-to-csv.vue'), + icon: List, + createdAt: new Date('2023-06-18'), +}); diff --git a/src/tools/json-to-csv/json-to-csv.e2e.spec.ts b/src/tools/json-to-csv/json-to-csv.e2e.spec.ts new file mode 100644 index 0000000..840469c --- /dev/null +++ b/src/tools/json-to-csv/json-to-csv.e2e.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Tool - JSON to CSV', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/json-to-csv'); + }); + + test('Has correct title', async ({ page }) => { + await expect(page).toHaveTitle('JSON to CSV - IT Tools'); + }); + + test('Provided json is converted to csv', async ({ page }) => { + await page.getByTestId('input').fill(` +[ + {'Age': 18.0, 'Salary': 20000.0, 'Gender': 'Male', 'Country': 'Germany', 'Purchased': 'N'}, + {'Age': 19.0, 'Salary': 22000.0, 'Gender': 'Female', 'Country': 'France', 'Purchased': 'N'}, +] + `); + + const generatedJson = await page.getByTestId('area-content').innerText(); + + expect(generatedJson.trim()).toEqual(` +Age,Salary,Gender,Country,Purchased +18,20000,Male,Germany,N +19,22000,Female,France,N + `.trim(), + ); + }); +}); diff --git a/src/tools/json-to-csv/json-to-csv.service.test.ts b/src/tools/json-to-csv/json-to-csv.service.test.ts new file mode 100644 index 0000000..c27bf9b --- /dev/null +++ b/src/tools/json-to-csv/json-to-csv.service.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest'; +import { convertArrayToCsv, getHeaders } from './json-to-csv.service'; + +describe('json-to-csv service', () => { + describe('getHeaders', () => { + it('extracts all the keys from the array of objects', () => { + expect(getHeaders({ array: [{ a: 1, b: 2 }, { a: 3, c: 4 }] })).toEqual(['a', 'b', 'c']); + }); + + it('returns an empty array if the array is empty', () => { + expect(getHeaders({ array: [] })).toEqual([]); + }); + }); + + describe('convertArrayToCsv', () => { + it('converts an array of objects to a CSV string', () => { + const array = [ + { a: 1, b: 2 }, + { a: 3, b: 4 }, + + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + 1,2 + 3,4" + `); + }); + + it('converts an array of objects with different keys to a CSV string', () => { + const array = [ + { a: 1, b: 2 }, + { a: 3, c: 4 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b,c + 1,2, + 3,,4" + `); + }); + + it('when a value is null, it is converted to the string "null"', () => { + const array = [ + { a: null, b: 2 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + null,2" + `); + }); + + it('when a value is undefined, it is converted to an empty string', () => { + const array = [ + { a: undefined, b: 2 }, + { b: 3 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + ,2 + ,3" + `); + }); + + it('when a value contains a comma, it is wrapped in double quotes', () => { + const array = [ + { a: 'hello, world', b: 2 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + \\"hello, world\\",2" + `); + }); + + it('when a value contains a double quote, it is escaped with another double quote', () => { + const array = [ + { a: 'hello "world"', b: 2 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + hello \\\\\\"world\\\\\\",2" + `); + }); + }); +}); diff --git a/src/tools/json-to-csv/json-to-csv.service.ts b/src/tools/json-to-csv/json-to-csv.service.ts new file mode 100644 index 0000000..ab3c04e --- /dev/null +++ b/src/tools/json-to-csv/json-to-csv.service.ts @@ -0,0 +1,35 @@ +export { getHeaders, convertArrayToCsv }; + +function getHeaders({ array }: { array: Record[] }): string[] { + const headers = new Set(); + + array.forEach(item => Object.keys(item).forEach(key => headers.add(key))); + + return Array.from(headers); +} + +function serializeValue(value: unknown): string { + if (value === null) { + return 'null'; + } + + if (value === undefined) { + return ''; + } + + const valueAsString = String(value).replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/"/g, '\\"'); + + if (valueAsString.includes(',')) { + return `"${valueAsString}"`; + } + + return valueAsString; +} + +function convertArrayToCsv({ array }: { array: Record[] }): string { + const headers = getHeaders({ array }); + + const rows = array.map(item => headers.map(header => serializeValue(item[header]))); + + return [headers.join(','), ...rows].join('\n'); +} diff --git a/src/tools/json-to-csv/json-to-csv.vue b/src/tools/json-to-csv/json-to-csv.vue new file mode 100644 index 0000000..cf15400 --- /dev/null +++ b/src/tools/json-to-csv/json-to-csv.vue @@ -0,0 +1,32 @@ + + + -- cgit v1.2.3