4
\$\begingroup\$

I am working on an Android unit converter application, but I came to the point where I have to assign the formulas to the calculation method. At the moment I am using a switch-case statement, as it is more neat than an if-else, but still this proves to be very, very lengthy.

The way the application works is the user selects type of unit to convert, e.g. length. Then 2 spinners show up, one for the "From" unit and one for the "To" unit. On calculation the code picks up what is chosen in the spinners and determines the right formula to use.

Here is a code snippet for only one unit conversion:

switch (unitFrom) {
    case "Kilometers": switch (unitTo){
        case "Miles": initialValue = (initialValue / 1.6);
            break;
        case "Metres": initialValue *= 1000;
            break;
        case "Centimetres": initialValue *= 100000;
            break;
        case "Millimetres": initialValue *= 1000000;
            break;
        case "Feet": initialValue *= 3280.8399;
            break;
        case "Yards": initialValue *= 1093.6133;
            break;
        case "Inches": initialValue *= 39370.0787;
            break;
        case "Nautical Miles": initialValue *= 0.539956803;
            break;
        case "Fathoms": initialValue *= 546.806649;
            break;
        case "Light Years": initialValue *= (1.05702341*(10^-13));
            break;
    }
}

This will become so, so long after just few units being written down, so I was wondering if there's a better way of doing things. I am open to all sort of changes.

\$\endgroup\$
3
\$\begingroup\$

The problem with code like this is that it grows very quickly.

switch (unitFrom) {
    case "Kilometers": switch (unitTo){

Adding one unit means that you have to provide conversions to every other unit. But there's a pattern in your code:

    case "Metres": initialValue *= 1000;
        break;
    case "Centimetres": initialValue *= 100000;
        break;
    case "Millimetres": initialValue *= 1000000;
        break;
    case "Feet": initialValue *= 3280.8399;
        break;
    case "Yards": initialValue *= 1093.6133;
        break;
    case "Inches": initialValue *= 39370.0787;

Each unit is essentially just a factor. And the conversion is just the multiplication of the length value with that factor.

Take a look at the SI unit system, where such factors are actually part of the unit:

1 mm = =1 millimeter = 1 * milli * meter = 1 * 10^(-3) * meter

"milli" which would generally be considered part of the unit is actually just a number with a name: 10^(-3)

Notice that the opposite conversion means to divide by that factor.


To simplify the conversion, you can choose a common base unit. Now you don't have to convert every unit to every other unit but only have to specify one conversion: to/from the base unit.

Here's an example class doing this:

import java.util.HashMap;
import java.util.Set;

public class Converter
{
    public static void main (String[] args)
    {
        Converter converter = new Converter();

        Set<String> units = converter.availableUnits();

        double value = 5.4;

        for (String unitFrom : units)
        {
            System.out.println(value + " " + unitFrom + " are:");

            for (String unitTo : units)
            {
                System.out.println("\t" + converter.convert(unitFrom, unitTo, value) + " " + unitTo);
            }

            System.out.println();
        }
    }

    private HashMap<String, Double> _units;

    public Converter()
    {
        _units = new HashMap<String, Double>();

        _units.put("Millimetres", 1.0);
        _units.put("Metres", 1000.0);
        _units.put("Inches", 25.4);
        _units.put("Feet", 304.8);
        _units.put("Yards", 914.4);
    }

    public double convert(String from, String to, double value)
    {
        return value * _units.get(from) / _units.get(to);
    }

    private Set<String> availableUnits()
    {
        return _units.keySet();
    }
}

The program does every conversion possible.

I kept the conversion from one unit to itself in as a quick test. If you don't want that, opt-out when unitFrom == unitTo.

5.4 Millimetres are:
    5.4 Millimetres
    0.017716535433070866 Feet
    0.21259842519685043 Inches
    0.0054 Metres
    0.005905511811023623 Yards

5.4 Feet are:
    1645.92 Millimetres
    5.4 Feet
    64.80000000000001 Inches
    1.64592 Metres
    1.8 Yards

5.4 Inches are:
    137.16 Millimetres
    0.44999999999999996 Feet
    5.4 Inches
    0.13716 Metres
    0.15 Yards

5.4 Metres are:
    5400.0 Millimetres
    17.716535433070867 Feet
    212.5984251968504 Inches
    5.4 Metres
    5.905511811023622 Yards

5.4 Yards are:
    4937.76 Millimetres
    16.2 Feet
    194.4 Inches
    4.93776 Metres
    5.4 Yards

Additional thoughts:

The name of each unit is only specified once. Retyping string literals is prone to error "Millimetres" != "Millimeters", which the compiler cannot catch.

You could go further and define a class Unit, which besides the name and factor can contain, an abbreviation, plural and singular forms, etc.

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

In order to optimize your distance converter, you should consider using a "common denominator". Since I am from Europe (not UK), I am biased towards International System of Units, so I would choose 1 meter (1m), although the presence of light years may lead to at least 1km.

Personally, I like to store data in database, since they can be easily read and changed even by back-end developers, so the structure could be like this (I use MSSQL style, but it should be almost identical to any other SQL flavor).

Unit
----
UnitId INT
UnitName VARCHAR(100)
RelativeValue FLOAT

It should contain the following values (skip the IDs, they are not relevant)

'Meter', 1.0
'Mile', 1609.344
'Millimeter', 0.001
'Kilometer', 1000.0

and so on

E.g. When you want to express x kilometers in miles, you should do like this:

nb_miles = x * {RelativeValue for kilometer} / {RelativeValue for mile}

that is

nb_miles = x * 1000.0 / 1609.344

This approach should greatly simplify the code:

double unitConvert(double value, string unitFrom, string unitTo) {
    // this should fetch value from DB or your proffered storage for the units
    // also, some error handling should be done here
    double fromRelativeValue = getRelativeValue(unitFrom);
    double toRelativeValue = getRelativeValue(unitTo);

    // some overflow check should be done here
    return value * fromRelativeValue / toRelativeValue;
}

It is also recommended to use enums for your units, so that unitConvert caller cannot provide an invalid unit:

public enum MeasurementUnit {
    Meter,
    Mile,
    Millimeter,
    Kilometer,
    // --
}
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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