@GeoffreyDeSmet
by Geoffrey De Smet
Credits: @loicsuberville
Math is perfect
~ math?
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
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".
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
Input
1.0 + 9.0
10.0
0.1 + 0.9
1.0
0.01 + 0.09
0.09999999999999999 // Compound rounding error
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%
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
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
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.
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
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 |
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
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
An escape character is a character
which invokes an alternative interpretation
on subsequent characters in a character sequence.
Failure to handle escape characters correctly
often causes security issues (SQL inject, XSS, ...)
Imagine Me & You
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 |
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.
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.
Run all your tests in a different timezone
java -Duser.timezone=America/New_York ...
java -Duser.timezone=Europe/Paris ...
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 |