c++stringif-statement

Way to reduce else if statements when using string as condition


I'm making a terminal-like program (to calculate currency) with custom commands as input but I have a problem.

Every time I implement a new command, I have to add a new else if statement. This wouldn't be a problem but for a terminal-like program there can be a lot of commands.

Here is my code:

#include <iostream>
#include <string>
#include <Windows.h>
#include <math.h>

float user_balance = 0.0f;
float eur_in_czk = 24.0f;   //amount of czk in single euro
std::string currency = "czk";
bool czk_to_eur_enabled = true;
bool eur_to_czk_enabled = false;

//------------------START method definition---------------------------------------------------------
void czk_to_eur()
{
    if (czk_to_eur_enabled) //to prevent using twice in a row
    {
        user_balance /= eur_in_czk;
        user_balance = floorf(user_balance * 100) / 100;    //limit to two decimal numbers
        currency = "eur";
        czk_to_eur_enabled = false;
        eur_to_czk_enabled = true;
    }
    else
    {
        std::cout << "Your savings are already converted to  " << currency << "!" << std::endl;
    }
}

void eur_to_czk()
{
    if (eur_to_czk_enabled) //to prevent using twice in a row
    {
        user_balance *= eur_in_czk;
        user_balance = floorf(user_balance * 100) / 100;    //limit to two decimal numbers
        currency = "czk";
        eur_to_czk_enabled = false;
        czk_to_eur_enabled = true;
    }
    else
    {
        std::cout << "Your savings are already converted to " << currency << "!" << std::endl;
    }
}

void set_balance(float new_balance)
{
    user_balance = new_balance;
}

void add_balance(float new_balance)
{
    user_balance += new_balance;
}
//------------------END method definition-----------------------------------------------------------


int main()
{
    bool main_loop = true;  //main loop enabler
    float input_money;
    std::string user_command = "";

    std::cout << "This is currency converter v1.0 (czk to eur and back)\n\n\n" << std::endl;

    while (main_loop)   //main loop for currency converter
    {
        std::cout << "Input: ";
        std::cin >> user_command;
        std::cout << std::endl;

        if ((user_command == "setbal") || (user_command == "SETBAL"))
        {
            std::cout << "Your balance is " << user_balance << " " << currency << ".\n";
            std::cout << "Please enter desired value (" << currency << "): ";
            std::cin >> input_money;
            set_balance(input_money);
            std::cout << "\n" << std::endl;
        }
        else if ((user_command == "addbal") || (user_command == "ADDBAL"))
        {
            std::cout << "Your balance is " << user_balance << " " << currency << ".\n";
            std::cout << "Please enter desired value (" << currency << "): ";
            std::cin >> input_money;
            add_balance(input_money);
            std::cout << "\n" << std::endl;
        }
        else if ((user_command == "balance") || (user_command == "BALANCE"))
        {
            std::cout << "Your balance is " << user_balance << " " << currency << "." << std::endl;
        }
        else if ((user_command == "curstat") || (user_command == "CURSTAT"))
        {
            std::cout << "Currency status is " << eur_in_czk << " czk in 1 eur." << std::endl;
        }
        else if ((user_command == "toeur") || (user_command == "TOEUR"))
        {
            czk_to_eur();
        }
        else if ((user_command == "toczk") || (user_command == "TOCZK"))
        {
            eur_to_czk();
        }
        else if ((user_command == "cheuv") || (user_command == "CHEUV"))
        {
            std::cout << "Change eur value (" << eur_in_czk << "): ";
            std::cin >> eur_in_czk;
            std::cout << std::endl;
        }
        else if ((user_command == "help") || (user_command == "HELP"))
        {
            std::cout << "SETBAL        Sets balance.\n"
                      << "ADDBAL        Adds balance.\n"
                      << "BALANCE       Shows current balance.\n"
                      << "CURSTAT       Shows currency status.\n"
                      << "TOEUR         Converts czk to eur.\n"
                      << "TOCZK         Converts eur to czk.\n"
                      << "CHEUV         Changes eur currency value.\n"
                      << "CLS           Cleans terminal history.\n"
                      << "EXIT          Exits program.\n" << std::endl;
        }
        else if ((user_command == "cls") || (user_command == "CLS"))
        {
            system("CLS");  //funtion from Windows.h library
        }
        else if ((user_command == "exit") || (user_command == "EXIT"))
        {
            main_loop = false;
        }
        else
        {
            std::cout << "'" << user_command << "'"
                      << "is not recognized as an internal or external command!\n";
            std::cout << "Type 'HELP' to see available commands.\n" << std::endl;
        }
    }

    return 0;
}

