1

i am trying to make a unit converter using javafx - i've been searching around for two days on how can i reduces these if-statements. somehow, i found some similar issue but it didn't help me since i am new with Java - i do not know the right approach in my case.

Hope you guys could help me -

thanks

/**
     * CELSIUS TO
     */
    Celsius celsius = new Celsius(parseInput);
        if(cbOne.getValue().equals("Celsius")) {
            if(cbTwo.getValue().equals("Celsius") ) {
                showAnswer.setText(celsius.celsiusToCelsius());
            }
            if(cbTwo.getValue().equals("Fahrenheit")) {
                showAnswer.setText(celsius.celsiusToFahrenheit());
            }
            if(cbTwo.getValue().equals("Kelvin")) {
                showAnswer.setText(celsius.celsiusToKelvin());
            }
        }
    /**
     * FAHRENHEIT TO
     */
    Fahrenheit fahr = new Fahrenheit(parseInput);
        if(cbOne.getValue().equals("Fahrenheit") ) {
            if(cbTwo.getValue().equals("Celsius") ) {
                showAnswer.setText(fahr.fahrenheitToCelsius());
            }
            if(cbTwo.getValue().equals("Fahrenheit")) {
                showAnswer.setText(fahr.fahrenheitToFahrenheit());
            }
            if(cbTwo.getValue().equals("Kelvin")) {
                showAnswer.setText(fahr.fahrenheitToKelvin());
            }
        }
    /**
     * KELVIN TO
     */
    Kelvin kelvin = new Kelvin(parseInput);
        if(cbOne.getValue().equals("Kelvin")) {
            if(cbTwo.getValue().equals("Celsius") ) {
                showAnswer.setText(kelvin.kelvinToCelsius());
            }
            if(cbTwo.getValue().equals("Fahrenheit")) {
                showAnswer.setText(kelvin.kelvinToFahrenheit());
            }
            if(cbTwo.getValue().equals("Kelvin")) {
                showAnswer.setText(kelvin.kelvinToKelvin());
            }
        }
    }
i-faith
  • 449
  • 1
  • 3
  • 14
  • You could use ternary http://alvinalexander.com/java/edu/pj/pj010018 – Kiraged Nov 18 '14 at 17:08
  • 4
    There are any number of ways this could be done, including a simple map of symbol-to-symbol keys with conversion implementations, e.g., "FK" => implementation of `convert(input)`. Using a ternary here would be... suspect at best. – Dave Newton Nov 18 '14 at 17:09
  • IMHO, anything that requires a method or class for cases where no conversion is required is not a good approach. You can use a simple flow control structure (i.e. `if` statement) to simply return the original value if no conversion is required. – hfontanez Nov 18 '14 at 19:12
  • @hfontanez You could post that as an answer because it reduces the OP's `if` statements. – Radiodef Nov 18 '14 at 20:51
  • @Radiodef the answer was provided by LeffeBrune. I upvoted that answer because I believed to be the best answer provided. No need to repost. – hfontanez Nov 18 '14 at 21:12

8 Answers8

4

Actually your if statements are fine for a small program. They are perfectly clear. You may, however, reduce redundancy by checking that cbOne.getValue().equals(cbTwo.getValue()). This will trade 3 of your if statements for 1.

If you had lots of these, you would benefit from a Map and interface scheme.

interface Converter {
    double convert(double from);
}

static final Map<String, Map<String, Converter>> converters = (
    new HashMap<String, Map<String, Converter>>()
);
static {
    Map<String, Converter> fromCelsius = new HashMap<String, Converter>();

    fromCelsius.put(   "Celsius", new NoConversionConverter()       );
    fromCelsius.put("Fahrenheit", new CelsiusToFahrenheitConverter());
    fromCelsius.put(    "Kelvin", new CelsiusToKelvinConverter()    );

    converters.put("Celsius", fromCelsius);

    ...
}

static Converter getConverter(String from, String to) {
    Map<String, Converter> fromMap = converters.get(from);
    return fromMap == null ? null : fromMap.get(to);
}

