CSc 220 Assignment 4

This Unit Is Engaged

Assigned
Due

Oct 22
90 pts
Nov 6
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:
  1. A command which attempts to define a dimension or unit which is already defined, report the error and take no other action.
  2. When a command uses a unit which has not been defined, report the error and take no other action.
  3. 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.