From a5667a064f3949fce87a2d9a7e044f5445d05323 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Thu, 12 Mar 2026 22:52:34 +0200 Subject: [PATCH 1/7] Added initialization of repl and handling default comands --- src/commands.js | 7 +++++ src/main.js | 7 +++++ src/navigation.js | 24 +++++++++++++++ src/repl.js | 72 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/errors.js | 11 +++++++ 5 files changed, 121 insertions(+) create mode 100644 src/commands.js create mode 100644 src/utils/errors.js diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..84fa287 --- /dev/null +++ b/src/commands.js @@ -0,0 +1,7 @@ +import { upHandler, cdHandler, lsHandler } from './navigation.js' + +export const COMMAND_HANDLERS_MAP = { + up: upHandler, + cd: cdHandler, + ls: lsHandler +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index e69de29..c344a62 100644 --- a/src/main.js +++ b/src/main.js @@ -0,0 +1,7 @@ +import { initRepl } from './repl.js'; + +const init = () => { + initRepl(); +} + +init(); \ No newline at end of file diff --git a/src/navigation.js b/src/navigation.js index e69de29..8efe0f8 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -0,0 +1,24 @@ +import os from 'node:os'; + +let currentDirectory = os.homedir(); + +export const onChangeCurrentDirectory = (newDirectory) => { + currentDirectory = newDirectory; +} + +export const getCurrentDirectory = () => { + return currentDirectory; +} + +export const upHandler = () => { + console.log('upHandler'); + +} + +export const cdHandler = () => { + console.log('cdHandler'); +} + +export const lsHandler = () => { + console.log('lsHandler'); +} \ No newline at end of file diff --git a/src/repl.js b/src/repl.js index e69de29..07c11e5 100644 --- a/src/repl.js +++ b/src/repl.js @@ -0,0 +1,72 @@ + + +import readline from 'node:readline'; +import { InvalidInputError, INVALID_INPUT_ERROR_CODE } from './utils/errors.js'; +import { getCurrentDirectory } from './navigation.js'; +import { COMMAND_HANDLERS_MAP } from './commands.js'; + +const WELCOME_TEXT = 'Welcome to Data Processing CLI!'; +const EXIT_TEXT = 'Thank you for using Data Processing CLI!'; +const INVALID_COMMAND_TEXT = 'Invalid input'; +const OPERATION_FAILED_TEXT = 'Operation failed'; +const CURRENT_DIRECTORY_PREFIX = 'You are currently in'; + +export const initRepl = () => { + const onSuccess = () => { + console.log(`${CURRENT_DIRECTORY_PREFIX} ${getCurrentDirectory()}`); + } + + const onError = (err) => { + if (err.code === INVALID_INPUT_ERROR_CODE) { + console.log(INVALID_COMMAND_TEXT); + } else { + console.log(OPERATION_FAILED_TEXT); + } + } + + const handleCommand = async(command, commandArgs) => { + if (command in COMMAND_HANDLERS_MAP) { + try { + await COMMAND_HANDLERS_MAP[command](commandArgs); + onSuccess() + } catch (err) { + onError(err) + } + + } else { + onError(new InvalidInputError()); + } + } + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: '> ' + }); + + rl.on('line', async (line) => { + const lineTrimmed = line.trim().toLowerCase(); + const [command, ...commandArgs] = lineTrimmed.split(' '); + + if (command === '.exit' && commandArgs.length === 0) { + rl.close() + } else { + await handleCommand(command, commandArgs); + rl.prompt(); + } + }); + + rl.on('SIGINT', () => { + rl.close() + }); + + rl.on('close', () => { + console.log(EXIT_TEXT) + process.exit(0); + }); + + console.log(WELCOME_TEXT); + // Initial display of directory + onSuccess(); + rl.prompt(); +} diff --git a/src/utils/errors.js b/src/utils/errors.js new file mode 100644 index 0000000..4402dc9 --- /dev/null +++ b/src/utils/errors.js @@ -0,0 +1,11 @@ +export const INVALID_INPUT_ERROR_CODE = 'INVALID_INPUT'; + +export class InvalidInputError extends Error { + constructor(message) { + super(message); + this.name = 'InvalidInputError'; + this.code = INVALID_INPUT_ERROR_CODE; + + Error.captureStackTrace(this, this.constructor); + } +} \ No newline at end of file From 5a4d9ac03a79f54c81f6c5f4358819587fe8e250 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Thu, 12 Mar 2026 23:14:31 +0200 Subject: [PATCH 2/7] Added up commad --- src/main.js | 4 ++++ src/navigation.js | 22 ++++++++++------------ src/repl.js | 48 +++++++++++++++++++++++------------------------ 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/main.js b/src/main.js index c344a62..628c711 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,10 @@ +import process from 'node:process'; +import os from 'node:os'; import { initRepl } from './repl.js'; const init = () => { + process.chdir(os.homedir()); + initRepl(); } diff --git a/src/navigation.js b/src/navigation.js index 8efe0f8..ff53143 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -1,18 +1,16 @@ import os from 'node:os'; - -let currentDirectory = os.homedir(); - -export const onChangeCurrentDirectory = (newDirectory) => { - currentDirectory = newDirectory; -} - -export const getCurrentDirectory = () => { - return currentDirectory; -} +import path from 'node:path'; +import process from 'node:process'; export const upHandler = () => { - console.log('upHandler'); - + if (process.cwd() !== os.homedir()) { + const newDirectory = process.cwd() + .split(path.sep) + .slice(0, -1) + .join(path.sep); + + process.chdir(newDirectory); + } } export const cdHandler = () => { diff --git a/src/repl.js b/src/repl.js index 07c11e5..83b8505 100644 --- a/src/repl.js +++ b/src/repl.js @@ -1,8 +1,8 @@ import readline from 'node:readline'; +import process from 'node:process'; import { InvalidInputError, INVALID_INPUT_ERROR_CODE } from './utils/errors.js'; -import { getCurrentDirectory } from './navigation.js'; import { COMMAND_HANDLERS_MAP } from './commands.js'; const WELCOME_TEXT = 'Welcome to Data Processing CLI!'; @@ -11,33 +11,33 @@ const INVALID_COMMAND_TEXT = 'Invalid input'; const OPERATION_FAILED_TEXT = 'Operation failed'; const CURRENT_DIRECTORY_PREFIX = 'You are currently in'; -export const initRepl = () => { - const onSuccess = () => { - console.log(`${CURRENT_DIRECTORY_PREFIX} ${getCurrentDirectory()}`); - } +const onSuccess = () => { + console.log(`${CURRENT_DIRECTORY_PREFIX} ${process.cwd()}`); +} - const onError = (err) => { - if (err.code === INVALID_INPUT_ERROR_CODE) { - console.log(INVALID_COMMAND_TEXT); - } else { - console.log(OPERATION_FAILED_TEXT); - } +const onError = (err) => { + if (err.code === INVALID_INPUT_ERROR_CODE) { + console.log(INVALID_COMMAND_TEXT); + } else { + console.log(OPERATION_FAILED_TEXT); } - - const handleCommand = async(command, commandArgs) => { - if (command in COMMAND_HANDLERS_MAP) { - try { - await COMMAND_HANDLERS_MAP[command](commandArgs); - onSuccess() - } catch (err) { - onError(err) - } - - } else { - onError(new InvalidInputError()); +} + +const handleCommand = async(command, commandArgs) => { + if (command in COMMAND_HANDLERS_MAP) { + try { + await COMMAND_HANDLERS_MAP[command](commandArgs); + onSuccess() + } catch (err) { + onError(err) } + + } else { + onError(new InvalidInputError()); } +} +export const initRepl = () => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -57,7 +57,7 @@ export const initRepl = () => { }); rl.on('SIGINT', () => { - rl.close() + rl.close(); }); rl.on('close', () => { From 41222974a3fa65c0f9caf5c946b71b1ba086a585 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 14:14:26 +0200 Subject: [PATCH 3/7] Added cd command --- src/commands.js | 2 +- src/main.js | 2 +- src/navigation.js | 22 ++++++++++++++++++---- src/repl.js | 4 +++- src/utils/errors.js | 2 +- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/commands.js b/src/commands.js index 84fa287..1b97e71 100644 --- a/src/commands.js +++ b/src/commands.js @@ -4,4 +4,4 @@ export const COMMAND_HANDLERS_MAP = { up: upHandler, cd: cdHandler, ls: lsHandler -} \ No newline at end of file +} diff --git a/src/main.js b/src/main.js index 628c711..9e469e6 100644 --- a/src/main.js +++ b/src/main.js @@ -8,4 +8,4 @@ const init = () => { initRepl(); } -init(); \ No newline at end of file +init(); diff --git a/src/navigation.js b/src/navigation.js index ff53143..0f6011d 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -1,6 +1,8 @@ import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; +import fs from 'node:fs/promises'; +import { InvalidInputError } from './utils/errors.js'; export const upHandler = () => { if (process.cwd() !== os.homedir()) { @@ -8,15 +10,27 @@ export const upHandler = () => { .split(path.sep) .slice(0, -1) .join(path.sep); - + process.chdir(newDirectory); } } -export const cdHandler = () => { - console.log('cdHandler'); +export const cdHandler = async (args) => { + const pathToDirectory = args[0]; + if (!pathToDirectory) { + throw new InvalidInputError('No path/to/directory'); + } + const newDirPath = path.resolve(process.cwd(), pathToDirectory); + + const stats = await fs.stat(newDirPath); + + if (stats.isDirectory()) { + process.chdir(newDirPath); + } else { + throw new InvalidInputError('Path is not a directory'); + } } export const lsHandler = () => { console.log('lsHandler'); -} \ No newline at end of file +} diff --git a/src/repl.js b/src/repl.js index 83b8505..949eedf 100644 --- a/src/repl.js +++ b/src/repl.js @@ -16,6 +16,8 @@ const onSuccess = () => { } const onError = (err) => { + // For testing only, TODO: delete before review + console.log(err); if (err.code === INVALID_INPUT_ERROR_CODE) { console.log(INVALID_COMMAND_TEXT); } else { @@ -33,7 +35,7 @@ const handleCommand = async(command, commandArgs) => { } } else { - onError(new InvalidInputError()); + onError(new InvalidInputError('Invalid command')); } } diff --git a/src/utils/errors.js b/src/utils/errors.js index 4402dc9..21e3055 100644 --- a/src/utils/errors.js +++ b/src/utils/errors.js @@ -8,4 +8,4 @@ export class InvalidInputError extends Error { Error.captureStackTrace(this, this.constructor); } -} \ No newline at end of file +} From bcd94b44c336cdd49416ec6d6a27fcafb400d245 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 14:56:58 +0200 Subject: [PATCH 4/7] Added console color and ls command --- src/navigation.js | 28 ++++++++++++++++++++++++++-- src/repl.js | 14 ++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/navigation.js b/src/navigation.js index 0f6011d..0ca8dee 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -31,6 +31,30 @@ export const cdHandler = async (args) => { } } -export const lsHandler = () => { - console.log('lsHandler'); +export const lsHandler = async () => { + const dirEntities = await fs.readdir(process.cwd(), { withFileTypes: true }); + + const folders = []; + const files = []; + + let maxEntitName = 0; + + dirEntities.forEach((dirEnt) => { + maxEntitName = Math.max(maxEntitName, dirEnt.name.length); + + if (dirEnt.isDirectory()) { + folders.push(dirEnt.name); + } else if (dirEnt.isFile()) { + files.push(dirEnt.name); + } + }); + + const formatEntityName = (entitiName) => `${entitiName}${' '.repeat(maxEntitName - entitiName.length)}` + + folders.sort((a, b) => a.localeCompare(b)).forEach((ent) => { + console.log(`${formatEntityName(ent)} [folder]`) + }); + files.sort((a, b) => a.localeCompare(b)).forEach((ent) => { + console.log(`${formatEntityName(ent)} [file]`) + }); } diff --git a/src/repl.js b/src/repl.js index 949eedf..9c2bf71 100644 --- a/src/repl.js +++ b/src/repl.js @@ -11,17 +11,23 @@ const INVALID_COMMAND_TEXT = 'Invalid input'; const OPERATION_FAILED_TEXT = 'Operation failed'; const CURRENT_DIRECTORY_PREFIX = 'You are currently in'; +const ANSI_COLORS = { + red: '\x1b[31m', + green: '\x1b[32m', +}; +const ANSI_COLOR_RESET = '\x1b[0m'; + const onSuccess = () => { - console.log(`${CURRENT_DIRECTORY_PREFIX} ${process.cwd()}`); + console.log(`${ANSI_COLORS.green}${CURRENT_DIRECTORY_PREFIX} ${process.cwd()}${ANSI_COLOR_RESET}`); } const onError = (err) => { - // For testing only, TODO: delete before review + // For testing only, TODO: delete before review next line console.log(err); if (err.code === INVALID_INPUT_ERROR_CODE) { - console.log(INVALID_COMMAND_TEXT); + console.log(`${ANSI_COLORS.red}${INVALID_COMMAND_TEXT}${ANSI_COLOR_RESET}`); } else { - console.log(OPERATION_FAILED_TEXT); + console.log(`${ANSI_COLORS.red}${OPERATION_FAILED_TEXT}${ANSI_COLOR_RESET}`); } } From f65014225937a04eb2b2da18713697c25e64ef4c Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 18:02:57 +0200 Subject: [PATCH 5/7] Added csv to json, handling of custom cwd --- src/commands.js | 6 ++- src/commands/csvToJson.js | 90 +++++++++++++++++++++++++++++++++++++++ src/commands/jsonToCsv.js | 3 ++ src/cwdState.js | 17 ++++++++ src/main.js | 4 -- src/navigation.js | 22 ++++------ src/repl.js | 3 +- src/utils/argParser.js | 32 ++++++++++++++ src/utils/pathResolver.js | 6 +++ 9 files changed, 164 insertions(+), 19 deletions(-) create mode 100644 src/cwdState.js diff --git a/src/commands.js b/src/commands.js index 1b97e71..24a69db 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,7 +1,11 @@ +import { csvToJsonHandler } from './commands/csvToJson.js' +import { jsonToCsvHandler } from './commands/jsonToCsv.js' import { upHandler, cdHandler, lsHandler } from './navigation.js' export const COMMAND_HANDLERS_MAP = { up: upHandler, cd: cdHandler, - ls: lsHandler + ls: lsHandler, + 'csv-to-json': csvToJsonHandler, + 'json-to-csv': jsonToCsvHandler, } diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js index e69de29..ff0522b 100644 --- a/src/commands/csvToJson.js +++ b/src/commands/csvToJson.js @@ -0,0 +1,90 @@ +import { Transform } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { argParser } from '../utils/argParser.js'; +import { pathResolver } from '../utils/pathResolver.js'; + +const EXTRA_HEADER_NAME = 'Extra'; +const INDENT = 2; + +export const csvToJsonHandler = async (args) => { + const inputArg = argParser(args, 'input', true); + const outputArg = argParser(args, 'output', true); + + const inputPath = pathResolver(inputArg); + const outputPath = pathResolver(outputArg); + + const inputStream = createReadStream(inputPath, { encoding: 'utf-8' }); + const outputStream = createWriteStream(outputPath, { encoding: 'utf-8' }); + + let transformBuffer = ''; + let headers = []; + let isFirstDataRow = true; + + const getDataPrefix = () => { + let prefix = ',\n'; + if (isFirstDataRow) { + prefix = ''; + isFirstDataRow = false; + } + return prefix; + } + + const transformDataToJsonString = (data) => { + const obj = {}; + let unknownHeaderCounter = 1; + data.forEach((d, i) => { + let header; + if (headers[i]) { + header = headers[i]; + } else { + header = `${EXTRA_HEADER_NAME}${unknownHeaderCounter}`; + unknownHeaderCounter++; + } + obj[header] = d; + }); + return JSON.stringify(obj, null, INDENT).replace(/^/gm, ' '.repeat(INDENT)); + } + + const transformStream = new Transform({ + transform(chunk, _, callback) { + transformBuffer += chunk; + + const lines = transformBuffer.split(/\r?\n/); + transformBuffer = lines.pop(); + + lines.forEach((line) => { + if (!line.trim()) { + return; + } + + const parsedLineData = line.split(','); + + if (!headers.length) { + headers = parsedLineData.map(h => h.trim()); + this.push('[\n'); + } else { + this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}`); + } + }); + + callback(); + }, + flush(callback) { + if (!transformBuffer.trim()) { + return '\n]'; + } + + const parsedLineData = transformBuffer.split(','); + this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}\n]`); + + transformBuffer = ''; + callback(); + } + }) + inputStream.on('error', (err) => { throw err }); + transformStream.on('error', (err) => { throw err }); + outputStream.on('error', (err) => { throw err }); + + await pipeline(inputStream, transformStream, outputStream); +} \ No newline at end of file diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index e69de29..b659f52 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -0,0 +1,3 @@ +export const jsonToCsvHandler = async () => { + +} \ No newline at end of file diff --git a/src/cwdState.js b/src/cwdState.js new file mode 100644 index 0000000..e118cd3 --- /dev/null +++ b/src/cwdState.js @@ -0,0 +1,17 @@ +import os from 'node:os'; +import { InvalidInputError } from './utils/errors.js'; + +export const initialCwd = os.homedir(); +let cwd = initialCwd; + +export const getCwd = () => { + return cwd; +} + +// dir - absolute path +export const chCwd = (dir) => { + if (!dir.startsWith(initialCwd)) { + throw new InvalidInputError('Cannot go higher than initial directory'); + } + cwd = dir; +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 9e469e6..acff6a3 100644 --- a/src/main.js +++ b/src/main.js @@ -1,10 +1,6 @@ -import process from 'node:process'; -import os from 'node:os'; import { initRepl } from './repl.js'; const init = () => { - process.chdir(os.homedir()); - initRepl(); } diff --git a/src/navigation.js b/src/navigation.js index 0ca8dee..bbac816 100644 --- a/src/navigation.js +++ b/src/navigation.js @@ -1,17 +1,13 @@ -import os from 'node:os'; -import path from 'node:path'; -import process from 'node:process'; import fs from 'node:fs/promises'; +import { getCwd, chCwd, initialCwd } from './cwdState.js'; import { InvalidInputError } from './utils/errors.js'; +import { pathResolver } from './utils/pathResolver.js'; -export const upHandler = () => { - if (process.cwd() !== os.homedir()) { - const newDirectory = process.cwd() - .split(path.sep) - .slice(0, -1) - .join(path.sep); - process.chdir(newDirectory); +export const upHandler = () => { + if (getCwd() !== initialCwd) { + const newDirectory = pathResolver(`${getCwd}/..`); + chDir(newDirectory); } } @@ -20,19 +16,19 @@ export const cdHandler = async (args) => { if (!pathToDirectory) { throw new InvalidInputError('No path/to/directory'); } - const newDirPath = path.resolve(process.cwd(), pathToDirectory); + const newDirPath = pathResolver(pathToDirectory); const stats = await fs.stat(newDirPath); if (stats.isDirectory()) { - process.chdir(newDirPath); + chCwd(newDirPath) } else { throw new InvalidInputError('Path is not a directory'); } } export const lsHandler = async () => { - const dirEntities = await fs.readdir(process.cwd(), { withFileTypes: true }); + const dirEntities = await fs.readdir(getCwd(), { withFileTypes: true }); const folders = []; const files = []; diff --git a/src/repl.js b/src/repl.js index 9c2bf71..a41c61f 100644 --- a/src/repl.js +++ b/src/repl.js @@ -2,6 +2,7 @@ import readline from 'node:readline'; import process from 'node:process'; +import { getCwd } from './cwdState.js'; import { InvalidInputError, INVALID_INPUT_ERROR_CODE } from './utils/errors.js'; import { COMMAND_HANDLERS_MAP } from './commands.js'; @@ -18,7 +19,7 @@ const ANSI_COLORS = { const ANSI_COLOR_RESET = '\x1b[0m'; const onSuccess = () => { - console.log(`${ANSI_COLORS.green}${CURRENT_DIRECTORY_PREFIX} ${process.cwd()}${ANSI_COLOR_RESET}`); + console.log(`${ANSI_COLORS.green}${CURRENT_DIRECTORY_PREFIX} ${getCwd()}${ANSI_COLOR_RESET}`); } const onError = (err) => { diff --git a/src/utils/argParser.js b/src/utils/argParser.js index e69de29..9dddadd 100644 --- a/src/utils/argParser.js +++ b/src/utils/argParser.js @@ -0,0 +1,32 @@ +import { InvalidInputError } from './errors.js'; + +export const argParser = (args, argName, required = false) => { + const argIndex = args.findIndex(arg => arg.startsWith(`--${argName}`)); + + if (argIndex === -1) { + if (required) { + throw new InvalidInputError('Argument is required'); + } else { + return; + } + } + + const arg = args[argIndex]; + let argValue; + + if (arg.includes('=')) { + argValue = arg.split('=')[1]; + } else { + const nextArgvValue = args[argIndex + 1]; + + if (nextArgvValue && !nextArgvValue.startsWith('--')) { + argValue = nextArgvValue; + } + } + + if (!argValue && required) { + throw new InvalidInputError('Argument is required'); + } + + return argValue; +} \ No newline at end of file diff --git a/src/utils/pathResolver.js b/src/utils/pathResolver.js index e69de29..2b2cb9c 100644 --- a/src/utils/pathResolver.js +++ b/src/utils/pathResolver.js @@ -0,0 +1,6 @@ +import path from 'node:path'; +import { getCwd } from '../cwdState.js'; + +export const pathResolver = (pathArg) => { + return path.resolve(getCwd(), pathArg); +} \ No newline at end of file From 745d526a428273d821861f2673b8e57fad6c1af8 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 21:04:13 +0200 Subject: [PATCH 6/7] Added json to csv --- src/commands/csvToJson.js | 51 ++++++++++++------------------ src/commands/jsonToCsv.js | 37 ++++++++++++++++++++-- src/repl.js | 2 +- src/utils/argParser.js | 2 +- src/utils/fileConversionHandler.js | 25 +++++++++++++++ 5 files changed, 82 insertions(+), 35 deletions(-) create mode 100644 src/utils/fileConversionHandler.js diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js index ff0522b..af3c1bb 100644 --- a/src/commands/csvToJson.js +++ b/src/commands/csvToJson.js @@ -1,30 +1,19 @@ import { Transform } from 'node:stream'; -import { pipeline } from 'node:stream/promises'; -import { createReadStream, createWriteStream } from 'node:fs'; -import { argParser } from '../utils/argParser.js'; -import { pathResolver } from '../utils/pathResolver.js'; +import { fileConfersionHandler } from '../utils/fileConversionHandler.js'; const EXTRA_HEADER_NAME = 'Extra'; const INDENT = 2; +const LINE_SEPARATOR = /\r?\n/; -export const csvToJsonHandler = async (args) => { - const inputArg = argParser(args, 'input', true); - const outputArg = argParser(args, 'output', true); - - const inputPath = pathResolver(inputArg); - const outputPath = pathResolver(outputArg); - - const inputStream = createReadStream(inputPath, { encoding: 'utf-8' }); - const outputStream = createWriteStream(outputPath, { encoding: 'utf-8' }); - +const getCsvToJsonTransformSteam = () => { let transformBuffer = ''; let headers = []; let isFirstDataRow = true; const getDataPrefix = () => { - let prefix = ',\n'; + let prefix = `,\n${' '.repeat(INDENT)}`; if (isFirstDataRow) { - prefix = ''; + prefix = ' '.repeat(INDENT); isFirstDataRow = false; } return prefix; @@ -43,23 +32,23 @@ export const csvToJsonHandler = async (args) => { } obj[header] = d; }); - return JSON.stringify(obj, null, INDENT).replace(/^/gm, ' '.repeat(INDENT)); + return JSON.stringify(obj); } - - const transformStream = new Transform({ + + return new Transform({ transform(chunk, _, callback) { transformBuffer += chunk; - const lines = transformBuffer.split(/\r?\n/); + const lines = transformBuffer.split(LINE_SEPARATOR); transformBuffer = lines.pop(); - + lines.forEach((line) => { if (!line.trim()) { return; } - + const parsedLineData = line.split(','); - + if (!headers.length) { headers = parsedLineData.map(h => h.trim()); this.push('[\n'); @@ -74,17 +63,17 @@ export const csvToJsonHandler = async (args) => { if (!transformBuffer.trim()) { return '\n]'; } - + const parsedLineData = transformBuffer.split(','); - this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}\n]`); + this.push(`${getDataPrefix()}${transformDataToJsonString(parsedLineData)}\n]\n`); transformBuffer = ''; callback(); } - }) - inputStream.on('error', (err) => { throw err }); - transformStream.on('error', (err) => { throw err }); - outputStream.on('error', (err) => { throw err }); + }); +} - await pipeline(inputStream, transformStream, outputStream); -} \ No newline at end of file +export const csvToJsonHandler = async (args) => { + const transformStream = getCsvToJsonTransformSteam(); + return fileConfersionHandler(args, '.csv', transformStream); +} diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index b659f52..d2cc13c 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -1,3 +1,36 @@ -export const jsonToCsvHandler = async () => { +import { Transform } from 'node:stream'; +import { fileConfersionHandler } from '../utils/fileConversionHandler.js'; -} \ No newline at end of file +const getJsonToCsvTransformSteam = () => { + let jsonStringBuffer = ''; + + return new Transform({ + transform(chunk, _, callback) { + jsonStringBuffer += chunk; + callback(); + }, + flush(callback) { + const jsonData = JSON.parse(jsonStringBuffer); + if (!Array.isArray(jsonData)) { + throw new Error('Invalid json type'); + } + + const headerSet = new Set(); + jsonData.map((row) => Object.keys(row).forEach(h => headerSet.add(h))); + const headers = Array.from(headerSet); + + this.push(`${headers.join(',')}\n`) + + jsonData.forEach(row => { + const valuesInHeadersOrder = headers.map((header) => row[header] || ''); + this.push(`${valuesInHeadersOrder.join(',')}\n`); + }) + callback(); + } + }); +} + +export const jsonToCsvHandler = async (args) => { + const transformStream = getJsonToCsvTransformSteam(); + return fileConfersionHandler(args, '.json', transformStream); +} diff --git a/src/repl.js b/src/repl.js index a41c61f..57492f6 100644 --- a/src/repl.js +++ b/src/repl.js @@ -54,7 +54,7 @@ export const initRepl = () => { }); rl.on('line', async (line) => { - const lineTrimmed = line.trim().toLowerCase(); + const lineTrimmed = line.trim(); const [command, ...commandArgs] = lineTrimmed.split(' '); if (command === '.exit' && commandArgs.length === 0) { diff --git a/src/utils/argParser.js b/src/utils/argParser.js index 9dddadd..8051e95 100644 --- a/src/utils/argParser.js +++ b/src/utils/argParser.js @@ -2,7 +2,7 @@ import { InvalidInputError } from './errors.js'; export const argParser = (args, argName, required = false) => { const argIndex = args.findIndex(arg => arg.startsWith(`--${argName}`)); - + if (argIndex === -1) { if (required) { throw new InvalidInputError('Argument is required'); diff --git a/src/utils/fileConversionHandler.js b/src/utils/fileConversionHandler.js new file mode 100644 index 0000000..5320df4 --- /dev/null +++ b/src/utils/fileConversionHandler.js @@ -0,0 +1,25 @@ +import { pipeline } from 'node:stream/promises'; +import { extname } from 'node:path'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { argParser } from '../utils/argParser.js'; +import { pathResolver } from '../utils/pathResolver.js'; +import { InvalidInputError } from '../utils/errors.js'; + +export const fileConfersionHandler = async (args, fileExtention, transformStream) => { + const inputArg = argParser(args, 'input', true); + const outputArg = argParser(args, 'output', true); + + const inputPath = pathResolver(inputArg); + const outputPath = pathResolver(outputArg); + + const inputFileExt = extname(inputPath).toLowerCase(); + + if (inputFileExt !== fileExtention) { + throw new InvalidInputError('Invalid file extention'); + } + + const inputStream = createReadStream(inputPath, { encoding: 'utf-8' }); + const outputStream = createWriteStream(outputPath, { encoding: 'utf-8' }); + + await pipeline(inputStream, transformStream, outputStream); +}; \ No newline at end of file From f138d1880e08e62cd1050d5d9a03ff4dc22c7f77 Mon Sep 17 00:00:00 2001 From: BorysovskaOA Date: Fri, 13 Mar 2026 22:13:33 +0200 Subject: [PATCH 7/7] Added count --- src/commands.js | 2 ++ src/commands/count.js | 56 +++++++++++++++++++++++++++++++++++++++ src/commands/jsonToCsv.js | 2 ++ 3 files changed, 60 insertions(+) diff --git a/src/commands.js b/src/commands.js index 24a69db..72f1082 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,3 +1,4 @@ +import { countHandler } from './commands/count.js' import { csvToJsonHandler } from './commands/csvToJson.js' import { jsonToCsvHandler } from './commands/jsonToCsv.js' import { upHandler, cdHandler, lsHandler } from './navigation.js' @@ -8,4 +9,5 @@ export const COMMAND_HANDLERS_MAP = { ls: lsHandler, 'csv-to-json': csvToJsonHandler, 'json-to-csv': jsonToCsvHandler, + count: countHandler, } diff --git a/src/commands/count.js b/src/commands/count.js index e69de29..bc49c3d 100644 --- a/src/commands/count.js +++ b/src/commands/count.js @@ -0,0 +1,56 @@ + +import { extname } from 'node:path'; +import { createReadStream } from 'node:fs'; +import { argParser } from '../utils/argParser.js'; +import { pathResolver } from '../utils/pathResolver.js'; +import { InvalidInputError } from '../utils/errors.js'; + +const LINE_SEPARATOR = /\r?\n/; +const WORD_SEPARATOR = /\s+/; + +const getWordsFromString = (str) => { + return str.split(WORD_SEPARATOR).filter(w => w.length > 0); +} + +export const countHandler = async (args) => { + const inputArg = argParser(args, 'input', true); + const inputPath = pathResolver(inputArg); + + const inputFileExt = extname(inputPath).toLowerCase(); + + if (inputFileExt !== '.txt') { + throw new InvalidInputError('Invalid file extention'); + } + + const readableStream = createReadStream(inputPath, { encoding: 'utf-8' }); + + let buffer = ''; + let linesCount = 0; + let wordsCount = 0; + let charsCount = 0; + + for await (const chunk of readableStream) { + buffer += chunk; + charsCount += chunk.length; + + const lines = buffer.split(LINE_SEPARATOR); + buffer = lines.pop(); + + linesCount += lines.length; + + const words = getWordsFromString(lines.join(' ')); + wordsCount += words.length; + } + + if (buffer.length) { + charsCount += buffer.length; + linesCount++; + + const words = getWordsFromString(buffer); + wordsCount += words.length; + } + + console.log(`Lines: ${linesCount}`); + console.log(`Words: ${wordsCount}`); + console.log(`Characters: ${charsCount}`); +} \ No newline at end of file diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index d2cc13c..34ea0ee 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -11,6 +11,7 @@ const getJsonToCsvTransformSteam = () => { }, flush(callback) { const jsonData = JSON.parse(jsonStringBuffer); + jsonStringBuffer = ''; if (!Array.isArray(jsonData)) { throw new Error('Invalid json type'); } @@ -25,6 +26,7 @@ const getJsonToCsvTransformSteam = () => { const valuesInHeadersOrder = headers.map((header) => row[header] || ''); this.push(`${valuesInHeadersOrder.join(',')}\n`); }) + callback(); } });