Skip to content

Commit e9ea5ca

Browse files
committed
Merge pull request #12 from FritzMock/master
Hidden typecast for Float objects in JSONobject.increment(String key)
2 parents dc7c59b + fa79826 commit e9ea5ca

1 file changed

Lines changed: 63 additions & 1 deletion

File tree

JSONObjectTest.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ public void jsonInvalidNumberValues() {
493493
"\"negativeHexFloat\":-0x1.fffp1,"+
494494
"\"hexFloat\":0x1.0P-1074,"+
495495
"\"floatIdentifier\":0.1f,"+
496-
"\"doubleIdentifier\":0.1d,"+
496+
"\"doubleIdentifier\":0.1d"+
497497
"}";
498498
JSONObject jsonObject = new JSONObject(str);
499499
Object obj;
@@ -694,6 +694,20 @@ public void jsonObjectIncrement() {
694694
"\"keyLong\":9999999993,"+
695695
"\"keyDouble\":3.1,"+
696696
// TODO: not sure if this will work on other platforms
697+
698+
// Should work the same way on any platform! @see https://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.2.3
699+
// This is the effect of a float to double conversion and is inherent to the shortcomings of the IEEE 754 format, when
700+
// converting 32-bit into double-precision 64-bit.
701+
// Java type-casts float to double. A 32 bit float is type-casted to 64 bit double by simply appending zero-bits to the
702+
// mantissa (and extended the signed exponent by 3 bits.) and there is no way to obtain more information than it is
703+
// stored in the 32-bits float.
704+
705+
// Like 1/3 cannot be represented as base10 number because it is periodically, 1/5 (for example) cannot be represented
706+
// as base2 number since it is periodically in base2 (take a look at http://www.h-schmidt.net/FloatConverter/)
707+
// The same happens to 3.1, that decimal number (base10 representation) is periodic in base2 representation, therefore
708+
// appending zero-bits is inaccurate. Only repeating the periodically occuring bits (0110) would be a proper conversion.
709+
// However one cannot detect from a 32 bit IEE754 representation which bits would "repeat infinitely", since the missing
710+
// bits would not fit into the 32 bit float, i.e. the information needed simply is not there!
697711
"\"keyFloat\":3.0999999046325684,"+
698712
"}";
699713
JSONObject jsonObject = new JSONObject(str);
@@ -709,6 +723,54 @@ public void jsonObjectIncrement() {
709723
jsonObject.increment("keyFloat");
710724
JSONObject expectedJsonObject = new JSONObject(expectedStr);
711725
Util.compareActualVsExpectedJsonObjects(jsonObject, expectedJsonObject);
726+
727+
/*
728+
float f = 3.1f;
729+
double df = (double) f;
730+
double d = 3.1d;
731+
System.out.println(Integer.toBinaryString(Float.floatToRawIntBits(f)));
732+
System.out.println(Long.toBinaryString(Double.doubleToRawLongBits(df)));
733+
System.out.println(Long.toBinaryString(Double.doubleToRawLongBits(d)));
734+
735+
- Float:
736+
seeeeeeeemmmmmmmmmmmmmmmmmmmmmmm
737+
1000000010001100110011001100110
738+
- Double
739+
seeeeeeeeeeemmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
740+
10000000 10001100110011001100110
741+
100000000001000110011001100110011000000000000000000000000000000
742+
100000000001000110011001100110011001100110011001100110011001101
743+
*/
744+
// Examples of well documented but probably unexpected behavior in java / with 32-bit float to 64-bit float conversion.
745+
assertFalse("Document unexpected behaviour with explicit type-casting float as double!", (double)0.2f == 0.2d );
746+
assertFalse("Document unexpected behaviour with implicit type-cast!", 0.2f == 0.2d );
747+
Double d1 = new Double( 1.1f );
748+
Double d2 = new Double( "1.1f" );
749+
assertFalse( "Document implicit type cast from float to double before calling Double(double d) constructor", d1.equals( d2 ) );
750+
751+
assertTrue( "Correctly converting float to double via base10 (string) representation!", new Double( 3.1d ).equals( new Double( new Float( 3.1f ).toString() ) ) );
752+
753+
// Pinpointing the not so obvious "buggy" conversion from float to double in JSONObject
754+
JSONObject jo = new JSONObject();
755+
jo.put( "bug", 3.1f ); // will call put( String key, double value ) with implicit and "buggy" type-cast from float to double
756+
assertFalse( "The java-compiler did add some zero bits for you to the mantissa (unexpected, but well documented)", jo.get( "bug" ).equals( new Double( 3.1d ) ) );
757+
758+
JSONObject inc = new JSONObject();
759+
inc.put( "bug", new Float( 3.1f ) ); // This will put in instance of Float into JSONObject, i.e. call put( String key, Object value )
760+
assertTrue( "Everything is ok here!", inc.get( "bug" ) instanceof Float );
761+
inc.increment( "bug" ); // after adding 1, increment will call put( String key, double value ) with implicit and "buggy" type-cast from float to double!
762+
// this.put(key, (Float) value + 1);
763+
// 1. The (Object)value will be typecasted to (Float)value since it is an instanceof Float actually nothing is done.
764+
// 2. Float instance will be autoboxed into float because the + operator will work on primitives not Objects!
765+
// 3. A float+float operation will be performed and results into a float primitive.
766+
// 4. There is no method that matches the signature put( String key, float value), java-compiler will choose the method
767+
// put( String key, double value) and does an implicit type-cast(!) by appending zero-bits to the mantissa
768+
assertTrue( "JSONObject increment unexpected behavior, Float will not stay Float!", jo.get( "bug" ) instanceof Float );
769+
// correct implementation (with change of behavior) would be:
770+
// this.put(key, new Float((Float) value + 1));
771+
// Probably it would be better to deprecate the method and remove some day, while convenient processing the "payload" is not
772+
// really in the the scope of a JSON-library (IMHO.)
773+
712774
}
713775

714776
@Test

0 commit comments

Comments
 (0)