Posted inInformation Technology / Thank You Sir May I Have Another

Roman Numeral Calculator

Math slide calculator

If you work long enough in IT you will start to really get tired of all the cutesy ways companies try to “select” developers/consultants. Some of the worst ones send people to take on-line tests from commercial sites at the candidates own expense. I don’t see that much on reqs anymore. Why? Because only the most desperate of bottom feeders would do it. In truth the candidates were a good match because only a bottom feeding company would ask a programmer to take an on-line test if they had other developers using the same language.

A trend I’m seeing a lot of now is the “spend a day with us” thing. They usually have you spend a couple of hours coding a solution to one of the on-line problems. One of the most often used is the Roman Number Calculator. There are many of these on-line. Since today was a frustrating day and I get tired of seeing that, here is the code for one using Qt which was hacked out in roughly the 2 hour time. Hard to tell how long because I had multiple pimps calling me trying to tell me illegal alien wages really were market rate.

The Code

I didn’t put much in the way of comments nor did I get really fancy. I actually chose to code it because I hadn’t done a console app in a long time.

#include <QCoreApplication>
#include <QDebug>
#include <QTimer>

#include "consoleinput.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    ConsoleInput console;

    bool b = a.connect(&console, &ConsoleInput::quit, &a, &QCoreApplication::quit);
    qDebug() << "result of connect " << b;

    QTimer::singleShot(0, &console, &ConsoleInput::run);

    return a.exec();
}

What is interesting in our main.cpp is the QTimer line. You cannot cleanly exit unless you are running inside of the event loop. It is one we all forget and can cost us lots of hours.

// consoleinput.h
#ifndef CONSOLEINPUT_H
#define CONSOLEINPUT_H

#include <QObject>

class ConsoleInput : public QObject
{
    Q_OBJECT
public:
    explicit ConsoleInput(QObject *parent = 0);

signals:
    void quit();

public slots:
    void run();
};

#endif // CONSOLEINPUT_H
// consoleinput.cpp

#include "consoleinput.h"
#include "parseromannumbertext.h"
#include <QDebug>
#include <QTextStream>
#include <QFile>
#include <QStringList>
#include <iostream>

ConsoleInput::ConsoleInput(QObject *parent) : QObject(parent)
{

}


void ConsoleInput::run()
{
    ParseRomanNumberText p;
    qDebug() << " 24 : " << p.toString(24);
    qDebug() << " 39 : " << p.toString(39);
    qDebug() << " 49 : " << p.toString(49);
    qDebug() << " 449 : " << p.toString(449);
    qDebug() << " 999 : " << p.toString(999);

    qDebug() << " toInt() tests ";
    qDebug() << "  XXIV : " << p.toInt("XXIV");
    qDebug() << "  XXXIX : " << p.toInt("XXXIX");
    qDebug() << "  XLIX : " << p.toInt("XLIX");
    qDebug() << "  CDXLIX : " << p.toInt("CDXLIX");
    qDebug() << "  CMXCIX : " << p.toInt("CMXCIX");

    qDebug() << "  CC13 : " << p.toInt("CC13");

    bool endOfJob = false;
    QTextStream in(stdin);
    qDebug() << "type exit to quit";
    while (!in.atEnd()  &&  !endOfJob)
    {
        QString line = in.readLine();
        line = line.toUpper();
        if (line.compare("EXIT", Qt::CaseInsensitive) == 0)
        {
            endOfJob = true;
            continue;
        }
        else
        {
            QStringList lst;

            if (line.indexOf('+') > -1)
            {
                lst << line.left(line.indexOf('+')) << "+" << line.right(line.indexOf('+')-1);  // + has special meaning to input stream
            }
            else if (line.indexOf('-') > -1)
            {
                lst << line.left(line.indexOf('-')) << "-" << line.right(line.indexOf('-'));
            }
            else if (line.indexOf('*') > -1 )
            {
                lst << line.left(line.indexOf('*')) << "*" << line.right(line.indexOf('*')-1);  // * has special meaning to input stream
            }
            else if (line.indexOf('/') > -1)
            {
                lst << line.left(line.indexOf('/')) << "/" << line.right(line.indexOf('/'));
            }

            if (lst.size() > 0)
            {
                if (p.isValidRomanNumber(lst[0]) && p.isValidRomanNumber(lst[2]))
                {
                    int x = p.toInt( lst[0]);
                    int y = p.toInt(lst[2]);
                    int result=0;
                    if (lst[1].compare("+") == 0)
                    {
                        result = x + y;
                    }
                    else if (lst[1].compare("-") == 0)
                    {
                        result = x - y;
                    }
                    else if (lst[1].compare("*") == 0)
                    {
                        result = x * y;
                    }
                    else if (lst[1].compare("/") == 0)
                    {
                        result = x / y;
                    }

                    qDebug() << line << " = " << p.toString(result);
                }
                else
                {
                    qDebug() << "both values must be valid roman numbers";
                }
            }
        }
    }

    emit quit();
}

The only lines of note here are the parsing of + and * operations. Those appear to be special characters for this particular input method as they get doubled in the actual stream. Yes, I should know why and I used to but, I’m certain some little geek will chime in. All you really have to know is there be dragons and move on. Why there are dragons doesn’t really matter, just know the names of the dragons and avoid them.

// parseromannumbertext.h

#ifndef PARSEROMANNUMBERTEXT_H
#define PARSEROMANNUMBERTEXT_H

#include <QObject>
#include <QMap>

class ParseRomanNumberText : public QObject
{
    Q_OBJECT
public:
    explicit ParseRomanNumberText(QObject *parent = 0);

    int toInt( QString romanNumberText);
    QString toString( int value);
    bool isValidRomanNumber(QString romanNumberText);


signals:

public slots:
private:
    QMap<QChar, int> m_letterMap;
};

