@GeoffreyDeSmet



1 + 9 == 10
but
0.01 + 0.09 != 0.10

by Geoffrey De Smet

Who am I?

  • Creator of OptaPlanner
    • Open Source Solver AI for Java
  • Contributor to 30+ Open Source projects

Human languages

English

  • Ear
  • Bear
  • Hear
  • Here
  • There

Credits: @loicsuberville

Dutch

  • de man
  • de vrouw
  • de jongen
  • het meisje

French

Math

  • 1 + 9 = 10
  • 10 + 90 = 100
  • 0.01 + 0.09 = 0.10

Math is perfect

Computer languages

~ math?

Numbers

Average of 2 numbers

public int average(int a, int b) {
    return (a + b) / 2;
}

Input

average(1000, 2000)
1500
average(1000000, 2000000)
1500000
average(1000000000, 2000000000)
-647483648 // Overflow on a + b

Ariane 5 (1996)

Lift off

Overflow

The horizontal bias variable was unprotected from overflow because it was thought that it was "physically limited or that there was a large margin of error".

Integer overflow

Problem

public int average(int a, int b) {
    return (a + b) / 2;
}
public int average(int a, int b) {
    double c = (a + b) / 2.0;
    return (int) c;
}

Solution

public int average(int a, int b) {
    long c = (((long) a) + b) / 2L;
    return (int) c;
}
average(1000000000, 2000000000)
1500000000

Sum of floating point numbers

Input

1.0 + 9.0
10.0
0.1 + 0.9
1.0
0.01 + 0.09
0.09999999999999999 // Compound rounding error

Double precision floating point (Wikipedia)

Source: https://en.wikipedia.org/wiki/Double-precision_floating-point_format

Translation: every double value is
an integer divided by a multiplication of 2

Failure rate

Sum of 2 numbers between 0.00 and 1.00


  0.01 + 0.05 != 0.06
  0.01 + 0.06 != 0.07
  0.01 + 0.09 != 0.10
  0.01 + 0.14 != 0.15
  0.01 + 0.17 != 0.18
  0.01 + 0.20 != 0.21
  0.01 + 0.23 != 0.24
  0.01 + 0.28 != 0.29
  ...
  0.99 + 0.87 != 1.86
  0.99 + 0.90 != 1.89
  0.99 + 0.92 != 1.91

2106 failures (21%) out of 10000 sums

Failure rate: 21%

Compound rounding error

Problem

public double sum(double a, double b) {
    return a + b;
}

Solution

public BigDecimal sum(BigDecimal a, BigDecimal b) {
    return a.add(b);
}
sum(new BigDecimal("0.01"), new BigDecimal("0.09"))
0.10

or

public long sum(long aMillis, long bMillis) {
    return a + b; // Faster than BigDecimal.add()
}
sum(10, 90) // 10 millis is 0.010 and 90 millis is 0.090
100 // 100 millis is 0.100

Side effect

Floating point arithmetic is not associative

double a = 0.0;
for (int i = 0; i < 1000000; i++) {
    a += 0.03 + 0.02 + 0.01;
    System.out.println(a);
    a -= 0.01 + 0.02 + 0.03;
}
0.060000000000000005
0.06000000000000001
0.06000000000000002
0.060000000000000026
0.06000000000000003
0.06000000000000004
...
0.06000000000069386
0.060000000000693866
0.06000000000069387
0.06000000000069388
0.06000000000069389

Patriot Missile Failure (1991)

The small chopping error, when multiplied by the large number giving the time in tenths of a second, led to a significant error.

The Patriot missile battery had been in operation for 100 hours, by which time the system's internal clock had drifted by one-third of a second. Due to the missile's speed this was equivalent to a miss distance of 600 meters.

Total calculation

Long and double are 64-bit

Input

double a = 9000L;
9000.0
double a = 9000000000L;
9000000000.0
double a = 9007199254740993L;
9007199254740992.0 // Rounding error
double a = 9007199254740992.0;
a == a + 1.0
true // Wrong

How do I make my code safe?

QA cheat sheet numbers

