feat: csv-to-yaml

This commit is contained in:
Chesterkxng
2025-04-05 03:54:57 +02:00
parent fcb20a24f8
commit a5b8e989dd
6 changed files with 345 additions and 1 deletions

View File

@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest';
import { main } from './service';
import { InitialValuesType } from './types';
// filepath: c:\CODE\omni-tools\src\pages\tools\csv\csv-to-yaml\csv-to-yaml.service.test.ts
describe('main', () => {
const defaultOptions: InitialValuesType = {
csvSeparator: ',',
quoteCharacter: '"',
commentCharacter: '#',
emptyLines: false,
headerRow: true,
spaces: 2
};
it('should return empty string for empty input', () => {
const result = main('', defaultOptions);
expect(result).toBe('');
});
it('should return this if header is set to false', () => {
const options = { ...defaultOptions, headerRow: false };
const result = main('John,30\nEmma,50', options);
expect(result).toBe(' - John\n - 30\n-\n - Emma\n - 50');
});
it('should return this header is set to true', () => {
const options = { ...defaultOptions };
const result = main('Name,Age\nJohn,30\nEmma,50', options);
expect(result).toBe(' Name: John\n Age: 30\n-\n Name: Emma\n Age: 50');
});
it('should return this header is set to true and comment flag set', () => {
const options = { ...defaultOptions, commentcharacter: '#' };
const result = main('Name,Age\nJohn,30\n#Emma,50', options);
expect(result).toBe(' Name: John\n Age: 30');
});
it('should return this header is set to true and spaces is set to 3', () => {
const options = { ...defaultOptions, spaces: 3 };
const result = main('Name,Age\nJohn,30\nEmma,50', options);
expect(result).toBe(
' Name: John\n Age: 30\n-\n Name: Emma\n Age: 50'
);
});
});

View File

@@ -0,0 +1,201 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { GetGroupsType } from '@components/options/ToolOptions';
import { CardExampleType } from '@components/examples/ToolExamples';
import { main } from './service';
import { InitialValuesType } from './types';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
const initialValues: InitialValuesType = {
csvSeparator: ',',
quoteCharacter: '"',
commentCharacter: '#',
emptyLines: true,
headerRow: true,
spaces: 2
};
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Convert Music Playlist CSV to YAML',
description:
'In this example, we transform a short CSV file containing a music playlist into structured YAML data. The input CSV contains five records with three columns each and the output YAML contains five lists of lists (one list for each CSV record). In YAML, lists start with the "-" symbol and the nested lists are indented with two spaces',
sampleText: `The Beatles,"Yesterday",Pop Rock
Queen,"Bohemian Rhapsody",Rock
Nirvana,"Smells Like Teen Spirit",Grunge
Michael Jackson,"Billie Jean",Pop
Stevie Wonder,"Superstition",Funk`,
sampleResult: ` - The Beatles
- Yesterday
- Pop Rock
-
- Queen
- Bohemian Rhapsody
- Rock
-
- Nirvana
- Smells Like Teen Spirit
- Grunge
-
- Michael Jackson
- Billie Jean
- Pop
-
- Stevie Wonder
- Superstition
- Funk`,
sampleOptions: {
...initialValues,
headerRow: false
}
},
{
title: 'Planetary CSV Data',
description:
'In this example, we are working with CSV data that summarizes key properties of three planets in our solar system. The data consists of three columns with headers "planet", "relative mass" (with "1" being the mass of earth), and "satellites". To preserve the header names in the output YAML data, we enable the "Transform Headers" option, creating a YAML file that contains a list of YAML objects, where each object has three keys: "planet", "relative mass", and "satellites".',
sampleText: `planet,relative mass,satellites
Venus,0.815,0
Earth,1.000,1
Mars,0.107,2`,
sampleResult: ` planet: Venus
relative mass: 0.815
satellites: '0'
-
planet: Earth
relative mass: 1.000
satellites: '1'
-
planet: Mars
relative mass: 0.107
satellites: '2'`,
sampleOptions: {
...initialValues
}
},
{
title: 'Convert Non-standard CSV to YAML',
description:
'In this example, we convert a CSV file with non-standard formatting into a regular YAML file. The input data uses a semicolon as a separator for the "product", "quantity", and "price" fields. It also contains empty lines and lines that are commented out. To make the program work with this custom CSV file, we input the semicolon symbol in the CSV delimiter options. To skip comments, we specify "#" as the symbol that starts comments. And to remove empty lines, we activate the option for skipping blank lines (that do not contain any symbols). In the output, we obtain a YAML file that contains a list of three objects, which use CSV headers as keys. Additionally, the objects in the YAML file are indented with four spaces.',
sampleText: `item;quantity;price
milk;2;3.50
#eggs;12;2.99
bread;1;4.25
#apples;4;1.99
cheese;1;8.99`,
sampleResult: ` item: milk
quantity: 2
price: 3.50
-
item: bread
quantity: 1
price: 4.25
-
item: cheese
quantity: 1
price: 8.99`,
sampleOptions: {
...initialValues,
csvSeparator: ';'
}
}
];
export default function CsvToYaml({
title,
longDescription
}: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: InitialValuesType, input: string) => {
setResult(main(input, optionsValues));
};
const getGroups: GetGroupsType<InitialValuesType> | null = ({
values,
updateField
}) => [
{
title: 'Adjust CSV input',
component: (
<Box>
<TextFieldWithDesc
value={values.csvSeparator}
onOwnChange={(val) => updateField('csvSeparator', val)}
description={
'Enter the character used to delimit columns in the CSV file.'
}
/>
<TextFieldWithDesc
value={values.quoteCharacter}
onOwnChange={(val) => updateField('quoteCharacter', val)}
description={
'Enter the quote character used to quote the CSV fields.'
}
/>
<TextFieldWithDesc
value={values.commentCharacter}
onOwnChange={(val) => updateField('commentCharacter', val)}
description={
'Enter the character indicating the start of a comment line. Lines starting with this symbol will be skipped.'
}
/>
</Box>
)
},
{
title: 'Conversion Options',
component: (
<Box>
<CheckboxWithDesc
checked={values.headerRow}
onChange={(value) => updateField('headerRow', value)}
title="Use Headers"
description="Keep the first row as column names."
/>
<CheckboxWithDesc
checked={values.emptyLines}
onChange={(value) => updateField('emptyLines', value)}
title="Ignore Lines with No Data"
description="Enable to prevent the conversion of empty lines in the input CSV file."
/>
</Box>
)
},
{
title: 'Adjust YAML indentation',
component: (
<Box>
<TextFieldWithDesc
value={values.spaces}
type="number"
onOwnChange={(val) => updateField('spaces', Number(val))}
inputProps={{ min: 0 }}
description={
'Set the number of spaces to use for YAML indentation.'
}
/>
</Box>
)
}
];
return (
<ToolContent
title={title}
input={input}
inputComponent={<ToolTextInput value={input} onChange={setInput} />}
resultComponent={<ToolTextResult value={result} />}
initialValues={initialValues}
exampleCards={exampleCards}
getGroups={getGroups}
setInput={setInput}
compute={compute}
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
/>
);
}