#endif // PARSEROMANNUMBERTEXT_H

The closest I came to cute here was using a QMap to translate roman numbers to numeric values.

// parseromannumbertext.cpp

#include "parseromannumbertext.h"
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QDebug>
#include <QByteArray>

ParseRomanNumberText::ParseRomanNumberText(QObject *parent) : QObject(parent)
{
    m_letterMap['I']    = 1;
    m_letterMap['V']    = 5;
    m_letterMap['X']    = 10;
    m_letterMap['L']    = 50;
    m_letterMap['C']    = 100;
    m_letterMap['D']    = 500;
    m_letterMap['M']    = 1000;
}

int ParseRomanNumberText::toInt(QString romanNumberText)
{
    int retVal = 0;

    QString workStr = romanNumberText.toUpper();
    if (isValidRomanNumber(workStr))
    {
        // Roman numbers must be in ever decreasing value.
        // If the digit following the current digit is higher in value
        // then it is subtracted from the next digit.
        QByteArray letters = workStr.toLatin1();

        int x=0;
        while( x < letters.size())
        {
            bool isSpecial = false;
            int y = x+1;
            if (y < letters.size())
            {
                if (m_letterMap[ QChar(letters[x])] < m_letterMap[ QChar(letters[y])])
                {
                    // Handle case of 9 and 4 here
                    isSpecial = true;
                    retVal += ( m_letterMap[ QChar(letters[y])] - m_letterMap[ QChar(letters[x])]);
                    ++x;  // consume second letter
                }
            }

            if (!isSpecial)
            {
                retVal += m_letterMap[ QChar(letters[x])];
            }
            ++x;
        }

    }
    else
    {
        qDebug() << " Number: " << romanNumberText << " is not valid";
    }
    return retVal;
}

QString ParseRomanNumberText::toString(int value)
{
    QString retVal = "invalid number";
    if (value > 4000)
    {
        return retVal;
    }

    int workValue = value;

    int mCount = workValue / m_letterMap['M'];

    workValue -= (mCount * m_letterMap['M']);

    int cCount = workValue / m_letterMap['C'];

    workValue -= (cCount * m_letterMap['C']);

    int xCount = workValue / m_letterMap['X'];

    workValue -= (xCount * m_letterMap['X']);

    int iCount = workValue;


    QString mString( mCount, 'M');


    QString cString;

    switch(cCount)
    {
    case 4:
        cString = "CD";  // 400
        break;
    case 5:
        cString = "D";
        break;
    case 6:
        cString = "DC";
    case 7:
        cString = "DCC";
        break;
    case 8:
        cString = "DCCC";
        break;
    case 9:
        cString = "CM"; // 900
        break;
    default:
        cString = QString(cCount, 'C');
        break;
    }



    QString xString;

    switch(xCount)
    {
    case 4:
        xString = "XL";     // 40
        break;
    case 5:
        xString = "L";      // 50
    case 6:
        xString = "LX";
        break;
    case 7:
        xString = "LXX";
        break;
    case 8:
        xString = "LXXX";
        break;
    case 9:
        xString = "XC";
        break;
    default:
        xString = QString(xCount, 'X');
        break;
    }

    QString iString;
    switch(iCount)
    {
    case 4:
        iString = "IV";
        break;
    case 5:
        iString = "V";
        break;
    case 6:
        iString = "VI";
        break;
    case 7:
        iString = "VII";
        break;
    case 8:
        iString = "VII";
        break;
    case 9:
        iString = "IX";
        break;
    default:
        iString = QString(iCount, 'I');
        break;
    }

    retVal = "";
    QTextStream stream(&retVal);

    stream << mString << cString << xString << iString;

    return retVal;

}

bool ParseRomanNumberText::isValidRomanNumber(QString romanNumberText)
{
    bool retVal = false;

    QString workStr = romanNumberText;

    workStr.remove('M');
    workStr.remove('D');
    workStr.remove('C');
    workStr.remove('L');
    workStr.remove('X');
    workStr.remove('V');
    workStr.remove('I');

    if (workStr.length() < 1)
    {
        retVal = true;
    }
    return retVal;
}

I burned a lot of time yicking around with regular expressions for the isValidRomanNumber() routine. I really hate regular expressions and mostly wish they would be banned from the programming world. Few more cryptic and undocumented strings ever get created in any program. This is especially true for the 12 year old boys trapped in older bodies who claim

If the code is properly written it does not need documentation.

If you ever utter that you are a loser who should not be allowed anywhere near a keyboard. You write worthless throw away code which will never be maintainable. Anyone who utters that phrase does not have production code which has been in place for 10+ years through business changes. I have code which has been in place for multiple decades at client sites and it has went through multiple sets of sweeping changes due to new input sources combined with business rules changes. It has a ton of comments in it and I still talk with the people who maintain it. If you follow the above philosophy programmers around the world will universally curse your name and pray to various deities that you were born sterile.

The QMap doesn’t serve much purpose in the toString() method but it sure does in toInt(). Yes, I could have been cuter and not had the switches, but this is something which is supposed to be done within a couple of hours.

The stumbling block for most people, myself included, is that we forget the 5’s are Red Herrings, hence my hack like switches. I burned a bit of time forgetting that piece of wisdom and trying to be cute. The basic trick is to divide by the 10’s and let the 5’s fall where they may.

You really do have to have some kind of hack for the nines though.

A really ingenious little geek would take this example and change the switches to make use of the QMap. Think about it. You just need a version of the QMap which is indexed by value not letter and you could do some _crazy_ stuff generating the strings.

So, there you go. Take it, clean it up, rename everything in the editor and, most importantly do the other QMap for string creation. You will be ready to skip through a test you really shouldn’t have to take.