3
\$\begingroup\$

I'm trying to create some helper functions that will allow me to convert from one unit of measurement to another. I eventually will need to be able to do speeds and temperatures as well, but so far, I've only implemented lengths. I'm not using C++11 for this solution.

Here are my structs. My structs are basically wrappers around enums for type safety.

namespace MyNS
{
    struct MetricValue {
        enum MetricPrefixType
        {
            yocto = -24,
            zepto = -21,
            atto = -18,
            femto = -15,
            pico = -12,
            nano = -9,
            micro = -6,
            milli = -3,
            centi = -2,
            deci = -1,
            base = 0,
            deca = 1,
            hecto = 2,
            kilo = 3,
            mega = 6,
            giga = 9,
            tera = 12,
            peta = 15,
            exa = 18,
            zetta = 21,
            yotta = 24
        } type ;

        MetricValue (MetricPrefixType type = MetricPrefixType::base) : type (type)
        {
        }
    };

    struct ImperialLengthValue {
        enum ImperialLengthType
        {
            inch = 1,
            foot = 12,
            yard = 36,
            mile = 63360
        } type ;

        ImperialLengthValue (ImperialLengthType type = ImperialLengthType::inch) : type (type)
        {
        }
    };

} // end of namespace MyNS

Here are my functions. abs() and pow() are helper functions. My implementation of pow() is meant to handle negative powers properly.

namespace MyNS {
    template <typename T>
    T abs (T tVal) {
        return (tVal > 0) ? tVal : -tVal ;
    }

    double pow (double base, int power)
    {
        if (power == 0) {
            return 1 ;
        }

        if (power == 1) {
            return base ;
        }

        double dVal = base ;
        int absPower = MyNS::abs (power) ;

        for (int n = 1; n < absPower; ++n) {
            dVal *= base ;
        }

        if (power < 0) {
            dVal = 1 / dVal ;
        }

        return dVal ;
    }

    template <typename T>
    T ConvertLength (T tVal, MetricValue from, MetricValue to)
    {
        return static_cast <T> (tVal * MyNS::pow (10, from.type - to.type)) ;
    }

    template <typename T>
    T ConvertLength (T tVal, MetricValue from, ImperialLengthValue to)
    {
        // Convert to meters, then to inches, then to imperial type.
        return static_cast <T> (tVal * MyNS::pow (10, from.type) * 39.3701 / to.type) ;
    }

    template <typename T>
    T ConvertLength (T tVal, ImperialLengthValue from, MetricValue to)
    {
        // Convert to inches, then to meters, then to metric type.
        return static_cast <T> (tVal * from.type * 0.0254 / MyNS::pow (10, to.type)) ;
    }

    template <typename T>
    T ConvertLength (T tVal, ImperialLengthValue from, ImperialLengthValue to)
    {
        return static_cast <T> (tVal * from.type / to.type) ;
    }

} // end of namespace MyNS

Here is some example usage:

int main (void)
{
    typedef MyNS::MetricValue MetricValue ;
    typedef MyNS::ImperialLengthValue ImperialValue ;

    double d1 = MyNS::ConvertLength <double> (18000000, MetricValue (MetricValue::micro), MetricValue (MetricValue::deca)) ;
    double d2 = MyNS::ConvertLength <double> (0.5, MetricValue (MetricValue::kilo), MetricValue (MetricValue::milli)) ;

    double d3 = MyNS::ConvertLength <double> (500, MetricValue (MetricValue::centi), ImperialValue (ImperialValue::foot)) ;
    double d4 = MyNS::ConvertLength <double> (25, MetricValue (MetricValue::kilo), ImperialValue (ImperialValue::yard)) ;

    double d5 = MyNS::ConvertLength <double> (15500, ImperialValue (ImperialValue::foot), MetricValue (MetricValue::kilo)) ;
    double d6 = MyNS::ConvertLength <double> (0.5, ImperialValue (ImperialValue::mile), MetricValue (MetricValue::deci)) ;

    double d7 = MyNS::ConvertLength <double> (63500, ImperialValue (ImperialValue::inch), ImperialValue (ImperialValue::mile)) ;
    double d8 = MyNS::ConvertLength <double> (3, ImperialValue (ImperialValue::yard), ImperialValue (ImperialValue::foot)) ;

    return 0 ;
}
\$\endgroup\$
2
  • \$\begingroup\$ @GregHewgill Unless I'm misunderstanding your comment, there would not be a "zeptomile". \$\endgroup\$
    – jliv902
    May 23, 2014 at 0:27
  • \$\begingroup\$ Related. \$\endgroup\$ May 23, 2014 at 1:02

2 Answers 2

1
\$\begingroup\$

I don't think you should have separate types for metric and imperial lengths. A length is a length; metric and imperial are ways to express that length (you could consider them to be different views on the underlying physical truth).

