Skip to content

Commit 9dca9c3

Browse files
committed
feat(new-tool): csv to json converter
1 parent d3b32cc commit 9dca9c3

File tree

12 files changed

+212
-6
lines changed

12 files changed

+212
-6
lines changed

components.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ declare module '@vue/runtime-core' {
5858
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
5959
CSelect: typeof import('./src/ui/c-select/c-select.vue')['default']
6060
'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default']
61+
CsvToJson: typeof import('./src/tools/csv-to-json/csv-to-json.vue')['default']
6162
CTable: typeof import('./src/ui/c-table/c-table.vue')['default']
6263
'CTable.demo': typeof import('./src/ui/c-table/c-table.demo.vue')['default']
6364
CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default']
@@ -159,6 +160,7 @@ declare module '@vue/runtime-core' {
159160
RouterLink: typeof import('vue-router')['RouterLink']
160161
RouterView: typeof import('vue-router')['RouterView']
161162
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
163+
SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default']
162164
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
163165
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
164166
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']

locales/en.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ tools:
104104
title: JSON to CSV
105105
description: Convert JSON to CSV with automatic header detection.
106106

107+
csv-to-json:
108+
title: CSV to JSON
109+
description: Convert CSV to JSON with automatic header detection.
110+
107111
camera-recorder:
108112
title: Camera recorder
109113
description: Take a picture or record a video from your webcam or camera.

locales/es.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ home:
44
favoriteTools: 'Tus herramientas favoritas'
55
allTools: 'Todas las herramientas'
66
subtitle: 'Herramientas practicas para desarrolladores'
7-
toggleMenu: 'Toggle menu'
8-
home: Home
7+
toggleMenu: 'Alternar menú'
8+
home: Inicio
99
uiLib: 'UI Lib'
1010
support: 'Apoyar el desarrollo de IT-Tools'
1111
buyMeACoffee: 'Buy me a coffee'
@@ -48,7 +48,7 @@ about:
4848
notFound: '404 Not Found'
4949
sorry: 'Lo sentimos, esta página no parece existir'
5050
maybe: 'Tal vez el caché esté haciendo cosas raras, ¿probamos a refrescar forzosamente?'
51-
backHome: 'Back home'
51+
backHome: 'Volver al inicio'
5252
favoriteButton:
5353
remove: 'Quitar de favoritos'
5454
add: 'Añadir a favoritos'
@@ -60,12 +60,16 @@ tools:
6060
categories:
6161
favorite-tools: 'Tus herramientas favoritas'
6262
crypto: Crypto
63-
converter: Converter
63+
converter: Conversor
6464
web: Web
65-
images and videos: 'Images & Videos'
65+
images and videos: 'Imágenes y vídeos'
6666
development: Development
6767
network: Network
6868
math: Math
6969
measurement: Measurement
7070
text: Text
71-
data: Data
71+
data: Data
72+
73+
csv-to-json:
74+
title: CSV a JSON
75+
description: Convierte CSV a JSON con detección automática de cabeceras.

locales/fr.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,7 @@ tools:
7979
copied: Le token a été copié
8080
length: Longueur
8181
tokenPlaceholder: Le token...
82+
83+
csv-to-json:
84+
title: CSV vers JSON
85+
description: Convertit les fichiers CSV en JSON avec détection automatique des en-têtes.

locales/pt.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,7 @@ tools:
6969
measurement: 'Medidas'
7070
text: 'Texto'
7171
data: 'Dados'
72+
73+
csv-to-json:
74+
title: CSV para JSON
75+
description: Converte CSV para JSON com detecção automática de cabeçalhos.

locales/uk.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,7 @@ tools:
6969
measurement: Вимірювання
7070
text: Текст
7171
data: Дані
72+
73+
csv-to-json:
74+
title: 'CSV в JSON'
75+
description: 'Конвертуйте CSV в JSON з автоматичним визначенням заголовків.'
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.describe('Tool - CSV to JSON', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/csv-to-json');
6+
});
7+
8+
test('Has correct title', async ({ page }) => {
9+
await expect(page).toHaveTitle('CSV to JSON - IT Tools');
10+
});
11+
12+
test('Provided csv is converted to json', async ({ page }) => {
13+
await page.getByTestId('input').fill(`
14+
Age,Salary,Gender,Country,Purchased
15+
18,20000,Male,Germany,N
16+
19,22000,Female,France,N
17+
`);
18+
19+
const generatedJson = await page.getByTestId('area-content').innerText();
20+
21+
expect(generatedJson.trim()).toEqual(`
22+
[
23+
{"Age": "18", "Salary": "20000", "Gender": "Male", "Country": "Germany", "Purchased": "N"},
24+
{"Age": "19", "Salary": "22000", "Gender": "Female", "Country": "France", "Purchased": "N"}
25+
]
26+
`.trim(),
27+
);
28+
});
29+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { convertCsvToArray, getHeaders } from './csv-to-json.service';
3+
4+
describe('csv-to-json service', () => {
5+
describe('getHeaders', () => {
6+
it('extracts all the keys from the first line of the CSV', () => {
7+
expect(getHeaders('a,b,c\n1,2,3\n4,5,6')).toEqual(['a', 'b', 'c']);
8+
});
9+
10+
it('returns an empty array if the CSV is empty', () => {
11+
expect(getHeaders('')).toEqual([]);
12+
});
13+
});
14+
15+
describe('convertCsvToArray', () => {
16+
it('converts a CSV string to an array of objects', () => {
17+
const csv = 'a,b\n1,2\n3,4';
18+
19+
expect(convertCsvToArray(csv)).toEqual([
20+
{ a: '1', b: '2' },
21+
{ a: '3', b: '4' },
22+
]);
23+
});
24+
25+
it('converts a CSV string with different keys to an array of objects', () => {
26+
const csv = 'a,b,c\n1,2,\n3,,4';
27+
28+
expect(convertCsvToArray(csv)).toEqual([
29+
{ a: '1', b: '2', c: undefined },
30+
{ a: '3', b: undefined, c: '4' },
31+
]);
32+
});
33+
34+
it('when a value is "null", it is converted to null', () => {
35+
const csv = 'a,b\nnull,2';
36+
37+
expect(convertCsvToArray(csv)).toEqual([
38+
{ a: null, b: '2' },
39+
]);
40+
});
41+
42+
it('when a value is empty, it is converted to undefined', () => {
43+
const csv = 'a,b\n,2\n,3';
44+
45+
expect(convertCsvToArray(csv)).toEqual([
46+
{ a: undefined, b: '2' },
47+
{ a: undefined, b: '3' },
48+
]);
49+
});
50+
51+
it('when a value is wrapped in double quotes, the quotes are removed', () => {
52+
const csv = 'a,b\n"hello, world",2';
53+
54+
expect(convertCsvToArray(csv)).toEqual([
55+
{ a: 'hello, world', b: '2' },
56+
]);
57+
});
58+
59+
it('when a value contains an escaped double quote, the escape character is removed', () => {
60+
const csv = 'a,b\nhello \\"world\\",2';
61+
62+
expect(convertCsvToArray(csv)).toEqual([
63+
{ a: 'hello "world"', b: '2' },
64+
]);
65+
});
66+
});
67+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export { getHeaders, convertCsvToArray };
2+
3+
function getHeaders(csv: string): string[] {
4+
if (csv.trim() === '') {
5+
return [];
6+
}
7+
8+
const firstLine = csv.split('\n')[0];
9+
return firstLine.split(/[,;]/).map(header => header.trim());
10+
}
11+
function deserializeValue(value: string): unknown {
12+
if (value === 'null') {
13+
return null;
14+
}
15+
16+
if (value === '') {
17+
return undefined;
18+
}
19+
20+
const valueAsString = value.replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\"/g, '"');
21+
22+
if (valueAsString.startsWith('"') && valueAsString.endsWith('"')) {
23+
return valueAsString.slice(1, -1);
24+
}
25+
26+
return valueAsString;
27+
}
28+
29+
function convertCsvToArray(csv: string): Record<string, unknown>[] {
30+
const lines = csv.split('\n');
31+
const headers = getHeaders(csv);
32+
33+
return lines.slice(1).map(line => {
34+
// Split on comma or semicolon not within quotes
35+
const data = line.split(/[,;](?=(?:(?:[^"]*"){2})*[^"]*$)/).map(value => value.trim());
36+
return headers.reduce((obj, header, index) => {
37+
obj[header] = deserializeValue(data[index]);
38+
return obj;
39+
}, {} as Record<string, unknown>);
40+
});
41+
}

src/tools/csv-to-json/csv-to-json.vue

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script setup lang="ts">
2+
import { convertCsvToArray } from './csv-to-json.service';
3+
import FormatTransformer from '@/components/FormatTransformer.vue';
4+
import type { UseValidationRule } from '@/composable/validation';
5+
import { withDefaultOnError } from '@/utils/defaults';
6+
7+
function transformer(value: string) {
8+
return withDefaultOnError(() => {
9+
if (value === '') {
10+
return '';
11+
}
12+
return JSON.stringify(convertCsvToArray(value), null, 2);
13+
}, '');
14+
}
15+
16+
const rules: UseValidationRule<string>[] = [
17+
{
18+
validator: (v: string) => v === '' || ((v.includes(',') || v.includes(';')) && v.includes('\n')),
19+
message: 'Provided CSV is not valid.',
20+
},
21+
];
22+
</script>
23+
24+
<template>
25+
<FormatTransformer
26+
input-label="Your raw CSV"
27+
input-placeholder="Paste your raw CSV here..."
28+
output-label="JSON version of your CSV"
29+
:input-validation-rules="rules"
30+
:transformer="transformer"
31+
/>
32+
</template>

0 commit comments

Comments
 (0)