View File

@@ -0,0 +1,15 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('csv', {
name: 'Csv to yaml',
path: 'csv-to-yaml',
icon: 'nonicons:yaml-16',
description:
'Just upload your CSV file in the form below and it will automatically get converted to a YAML file. In the tool options, you can specify the field delimiter character, field quote character, and comment character to adapt the tool to custom CSV formats. Additionally, you can select the output YAML format: one that preserves CSV headers or one that excludes CSV headers.',
shortDescription: 'Quickly convert a CSV file to a YAML file.',
keywords: ['csv', 'to', 'yaml'],
longDescription:
'This tool transforms CSV (Comma Separated Values) data into the YAML (Yet Another Markup Language) data. CSV is a simple, tabular format that is used to represent matrix-like data types consisting of rows and columns. YAML, on the other hand, is a more advanced format (actually a superset of JSON), which creates more human-readable data for serialization, and it supports lists, dictionaries, and nested objects. This program supports various input CSV formats the input data can be comma-separated (default), semicolon-separated, pipe-separated, or use another completely different delimiter. You can specify the exact delimiter your data uses in the options. Similarly, in the options, you can specify the quote character that is used to wrap CSV fields (by default a double-quote symbol). You can also skip lines that start with comments by specifying the comment symbols in the options. This allows you to keep your data clean by skipping unnecessary lines. There are two ways to convert CSV to YAML. The first method converts each CSV row into a YAML list. The second method extracts headers from the first CSV row and creates YAML objects with keys based on these headers. You can also customize the output YAML format by specifying the number of spaces for indenting YAML structures. If you need to perform the reverse conversion, that is, transform YAML into CSV, you can use our Convert YAML to CSV tool. Csv-abulous!',
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,72 @@
import { InitialValuesType } from './types';
import { getCsvHeaders, splitCsv } from '@utils/csv';
import { unquoteIfQuoted } from '@utils/string';
function toYaml(
input: Record<string, string>[] | string[][],
indentSpaces: number = 2
): string {
const indent = ' '.repeat(indentSpaces);
if (
Array.isArray(input) &&
input.length > 0 &&
typeof input[0] === 'object' &&
!Array.isArray(input[0])
) {
return (input as Record<string, string>[])
.map((obj) => {
const lines = Object.entries(obj)
.map(([key, value]) => `${indent}${key}: ${value}`)
.join('\n');
return `${lines}`;
})
.join('\n-\n');
}
// If input is string[][].
if (Array.isArray(input) && Array.isArray(input[0])) {
return (input as string[][])
.map((row) => {
return row.map((cell) => `${indent}- ${cell}`).join('\n');
})
.join('\n-\n');
}
return 'invalid input';
}
export function main(input: string, options: InitialValuesType): string {
if (!input) {
return '';
}
const rows = splitCsv(
input,
true,
options.commentCharacter,
options.emptyLines,
options.csvSeparator
);
rows.forEach((row) => {
row.forEach((cell, cellIndex) => {
row[cellIndex] = unquoteIfQuoted(cell, options.quoteCharacter);
});
});
if (options.headerRow) {
const headerRow = getCsvHeaders(input, options.csvSeparator);
const result: Record<string, string>[] = rows.slice(1).map((row) => {
const entry: Record<string, string> = {};
headerRow.forEach((header, headerIndex) => {
entry[header] = row[headerIndex] ?? '';
});
return entry;
});
return toYaml(result, options.spaces);
}
return toYaml(rows, options.spaces);
}

View File

@@ -0,0 +1,8 @@
export type InitialValuesType = {
csvSeparator: string;
quoteCharacter: string;
commentCharacter: string;
emptyLines: boolean;
headerRow: boolean;
spaces: number;
};

View File

@@ -1,3 +1,4 @@
import { tool as csvToYaml } from './csv-to-yaml/meta';
import { tool as csvToJson } from './csv-to-json/meta';
import { tool as csvToXml } from './csv-to-xml/meta';
import { tool as csvToRowsColumns } from './csv-rows-to-columns/meta';
@@ -9,5 +10,6 @@ export const csvTools = [
csvToXml,
csvToRowsColumns,
csvToTsv,
swapCsvColumns
swapCsvColumns,
csvToYaml
];