Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

329: Retain floating point precision when converting floats #331

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions src/main/java/tech/units/indriya/function/DefaultNumberSystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,10 @@ public Number reciprocal(Number number) {
return ((RationalNumber) number).reciprocal();
}
if(number instanceof Double) {
return RationalNumber.of((double)number).reciprocal();
return RationalNumber.of(number.doubleValue()).reciprocal();
}
if(number instanceof Float) {
return RationalNumber.of(number.doubleValue()).reciprocal();
return RationalNumber.of(number.floatValue()).reciprocal();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not introduce a RationalNumber.of(float) for the reasons discussed in (#332).

}
throw unsupportedNumberType(number);
}
Expand Down Expand Up @@ -615,7 +615,7 @@ private BigDecimal toBigDecimal(Number number) {
return BigDecimal.valueOf(number.longValue());
}
if(number instanceof Double || number instanceof Float) {
return BigDecimal.valueOf(number.doubleValue());
return new BigDecimal(number.toString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not convert floats to String and then to BigDecimal if the general scheme (as up for discussion in #332) is to first properly extend floats to double before converting to BigDecimal. If we agree on this scheme we should be consistent with it throughout the entire implementation.

}
if(number instanceof RationalNumber) {
throw unexpectedCodeReach();
Expand Down Expand Up @@ -668,7 +668,7 @@ private Number addWideAndNarrow(
}

if(narrow instanceof Double || narrow instanceof Float) {
return ((BigDecimal) wide).add(BigDecimal.valueOf(narrow.doubleValue()), Calculus.MATH_CONTEXT);
return ((BigDecimal) wide).add(new BigDecimal(narrow.toString()), Calculus.MATH_CONTEXT);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito: consistency

}

if(narrow instanceof RationalNumber) {
Expand All @@ -685,23 +685,23 @@ private Number addWideAndNarrow(

if(narrow instanceof Double || narrow instanceof Float) {
//converting to BigDecimal, because especially fractional addition is sensitive to precision loss
return BigDecimal.valueOf(wide.doubleValue())
.add(BigDecimal.valueOf(narrow.doubleValue()));
return new BigDecimal(wide.toString())
.add(new BigDecimal(narrow.toString()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito: consistency

}

if(narrow instanceof RationalNumber) {
//TODO[220] can we do better than that, eg. by converting BigDecimal to RationalNumber
return BigDecimal.valueOf(wide.doubleValue())
return new BigDecimal(wide.toString())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point wide is potentially a float, so don't break consistency.

.add(((RationalNumber) narrow).bigDecimalValue());
}

if(narrow instanceof BigInteger) {
return BigDecimal.valueOf(wide.doubleValue())
return new BigDecimal(wide.toString())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito: consistency

.add(new BigDecimal((BigInteger) narrow));
}

// at this point we know, that 'narrow' is one of {(Atomic)Long, (Atomic)Integer, Short, Byte}
return BigDecimal.valueOf(wide.doubleValue())
return new BigDecimal(wide.toString())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point wide is potentially a float, so don't break consistency.

.add(BigDecimal.valueOf(narrow.longValue()));

}
Expand Down Expand Up @@ -748,7 +748,7 @@ private Number multiplyWideAndNarrow(
}

if(narrow instanceof Double || narrow instanceof Float) {
return ((BigDecimal) wide).multiply(BigDecimal.valueOf(narrow.doubleValue()), Calculus.MATH_CONTEXT);
return ((BigDecimal) wide).multiply(new BigDecimal(narrow.toString()), Calculus.MATH_CONTEXT);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito: consistency

}

if(narrow instanceof RationalNumber) {
Expand All @@ -765,23 +765,28 @@ private Number multiplyWideAndNarrow(

if(narrow instanceof Double || narrow instanceof Float) {
// not converting to BigDecimal, because fractional multiplication is not sensitive to precision loss
return wide.doubleValue() * narrow.doubleValue();
if (wide instanceof Float && narrow instanceof Float) {
// return float if both are floats
return wide.floatValue() * narrow.floatValue();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not do that, as by extension to double we can guarantee the float value multiplication not to overflow. Its a corner case granted, but why risk that?

} else {
return wide.doubleValue() * narrow.doubleValue();
}
}

if(narrow instanceof RationalNumber) {
//TODO[220] can we do better than that, eg. by converting BigDecimal to RationalNumber
return BigDecimal.valueOf(wide.doubleValue())
return new BigDecimal(wide.toString())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point wide is potentially a float, so don't break consistency.

.multiply(((RationalNumber) narrow).bigDecimalValue());
}

if(narrow instanceof BigInteger) {
return BigDecimal.valueOf(wide.doubleValue())
return new BigDecimal(wide.toString())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito: consistency

.multiply(new BigDecimal((BigInteger) narrow));
}

// at this point we know, that 'narrow' is one of {(Atomic)Long, (Atomic)Integer, Short, Byte}
return BigDecimal.valueOf(wide.doubleValue())
.multiply(BigDecimal.valueOf(narrow.longValue()));
return new BigDecimal(wide.toString())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point wide is potentially a float, so don't break consistency.

.multiply(BigDecimal.valueOf(narrow.longValue()));

}

Expand Down Expand Up @@ -821,7 +826,7 @@ private int compareWideVsNarrow(
}

if(narrow instanceof Double || narrow instanceof Float) {
return ((BigDecimal) wide).compareTo(BigDecimal.valueOf(narrow.doubleValue()));
return ((BigDecimal) wide).compareTo(new BigDecimal(narrow.toString()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito: consistency

}

if(narrow instanceof RationalNumber) {
Expand All @@ -837,22 +842,23 @@ private int compareWideVsNarrow(
// at this point we know, that wide is one of {Double, Float}

if(narrow instanceof Double || narrow instanceof Float) {
return Double.compare(wide.doubleValue(), narrow.doubleValue());
return new BigDecimal(wide.toString())
.compareTo(new BigDecimal(narrow.toString()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito: consistency

}

if(narrow instanceof RationalNumber) {
//TODO[220] can we do better than that, eg. by converting BigDecimal to RationalNumber
return BigDecimal.valueOf(wide.doubleValue())
return new BigDecimal(wide.toString())
.compareTo(((RationalNumber) narrow).bigDecimalValue());
}

if(narrow instanceof BigInteger) {
return BigDecimal.valueOf(wide.doubleValue())
return new BigDecimal(wide.toString())
.compareTo(new BigDecimal((BigInteger) narrow));
}

// at this point we know, that 'narrow' is one of {(Atomic)Long, (Atomic)Integer, Short, Byte}
return BigDecimal.valueOf(wide.doubleValue())
return new BigDecimal(wide.toString())
.compareTo(BigDecimal.valueOf(narrow.longValue()));

}
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/tech/units/indriya/function/RationalNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ public static RationalNumber of(double number) {
return of(decimalValue);
}

/**
* Returns a {@code RationalNumber} that represents the given float
* {@code number}, with an accuracy equivalent to {@code new BigDecimal(Float.toString(number))}.
*
* @param number
*/
public static RationalNumber of(float number) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, breaks the general scheme of first extending floats to double before converting them to BigDecimal or Rational.

// smh why does Java not have BigDecimal.valueOf(float) ?!
final BigDecimal decimalValue = new BigDecimal(Float.toString(number));
return of(decimalValue);
}

/**
* Returns a {@code RationalNumber} that represents the given BigDecimal decimalValue.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ public void testConvertMethod() {
assertEquals(-200, converter.convert(-100), 0.1);
}

@Test
public void testConvertMethodFloat() {
assertEquals(200, converter.convert(100f), 0.1);
assertEquals(0, converter.convert(0f));
assertEquals(-200, converter.convert(-100f), 0.1);
}

@Test
public void testEqualityOfTwoLogConverter() {
assertNotNull(converter);
Expand Down
26 changes: 26 additions & 0 deletions src/test/java/tech/units/indriya/function/UnitConverterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ public void testQuantity() {
assertEquals(targetUnit, quantResult1.getUnit());
}

@Test
public void testQuantityDouble() {
Quantity<Length> quantLength1 = Quantities.getQuantity(1.56, sourceUnit);
Quantity<Length> quantResult1 = quantLength1.to(targetUnit);
assertNotNull(quantResult1);
assertNumberEquals(156, quantResult1.getValue(), 1E-12);
assertEquals(targetUnit, quantResult1.getUnit());
}

@Test
public void testQuantityFloat() {
Quantity<Length> quantLength1 = Quantities.getQuantity(1.56f, sourceUnit);
Quantity<Length> quantResult1 = quantLength1.to(targetUnit);
assertNotNull(quantResult1);
assertNumberEquals(156, quantResult1.getValue(), 1E-12);
assertEquals(targetUnit, quantResult1.getUnit());
}

@Test
public void testKelvinToCelsius() {
Quantity<Temperature> sut = Quantities.getQuantity(273.15d, KELVIN).to(CELSIUS);
Expand All @@ -86,6 +104,14 @@ public void testKelvinToCelsius() {
assertNumberEquals(0, sut.getValue(), 1E-12);
}

@Test
public void testKelvinToCelsiusFloat() {
Quantity<Temperature> sut = Quantities.getQuantity(273.15f, KELVIN).to(CELSIUS);
assertNotNull(sut);
assertEquals(CELSIUS, sut.getUnit());
assertNumberEquals(0, sut.getValue(), 1E-12);
}

@Test
public void testConverterTo() {
assertEquals(KILOGRAM.getConverterTo(KILO(GRAM)), KILO(GRAM).getConverterTo(KILOGRAM));
Expand Down