diff --git a/Term2-HW1-CSV/Makefile b/Term2-HW1-CSV/Makefile new file mode 100644 index 0000000..d1e0a5d --- /dev/null +++ b/Term2-HW1-CSV/Makefile @@ -0,0 +1,28 @@ +CC = gcc +CFLAGS = -Iinclude -Wall -Wextra + +SRC = main.c src/csvConverter.c +TEST_SRC = tests/testCsvConverter.c + +OUT = program +TEST_OUT = testProgram + +all: $(OUT) + +$(OUT): $(SRC) + $(CC) $(CFLAGS) $(SRC) -o $(OUT) + +test: $(TEST_SRC) src/csvConverter.c + $(CC) $(CFLAGS) -DTEST_MAIN src/csvConverter.c $(TEST_SRC) -o $(TEST_OUT) + +clean: + rm -f $(OUT) $(TEST_OUT) + +format: + find . -path ./build -prune -o -type f \( -path "./src/*" -o -path "./include/*" -o -path "./tests/*" -o -name "main.c" \) \( -name '*.c' -o -name '*.h' \) -print | xargs clang-format --style=file -i + +format-check: + find . -path ./build -prune -o -type f \( -path "./src/*" -o -path "./include/*" -o -path "./tests/*" -o -name "main.c" \) \( -name '*.c' -o -name '*.h' \) -print | xargs clang-format --style=file --dry-run -Werror + +tidy: + clang-tidy $(SRC) $(TEST_SRC) -- -Iinclude diff --git a/Term2-HW1-CSV/include/csvConverter.h b/Term2-HW1-CSV/include/csvConverter.h new file mode 100644 index 0000000..7cb1f13 --- /dev/null +++ b/Term2-HW1-CSV/include/csvConverter.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include +#include + +char* readLine(FILE* f); +int getTrimmedLen(const char* start, int len); +void printRowSeparation(const char* lSymb, const char* mSymb, const char* rSymb, const char* symb, + int cols, int colLength, FILE* file); +int trimCsvField(char* field); +int isDouble(const char* str); +int convertCsvToTxt(FILE* csvFile, FILE* txtFile); diff --git a/Term2-HW1-CSV/input.csv b/Term2-HW1-CSV/input.csv new file mode 100644 index 0000000..6d4e85a --- /dev/null +++ b/Term2-HW1-CSV/input.csv @@ -0,0 +1,10 @@ +Name,Test1,Test2,Test3,Test4 +Kate,12.34.56,78,"Invalid double","12.3.4" +Leo,"""Wrapped""",55,"Multiple """" quotes","""""""" +Mia,0,0,"Empty string test","" +Noah,5,"","Empty numeric field","" +Olivia,3.0,"Text with , comma","Another ""quoted"" value",ExtraColumn,test over extra +Paul,"Widest fieldddddddddddddd" +Quinn,10,20 +Rachel,"quote",test +Sam,"""DoubleStart""",33,"""Edge"" case","End""" diff --git a/Term2-HW1-CSV/main.c b/Term2-HW1-CSV/main.c new file mode 100644 index 0000000..0db4e9b --- /dev/null +++ b/Term2-HW1-CSV/main.c @@ -0,0 +1,42 @@ +#include "include/csvConverter.h" + +int main() +{ + const int PATH_LIMIT = 100; + + // open .csv file + char csvPath[PATH_LIMIT]; + printf("Enter .csv path:\n"); + if (fgets(csvPath, PATH_LIMIT, stdin) == NULL) { + printf("input error!\n"); + return 1; + } + csvPath[strcspn(csvPath, "\n")] = '\0'; + FILE* csvFile = fopen(csvPath, "r"); + if (csvFile == NULL) { + printf("file not found!"); + return 1; + } + + // open .txt file + char txtPath[PATH_LIMIT]; + printf("Enter .txt path:\n"); + if (fgets(txtPath, PATH_LIMIT, stdin) == NULL) { + printf("input error!\n"); + return 1; + } + txtPath[strcspn(txtPath, "\n")] = '\0'; + FILE* txtFile = fopen(txtPath, "w"); + if (txtFile == NULL) { + printf("can't write into file!"); + fclose(csvFile); + return 1; + } + + int result = convertCsvToTxt(csvFile, txtFile); + + fclose(csvFile); + fclose(txtFile); + + return result; +} diff --git a/Term2-HW1-CSV/output.txt b/Term2-HW1-CSV/output.txt new file mode 100644 index 0000000..801a697 --- /dev/null +++ b/Term2-HW1-CSV/output.txt @@ -0,0 +1,21 @@ +╔═══════════════════════════╦═══════════════════════════╦═══════════════════════════╦═══════════════════════════╦═══════════════════════════╦═══════════════════════════╗ +║ Name ║ Test1 ║ Test2 ║ Test3 ║ Test4 ║ ║ +╠═══════════════════════════╬═══════════════════════════╬═══════════════════════════╬═══════════════════════════╬═══════════════════════════╬═══════════════════════════╣ +║ Kate │ 12.34.56 │ 78 │ Invalid double │ 12.3.4 │ ║ +╠───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────╣ +║ Leo │ "Wrapped" │ 55 │ Multiple "" quotes │ """ │ ║ +╠───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────╣ +║ Mia │ 0 │ 0 │ Empty string test │ │ ║ +╠───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────╣ +║ Noah │ 5 │ │ Empty numeric field │ │ ║ +╠───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────╣ +║ Olivia │ 3.0 │ Text with , comma │ Another "quoted" value │ ExtraColumn │ test over extra ║ +╠───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────╣ +║ Paul │ Widest fieldddddddddddddd │ │ │ │ ║ +╠───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────╣ +║ Quinn │ 10 │ 20 │ │ │ ║ +╠───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────╣ +║ Rachel │ quote │ test │ │ │ ║ +╠───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────┼───────────────────────────╣ +║ Sam │ "DoubleStart" │ 33 │ "Edge" case │ End" │ ║ +╚═══════════════════════════╧═══════════════════════════╧═══════════════════════════╧═══════════════════════════╧═══════════════════════════╧═══════════════════════════╝ diff --git a/Term2-HW1-CSV/src/csvConverter.c b/Term2-HW1-CSV/src/csvConverter.c new file mode 100644 index 0000000..653628e --- /dev/null +++ b/Term2-HW1-CSV/src/csvConverter.c @@ -0,0 +1,256 @@ +#include "../include/csvConverter.h" + +void printRowSeparation(const char* lSymb, const char* mSymb, const char* rSymb, const char* symb, + int cols, int colLength, FILE* file) +{ + fprintf(file, "%s", lSymb); + for (int i = 0; i < cols; ++i) { + for (int j = 0; j < colLength + 2; ++j) + fprintf(file, "%s", symb); + if (i < cols - 1) + fprintf(file, "%s", mSymb); + } + fprintf(file, "%s\n", rSymb); +} + +int isDouble(const char* str) +{ + char* endptr; + + while (isspace((unsigned char)*str)) + ++str; + + if (*str == '\0') + return 0; + + strtod(str, &endptr); + while (isspace((unsigned char)*endptr)) + ++endptr; + + return (*endptr == '\0'); +} + +char* readLine(FILE* f) +{ + int size = 4; + int len = 0; + + char* buffer = malloc(size); + if (!buffer) + return NULL; + + int c; + while ((c = fgetc(f)) != EOF && c != '\n') { + if (len + 1 >= size) { + size *= 2; + char* tmp = realloc(buffer, size); + if (!tmp) { + free(buffer); + return NULL; + } + buffer = tmp; + } + buffer[len++] = c; + } + + if (len == 0 && c == EOF) { + free(buffer); + return NULL; + } + + buffer[len] = '\0'; + return buffer; +} + +int trimCsvField(char* field) +{ + int len = strlen(field); + + if (len >= 2 && field[0] == '"' && field[len - 1] == '"') { + memmove(field, field + 1, len - 2); + field[len - 2] = '\0'; + len -= 2; + } + + char* src = field; + char* dst = field; + while (*src) { + if (*src == '"' && *(src + 1) == '"') { + *dst++ = '"'; + src += 2; + } else + *dst++ = *src++; + } + *dst = '\0'; + return dst - field; +} + +int getTrimmedLen(const char* start, int len) +{ + if (len >= 2 && start[0] == '"' && start[len - 1] == '"') { + start++; + len -= 2; + } + + int trimmedLen = 0; + int i = 0; + while (i < len) { + if (start[i] == '"' && i + 1 < len && start[i + 1] == '"') + i += 2; + else + ++i; + ++trimmedLen; + } + + return trimmedLen; +} + +int convertCsvToTxt(FILE* csvFile, FILE* txtFile) +{ + int maxCols = 0; + int maxColLen = 0; + + // pass 1 (compute maxCols and maxColLength) + char* line; + while ((line = readLine(csvFile)) != NULL) { + int cols = 0; + int inQuotes = 0; + char* start = line; + char* ptr = line; + while (1) { + if (*ptr == '"') { + if (*(ptr + 1) == '"') + ++ptr; + else + inQuotes = !inQuotes; + } + + if ((*ptr == ',' && (!inQuotes)) || *ptr == '\0') { + ++cols; + if (cols > maxCols) + maxCols = cols; + + int len = ptr - start; + int fieldLen = getTrimmedLen(start, len); + if (fieldLen > maxColLen) + maxColLen = fieldLen; + + if (*ptr == '\0') + break; + + start = ptr + 1; + } + ++ptr; + } + free(line); + + if (inQuotes) { + printf("unmatched quote in .csv file.\n"); + return 1; + } + } + + if (maxCols == 0) + return 0; + + rewind(csvFile); + + // pass 2 (read from csv file and writo into txt file) + int rowInd = 0; + char* nextLine = readLine(csvFile); + while (nextLine != NULL) { + line = nextLine; + nextLine = readLine(csvFile); + + if (rowInd == 0) + printRowSeparation("╔", "╦", "╗", "═", maxCols, maxColLen, txtFile); + + fprintf(txtFile, "║"); + + int printedCols = 0; + int inQuotes = 0; + char* start = line; + char* ptr = line; + while (1) { + if (*ptr == '"') { + if (*(ptr + 1) == '"') + ++ptr; + else + inQuotes = !inQuotes; + } + if ((*ptr == ',' && (!inQuotes)) || *ptr == '\0') { + int len = ptr - start; + char* field = malloc(len + 1); + memcpy(field, start, len); + field[len] = '\0'; + int fieldLen = trimCsvField(field); + + if (rowInd == 0) { + fprintf(txtFile, " %s", field); + for (int i = 0; i < maxColLen - fieldLen; ++i) + fputc(' ', txtFile); + fprintf(txtFile, " ║"); + } else { + if (isDouble(field)) { + for (int i = 0; i < maxColLen - fieldLen + 1; ++i) + fputc(' ', txtFile); + fprintf(txtFile, "%s ", field); + } else { + fprintf(txtFile, " %s", field); + for (int i = 0; i < maxColLen - fieldLen + 1; ++i) + fputc(' ', txtFile); + } + + if (printedCols == maxCols - 1) + fprintf(txtFile, "║"); + else + fprintf(txtFile, "│"); + } + + free(field); + ++printedCols; + + if (*ptr == '\0') + break; + + start = ptr + 1; + } + ++ptr; + } + + if (inQuotes) { + printf("unmatched quote in .csv file.\n"); + free(line); + return 1; + } + + while (printedCols < maxCols) { + for (int i = 0; i < maxColLen + 1; ++i) + fputc(' ', txtFile); + + if (printedCols == maxCols - 1) + fprintf(txtFile, " ║"); + else + fprintf(txtFile, " │"); + + ++printedCols; + } + + fputc('\n', txtFile); + + if (nextLine != NULL) { + if (rowInd == 0) + printRowSeparation("╠", "╬", "╣", "═", maxCols, maxColLen, txtFile); + else + printRowSeparation("╠", "┼", "╣", "─", maxCols, maxColLen, txtFile); + } + + free(line); + + ++rowInd; + } + + printRowSeparation("╚", "╧", "╝", "═", maxCols, maxColLen, txtFile); + + return 0; +} diff --git a/Term2-HW1-CSV/tests/testCsvConverter.c b/Term2-HW1-CSV/tests/testCsvConverter.c new file mode 100644 index 0000000..d1eb911 --- /dev/null +++ b/Term2-HW1-CSV/tests/testCsvConverter.c @@ -0,0 +1,92 @@ +#include "../include/csvConverter.h" + +void testIsDoubleValidInteger() +{ + int result = isDouble("123"); + if (result != 1) + printf("FAIL testIsDoubleValidInteger: expected 1, got %d\n", result); + else + printf("PASS testIsDoubleValidInteger\n"); +} + +void testIsDoubleValidDecimal() +{ + int result = isDouble("12.34"); + if (result != 1) + printf("FAIL testIsDoubleValidDecimal: expected 1, got %d\n", result); + else + printf("PASS testIsDoubleValidDecimal\n"); +} + +void testIsDoubleScientific() +{ + int result = isDouble("5e12"); + if (result != 1) + printf("FAIL testIsDoubleScientific: expected 1, got %d\n", result); + else + printf("PASS testIsDoubleScientific\n"); +} + +void testIsDoubleInvalid() +{ + int result = isDouble("abc"); + if (result != 0) + printf("FAIL testIsDoubleInvalid: expected 0, got %d\n", result); + else + printf("PASS testIsDoubleInvalid\n"); +} + +void testIsDoubleEmpty() +{ + int result = isDouble(""); + if (result != 0) + printf("FAIL testIsDoubleEmpty: expected 0, got %d\n", result); + else + printf("PASS testIsDoubleEmpty\n"); +} + +void testTrimCsvField() +{ + char str[] = "\"Test\""; + int len = trimCsvField(str); + + if (strcmp(str, "Test") != 0 || len != 4) + printf("FAIL testTrimCsvFieldSimple: expected \"Test\" (len 4), got \"%s\" (len %d)\n", str, len); + else + printf("PASS testTrimCsvFieldSimple\n"); +} + +void testGetTrimmedLenQuoted() +{ + int result = getTrimmedLen("\"Test\"", 6); + if (result != 4) + printf("FAIL testGetTrimmedLenQuoted: expected 4, got %d\n", result); + else + printf("PASS testGetTrimmedLenQuoted\n"); +} + +void testGetTrimmedLenUnquoted() +{ + int result = getTrimmedLen("test", 4); + if (result != 4) + printf("FAIL testGetTrimmedLenUnquoted: expected 4, got %d\n", result); + else + printf("PASS testGetTrimmedLenUnquoted\n"); +} + +#ifdef TEST_MAIN +int main() +{ + testIsDoubleValidInteger(); + testIsDoubleValidDecimal(); + testIsDoubleScientific(); + testIsDoubleInvalid(); + testIsDoubleEmpty(); + testTrimCsvField(); + testGetTrimmedLenQuoted(); + testGetTrimmedLenUnquoted(); + + printf("All tests executed\n"); + return 0; +} +#endif