views:

494

answers:

6

I'm toying with an application that is, roughly speaking, a sort of modeler application for the building industry. In the future I'd like it to be possible for the user to use both SI units and imperial. From what I understand, it's customary in the US building industry to use fractions of inches when specifying measurements, eg 3 1/2" - whereas in SI we'd write 3.5, not 3 1/2. I'm looking for a way to work with these different systems in my software - storing them, doing calculations on them etc, not only parsing what a users enters. It should be able to show the user a measurement in the way he entered it, yet being able to calculate with other measurements - for example add 3 cm to 1 1/2 inch. So if a user draws a length of wall of 5 feet and another one of 3 meters, the total measurement should be shown in the default unit system the user selected.

I'm undecided yet on how much flexibility I should add for entering data for the user; e.g. if he enters 1 foot 14 inches, should it should 2 feet 2 inches the next time the measurement is shown? However before I decide things like that, I'm looking for a way to store measurements in an exact form, which is what my question is about.

I'm using C++ and I've looked at Boost.Units, but that doesn't seem to offer a way to deal with fractions.

The simple option is to convert everything to millimeters, but rounding errors would make it impossible to go back to the exact measurement a user entered (if he entered it in imperial measurements). So I'll need something more complex.

For now I'm using a class that is tentatively named 'Distance' and looks conceptually like this:

class Distance
{
public:
    Distance(double value);
    // operators +, -, *, /
    Distance operator+(const Distance& that);
    ...etc...

    std::string StringForm(); // Returns a textual form of the value

    Distance operator=(double value);

private:
    <question: what should go here?>
}

This clearly shows where my problems are. The most obvious thing to do would be to have an enum that says whether this Distance is storing SI or imperial units, and have fields (doubles, presumably) that store the meters, centimeters and millimeters if it's in SI units and feet and inches if it's imperial. However this will make the implementation of the class littered with if(SI) else ..., and is very wasteful in memory. Plus I'd have to store a numerator and denominator for the feet and inches to be able to exactly store 1/3", for example.

So I'm looking for general design advice on how I should solve these problems, given my design requirements. Of course if there's a C++ library out there that already does these things, or a library in another language I could look at to copy concepts from, that would be great.

+1  A: 

You are right that converting all to one measurement type will give the user annoying rounding errors.

You should make a virtual baseclass that declares the operations.

You should have a concrete child class for each measurement system you want to implement : eg metric, US imperial, ancient roman. Internally this can store them in the most appropriate and accurate format - e.g. fractions of an inch for imperial.

You will need a string output mechanism for each child class (measurement system).

You should have a factory to turn string representations into instances of the appropriate class.

You will need to implement operations (such as add) for each child class, preserving the type, so an imperial + an imperial make an imperial.

You will need to implement cross type operations, and decide what you want to have happen if you add mm and inches. Should it output a metric or imperial class?

-Alex

Alex Brown
+5  A: 

I would definitely consider adding a Units property to the distance class. You could then overload the +, -, *, / (and related) operators so that arithmetic operations on distances is only possible when the units are the same type.

Personally, I would normalize all measurements into the lowest unit of measurement you will support in each system (eg. millimeters for SI, inches for imperial) but also store the users' entered representation. Perform all calculation in normalized form, but convert back to a more readable form when presenting to users.

You should also consider making instances of Distance immutable - and creating a new Distance whenever an arithmetic operation is performed.

Finally, you can create helper methods to convert between different units - and potentially even call these internally when performing arithmetic on distances with different units. Just convert everything to a common unit and then perform the calculation.

Personally, I would not go the route of creating multiple types for measurements in each system - I think you are better off consolidating the logic and allowing your system to treat measurements polymorphically.

