HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaMinor

Calculating length with different units

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
unitswithlengthdifferentcalculating

Problem

We have an exercise to design a simple library, which can calculate some length with different unit, e.g.

2m*2 + 20cm/2 - 5mm


and the result can be any of:

4.095m
409.5cm
4095mm


I want to design it in Object-oriented, and have a solution like this:

public class Length {

    private final double value;
    private final Unit unit;

    public Length(double value, Unit unit) {
        this.value = value;
        this.unit = unit;
    }

    @Override
    public boolean equals(Object obj) {
        Length length = (Length) obj;
        return this.unit.toMM(this.value) == length.unit.toMM(length.value);
    }

    public Length add(Length added) {
        return new Length(this.unit.toMM(this.value) + added.unit.toMM(added.value), Unit.mm);
    }

    public Length subtract(Length another) {
        return new Length(this.unit.toMM(this.value) - another.unit.toMM(another.value), Unit.mm);
    }

}

enum Unit {
    m(1000), cm(10), mm(1);
    private final int rate;

    Unit(int rate) {
        this.rate = rate;
    }

    public double toMM(double value) {
        return rate * value;
    }
}


It works, but I have several questions:

  • Is there anything wrong from the OO point of view?



  • Unit has a method toMM which is bind to a concrete type of unit mm(millimeter), I don't feel good about it, is there any way to improve it? Or it's just acceptable since we have to choose one?



  • Length has chosen mm for all kinds of unit, is it good? I'm looking for a way to avoid it, but not found



These questions are mostly based on this possible requirement change: don't want to support mm anymore. Most of the code has to be modified in this case.

Update: from this article, seems it's not a good idea to use enum here for different units?

Solution

An alternative approach would be to say that a length is measured in SI units (meters).

Also, if performance is not a concern, using a BigDecimal instead of a double would avoid potential rounding errors.

It could look like:

public class Length {
  private final BigDecimal meters;
  public Length(BigDecimal meters) { ... }
  public Length(BigDecimal value, Unit unit) { /*convert to m*/}

  public BigDecimal getLength(Unit unit) { /*convert to unit*/ }
  public BigDecimal getLength() { /*return length in SI, i.e. meters*/ }

  public int hashCode() { return meters.hashCode(); }
  public boolean equals(Object o) {
    /*usual checks */
    return meters.compareTo(other.meters) == 0;
  }
  //other methods
}


This also avoids having to arbitrarily choose a "base" unit (mm in your example).

Finally I would suggest defining an interface for Unit:

public interface Unit {
  BigDecimal toMeters(BigDecimal length);
  //and possibly additional helper methods:
  default BigDecimal convert(BigDecimal length, Unit sourceUnit) { ... }
}


Then your enum would be:

public enum StandardUnit implements Unit {
  MM("0.001"); //etc.

  private final BigDecimal multiplier;
  StandardUnit(String mulltiplier) { this.multiplier = new BigDecimal(multiplier); }
  public BigDecimal toMeters(BigDecimal length) { return length.multiply(multiplier); }
}


This will allow a more flexible design where you can add units by implementing the interface or use the provided standard units.

Code Snippets

public class Length {
  private final BigDecimal meters;
  public Length(BigDecimal meters) { ... }
  public Length(BigDecimal value, Unit unit) { /*convert to m*/}

  public BigDecimal getLength(Unit unit) { /*convert to unit*/ }
  public BigDecimal getLength() { /*return length in SI, i.e. meters*/ }

  public int hashCode() { return meters.hashCode(); }
  public boolean equals(Object o) {
    /*usual checks */
    return meters.compareTo(other.meters) == 0;
  }
  //other methods
}
public interface Unit {
  BigDecimal toMeters(BigDecimal length);
  //and possibly additional helper methods:
  default BigDecimal convert(BigDecimal length, Unit sourceUnit) { ... }
}
public enum StandardUnit implements Unit {
  MM("0.001"); //etc.

  private final BigDecimal multiplier;
  StandardUnit(String mulltiplier) { this.multiplier = new BigDecimal(multiplier); }
  public BigDecimal toMeters(BigDecimal length) { return length.multiply(multiplier); }
}

Context

StackExchange Code Review Q#112592, answer score: 6

Revisions (0)

No revisions yet.