TL;DR => use BigDecimal for Java and Boost.Multiprecision or std::decimal for C++ to handle money, detailed explanation with examples in the next article
Now that we have a spoiler and the problem is solved, let's see why we cannot just use double
without trouble.
The word itself is a bit oldish, for zoomers an existence of non 64 bit processors should sound like a floppy disk.
Here and after Java stands for Java 17 and C++ stands for C++ 20, if not specified differently.
Let's start from basics, and here is IEEE 754, this is a technical standard for the floating-point arithmetic's, several concepts are to be mentioned : infinity, NaN, signed zero : -0, +0.
Both Java and C++ implement IEEE 754 for float and double types, it has been a bit of a challenge for Java regarding the double rounding problem, see here: strictfp.
The first surprise arises when an engineer sees that there are -0
and 0
and they are equal, are they ? They are equal as per IEEE 754, so, if I sort an array in Java and C++, am I going to get -0
and 0
mixed ?
And the answer is different for both languages, let's see.
IEEE 754 : signed zero in C++
Check the three-way comparison.
Here is an example :
#include <compare>
#include <iostream>
int main()
{
double foo = -0.0;
double bar = 0.0;
std::cout<< (foo==bar ? "-0 and 0 are equal" : "-0 and 0 are NOT equal" )
<<std::endl;
auto res = foo <=> bar;
if (res < 0)
std::cout << "-0 is less than 0";
else if (res > 0)
std::cout << "-0 is greater than 0";
else // (res == 0)
std::cout << "-0 and 0 are equal";
}
#include <iostream>
#include <iterator>
#include <bits/stdc++.h>
int main()
{
auto show_me_the_money = [](auto& arr) {
std::copy(std::begin(arr), std::end(arr),
std::ostream_iterator<double>(std::cout, " "));
std::cout<<std::endl;
};
double arr[] = {1.0, -0.0, 0.0, -0.0, -1.0};
show_me_the_money(arr);
std::sort(arr, arr + sizeof(arr)/sizeof(arr[0]), std::less<double>());
show_me_the_money(arr);
}
Let me show you few tricks on how can we make precede the -0
before 0
.
If C++ had been taught in Hogwarts it would have been taught by Severus Snape, as you can cast pretty much everything. Here are different ways to get a sign of a zero :
#include <iostream>
#include<math.h>
union u {
double d;
uint64_t i;
};
int main()
{
auto cast = [](auto& d) {
uint64_t i = *reinterpret_cast<uint64_t *>(&d);
return i;
};
auto unite = [](auto& d) {
u _u;
_u.d =d;
return _u.i;
};
double foo = -0.0;
double bar = 0.0;
std::cout<<cast(foo)<<" "<<cast(bar)<<std::endl;
std::cout<<unite(foo)<<" "<<unite(bar)<<std::endl;
std::cout<<signbit(foo)<<" "<<signbit(bar)<<std::endl;
}
I'll use the signbit
function :
#include <iostream>
#include <iterator>
#include <bits/stdc++.h>
#include<math.h>
int main()
{
auto show_me_the_money = [](auto& arr) {
std::copy(std::begin(arr), std::end(arr),
std::ostream_iterator<double>(std::cout, " "));
std::cout<<std::endl;
};
std::less<double> less_double;
auto less_zero = [&]( const auto& lhs, const auto& rhs ){
auto res = less_double(lhs, rhs);
return res != 0 ? res : signbit (lhs) > signbit(rhs);
};
double arr[] = {1.0, -0.0, 0.0, -0.0, -1.0};
show_me_the_money(arr);
std::sort(arr, arr + sizeof(arr)/sizeof(arr[0]), less_zero);
show_me_the_money(arr);
}
With a custom comparator I can order -0
and 0
. The question is, how can we convert -0
to 0
?
The answer is pretty simple : you have to add 0
, let me show you :
#include <iostream>
#include <iterator>
#include <bits/stdc++.h>
int main()
{
auto show_me_the_money = [](auto& arr) {
std::copy(std::begin(arr), std::end(arr),
std::ostream_iterator<double>(std::cout, " "));
std::cout<<std::endl;
};
double arr[] = {1.0, -0.0, 0.0, -0.0, -1.0};
show_me_the_money(arr);
std::for_each(std::begin(arr), std::end(arr), [](double& d) { d+=0.0;});
std::sort(arr, arr + sizeof(arr)/sizeof(arr[0]), std::less<double>());
show_me_the_money(arr);
}
Alright, we have managed to handle zeros in C++.
IEEE 754 : signed zero in Java
Things in Java world looks simpler, see the source code of Double.compare
method for the details.
I've used w3schools Java compiler for tests :
public class Main {
public static void main(String args[]) {
double foo = -0.0;
double bar = 0.0;
System.out.println("-0 and 0 are equal " + (foo == bar));
System.out.println("-0 and 0 are ordered " + Double.compare(foo,bar));
}
}
Sorting zeros in an array is straight forward :
import java.util.*;
public class Main {
public static void main(String args[]) {
double [] arr = {1.0, -0.0, 0.0, -0.0, -1.0};
System.out.println(Arrays.toString(arr));
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
Same adding 0
trick applies :
import java.util.*;
public class Main {
public static void main(String args[]) {
double [] arr = {1.0, -0.0, 0.0, -0.0, -1.0};
System.out.println(Arrays.toString(arr));
arr = Arrays.stream(arr).map(v -> v+= 0.0).toArray();
System.out.println(Arrays.toString(arr));
}
}
Summary
Both C++ and Java implement IEEE 754 and following the standard you have two zeros which are equal. The results of a software we do might be used elsewhere, and having values with minus sign to be mixed with values with no sign can lead to the dramatic consequences. Still neither double nor float should be used for the money handling and I'll cover the reasons in the next article. Take care!
Top comments (0)