Why is Ceylon adding additional decimal part for Float type

57 views
Skip to first unread message

Mohammad Als

unread,
Feb 10, 2017, 12:15:41 AM2/10/17
to ceylon-users
I've the following entries for Book table in the database:
mysql> select * from book;
+----+------------------+-------+
| id | title            | price |
+----+------------------+-------+
|  1 | Core Java        | 45.99 |
|  2 | Advanced Java    | 45.99 |
|  6 | Java Collections | 17.99 |
|  7 | Spring Boot      | 33.99 |
+----+------------------+-------+

I used JOOQ to generate the relevant Java classes for using in my application. Book POJO generated is:

public class Book implements Serializable {

private static final long serialVersionUID = -1442621618;

private Integer id;
private String title;
private Float price;

public Book() {}
}

I also created a corresponding Book class in Ceylon as below:

shared class Book(title, price) {
late shared Integer id;
shared String title;
shared Float price;
print("Price::::======``price``");
shared actual Boolean equals(Object that) {
if (is Book that) {
return title==that.title;
}
else {
return false;
}
}

shared actual Integer hash {
variable value hash = 1;
hash = 31*hash + title.hash;
return hash;
}

string => "[Book:``id``->``title``::``price``]";
}

Now I'm converting the database object which is a Java object into the ceylon object using the following adapter

function convertToBook(DbBook dbBook) {
value book = Book(dbBook.title, FloatConverter().from(dbBook.price));
book.id = IntegerConverter().from(dbBook.id);
return book;
}

And the FloatConverter is simply converting Java Float to Ceylon Float no additional information gets added while doing the conversion.

shared class FloatConverter() satisfies Converter<JFloat,Float> {
    shared actual Float from(JFloat t) => t.floatValue();

shared actual Clazz<JFloat> fromType() => javaClass<JFloat>();

shared actual JFloat? to(Float? u) => if(exists u) then JFloat(u) else null;

shared actual Clazz<Float> toType() => javaClass<Float>();
}

But when the Book(title, price) constructor is invoked by the convertToBook() method the constructor adds additional decimal places to price field as below:

10:10:00,960 INFO  [stdout] (default task-1) Price::::======45.9900016784668
10:10:00,961 INFO  [stdout] (default task-1) Price::::======45.9900016784668
10:10:00,961 INFO  [stdout] (default task-1) Price::::======17.989999771118164
10:10:00,961 INFO  [stdout] (default task-1) Price::::======33.9900016784668

Why is Ceylon adding additional decimal places and is there a way to get around it?

Enrique Zamudio

unread,
Feb 10, 2017, 5:32:44 AM2/10/17
to ceylon-users
A Ceylon Float is compiled to a Java double. Those decimals are not being added by Ceylon but by the conversion from Float to Double; try this in pure Java and you should see the same result:

float f=0.99;
double d=f;
System.out.println(d);

Anyway for something like price in Java usually BigDecimal is recommended.

Mohammad Als

unread,
Feb 11, 2017, 3:49:56 AM2/11/17
to ceylon-users
import java.lang {JFloat=Float,Double}

shared void run(){
    print(Float(JFloat(49.99).floatValue()));
    print(Float(Double(49.99).doubleValue()));
    print(Float(49.99));
}

Output:
49.9900016784668
49.99
49.99

Java Double gets converted to Ceylon Float without additional precision values, whereas during Java Float to Ceylon Float conversion, additional precision values gets added.
I was expecting the results to be vice-versa. :)

Paul Ebermann

unread,
Feb 11, 2017, 8:41:07 PM2/11/17
to ceylon...@googlegroups.com
Hi,

2017-02-11 9:49 GMT+01:00 Mohammad Als <karim...@gmail.com>:
import java.lang {JFloat=Float,Double}

shared void run(){
    print(Float(JFloat(49.99).floatValue()));
    print(Float(Double(49.99).doubleValue()));
    print(Float(49.99));
}

Output:
49.9900016784668
49.99
49.99

Java Double gets converted to Ceylon Float without additional precision values, whereas during Java Float to Ceylon Float conversion, additional precision values gets added.
I was expecting the results to be vice-versa. :)

You see artifacts from decimal → binary → decimal conversion, with added and removed precision inbetween.
There is no way to represent the decimal fraction 49.99 exactly as a (finite) binary fraction, i.e. neither as a Java float or a Java double (= Ceylon Float).

http://www.exploringbinary.com/binary-converter/ tells me the binary value is 110001.11111101011100001010001111010111000010100011110101110000101000111101011100001010001111010111000010100011110101110000101000111101011100001010001111010111000010100011110101110000101000... with the periodic 11110101110000101000 part after the initial 110001.11.


So when you type the literal 49.99, the Ceylon compiler chooses the Float value which is nearest to the decimal value – that is 110001.11111101011100001010001111010111000010100011111 (last digit is result of rounding up).
In decimal, that would be 49.99000000000000198951966012828052043914794921875. (Binary → Decimal is always possible exactly, using the same number of fractional digits).

When trying to format this number to a string, the formatting engine (which is the same for Java and Ceylon on JVM, I think) is smart enough not to print all decimals, but just enough that it can be converted to this same Float – so now you get 49.99 back again. (The same is valid for Java's double, and it would work the same in Java with 49.99f and a float.)

Converting a Ceylon Float to a Java Double and back basically does nothing, so your second and third lines are the same.

In the first line, you are converting your Ceylon Float (= Java double) into a Java float. That one has less precision, so it is rounded to the nearest (Java) float value 110001.111111010111000011 (which is slightly bigger in this case – the last 1 was the result of rounding up). The result of that is then converted back into a Ceylon Float (which is basically appending some binary zeros at the end), and then conversion back to a (decimal) String. The exact decimal value would be 49.990001678466796875. This is now further away from the decimal 49.99, thus the converter needs to add some more digits so the decimal representation does uniquely identify your (binary) Float number.


I would guess directly converting the Java Float to String would output 49.99 again – try  print(JFloat(49.99));


Paul
Reply all
Reply to author
Forward
0 new messages