A Map is the common OOP solution. Instead of imperative/structured decision-making we configure the Map and the decision-making is hidden by abstraction.

This is extremely succinct in Java 8 when paired with an enum:

public enum Scale {
    CELSIUS, FAHRENHEIT, KELVIN;
    private final Map<Scale, DoubleUnaryOperator> ops = new HashMap<>();

    public DoubleUnaryOperator to(Scale to) {
        return to == this ? DoubleUnaryOperator.identity() : ops.get(to);
    }

    static {
        put(    CELSIUS, FAHRENHEIT, c -> c * 9.0 / 5.0 + 32.0     );
        put(    CELSIUS,     KELVIN, c -> c + 273.15               );
        put( FAHRENHEIT,    CELSIUS, f -> (f - 32.0) * 5.0 / 9.0   );
        put( FAHRENHEIT,     KELVIN, f -> (f + 459.67) * 5.0 / 9.0 );
        put(     KELVIN, FAHRENHEIT, k -> k * 9.0 / 5.0 + 459.67   );
        put(     KELVIN,    CELSIUS, k -> k - 273.15               );
    }

    private static void put(Scale from, Scale to, DoubleUnaryOperator op) {
        from.ops.put(to, op);
    }
}

Also extremely readable:

Scale      source = Scale.valueOf("CELSIUS");
Scale destination = Scale.valueOf("FAHRENHEIT");
double     result = source.to(destination).applyAsDouble(0.0);
Radiodef
  • 37,180
  • 14
  • 90
  • 125
  • 1
    While I love using maps to lookup a converter, the fact that you need a separate converter class for every scale pair is not optimal. – LeffeBrune Nov 18 '14 at 18:06
  • *"...the fact that you need a separate converter class for every scale pair is not optimal."* Yes, it is not optimal. The benefit here is that it is easier to modify. For example, adding distance conversion to the program is trivial. With Java 8 (not shown), this type of solution is also much simpler to manage. – Radiodef Nov 18 '14 at 18:32
  • @Radiodef I am not sure what feature of Java 8 you are referring to. My guess is you are referring to evolving interfaces. If that is the case, you will be right that it might be easier to manage (changes). However, there is no reason for that. There are only three temperature scales in use today. And, to my knowledge, there is no plans for new temperature scales to be created. Therefore, there is no need to code for this eventuality. In fact, it is considered a bad practice to code for enhancement for no reason. I think in this case, an optimal solution is best. – hfontanez Nov 18 '14 at 18:50
  • 2
    @hfontanez I think he refers to lambda expressions. With lambdas his code would look a lot more compact as he will not have separate (or even anonymous) classes for the converters. – LeffeBrune Nov 18 '14 at 18:53
  • @hfontanez Lambdas and method references make a Map slightly more succinct than any other solution. My Java 6 syntax makes this more verbose than it would be in modern versions. – Radiodef Nov 18 '14 at 18:54
  • @Radiodef lambda expressions were created to treat functionality as method argument, or code as data. Reducing verbosity is great. But I am not fully convinced that it might be better. – hfontanez Nov 18 '14 at 18:59
  • @Radiodef why don't you join me in [chat](http://chat.stackoverflow.com/rooms/info/65168/reducing-if-statement-java?tab=conversations)? – hfontanez Nov 18 '14 at 19:06
  • @hfontanez I don't see what there is to discuss. – Radiodef Nov 18 '14 at 19:26
  • @Radiodef you rather create a class called `NoConversionConverter` instead of using flow control to determine that no conversion is needed. If you don't see that as a bad thing, then you are right; there isn't anything to discuss. – hfontanez Nov 18 '14 at 19:34
  • @hfontanez It's called the [Null Object Pattern](http://en.wikipedia.org/wiki/Null_Object_pattern). Also called an identity function, where a unit is converted to itself. If you think identity function should be an error, that is really adjunct from my answer. That's why I don't see what there is to discuss. – Radiodef Nov 18 '14 at 19:43
  • this solution might be the best for my future application - and i appreciate all the help - @LeffeBrune but for this time - I'm gonna use your advised - – i-faith Nov 19 '14 at 02:43
