This Unit Is Engaged
You are to write an interactive
program for doing unit conversion.
The program provides commands to declare dimensions and define
units in those dimensions and their relative sizes.
Once units are defined, the user may request conversions between the units.
For instance:
[tom@localhost units]$ ./units2
> dim length m
> dim time s
> unit km 1000 m
> unit cm 0.01 m
> unit in 2.54 cm
> unit ft 12 in
> unit min 60 s
> unit hr 60 min
> unit day 24 hr
> conv 10.5 m ft
10.5 m = 34.4488 ft
> conv 3 day min
3 day = 4320 min
> conv 30 km in
30 km = 1.1811e+06 in
> conv 4 hr ft
Cannot convert hr to ft.
> conv 1 sowsear silkpurse
Unit names sowsear and silkpurse must exist.
> quit
Commands
The program should prompt and read commands. The command description
refers to a “dimension,” which is intended to be something like distance,
time, mass, etc. Of course, the program knows nothing of the concept
behind a dimension; to the program it's just some string. Each unit has
some dimension string assigned to it, and the program simply refuses to
convert between units with different dimension strings attached.
Also, to keep things simple, the program does not know that a distance
cubed is a volume, and there's no way to tell it such things.
There are four commands:
dim dimension baseunit
The dim command specifies a dimension (such as
distance, weight, time, etc.) and some base unit appropriate for that
dimension. Any appropriate unit may be chosen. The dimension and
baseunit may each be any non-blank string. Both dimension and
baseunit are
being defined, and must not already exist in that same role.
unit newunit factor oldunit
The factor is a positive floating-point number, while newunit
and oldunit are any non-blank strings which serve as unit names.
The oldunit must be some
unit name which has already been defined, and newunit is being
defined and must not have been defined previously.
This statement defines newunit to be equal to factor
oldunits.
conv amount fromunits tounits
The amount is any floating point number, and fromunits and
tounits are non-blank unit names which have already been defined.
This statement requests the program to convert the given quantity of
amount fromunits into the equivalent quantity
of in tounits.
quit
Does.
If you wish,
you may also create a
help command to list the available commands.
Only the
conv command has specific output. You should also
issue error messages when something goes wrong. It is possible to
check the input quite closely, but you should check and report
at least the following errors:
- A command which attempts to define a dimension or
unit which is already defined, report
the error and take no other action.
- When a command uses a unit which has not been defined, report
the error and take no other action.
- When a convert command attempts to convert from one dimension to
a different one, report
the error and take no other action.
You are encouraged to check the input more carefully and report other
types of errors, but the above are the ones required.
Procedure
Data Structure
Create a class to represent a unit. It is intended that you
define this class in the same file and main and produce a
single-file program. If you wish to give the class it's own
.h file, you welcome to do so. But do it right.
The class holds the dimension name and double number which is the
relative size of unit. The base unit (whatever is created with the
dim command) has size 1.0, and all others are relative to
that. The object does not need to hold the name of the unit
because it is going to be stored as the data in a map, and the
name will be its key.
This will not be a large class, but it must
provide, at minimum, the following public methods. You may add additional
ones if you like, but you don't need to.
unit(string dimension)
A constructor, which creates a unit object belonging to dimension.
This is the base unit for dimension, and will have a size of 1.0.
You might expect this will
be used by the dim command.
unit(double scale, const unit &base )
Construct a new unit based on an existing one. The size of the
new unit will be the product of scale and the size of base.
That just means the newly-created unit is made of scale
of base, so is scale times as large. Don't forget
that scale may be a fraction.
Of course, this is used by the unit command.
u.dim()
Extract the dimension name from some unit object u. It is
a constant method and returns a string.
u1.conforms(const unit &u2)
Returns boolean true if and only if
unit object u1 is in the same dimension as u2.
This is a constant method.
u1.cvt_to(const unit &u2)
Returns a double which is the factor needed to convert a quantity
in unit u1 to a quantity in unit u2. This is simply
the ratio of the size of u1 to the size of u2. (For example, if
u1 is twice as large as u2, you'll need twice as many
u2 to make the equivalent amount, and this ratio
yields 2.0.)
unit()
You will probably also want to make a constructor which takes zero
arguments. Set the dimension to empty string, and the
size to 0.0. This creates a perfectly meaningless unit, but you
probably want it. It is not required. There's an
explanation of this near the bottom of this document.
The simplest way to keep the data is to make a string for the
dimension and a double for the length. There are fancier
possibilities. You don't need to record the dimension
name in the object, but you may if you wish. Add a field for it,
and a setter and getter, but please don't change the constructors.
You will need to
keep track of the dimensions and units which have
been defined. For the dimensions, I just made a
set of strings, which I
can update in the
dim command. For the units, use a map
from the dimension name to the object, perhaps like this:
std::map<std::string,unit> units;
(any variable name you like, of course). So, you can implement your
commands like this:
- For the dim command, check that the dimension is new,
and make an entry in the map under the unit name using the first
constructor.
- For the unit command, check the names for proper existence or not,
then make an entry in the map under the unit name using the second
constructor. When checking the map for existence, do not try to use
subscripting; use the find or count methods.
- For the conv command, look up the unit and run the cvt_to
method to get the factor, then multiply by input quantity to get the
result for printing.
Even though we are using objects, you should not use
the
new operator in this program. You can insert new objects into the
map with expressions like
units["lumen"] = unit("brightness");
or
units.insert( { "furlong", unit(600.0, units["ft"]) } );
No
new is involved.
Reading
The simplest way to read commands and parameters is directly from
cin. You could use a pattern something like this:
std::cout << "> ";
while( . . . ) {
string cmd;
std::cin >> cmd;
if(cin == "dim") {
std::ustring dim, unit;
cin >> dim >> unit;
... Handle "dim" ...
} else if(cin == "unit") {
. . .
}
cout << "> ";
}
This can get ugly if the user omits a parameter, since C++
will just keep on reading to the next line and might eat the next command.
This is acceptable, since it works fine when the input is good, and i'm
not to interested in you spending time chasing I/O details.
But, if you want something that always keeps a command on one line,
even when the user messes up, you could use something like this:
std::string line;
std::cout << "> ";
while(getline(std::cin, line)) {
std::istringstream linereader(line);
string cmd;
linereader >> cmd;
if(cin == "dim") {
string dim, unit;
linereader >> dim >> unit;
... Handle "dim" ...
} else if(cin == "unit") {
. . .
}
cout << "> ";
}
This uses the outer loop to read by full lines, then creates
an
istringstream which re-reads the line as another stream.
This requires
#include <sstream>.
You may have used a similar trick in Java with an outer loop reading lines
and the body creating another
Scanner from the string read in.
Unit Construction and Map
The description of class unit above suggests creating
a constructor with zero parameters, even though it creates a useless
object. A no-argument constructor is called a “default constructor.”
The standard map object is implemented such that the subscript operation
cannot be used when the data type does not have a default constructor.
When a map is subscripted with a key that does not exist, the subscript
operation adds the item to the map, creating it with its default
constructor, since it does not know what parameters to use.
Even when the subscript is on the left of an assignment,
if the key is not already present, it must be created before the
item on the right side is copied over. Consequently, subscript
expressions will not compile when the value type has no default constructor.
To solve without subscripting,
use .at() when the key is known to exist, and .insert() to
add new items to the map. Or just
make life easier with a bogus default constructor. Your choice.
Submission
When your program works, is well-commented, and nicely indented,
submit over the web
here.