LBushkin
", I would normalize all measurements into the lowest unit of measurement you will support in each system (eg. millimeters for SI, inches for imperial) but also store the users' entered representation." This. You should only be converting at input and output, but don't mix units across system boundaries unless the user does this to you.
dmckee
What would be the advantage of making it immutable? I've been using CGAL for geometric computations in my application, and its 3D point class is immutable - I find it's a PITA to use, having to construct a new instance when all I want to do is add 1 to the x-axis value, for example. Rather than doint point.x += 1 I have to call the constructor with the y and z values of the original point, which is much more to type and more importantly a lot harder to read (i.e., you don't see easily what is happening). Just wondering what the rationale for such a design is.
Roel
@Roel: The advantage of making a measurement class immutable is that it *is* immutable. It is kind of expected that if you change the value of it in one place, it won't affect it in other places. The easiest way to prevent bugs of this nature is for it to be immutable. The extra pain is well worth the reduced likelihood of bugs, considering that most programmers' mental model of a measurement is that it is immutable (5 feet is always 5 feet, but my list might have things added and removed).
Brian
+1  A: 

Take a look at Martin Fowler's Money pattern from Patterns of Enterprise Application Architecture - it is directly applicable to this situation. Recommended reading. Fowler has also posted a short writeup on his site of the Quantity pattern, a more generic version of Money.

Matt Howells
Thanks, this looks very applicable.
Roel
A: 

I would suggest the use of the Decimal type rather than the Double type. It's slower, but the speed difference won't affect you.

As for rounding concerns, just have a maximum number of digits you'll display but store the full number internally. Even if you store 1 foot as 0.305 meters (1.00065617 feet), the user won't know if you round it to the nearest 100ths when displaying.

In reply to Roel: You're just as likely to be low as you are to be high (and also, it's 0.006, not 0.06). But if you had 1000 of the same measurement in a row like this and were 0.06 feet off, it would be 100.06 feet instead of 100 feet. Which perhaps does matter to the building industry. However, I used far fewer significant digits than is actually provided by the Decimal datatype, since I was demonstrating the rounding issue rather than demonstrating the precision issue. In practice, I think the tolerances are such that they will be outdone by the innacurracy that is inherent in trying to build something (e.g. the calculation might be off by a tiny number smaller than 1 nm but the crane's tolerances are probably much worse than 100nm).

Brian
Except that when you have 100 of those in a row, the measurements are 0.06 feet off.
Roel
A: 

US measurement (Some folks call it "English" or "Imperial" units; we like to blame others for our mistakes) requires you have Rational number package to store inches.

For metric, you have several distinct units (cm, m, km, etc.) with simple relationships. Think of them as separate units with simple conversion factors. m to cm is a *100 conversion. You can easily enumerate all of the *100, *1000, *.1, *.01 conversion factors among all possible metric distance measures.

For English, the units (inches, feet, yards, etc.) also have simple relationships. They just don't fetishize over decimal. 12's and 3's are used instead of 10's. Think of them as separate units with simple conversion factors. ft to in is a *12 conversion. Again, you can easily enumerate all of the *12, *36, *(1/12), *(1/36) combinations.

When someone enters 3' 8", you can normalize that to inches and convert it back correctly. Even if they enter 3' 14", your conversion back to 4' 2" is correct and expected. In some cases, it's desired.

In some cases -- even in English notation -- there is a desired output unit which not be the original input unit. For example, someone may have a long list of measurements in feet and inches, but want a sum in decimal feet. When you purchase in bulk, you don't care that it's 25' 6 7/16"; 25.54' is a good answer. You're buying 26' of lumber, which often means 3 10' boards.

Turns out that this units conversion works for m to ft and back, also. You can enumerate every combination of in, ft, yd, mm, cm, m, km conversions. The input units are normalized to something short (cm, in) and converted on output to the desired units (m, ft, yd, etc.)

You can store 1356 inches. That's fine. You can display 113' or 37.66 yds, or 37 yds 2 ft. depending on the output units the user selects.

This scheme works for anything except temperature.

The only snag is fractions of an inch. Doing this right requires a Rational number package. What you want is a class hierarchy like this.

Distance
|
+---- Float (no fractions, everything but inches)
|
+---- Rational (fractions used for inches)

The Rational measurements are in user-supplied fractional notation. If your Rational class override all the operators correctly, it works the same as Floating-point measurements. If it provides appropriate functions to convert between int and float, you should be able to intermix the two and get reasonable answers without much RTTI.


Here's what's weird. Inches use powers of 2, and have exact floating point representations. No 24.000000001 or other artifacts of conversions. Metric uses powers of 10, so you get all kinds of silly-looking 24.00000001 and 23.99999997 answers for metric calculations.

S.Lott
A: 

I think NASA once lost a Mars probe this way.

“So if a user draws a length of wall of 5 feet and another one of 3 meters, the total measurement should be shown in … “

It is very hard to imagine what a user who does this expects to happen. Unless I was absolutely sure I knew why the user was doing this, and what then was expected to happen, I would simply disallow it. If SI units are selected, imperial units are disallowed, and visa versa. In most cases, I think this is the best way you can help the user and prevent an ever widening spread of confusion.

When using imperial units I would internally store distances in units of 1/32th of an inch, converting to and from feet, inches and fractions as reuired for output/ input. This will avoid rounding errors and all the nasty problems of converting from binary represented decimal values and fractions.

Not also that in the building trade a “2 by 4” is not actually 2 inches by 4 inches.

"But having a plan in metric and importing a Sketchup model in imperial isn't far-fetched. I agree that mixing measurement system is a bad idea, but I find it quite frustrating in existing packages that limit the choice to one or the other, especially when you can only choose one at the moment you're making a new file/plan."

Yes. Importing and converting the units of a file or library of components is a vital and important feature where a program can be very useful. This means converting all the measurements from one system to the other. Decisions have to be made on how to do this ( round up round down to the nearest 1/4" or 1/8" etc ) These decisions have to be implemented, enforced and possibly made into user options. It is complicated and hard to get right. The complexity and instability should therefore be confined to a separate module that does one thing: convert units in a file or set of files. The rest of the program should use consistent sets of units without converting from one to other in piecemeal fashion. IMHO

ravenspoint
OK, having 2 walls next to each other in different measurement systems may be a concocted example. But having a plan in metric and importing a Sketchup model in imperial isn't far-fetched.I agree that mixing measurement system is a bad idea, but I find it quite frustrating in existing packages that limit the choice to one or the other, especially when you can only choose one at the moment you're making a new file/plan.
Roel