3

You do not need 3 classes to represent temperatures in different scales. Create a class that always keeps temperature in Kelvins internally and can convert it to any other scale for output. Having a class like this:

public final class Temperature {
  public enum Scale {
    Celsius, Fahrenheit, Kelvin
  }

  private final double temperature;

  private Temperature(double temperature) {
    this.temperature = temperature;
  }

  public static Temperature create(double temperature, Scale scale) {
    switch (scale) {
      case Celsius:
        return new Temperature(temperature + 273.15);
      case Fahrenheit:
        return new Temperature((temperature + 459.67) * 5.0 / 9.0);
      case Kelvin:
        return new Temperature(temperature);
      default:
        throw new IllegalArgumentException("Unknown scale");
    }
  }

  public double convertTo(Scale scale) {
    switch (scale) {
      case Celsius:
        return temperature - 273.15;
      case Fahrenheit:
        return temperature * 9.0 / 5.0 - 459.67;
      case Kelvin:
        return temperature;
      default:
        throw new IllegalArgumentException("Unknown scale");
    }
  }
}

Your code becomes:

Temperature temp = Temperature.create(parseInput, Scale.valueOf(cbOne.getValue()));
showAnswer.setText(temp.convertTo(Scale.valueOf(cbTwo.getValue())));
LeffeBrune
  • 3,441
  • 1
  • 23
  • 36
2

Your biggest issue is that you have created an n×m problem that ought to be a n+m problem.

To solves this, first define a canonical unit and dissolve the problem into two steps, convert from the source unit to the canonical unit, then convert from the canonical unit to the target unit.

For example, if you define kelvin to be the canonical unit, your code might resemble this:

switch(inputUnit.getValue())
{
  case "Fahrenheit": kelvin=fahrenheitToKelvin(input); break;
  case "Celsius":    kelvin=celsiusToKelvin(input); break;
  case "Kelvin":     kelvin=input; break;
  default: throw new AssertionError();
}
switch(outputUnit.getValue())
{
  case "Fahrenheit": output=kelvinToFahrenheit(kelvin); break;
  case "Celsius":    output=kelvinToCelsius(kelvin); break;
  case "Kelvin":     output=kelvin; break;
  default: throw new AssertionError();
}
showAnswer.setText(output);

I omitted string to number and number to string conversions as it should be obvious that these conversions need to be performed only once, outside the selector.

This principle can be used as well, if you use enums instead of Strings or replace the switch with a Map based approach as suggested by others. But the important thing is the two-step approach that allows you to maintain n input unit to canonical plus m canonical to output unit conversions rather than n input units times m output units conversions.

Holger
  • 285,553
  • 42
  • 434
  • 765
1

Since you are not going to have to modify this code later with more units, I would suggest you a ternary operator :

String s = cbTwo.getValue();
showAnswer.setText(s.equals("Celsius") ? fahr.fahrenheitToCelsius() :
    s.equals("Farenheit") ? fahr.fahrenheitToFahrenheit() : kelvin.kelvinToKelvin()); 

Note that it is not exactly equivalent, in the case that s does not match any of thestrings you have in your if statements.

Dici
  • 25,226
  • 7
  • 41
  • 82
  • Nested ternary expressions are usually not the most readable and understandable option. Consider instead an `if-else if` chain, or a `switch` on a string, or the map that @DaveNewton suggested above. – Andy Thomas Nov 18 '14 at 17:23
  • you forget to tell OP that you use 'Ternary Operator' . +1 good answer. – Fevly Pallar Nov 18 '14 at 17:23
  • No, I don't : `I would suggest you a ternary operator` – Dici Nov 18 '14 at 17:24
  • @AndyThomas with a proper indentation it is totally as readable as`if/else if` and `switch` statements and more compact. Using a map requires here to implement a new class/interface, which is probably not necessary for a code that don't have to be generic or reusable (there is not going to have much more unit systems to add) – Dici Nov 18 '14 at 17:30
