diff --git a/Makefile b/Makefile index 08cc1aa..9324d3d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,20 @@ +CC = cc +CFLAGS = -Wall -Wextra -Werror -pedantic -std=c99 +UNAME := $(shell uname) + all: - echo "Compiling" - gcc -o ropipe ropipe.c + @echo "Compiling" + $(CC) $(CFLAGS) -o ropipe ropipe.c + @echo "Done" + + install: +ifneq ($(UNAME), Linux) + @echo "Automatic installation is only supported on Linux" + @echo "Please copy the binary to a directory in your PATH" + @echo "and the manpage to your manpages directory" + @exit +endif ifeq (,$(wildcard ./ropipe)) make endif @@ -9,20 +22,26 @@ ifneq ($(shell id -u), 0) @echo "You must be root to install" else mkdir -p /usr/local/share/man/man1/ - echo "Copying man file" + @echo "Copying man file" cp ropipe.1 /usr/local/share/man/man1/ropipe.1 - echo "Installing binary" + @echo "Installing binary" cp ropipe /usr/bin/ropipe endif + uninstall: +ifneq ($(UNAME), Linux) + @echo "Automatic uninstalling is only supported on Linux" + @exit +endif ifneq ($(shell id -u), 0) @echo "You must be root to uninstall" else - echo "Removing man file" + @echo "Removing man file" rm -rf /usr/local/share/man/man1/ropipe.1 - echo "Uninstalling binary" + @echo "Uninstalling binary" rm -rf /usr/bin/ropipe endif + update: git pull make install diff --git a/README.md b/README.md index 8b89996..92e5dc9 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,63 @@ + # Ropipe -![GitHub](https://img.shields.io/github/license/MatMasIt/ropipe) +![License](https://img.shields.io/github/license/MatMasIt/ropipe) +![c](https://img.shields.io/badge/language-C-blue) + +> Convert Roman numbers to integers and vice-versa in pipes. -> Convert roman numbers to integers and vice-versa in pipes ## What is ropipe? -Ropipe converts decimal integers to roman numbers and vice versa, reading from stdin and printing to stdout, it is meant to primarily be used in pipes. -Default is integer to roman. -## Options -* -r : Roman to integer. Integer to roman is the default. -## Installation -1. Clone this repository -2. `cd` into it -3. run -```sh -sudo make install -``` -or -```sh -doas make install -``` -(The program is compiled at this time, you may check the script and the program out beforehand) +Ropipe converts base-10 integers to Roman numbers and vice versa, reading from stdin and printing to stdout. It is designed to be used primarily in pipes. By default, it converts integers to Roman numbers. + +## Options + +- `-r`: Roman to integer conversion. Integer to Roman is the default behavior. +- `-h`: Display help message and exit. +- `-s`: Fail silently, with no output on invalid input. +- `-q`: Accept quirky Roman numerals like IIII for IV. +- `-k`: Don't quit on invalid input; continue processing subsequent inputs. + + +You can use the options concatenated, like `-rsk` or individually, like `-r -s -k`. + + +## Installation + +1. Clone this repository: + ```sh + git clone https://github.com/MatMasIt/ropipe.git + cd ropipe + ``` + +2. Compile and install: + + ```sh + sudo make install + ``` + + or + + ```sh + doas make install + ``` + + The program is compiled during installation. You may review the script and the program before proceeding. + ## Examples -Once installed, you can find sample files in the project dir. + +Once installed, you can use `ropipe` in your shell commands. Below are some examples: --- -Example 1: + +**Example 1:** + ```sh ropipe < arabic.txt ``` -Result: + +**Result:** + ``` I X @@ -38,26 +66,38 @@ CCCXXI ``` --- -Example 2: + +**Example 2:** + ```sh ropipe -r < roman.txt ``` -Result: + +**Result:** + ``` 4 5 7 1200 ``` + ## Manpage -A manpage is installed alongside the program, try running `man ropipe` +A manpage is installed alongside the program. You can access it by running: + +```sh +man ropipe +``` ## Updates -There is no automatic update system as of yet, you are advised to regularly visit https://github.com/MatMasIt/ropipe, download and re-run the installer in order to get updates +There is no automatic update system currently implemented. Please visit [ropipe GitHub repository](https://github.com/MatMasIt/ropipe) regularly to check for updates. To update, download the latest version and re-run the installer. + +Nontheless, it's not a program that one would expect to change often, so manual updates should be rare. ## Make commands -* `make`: compile ropipe -* `make install`: install ropipe -* `make uninstall`: uninstall ropipe + +- `make`: compile ropipe +- `make install`: install ropipe +- `make uninstall`: uninstall ropipe diff --git a/ropipe.1 b/ropipe.1 index 18090fc..6f47351 100644 --- a/ropipe.1 +++ b/ropipe.1 @@ -1,15 +1,26 @@ -.TH ROPIPE 1 "06 October 2021" +.TH ROPIPE 1 "07 July 2024" .SH NAME -ropipe - Convert roman numbers to integers and vice-versa in pipes +ropipe - Convert Roman numerals to integers and vice-versa in pipes .SH SYNOPSIS -ropipe [ -r ] +ropipe [ -rskqh ] .SH DESCRIPTION -Ropipe converts decimal integers to roman numbers and vice versa, reading from stdin and printing to stdout, it is meant to primarily be used in pipes. -Default is integer to roman. +Ropipe converts decimal integers to Roman numerals and vice versa, reading from stdin and printing to stdout. It is meant to primarily be used in pipes. By default, it converts integers to Roman numerals. .SS Options .TP -r -Roman to integer. Integer to roman is the default. +Convert Roman numerals to integers. Integer to Roman is the default. +.TP +-s +Fails silently, with no output on invalid input. +.TP +-k +Don't quit on invalid input; continue processing subsequent inputs. +.TP +-q +Accept quirky Roman numerals like IIII for IV. +.TP +-h +Display this help message. .SH FILES .TP /usr/bin/ropipe diff --git a/ropipe.c b/ropipe.c index 0995c1b..674b7cf 100644 --- a/ropipe.c +++ b/ropipe.c @@ -1,138 +1,224 @@ -#include #include #include #include -#define RDIGITS 14 -int validateN(char c) { - int n = (int)c; - return c > 47 && n < 58; -} -/* - * Returns the value of a roman numeral digit - * @param {char} c - Roman digit - */ -static int roman_to_integer(char c) { - switch (c) { - case 'I': - return 1; - case 'V': - return 5; - case 'X': - return 10; - case 'L': - return 50; - case 'C': - return 100; - case 'D': - return 500; - case 'M': - return 1000; - default: - return 0; - } -} -/* - * Returns the interger value of a roman number - * @param {string} s - Roman number - */ -int roman_to_int(char *s) { - int i, int_num = roman_to_integer(s[0]); +#include +#include +#include +#include - for (i = 1; s[i] != '\0'; i++) { - int prev_num = roman_to_integer(s[i - 1]); - int cur_num = roman_to_integer(s[i]); - if (prev_num < cur_num) { - int_num = int_num - prev_num + (cur_num - prev_num); - } else { - int_num += cur_num; - } - } - return int_num; +#define MAX_ROMAN_LEN 77 // 76 characters + 1 for the null terminator +#define MAX_INT_LEN 6 // 65535 (5 characters) + 1 for the null terminator + +#define NUM_SYMBOLS 13 + +const uint16_t values[NUM_SYMBOLS] = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; +const char *symbols[NUM_SYMBOLS] = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}; +const uint8_t symbols_len[NUM_SYMBOLS] = {1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1}; + +void print_usage(const char *program_name) +{ + printf("Usage: %s [options] [input]\n", program_name); + printf("Options:\n"); + printf(" -r: Convert a Roman numeral to an integer\n"); + printf(" -s: Fails silently, with no output\n"); + printf(" -k: Don't quit on invalid input\n"); + printf(" -q: Accept quirky Roman numerals like IIII for IV (only valid with -r)\n"); + printf(" -h: Display this help message\n"); } -/* - * Map of roman digits symbols and their value - * @elem {string} sym - digit symbol - * @elem {int} val - digit symbol - */ -typedef struct { - char *sym; - int val; -} numeral; -/* - * Returns greatest numeral index - * @elem {numeral} nu - map of roman digits symbols and their value - * @elem {int} num - number - */ -int maxNume(numeral *nu, int num) { - int i, index; - for (i = 0; i < RDIGITS; i++) { // RDIGITS numerals in array - if (nu[i].val <= num) - index = i; - } - // gretest value numeral index, not greater than number - return index; -} -/* - * Prints roman number from integer - * @elem {numeral} nu - map of roman digits symbols and their value - * @elem {int} num - number - */ -void decToRoman(numeral *nu, int num) { - int max; - if (num != 0) { - max = maxNume(nu, num); - printf("%s", nu[max].sym); - num -= nu[max].val; // decrease number - decToRoman(nu, num); // recursively print numerals - } -} -/* - * Main - * @elem {int} argc - Argument count - * @elem {string[]} argv - Strings array - */ -int main(int argc, char *argv[]) { - int direction = 0; - /* - * 0: integer to roman (default) - * 1: roman to integer - */ - for (int i = 0; i < argc; i++) { - if (strcmp(argv[i], "-r")) { - direction = 1; - } else { - direction = 0; - } - } - numeral nume[RDIGITS] = {{"I", 1}, {"IV", 4}, {"V", 5}, {"IX", 9}, - {"X", 10}, {"XL", 40}, {"L", 50}, {"XC", 90}, - {"C", 100}, {"CD", 400}, {"D", 500}, {"CM", 900}, - {"M", 1000}, {"MMMM", 4000}}; - char str[51]; - int inputint = 0; - switch (direction) { - case 0: - while (scanf("%50s", str) && - !feof(stdin)) // read integers and convert to roman + +void int_to_roman(uint16_t num, char result[MAX_ROMAN_LEN]) +{ + if (num == 0) + return; + + char *result_ptr = result; + for (uint8_t i = 0; i < NUM_SYMBOLS; i++) { - printf("%d\n", roman_to_int(str)); - scanf("%*[^\n]"); // discard all until newline + while (num >= values[i]) + { + num -= values[i]; + result_ptr += sprintf(result_ptr, "%s", symbols[i]); + } + } +} + +void upper(char *str) +{ + for (char *p = str; *p; p++) + { + *p = toupper(*p); + } +} + +bool roman_to_int(char roman[MAX_ROMAN_LEN], uint16_t *result, bool allow_quirky) +{ + *result = 0; + upper(roman); + const char *roman_ptr = roman; + + int repetitions = 0; + char last_char = '\0'; + + for (uint8_t i = 0; i < NUM_SYMBOLS; i++) + { + while (strncmp(roman_ptr, symbols[i], symbols_len[i]) == 0) + { + // Validate repetition of the same numeral + if (!allow_quirky) + { + if (last_char == symbols[i][0]) + { + repetitions++; + if ((symbols_len[i] == 1 && repetitions >= 3 && symbols[i][0] != 'M') || + (symbols_len[i] == 2 && repetitions >= 1)) + { + return false; // Invalid due to too many repetitions + } + } + else + { + repetitions = 0; + } + } + + *result += values[i]; + roman_ptr += symbols_len[i]; + last_char = symbols[i][0]; + } + } + return *roman_ptr == '\0'; +} + +typedef uint8_t flag; + +#define ROMAN_TO_INT 0 +#define INT_TO_ROMAN 1 + +#define SILENT 0 +#define VERBOSE 1 + +#define QUIT 0 +#define NO_QUIT 1 + +int handle_int_to_roman(flag verbosity, flag quit) +{ + char line[MAX_INT_LEN]; + while (fgets(line, MAX_INT_LEN, stdin) != NULL) + { + char *endptr; + errno = 0; + long num = strtol(line, &endptr, 10); + if (errno != 0 || (*endptr != '\n' && *endptr != '\0') || num <= 0 || num > UINT16_MAX) + { + if (verbosity == VERBOSE) + { + fprintf(stderr, "Error: input must be a non-zero positive integer less than or equal to %d\n", UINT16_MAX); + } + if (quit == QUIT) + { + return 1; + } + } + else + { + char result[MAX_ROMAN_LEN]; + int_to_roman(num, result); + printf("%s\n", result); + } + } + return 0; +} + +int handle_roman_to_int(flag verbosity, flag quit, bool allow_quirky) +{ + char line[MAX_ROMAN_LEN]; + while (fgets(line, MAX_ROMAN_LEN, stdin) != NULL) + { + line[strcspn(line, "\n")] = '\0'; + uint16_t result; + if (!roman_to_int(line, &result, allow_quirky)) + { + if (verbosity == VERBOSE) + { + fprintf(stderr, "Error: input must be a valid Roman numeral\n"); + } + if (quit == QUIT) + { + return 1; + } + } + else + { + printf("%d\n", result); + } + } + return 0; +} + +int main(int argc, char *argv[]) +{ + flag mode = INT_TO_ROMAN; + flag verbosity = VERBOSE; + flag quit = QUIT; + bool allow_quirky = false; + + if (argc > 1) + { + for (int i = 1; i < argc; i++) + { + char *arg = argv[i]; + + if (arg[0] == '-') + { + for (int j = 1; arg[j] != '\0'; j++) + { + switch (arg[j]) + { + case 'r': + mode = ROMAN_TO_INT; + break; + case 's': + verbosity = SILENT; + break; + case 'k': + quit = NO_QUIT; + break; + case 'q': + if (mode == ROMAN_TO_INT) + { + allow_quirky = true; + } + else + { + fprintf(stderr, "Error: Option -q is only valid with -r (Roman to integer conversion)\n"); + return 1; + } + break; + case 'h': + print_usage(argv[0]); + return 0; + default: + fprintf(stderr, "Error: Unknown option '%c'\n", arg[j]); + return 1; + } + } + } + else + { + fprintf(stderr, "Error: Unknown argument '%s'\n", arg); + return 1; + } + } + } + + switch (mode) + { + case INT_TO_ROMAN: + return handle_int_to_roman(verbosity, quit); + case ROMAN_TO_INT: + return handle_roman_to_int(verbosity, quit, allow_quirky); + default: + fprintf(stderr, "Invalid mode\n"); + return 1; } - break; - case 1: - while (scanf("%9s", str) && - !feof(stdin)) // read roman and convert to integers - { - for (int i = 0; i < strlen(str); i++) { - if (!validateN(str[i])) - break; - } - sscanf(str, "%d", &inputint); - decToRoman(nume, inputint); - printf("\n"); - scanf("%*[^\n]"); // discard all until newline - } - break; - } - return 0; }