The bottom part of the code in while cycle is where the problem is.

Everything works fine but I would like to know, if there is any other way. And switch to my knowledge does not support string values as condition/dependency. (also I'm currently not using any custom classes and/or custom header files because this is just experiment.)

Is there any other way to do it?


Solution

  • Normally I would suggest using a std::map with a string as the key and a function as the value so that you could search the map for a command and then invoke the function associated with it. However, since that's already been mentioned in the comments I figured I'd get all fancy and provide a totally wack solution you probably shouldn't use.

    This wack solution allows you to use string literals in a switch/case statement. This is possible by taking advantage of a feature of modern C++ called user defined literals that allow you to produce objects of user-defined type by defining a user-defined suffix much in the same way you append U to a integer literal to specify an unsigned value.

    The first thing we'll do is define a user defined literal that produces a hash value that is calculated at compile time. Since this generates a hash value from the string it is possible to encounter collisions but that's dependant on the quality of the hash algorithm used. For our example we're going to use something simple. This following snippet defines a string literal with the suffix _C that generates our hash.

    constexpr uint32_t djb2Hash(const char* str, int index = 0)
    {
        return !str[index]
            ? 0x1505
            : (djb2Hash(str, index + 1) * 0x21) ^ str[index];
    }
    
    // Create a literal type for short-hand case strings
    constexpr uint32_t operator"" _C(const char str[], size_t /*size*/)
    {
        return djb2Hash(str);
    }
    

    Now every time the compiler sees a string literal in the format of "Hello World"_C it will produce a hash value and use that in place of the string.

    Now we'll apply this to your existing code. First we'll separate the code that takes the user command from cin and make the given command all lower case.

    std::string get_command()
    {
        std::cout << "Input: ";
    
        std::string user_command;
        std::cin >> user_command;
        std::cout << std::endl;
    
        std::transform(
            user_command.begin(),
            user_command.end(),
            user_command.begin(),
            [](char ch) { return static_cast<char>(std::tolower(ch)); });
    
        return user_command;
    }
    

    There now that we can get an all lowercase command from the user we need to process that so we'll take your original set of if/else statements and turn them into a simple switch/case statement instead. Now since we can't actually use string literals in the switch/case statement we'll have to fudge a little bit and generate the hash value of the users command for the switch part of the code. We'll also take all of your commands and add the _C suffix to them so that the compiler automatically generates our hash values for us.

    int main()
    {
        bool main_loop = true;  //main loop enabler
    
        std::cout << "This is currency converter v1.0 (czk to eur and back)\n\n\n" << std::endl;
    
        while (main_loop)   //main loop for currency converter
        {
            const auto user_command(get_command());
            switch(djb2Hash(user_command.c_str()))
            {
            case "setbal"_C:
                std::cout << "Set balance command\n";
                break;
    
            case "addbal"_C:
                std::cout << "Add balance command\n";
                break;
    
            case "balance"_C:
                std::cout << "Get balance command\n";
                break;
    
            case "curstat"_C:
                std::cout << "Get current status command\n";
                break;
    
            case "help"_C:
                std::cout << "Get help command\n";
                break;
    
            case "exit"_C:
                main_loop = false;
                break;
    
            default:
                std::cout
                    << "'" << user_command << "'"
                    << "is not recognized as an internal or external command!\n"
                    << "Type 'HELP' to see available commands.\n" << std::endl;
            }
        }
    }
    

    And there you have it. A totally wack solution! Now keep in mind that we're not really using strings in the switch/case statement, we're just hiding most of the details of generating hash values which are then used.