With just two length types, we need four functions to convert between them. What happens when we also want to use (for example) ancient Egyptian cubits, or American survey feet? When we have n measurement types, we'll need functions, which will quickly become unmanageable.

A good way to avoid this quadratic explosion is to choose one base unit that is used internally (I recommend metres), and to convert to and from that unit when interacting with users.

Instead of computing powers of ten every time they are needed, it's simpler to use a set of floating-point constants in place of the integer powers we're using now.


Specific things that could be improved:

  • MyNS::abs() could simply be replaced by std::abs() from <cmath>. Beware that it can have undefined behaviour with the most negative values, though this shouldn't be an issue where we use it.
  • MyNS::pow() could be implemented using binary exponentiation.

Suggested code

I'm just going to use double throughout, because we don't have template constants in C++03. In modern C++, we can use constexpr, and drop the constructor that's no longer required.

struct length
{
    double metres;

    // constructor not needed in modern C++, with aggregate initialisation
    explicit length(double m) : metres(m) {}

    length& operator+=(const length o) {
        metres += o.metres;
        return *this;
    }
    length& operator-=(const length o) {
        metres -= o.metres;
        return *this;
    }
    length& operator*=(const double o) {
        metres *= o;
        return *this;
    }
    length& operator/=(const double o) {
        metres /= o;
        return *this;
    }
    double operator/(const length o) const {
        return metres / o.metres;
    }
};

length operator+(length a, length b)
{
    return a += b;
}
length operator-(length a, length b)
{
    return a -= b;
}
length operator*(length a, double b)
{
    return a *= b;
}
length operator*(double a, length b)
{
    return b *= a;
}
length operator/(length a, double b)
{
    return a /= b;
}


namespace metric
{
    namespace prefix
    {
        static const double yocto = 1e-24;
        static const double zepto = 1e-21;
        static const double atto  = 1e-18;
        static const double femto = 1e-15;
        static const double pico  = 1e-12;
        static const double nano  = 1e-9;
        static const double micro = 1e-6;
        static const double milli = 1e-3;
        static const double centi = 1e-2;
        static const double deci  = 1e-1;
        static const double deca  = 1e1;
        static const double hecto = 1e2;
        static const double kilo  = 1e3;
        static const double mega  = 1e6;
        static const double giga  = 1e9;
        static const double tera  = 1e12;
        static const double peta  = 1e15;
        static const double exa   = 1e18;
        static const double zetta = 1e21;
        static const double yotta = 1e24;
    }

    static const length metre = length(1);
}

namespace imperial
{
    static const length inch = length(0.0254);
    static const length foot = 12 * inch;
    static const length yard = 3 * foot;
    static const length mile = 1760 * yard;
}

Example usage:

#include <iostream>
int main()
{
    const length kilometre = metric::prefix::kilo * metric::metre;
    std::cout << "1 mile is "
              << imperial::mile / kilometre
              << " kilometres.\n";
}

Next steps

Consider how you would multiply lengths to create areas or volumes. A good approach that will serve well when we extend to other types of quantity is to have a type templated on how many lengths are multiplied together:

template<int LengthDimension>
struct quantity

When we become interested in speeds, accelerations and flow rates, we'll have

template<int LengthDimension, int TimeDimension>
struct quantity

and so on.

\$\endgroup\$
1
\$\begingroup\$

Small review

double pow(double base, int power)

  • Special test if (power == 1) { return base ; } not needed.

  • Rather than O(power) algorithm, use O(log(power)) algorithm: Exponentiation by squaring.

  • Handle corner cases better: 1) pow(x, p) is close to, but not zero yet pow(x,-p) overflows like pow(10, -310), 2) Avoid undefined behavior of -power when power == INT_MIN.

  • Consider a name that does not conflict with pow() from <cmath>.

// Sample untested code - (under the weather).

double pow_u(double base, unsigned power) {
  double y = 1.0;
  while (power > 0) {
    if (power % 2) {
      y *= base;
    }
    base *= base;
    power /= 2;
  }
  return y;
}

double pow_i(double base, int power) {
  if (power < 0) {
    unsigned power_half = (unsigned) -(power / 2);
    double y = 1.0 / pow_u(base, power_half);
    y *= y;
    if (power % 2) {
      y /= base;
    }
    return y;
  }
  return pow_u(base, (unsigned) power);
}

Use better conversions constants

// (tVal * MyNS::pow (10, from.type) * 39.3701 / to.type) ;
(tVal * MyNS::pow (10, from.type) * (10000.0/254) / to.type) ;
  • IMO, use whole number conversion constants as many metric/imperial conversions are exactly expressible using an integer ratio - as above. Let the compiler determine the best double conversion factor. This will improve the conversion in various edge cases.

Notes:
231 cubic inches = 1 gallon, exactly.
1 pound = 7000 grains, exactly.
100,000,000 pound = 45,359,237.0 kg exactly
c is exactly 299,792,458 meters per second

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.