1
\$\begingroup\$

This is my second C++ program. Again, I wrote it all by myself. I only started learning C++ yesterday.

The concept of the program is simple; it should read input from std::cin and print the output to std::cout.

There are two functions that implement the bulk of the logic: a function to interpret numerals from a given base to its numerical value, and a function to represent a given number in a given base.

The base must be between 2 and 36. The reason is simple: I represent the number as strings, and I follow the convention of hexadecimal. Hexadecimal uses Arabic digits 0-9 and Basic Latin Alphabet a-f. There are 26 letters in the alphabet; if I use all of them I can represent base-36 numerals. For higher bases more symbols would be needed, but there aren't any good extensions to this.

My script only processes integers; it supports negative numbers and does some input validation. I use the ASCII character code to get the numerical value directly by decreasing the character code according to its value, and the case of the input can be mixed.

I stop the execution of the script if any character is not in the base-36 character set, or it has the same or higher numerical value than the base. I didn't do overflow and underflow checking though; those are currently beyond me.

The logic to interpret numerals from base is simple: initialize a variable n to 0, for each digit in string, multiply n by base, add the numerical value of digit to n and assign the result back to n, done.

The logic to represent numbers is its direct opposite: keep doing integer division with number as numerator and base as denominator, get the remainder as digit, collect the string representation in reverse order, and assign the quotient to number. Stop the loop when the number is 0.

The program should keep asking for commands and print the execution result to console, until the input equals 'q'. The commands should be a space delimited string consisting of 3 fields: the first field is the number/numeral to process, which can be in any supported base; the third is the base to convert from/to, which must be in decimal; The second is the keyword, it must be either "to" or "from".

The keyword determines which function should be called, if it is "to", the command means converting the number represented in decimal to the target base, else if it is "from", the command means to interpret the numeral in the given base.

I also wrote the function to split the string entirely by myself.


Code

#include <format>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>


std::vector<std::string> split_string(std::string str, std::string delim) {
    std::vector<std::string> fields;
    int length = delim.length();
    int pos = 0;
    std::string field = "";
    std::string buffer = "";
    for (char& c : str) {
        if (c == delim[pos]) {
            buffer += c;
            pos++;
        }
        else {
            field += buffer + c;
            buffer = "";
            pos = 0;
        }
        if (pos == length) {
            fields.push_back(field);
            field = "";
            buffer = "";
            pos = 0;
        }
    }
    if (field != "") {
        fields.push_back(field);
    }
    return fields;
}


uint64_t interpret_base(std::string str, int base = 10) {
    if (base < 2 or base > 36) {
        throw std::invalid_argument(std::format("base {} is not between 2 and 36", base));
    }
    uint64_t number = 0;
    for (char& c : str) {
        int digit = c;
        if (digit < 58) {
            digit -= 48;
        }
        else if (digit < 91)
        {
            digit -= 55;
        }
        else {
            digit -= 87;
        }
        if (digit < 0 or digit >= base) {
            throw std::invalid_argument(std::format("input string is not a valid integer in base {}", base));
        }
        number = number * base + digit;
    }
    return number;
}


std::string base_repr(uint64_t number, int base) {
    if (base < 2 or base > 36) {
        throw std::invalid_argument(std::format("input base {} is between 2 to 36", base));
    }
    std::string repr = "";
    int digit;
    while (number) {
        uint64_t quotient = number / base;
        digit = number - quotient * base;
        number = quotient;
        if (digit < 10) {
            digit += 48;
        }
        else {
            digit += 87;
        }
        repr = char(digit) + repr;
    }
    return repr;
}


std::string execute_command(std::string command) {
    std::vector<std::string> fields = split_string(command, " ");
    int size = fields.size();
    if (size != 3) {
        throw std::invalid_argument("command must have three fields");
    }
    std::string option = fields[1];
    int base = interpret_base(fields[2]);
    std::string sign = "";
    std::string numeral = fields[0];
    char first = numeral[0];
    if (first == '-' or first == '+') {
        if (first == '-') {
            sign = "-";
        }
        numeral = numeral.substr(1, numeral.size() - 1);
    }
    if (option == "from") {
        return sign + std::to_string(interpret_base(numeral, base));
    }
    else if (option == "to") {
        uint64_t number = interpret_base(numeral);
        return sign + base_repr(number, base);
    }
    else {
        throw std::invalid_argument("option is not supported");
    }
}


int main() {
    std::string input;
    std::cout << "input a numeral, operation and base (use space as delimiter)\n";
    while (true) {
        std::cout << ">>> ";
        std::getline(std::cin, input);
        if (input == "q") { 
            break; 
        }
        std::cout << execute_command(input) + '\n';
    }
    return 0;
}

Test

PS C:\Users\Xeni> C:\Users\Xeni\source\repos\number_converter\x64\Release\number_converter.exe
input a numeral, operation and base (use space as delimiter)
>>> -123456789 to 36
-21i3v9
>>> -21i3v9 from 36
-123456789
>>> stirlingite from 36
105370617716134466
>>> 105370617716134466 to 36
stirlingite
>>> 242516 to 36
574k
>>> 574k from 36
242516
>>> 75bcd15 from 16
123456789
>>> 123456789 to 16
75bcd15
>>> q
PS C:\Users\Xeni>

How can it be improved?


I now use unsigned 64-bit integer for maximum representable integer range, I just add the sign as a string. And this is as far as I can go.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Interface

The limit of base to maximum 36 is quite reasonable, as is the requirement that it be provided in decimal.

I find it strange that we only convert from and to decimal (i.e. one of input and output must be base-10). For an arbitrary converter, I would expect to be able to specify input radix and output radix, and not need to use decimal as an intermediate. It might also be useful to specify the input and output radices as command-line arguments, thus allowing the user to stream a whole series of values for conversion.

Instead of having to enter q as the last input, we should also stop processing when we reach end-of-file on input. Again, this makes it easier to use if we're streaming a series of numbers from some other process.


Implementation

A 64-bit integer is not necessarily the "maximum representable integer range" - <cstdint> provides std::intmax_t for that purpose.

Restricting to systems that use ASCII as character encoding isn't necessary (and even if it were, then using character constants such as '0' is much clearer to readers than bare numbers (48).

We can convert independently of character coding:

#include <cctype>
#include <string_view>
        static const std::string_view all_digits{"0123456789abcdefghijklmnopqrstuvwxyz"};
        auto n = all_digits.find(std::tolower(static_cast<unsigned char>(c)));
        if (n == std::string_view::npos or n >= base) {
            throw std::invalid_argument{std::format("{}  is not a valid digit in base {}",
                                                    c, base)};
        }
        number = number * base + n;

(There are faster methods - e.g. using an unsigned char sized lookup table, but this serves to demonstrate the point, and speed isn't a big concern at this stage).

We throw exceptions from various places, but never catch any. That leads to a poor user experience; depending on platform that might include a selection of diagnostics, perhaps also a stack trace, as well as exiting with a failure status. Consider handling recoverable errors and proceeding to the next line of input where possible, and exit only when we can't recover (such as a stream error or EOF of the input).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.