Expression Actual result
1000000000 + 2000000000 -1294967296
0.01 + 0.09 0.09999999999999999
0.01 + 0.05 0.060000000000000005
0.01 + 0.02 + 0.03 0.06
0.03 + 0.02 + 0.01 0.060000000000000005
(double) 9007199254740993L 9007199254740992.0
9007199254740992.0 + 1.0 9007199254740992.0
9007199254740992.0 + 3.0 9007199254740996.0

Text

Valid name

public boolean isValidFirstName(String firstName) {
    return firstName.matches("\w+");
}

Input

isValidFirstName("Alexander")
true
isValidFirstName("4l3x4nd3r")
false
isValidFirstName("Chloé")) // French name
false // Wrong
isValidFirstName("りく")) // Riku (Japanese name)
false // Wrong

Regular expressions for non-english

Problem

public boolean isValidFirstName(String firstName) {
    return firstName.matches("\w+");
}

Solution

public boolean isValidFirstName(String firstName) {
    return firstName.matches("(?U)\w+");
}
isValidFirstName("Chloé")) // French name Chloe
true
isValidFirstName("りく")) // Japanese name Riku
true

Typical encoding issues

Default encoding

  • Linux: UTF-8
  • Mac: UTF-8
  • Windows (Western Europe): windows-1252
    • Windows JDK 18+: UTF-8 (JEP 400)

Escape characters

An escape character is a character
which invokes an alternative interpretation
on subsequent characters in a character sequence.

  • Java string literal: \ (backslash)
  • JSON: \ (backslash)
  • XML: & (ampersand)

Failure to handle escape characters correctly
often causes security issues (SQL inject, XSS, ...)

Digital TV

Imagine Me &amp; You

QA cheat sheet text

String Why
Allô (French telephone hello) ISO 8859-1
€ (euro) Since 1996, not in 8859-1
Hallå (Swedish hello) Mostly ASCII
Здравствуйте (Russian hello) Looks a bit like ASCII
こんにちは (Japanese hello) No ASCII whatsoever
≠ (different) ⇔ (iff) ∑ (sum) Math symbols
\ (backslash) " (double) ' (single) Java/SQL/... special chars
& (ampersand) < (lower than) XML special chars
` (slant) # (number sign) $ (dollar) Shell special chars

Dates and time

Days between 2 dates

private static final long MILLISECONDS_IN_DAY = 24L * 60L * 60L * 1000L;

public long daysBetween(Date a, Date b) {
    return (b.getTime() - a.getTime()) / MILLISECONDS_IN_DAY;
}

Input

daysBetween(parse("2017-02-01"), parse("2017-02-02"))
1
daysBetween(parse("2017-03-12"), parse("2017-03-13"))
1 // In UK and France
0 // In US, because of Daylight Saving Time
daysBetween(parse("2017-03-26"), parse("2017-03-27"))
0 // In UK and France because of Daylight Saving Time
1 // In US



One day is usually 24 hours.

Days are not a multiple of hours

Problem

private static final long MILLISECONDS_IN_DAY = 24L * 60L * 60L * 1000L;

public long daysBetween(Date a, Date b) {
    return (b.getTime() - a.getTime()) / MILLISECONDS_IN_DAY;
}

Solution: Never use java.util.Date!

public long daysBetween(LocalDate a, LocalDate b) {
    return ChronoUnit.DAYS.between(a, b);
}
TimeZone.setDefault(TimeZone.getTimeZone("America/New_York"));
daysBetween(LocalDate.of(2017, 3, 12), LocalDate.of(2017, 3, 13))
1
daysBetween(LocalDate.of(2017, 3, 26), LocalDate.of(2017, 3, 27))
1

Always use java.time classes.

Tip

Run all your tests in a different timezone

java -Duser.timezone=America/New_York ...
java -Duser.timezone=Europe/Paris ...

QA cheat sheet dates and time

Expression Actual result
From 2017-03-12 00:00
to 2017-03-13 00:00
23 hours in America/New_York
From 2017-03-26 00:00
to 2017-03-27 00:00
23 hours in Europe/Paris
From 2017-10-29 00:00
to 2017-10-30 00:00
25 hours in Europe/Paris
From 2017-11-05 00:00
to 2017-11-06 00:00
25 hours in America/New_York

Q & A

QA cheat sheet

QR code

@GeoffreyDeSmet