patterncppMinor
Parsing command input with simple bimaps
Viewed 0 times
simplewithbimapsinputparsingcommand
Problem
I've been playing with a simple toyrobot exercise to brush up some of my forgotten language skills and was wondering if there was a nicer way to implement my main command parsing loop. I think the use of the two maps and the nested ternary operator is readable but would be interested in seeing other approaches.
#include "toyrobot.hpp"
#include
#include
#include
#include
int main () {
using namespace std;
map dirtoi = {{"NORTH", 0}, {"EAST", 1}, {"SOUTH", 2}, {"WEST", 3}};
map itodir = {{0, "NORTH"}, {1, "EAST"}, {2, "SOUTH"}, {3, "WEST"}};
regex place("^PLACE (\\d),(\\d),(NORTH|EAST|SOUTH|WEST)$");
Robot robot = Robot();
Option table = Some(Table(Point(0,0),Point(4,4)));
smatch m;
string l;
while (getline(cin, l))
{
l == "MOVE" ? robot.move() :
l == "LEFT" ? robot.left() :
l == "RIGHT" ? robot.right() :
l == "REPORT" ? robot.report(itodir) :
regex_match (l,m,place) ? robot.place(Point(stoi(m[1]), stoi(m[2]))
, dirtoi.at(m[3])/2.0, table) :
(void)0;
}
}Solution
Here are some things I saw and some suggestions for how you might improve your code.
Try not to repeat information
I don't much care for the two parallel maps because they have the same information but in two different places and in two different constructs. What you actually seem to need is a way to go from a
Note that is, in fact, a templated class so it could be used with
Use
Your
Separate parsing from actions
The code is actually attempting to do two different things: it is parsing a line from input and then calling some action based on what it finds. It's probably useful to separate those two a little more clearly. I'd probably use
By reusing the
I'm not happy about having the parallel constructs of
Use better variable names
Generally, single letter variable names other than
Use a
Because your parser is selecting some action based on the input, we can make that much more explicit (and easier to read and maintain) by using a
Try not to repeat information
I don't much care for the two parallel maps because they have the same information but in two different places and in two different constructs. What you actually seem to need is a way to go from a
std::string to an int and vice-versa. For that, I'd write a simple class that does this:template
class Bimap
{
public:
Bimap(std::initializer_list init) : names{init} {}
const T& operator[](int i) const { return names[i]; }
int operator[](const T& n) const {
for (unsigned i=0; i names;
};Note that is, in fact, a templated class so it could be used with
std::string or std::wstring or any other class that implements operator== and can be placed in a std::vector. The simple linear search is not as efficient as a std::map but it's probably just fine for short lists. Using it in main is just this line:const Bimap dir{{"NORTH", "EAST", "SOUTH", "WEST"}};Use
const where practicalYour
regex and std::map variables could and should be const. Separate parsing from actions
The code is actually attempting to do two different things: it is parsing a line from input and then calling some action based on what it finds. It's probably useful to separate those two a little more clearly. I'd probably use
flex and bison if the commands become more complex, but for now you can simply use a more complete regex. Here's one way to do that:const Bimap simplecmd{{"MOVE", "LEFT", "RIGHT", "REPORT"}};
enum scmd { MOVE, LEFT, RIGHT, REPORT };
const regex command("^(MOVE|LEFT|RIGHT|REPORT)|PLACE (\\d),(\\d),(NORTH|EAST|SOUTH|WEST)$");By reusing the
Bimap class defined above, we can change any command into an integer. By expressing all possible commands within a regex, we can simply ignore any line which doesn't match (or perhaps print an error message). I'm not happy about having the parallel constructs of
simplecmd and the enum scmd but couldn't think of an easy way to avoid it in this case. We can rationalize it by saying that if one wished to have, say, both French and English versions, only the simplecmd would need to changed and not the enum.Use better variable names
Generally, single letter variable names other than
i and j for loop variables, are best avoided in favor of longer names. The variable m is not too terrible, but the variable l is definitely a poor choice, not least because it's easily mistaken for the digit 1 or the letter i. I changed it to line in the rewrite I did.Use a
switch statement where appropriateBecause your parser is selecting some action based on the input, we can make that much more explicit (and easier to read and maintain) by using a
switch statement instead of the chained ternary operator. Here's one way to do it, incorporating all of the other suggestions:smatch m;
string line;
while (getline(cin, line))
{
if (regex_match(line, m, command)) {
switch(simplecmd[m[1]]) {
case MOVE:
robot.move();
break;
case LEFT:
robot.left();
break;
case RIGHT:
robot.right();
break;
case REPORT:
robot.report();
break;
default: // must be PLACE
robot.place(Point(std::stoi(m[2]), std::stoi(m[3])), dir[m[4]], table);
}
}
}Code Snippets
template <class T>
class Bimap
{
public:
Bimap(std::initializer_list<T> init) : names{init} {}
const T& operator[](int i) const { return names[i]; }
int operator[](const T& n) const {
for (unsigned i=0; i < names.size(); ++i) {
if (names[i] == n)
return i;
}
return -1;
}
private:
std::vector<T> names;
};const Bimap<string> dir{{"NORTH", "EAST", "SOUTH", "WEST"}};const Bimap<string> simplecmd{{"MOVE", "LEFT", "RIGHT", "REPORT"}};
enum scmd { MOVE, LEFT, RIGHT, REPORT };
const regex command("^(MOVE|LEFT|RIGHT|REPORT)|PLACE (\\d),(\\d),(NORTH|EAST|SOUTH|WEST)$");smatch m;
string line;
while (getline(cin, line))
{
if (regex_match(line, m, command)) {
switch(simplecmd[m[1]]) {
case MOVE:
robot.move();
break;
case LEFT:
robot.left();
break;
case RIGHT:
robot.right();
break;
case REPORT:
robot.report();
break;
default: // must be PLACE
robot.place(Point(std::stoi(m[2]), std::stoi(m[3])), dir[m[4]], table);
}
}
}Context
StackExchange Code Review Q#92710, answer score: 3
Revisions (0)
No revisions yet.