Skip to content

Commit fbb14cc

Browse files
committed
Merge branch 'up/feat/csv-to-json' into chore/all-my-stuffs
2 parents 57b0978 + 9dca9c3 commit fbb14cc

File tree

11 files changed

+209
-5
lines changed

11 files changed

+209
-5
lines changed

locales/en.yml

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

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

locales/es.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ home:
55
allTools: 'Todas las herramientas'
66
favoritesDndToolTip: 'Arrastra y suelta para reordenar favoritos'
77
subtitle: 'Herramientas practicas para desarrolladores'
8-
toggleMenu: 'Toggle menu'
9-
home: Home
8+
toggleMenu: 'Alternar menú'
9+
home: Inicio
1010
uiLib: 'UI Lib'
1111
support: 'Apoyar el desarrollo de IT-Tools'
1212
buyMeACoffee: 'Buy me a coffee'
@@ -49,7 +49,7 @@ about:
4949
notFound: '404 Not Found'
5050
sorry: 'Lo sentimos, esta página no parece existir'
5151
maybe: 'Tal vez el caché esté haciendo cosas raras, ¿probamos a refrescar forzosamente?'
52-
backHome: 'Back home'
52+
backHome: 'Volver al inicio'
5353
favoriteButton:
5454
remove: 'Quitar de favoritos'
5555
add: 'Añadir a favoritos'
@@ -61,12 +61,16 @@ tools:
6161
categories:
6262
favorite-tools: 'Tus herramientas favoritas'
6363
crypto: Crypto
64-
converter: Converter
64+
converter: Conversor
6565
web: Web
66-
images and videos: 'Images & Videos'
66+
images and videos: 'Imágenes y vídeos'
6767
development: Development
6868
network: Network
6969
math: Math
7070
measurement: Measurement
7171
text: Text
7272
data: Data
73+
74+
csv-to-json:
75+
title: CSV a JSON
76+
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
@@ -80,3 +80,7 @@ tools:
8080
copied: Le token a été copié
8181
length: Longueur
8282
tokenPlaceholder: Le token...
83+
84+
csv-to-json:
85+
title: CSV vers JSON
86+
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
@@ -70,3 +70,7 @@ tools:
7070
measurement: 'Medidas'
7171
text: 'Texto'
7272
data: 'Dados'
73+
74+
csv-to-json:
75+
title: CSV para JSON
76+
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
@@ -70,3 +70,7 @@ tools:
7070
measurement: Вимірювання
7171
text: Текст
7272
data: Дані
73+
74+
csv-to-json:
75+
title: 'CSV в JSON'
76+
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>

src/tools/csv-to-json/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ArrowsShuffle } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
import { translate } from '@/plugins/i18n.plugin';
4+
5+
export const tool = defineTool({
6+
name: translate('tools.csv-to-json.title'),
7+
path: '/csv-to-json',
8+
description: translate('tools.csv-to-json.description'),
9+
keywords: ['csv', 'to', 'json', 'convert'],
10+
component: () => import('./csv-to-json.vue'),
11+
icon: ArrowsShuffle,
12+
createdAt: new Date('2024-04-12'),
13+
});

0 commit comments

Comments
 (0)