1

You can convert any input value to Kelvin and then convert from Kelvin to the desired result:

String unit = cbOne.getValue();
double inputInKelvin;
String outUnit = cbTwo.getValue();

// parse
if ( unit.equals("Celsius") ) inputInKelvin = new Celsius(parseInput).celsiusToKelvin();
else if ( unit.equals("Fahrenheit") ) inputInKelvin = new Fahrenheit(parseInput).fahrenheitToKelvin();
else inputInKelvin = new Kelvin(parseInput).kelvinToKelvin();

// output
Kelvin kelvin = new Kelvin(inputInKelvin);
if ( unit.equals("Celsius") ) showAnswer.setText(kelvin.kelvinToCelsius());
else if ( unit.equals("Fahrenheit") ) showAnswer.setText(kelvin.kelvinToFahrenheit());
else showAnswer.setText( kelvin.kelvinToKelvin() );

It would get even more readable if you parse the String to a double first and then just have one Converter class.

Christian
  • 13,285
  • 2
  • 32
  • 49
0

While a ternary operator would work, it's difficult to read and maintain.

Slightly better could be a switch statement within the first if statement. However, your code as written is adequately understandable and doesn't need to change from that perspective.

However, to really make this code better, it needs to be re-factored.

There are two troublesome characteristics of the code as written. First, you instantiate all temperature objects when only one is used.

Second, the UI and the business logic are too tightly coupled. if you change the name of the UI elements, you'll have to change the business logic. Also, if you add additional ways for your application to convert temperature, you won't be able to re-use this code.

kevintechie
  • 1,441
  • 1
  • 13
  • 15
0

You can also do something like this:

 final int CELSIUS = 1,FARANHITE = 2,KELVIN = 3;

then use switch statment like this

 int key = StringToInt(firstValue)*10 + StringTOInt(secondValue);
 //This will give you 9 unique codes 11 12 13 21 22 23 ....
 switch(key)
 {
       default: case 11: case 22: case 33: break;
       //do nothing since no conversion required

       case 12://Celsius to faranhite
       case 13://celsius to kelvin
       .
       .
       //and so on     

 }
mchouhan_google
  • 1,775
  • 1
  • 20
  • 28
0

Do like this

create an InterfaceForConvertorTypes

public interface Convertor {
    public String convert(int parseInt);
    public boolean accept(String from, String to);
}

then implement convertor types and register them in registery collection. with a single IF you can achieve what you want.

List<Convertor> convertors = new ArrayList<Convertor>();

    Convertor CelsiusToCelsius = new Convertor() {

        @Override
        public String convert(int parseInt) {
            Celsius celsius = new Celsius(parseInt);
            return celsius.celsiusToCelsius();
        }

        @Override
        public boolean accept(String from, String to) {
            return from.equals("Celsius") && to.equals("Celsius");
        }
    };


    Convertor CelsiusToFah = new Convertor() {

        @Override
        public String convert(int parseInt) {
            Celsius celsius = new Celsius(parseInt);
            return celsius.celsiusToFahrenheit();
        }

        @Override
        public boolean accept(String from, String to) {
            return from.equals("Celsius") && to.equals("Fahrenheit");
        }
    };

    Convertor CelsiusToKelvin = new Convertor() {

        @Override
        public String convert(int parseInt) {
            Celsius celsius = new Celsius(parseInt);
            return celsius.celsiusToFahrenheit();
        }

        @Override
        public boolean accept(String from, String to) {
            return from.equals("Celsius") && to.equals("Kelvin");
        }
    };

   // create Rest of Convertor like above

    convertors.add(CelsiusToFah);
    convertors.add(CelsiusToCelsius);
    convertors.add(CelsiusToKelvin);
    // register rest of convertor


    //Thats it!
    for(Convertor convertor:convertors) {
        if(convertor.accept(cbOne.getValue(), cbTwo.getValue())) {
            showAnswer.setText(convertor.convert(parseInput));
        }
    }
Morteza Adi
  • 2,413
  • 2
  • 22
  • 37