Skip to content

Commit 3c97b1d

Browse files
authored
Merge pull request #2 from Synt4xErr0r4/rounding
Added support for different rouding modes (version 2.1.0)
2 parents 80cee3d + 77ece75 commit 3c97b1d

File tree

10 files changed

+762
-48
lines changed

10 files changed

+762
-48
lines changed

README.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This library can convert `java.math.BigDecimal`s into IEEE 754 binary representa
88

99
## Getting started
1010

11-
In order to use the code, you can either [download the jar](https://github.com/Synt4xErr0r4/ieee754-java/releases/download/2.0.0/ieee754-java-2.0.0.jar), or use the Maven dependency:
11+
In order to use the code, you can either [download the jar](https://github.com/Synt4xErr0r4/ieee754-java/releases/download/2.0.0/ieee754-java-2.1.0.jar), or use the Maven dependency:
1212

1313
```xml
1414
<!-- Repository -->
@@ -23,7 +23,7 @@ In order to use the code, you can either [download the jar](https://github.com/S
2323
<dependency>
2424
<groupId>at.syntaxerror</groupId>
2525
<artifactId>ieee754-java</artifactId>
26-
<version>2.0.0</version>
26+
<version>2.1.0</version>
2727
</dependency>
2828
```
2929

@@ -67,6 +67,8 @@ import at.syntaxerror.ieee754.binary.Binary32;
6767
Binary32 value = Binary32.FACTORY.create(3.14159);
6868
```
6969

70+
*Note: using native `float` and `double` might lead to inaccurate results due to rounding errors. It is recommended to use `BigDecimal` instead.*
71+
7072
Now, the function `encode` in `Binary32` can be used to get the number's binary representation:
7173

7274
```java
@@ -89,7 +91,7 @@ The value can then be retrieved for further computations as a `BigDecimal` via t
8991
BigDecimal bigdec = decoded.getBigDecimal();
9092
```
9193

92-
This is applicable to all predefined types; Also, any class inheriting from `Floating<T>` (or its subclasses `Binary<T>` and `Decimal<T>`) has access to various helper methods, which are listed in the [JavaDoc](https://javadoc.syntaxerror.at/ieee754-java/latest/ieee754java/at/syntaxerror/ieee754/Floating.html).
94+
This is applicable to all predefined types; also, any class inheriting from `Floating<T>` (or its subclasses `Binary<T>` and `Decimal<T>`) has access to various helper methods, which are listed in the [JavaDoc](https://javadoc.syntaxerror.at/ieee754-java/latest/ieee754java/at/syntaxerror/ieee754/Floating.html).
9395

9496
### Decimal Encoding
9597

@@ -132,13 +134,50 @@ Take a look at the various predefined types to see how they are implemented.
132134

133135
### Rounding
134136

135-
Not all numbers can be encoded with full precision. In such cases, rounding is performed.
136-
The rounding mode used in this library is the default rounding mode for IEEE 754 floating points: round to nearest, ties to even.
137+
Some numbers cannot be encoded with full precision. In such cases, rounding is performed.
138+
139+
There are five IEEE 754 rounding modes:
140+
141+
- `TIES_EVEN`: round to nearest, ties to even
142+
- rounds to the nearest value
143+
- if the number falls midway, it is rounded to the nearest even value.
144+
- `TIES_AWAY`: round to nearest, ties away from 0
145+
- rounds to the nearest value
146+
- if the number falls midway, it is rounded to the nearest value above (positive numbers) or below (negative numbers).
147+
- `TOWARD_ZERO`: round toward 0 (aka. truncating)
148+
- `TOWARD_POSITIVE`: round toward +∞ (aka. rounding up, ceiling)
149+
- `TOWARD_NEGATIVE`: round toward -∞ (aka. rounding down, floor)
150+
151+
The default rounding mode used for encoding is `TIES_EVEN`. This can be changed by altering the `DEFAULT_ROUNDING` field
152+
in the `Rounding` class, e.g.:
153+
154+
```java
155+
import at.syntaxerror.ieee754.rounding.Rounding;
156+
157+
/* ... */
158+
159+
Rounding.DEFAULT_ROUNDING = Rounding.TOWARD_ZERO;
160+
```
137161

138162
## Documentation
139163

140164
The JavaDoc for the latest version can be found [here](https://javadoc.syntaxerror.at/ieee754-java/latest).
141165

166+
## Changelog
167+
168+
### 2.1.0
169+
170+
- Added support for different rounding modes
171+
- Improved performance for very small numbers
172+
173+
### 2.0.0
174+
175+
- Added decimal floating-point formats `Decimal32`, `Decimal64` and `Decimal128`
176+
177+
### 1.0.0
178+
179+
- Added binary floating-point formats `Binary16`, `Binary32`, `Binary64`, `Binary80`, `Binary128`, `Binary256`, `Binary512`, `Binary1024` and `Binary2048`
180+
142181
## Dependencies
143182

144183
This project makes use of the following dependencies:

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<modelVersion>4.0.0</modelVersion>
33
<groupId>at.syntaxerror</groupId>
44
<artifactId>ieee754-java</artifactId>
5-
<version>2.0.0</version>
5+
<version>2.1.0</version>
66
<name>IEEE754-Java</name>
77
<description>A Java 19 library for converting between IEEE 754 binary and decimal and BigDecimal</description>
88
<licenses>

src/main/java/at/syntaxerror/ieee754/Floating.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public int intValue() {
253253
if(type != FloatingType.FINITE)
254254
throw new UnsupportedOperationException("Cannot convert non-finite number to integer");
255255

256-
return (int) intValue();
256+
return (int) longValue();
257257
}
258258

259259
/** {@inheritDoc} */

src/main/java/at/syntaxerror/ieee754/binary/BinaryCodec.java

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import at.syntaxerror.ieee754.FloatingCodec;
3434
import at.syntaxerror.ieee754.FloatingFactory;
3535
import at.syntaxerror.ieee754.FloatingType;
36+
import at.syntaxerror.ieee754.rounding.Rounding;
3637
import ch.obermuhlner.math.big.BigDecimalMath;
3738
import lombok.NonNull;
3839

@@ -47,6 +48,7 @@
4748
public final class BinaryCodec<T extends Binary<T>> extends FloatingCodec<T> {
4849

4950
private static final MathContext FLOOR = new MathContext(0, RoundingMode.FLOOR);
51+
private static final MathContext CONTEXT = new MathContext(800);
5052

5153
private static final BigDecimal TWO = BigDecimal.valueOf(2);
5254
private static final BigDecimal LOG10_2 = BigDecimalMath.log10(TWO, new MathContext(604, RoundingMode.HALF_EVEN));
@@ -163,7 +165,8 @@ public BigInteger encode(T value) {
163165
BigInteger significand = bigdec.toBigInteger();
164166

165167
// strip integer part
166-
BigDecimal fraction = bigdec.subtract(new BigDecimal(significand)).abs();
168+
BigDecimal fraction = bigdec.subtract(new BigDecimal(significand))
169+
.abs().stripTrailingZeros();
167170

168171
// remove sign
169172
significand = significand.abs();
@@ -178,6 +181,45 @@ public BigInteger encode(T value) {
178181
final int off = getOffset();
179182
final int eMin = getExponentRange().getKey();
180183

184+
boolean requireRounding = false;
185+
boolean guard = false;
186+
boolean round = false;
187+
boolean sticky = false;
188+
189+
if(fraction.compareTo(BigDecimal.ZERO) != 0) {
190+
// skip leading zeros. this heavily improves the performance for larger types
191+
192+
zeros = BigDecimalMath.log2(
193+
BigDecimalMath.reciprocal(fraction, CONTEXT),
194+
CONTEXT
195+
).setScale(0, RoundingMode.CEILING).intValue() - 1;
196+
197+
fraction = fraction.multiply(pow2(zeros)).stripTrailingZeros();
198+
199+
significand = significand.shiftLeft(zeros);
200+
201+
int bitCount = significand.bitLength();
202+
int threshold = this.significand + off;
203+
204+
if(zeros > -eMin)
205+
bitCount += zeros + eMin;
206+
207+
if(bitCount > threshold) {
208+
requireRounding = true;
209+
210+
fraction = fraction.multiply(BigDecimal.TWO);
211+
212+
round = fraction.intValue() != 0;
213+
214+
if(round)
215+
fraction = fraction.subtract(BigDecimal.ONE);
216+
217+
sticky = fraction.compareTo(BigDecimal.ZERO) != 0;
218+
219+
fraction = BigDecimal.ZERO;
220+
}
221+
}
222+
181223
/* - left-shift the significand by 1
182224
* - multiply the fraction by two
183225
* - bit-or the significand with the integer part of result (0 or 1)
@@ -204,30 +246,11 @@ public BigInteger encode(T value) {
204246

205247
// max number of bits reached
206248
if(significandLength > this.significand + off) {
249+
requireRounding = true;
207250

208-
// Round to nearest, ties to even
209-
210-
boolean guard = significand.testBit(0);
211-
boolean round = integerPart == 1;
212-
boolean sticky = fraction.compareTo(BigDecimal.ONE) != 0;
213-
214-
if((guard && round) || (round && sticky)) { // round up
215-
int bits = significand.bitLength();
216-
217-
significand = significand.add(BigInteger.ONE);
218-
219-
if(bits != significand.bitLength()) { // overflow occured, adjust exponent
220-
221-
significand = significand.clearBit(bits); // clear overflown bit
222-
++exp;
223-
224-
// if exponent is all 1s now, return Infinity
225-
if(exp == mask(this.exponent).intValue())
226-
return sign == -1
227-
? getNegativeInfinity()
228-
: getPositiveInfinity();
229-
}
230-
}
251+
guard = significand.testBit(0);
252+
round = integerPart == 1;
253+
sticky = fraction.compareTo(BigDecimal.ONE) != 0;
231254

232255
break;
233256
}
@@ -242,6 +265,25 @@ else if(bitCount == 0)
242265
++zeros;
243266
}
244267

268+
if(requireRounding && Rounding.DEFAULT_ROUNDING.roundBinary(sign == -1, guard, round, sticky)) {
269+
// round up
270+
int bits = significand.bitLength();
271+
272+
significand = significand.add(BigInteger.ONE);
273+
274+
if(bits != significand.bitLength()) { // overflow occured, adjust exponent
275+
276+
significand = significand.clearBit(bits); // clear overflown bit
277+
++exp;
278+
279+
// if exponent is all 1s now, return Infinity
280+
if(exp == mask(this.exponent).intValue())
281+
return sign == -1
282+
? getNegativeInfinity()
283+
: getPositiveInfinity();
284+
}
285+
}
286+
245287
int len = significand.bitLength();
246288

247289
// fix exponent if < 1
@@ -357,9 +399,12 @@ private BigDecimal pow2(int n) {
357399
return BigDecimal.ONE;
358400

359401
if(n < 0)
360-
return BigDecimal.ONE.divide(pow2(-n));
402+
return BigDecimalMath.reciprocal(pow2(-n), CONTEXT);
361403

362-
return BigDecimal.TWO.pow(n);
404+
return new BigDecimal(
405+
BigInteger.ONE.shiftLeft(n),
406+
0
407+
).stripTrailingZeros();
363408
}
364409

365410
// computes 2^(e_min-1)

src/main/java/at/syntaxerror/ieee754/decimal/DecimalCodec.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
import java.math.BigDecimal;
2626
import java.math.BigInteger;
27-
import java.math.RoundingMode;
2827
import java.util.HashMap;
2928
import java.util.Map;
3029
import java.util.function.Supplier;
@@ -33,6 +32,7 @@
3332
import at.syntaxerror.ieee754.FloatingFactory;
3433
import at.syntaxerror.ieee754.FloatingType;
3534
import at.syntaxerror.ieee754.binary.Binary;
35+
import at.syntaxerror.ieee754.rounding.Rounding;
3636
import lombok.NonNull;
3737

3838
/**
@@ -359,12 +359,14 @@ else if(scale < 2 - maxExp - max) {
359359

360360
// truncate the n least significant digits of the value
361361
private BigDecimal truncateLeastSignificant(BigDecimal value, int n) {
362+
BigDecimal precise = new BigDecimal(
363+
value.unscaledValue()
364+
.divide(BigInteger.TEN.pow(n - 1))
365+
).divide(BigDecimal.TEN);
366+
362367
return new BigDecimal(
363-
new BigDecimal(
364-
value.unscaledValue()
365-
.divide(BigInteger.TEN.pow(n - 1))
366-
).divide(BigDecimal.TEN)
367-
.setScale(0, RoundingMode.HALF_EVEN)
368+
Rounding.DEFAULT_ROUNDING
369+
.roundDecimal(precise)
368370
.toBigInteger(),
369371
value.scale() - n
370372
).stripTrailingZeros();

0 commit comments

Comments
 (0)