Merge branch 'Issue#318_fp-equality' of https://github.com/Crzyrndm/xlnt into Crzyrndm-Issue#318_fp-equality

This commit is contained in:
Thomas Fussell 2019-07-06 14:20:53 -04:00
commit 90b672cf6b
14 changed files with 415 additions and 89 deletions

View File

@ -23,8 +23,9 @@
#pragma once
#include <xlnt/xlnt_config.hpp>
#include <xlnt/utils/exceptions.hpp>
#include "xlnt/xlnt_config.hpp"
#include "xlnt/utils/exceptions.hpp"
#include "../source/detail/numeric_utils.hpp"
#include <type_traits>
namespace xlnt {
@ -49,6 +50,23 @@ class optional
using set_move_noexcept_t = typename std::conditional<std::is_nothrow_move_constructible<T>{} && std::is_nothrow_move_assignable<T>{}, std::true_type, std::false_type>::type;
using clear_noexcept_t = typename std::conditional<std::is_nothrow_destructible<T>{}, std::true_type, std::false_type>::type;
#endif
/// <summary>
/// Default equality operation, just uses operator==
/// </summary>
template <typename U = T, typename std::enable_if<!std::is_floating_point<U>::value>::type * = nullptr>
constexpr bool compare_equal(const U &lhs, const U &rhs) const
{
return lhs == rhs;
}
/// <summary>
/// equality operation for floating point numbers. Provides "fuzzy" equality
/// </summary>
template <typename U = T, typename std::enable_if<std::is_floating_point<U>::value>::type * = nullptr>
constexpr bool compare_equal(const U &lhs, const U &rhs) const
{
return detail::float_equals(lhs, rhs);
}
public:
/// <summary>
@ -281,7 +299,8 @@ public:
{
return true;
}
return value_ref() == other.value_ref();
// equality is overloaded to provide fuzzy equality when T is a fp number
return compare_equal(value_ref(), other.value_ref());
}
/// <summary>

View File

@ -1,44 +0,0 @@
// Copyright (c) 2014-2018 Thomas Fussell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, WRISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE
//
// @license: http://www.opensource.org/licenses/mit-license.php
// @author: see AUTHORS file
#pragma once
#include <xlnt/xlnt_config.hpp>
#include <sstream>
namespace xlnt {
/// <summary>
/// Takes in any nuber and outputs a string form of that number which will
/// serialise and deserialise without loss of precision
/// </summary>
template <typename Number>
std::string serialize_number_to_string(Number num)
{
// more digits and excel won't match
constexpr int Excel_Digit_Precision = 15; //sf
std::stringstream ss;
ss.precision(Excel_Digit_Precision);
ss << num;
return ss.str();
}
}

View File

@ -25,6 +25,7 @@
#include <xlnt/xlnt_config.hpp>
#include <xlnt/utils/optional.hpp>
#include "../source/detail/numeric_utils.hpp"
namespace xlnt {
@ -59,7 +60,7 @@ inline bool operator==(const sheet_format_properties &lhs, const sheet_format_pr
{
return lhs.base_col_width == rhs.base_col_width
&& lhs.default_column_width == rhs.default_column_width
&& lhs.default_row_height == rhs.default_row_height
&& detail::float_equals(lhs.default_row_height, rhs.default_row_height)
&& lhs.dy_descent == rhs.dy_descent;
}

View File

@ -43,7 +43,6 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-documentation-unknown-command") # ignore unknown commands in Javadoc-style comments
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-pragmas") # ignore Windows and GCC pragmas
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-warning-option") # ignore Windows and GCC pragmas
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-float-equal") # don't warn on uses of == for fp types
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-newline-eof") # no longer an issue with post-c++11 standards which mandate include add a newline if neccesary
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-covered-switch-default") # default is often added to switches for completeness or to cover future alternatives
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-exit-time-destructors") # this is just a warning to notify that the destructor will run during exit

View File

@ -22,7 +22,7 @@
// @author: see AUTHORS file
#include <detail/header_footer/header_footer_code.hpp>
#include <xlnt/utils/serialisation_utils.hpp>
#include <detail/numeric_utils.hpp>
namespace xlnt {
namespace detail {

View File

@ -33,6 +33,7 @@
#include <xlnt/utils/optional.hpp>
#include <detail/implementations/format_impl.hpp>
#include <detail/implementations/hyperlink_impl.hpp>
#include "../numeric_utils.hpp"
namespace xlnt {
namespace detail {
@ -75,7 +76,7 @@ inline bool operator==(const cell_impl &lhs, const cell_impl &rhs)
&& lhs.is_merged_ == rhs.is_merged_
&& lhs.phonetics_visible_ == rhs.phonetics_visible_
&& lhs.value_text_ == rhs.value_text_
&& lhs.value_numeric_ == rhs.value_numeric_
&& float_equals(lhs.value_numeric_, rhs.value_numeric_)
&& lhs.formula_ == rhs.formula_
&& lhs.hyperlink_ == rhs.hyperlink_
&& (lhs.format_.is_set() == rhs.format_.is_set() && (!lhs.format_.is_set() || *lhs.format_.get() == *rhs.format_.get()))

View File

@ -0,0 +1,119 @@
// Copyright (c) 2014-2018 Thomas Fussell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, WRISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE
//
// @license: http://www.opensource.org/licenses/mit-license.php
// @author: see AUTHORS file
#pragma once
#include <xlnt/xlnt_config.hpp>
#include <cmath>
#include <limits>
#include <sstream>
#include <type_traits>
namespace xlnt {
namespace detail {
/// <summary>
/// Takes in any number and outputs a string form of that number which will
/// serialise and deserialise without loss of precision
/// </summary>
template <typename Number>
std::string serialize_number_to_string(Number num)
{
// more digits and excel won't match
constexpr int Excel_Digit_Precision = 15; //sf
std::stringstream ss;
ss.precision(Excel_Digit_Precision);
ss << num;
return ss.str();
}
/// <summary>
/// constexpr abs
/// </summary>
template <typename Number>
constexpr Number abs(Number val)
{
return (val < Number{0}) ? -val : val;
};
/// <summary>
/// constexpr max
/// </summary>
template <typename NumberL, typename NumberR>
constexpr typename std::common_type<NumberL, NumberR>::type max(NumberL lval, NumberR rval)
{
return (lval < rval) ? rval : lval;
}
/// <summary>
/// constexpr min
/// </summary>
template <typename NumberL, typename NumberR>
constexpr typename std::common_type<NumberL, NumberR>::type min(NumberL lval, NumberR rval)
{
return (lval < rval) ? lval : rval;
}
/// <summary>
/// Floating point equality requires a bit of fuzzing due to the imprecise nature of fp calculation
/// References:
/// - Several blogs/articles were referenced with the following being the most useful
/// -- https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
/// -- http://realtimecollisiondetection.net/blog/?p=89
/// - Testing Frameworks {Catch2, Boost, Google}, primarily for selecting the default scale factor
/// -- None of these even remotely agree
/// </summary>
template <typename EpsilonType = float, // the type to extract epsilon from
typename LNumber, typename RNumber> // parameter types (deduced)
bool
float_equals(const LNumber &lhs, const RNumber &rhs,
int epsilon_scale = 20) // scale the "fuzzy" equality. Higher value gives a more tolerant comparison
{
// a type that lhs and rhs can agree on
using common_t = typename std::common_type<LNumber, RNumber>::type;
// asserts for sane usage
static_assert(std::is_floating_point<LNumber>::value || std::is_floating_point<RNumber>::value,
"Using this function with two integers is just wasting time. Use ==");
static_assert(std::numeric_limits<EpsilonType>::epsilon() < EpsilonType{1},
"epsilon >= 1.0 will cause all comparisons to return true");
// NANs always compare false with themselves
if (std::isnan(lhs) || std::isnan(rhs))
{
return false;
}
// epsilon type defaults to float because even if both args are a higher precision type
// either or both could have been promoted by prior operations
// if a higher precision is required, the template type can be changed
constexpr common_t epsilon = static_cast<common_t>(std::numeric_limits<EpsilonType>::epsilon());
// the "epsilon" then needs to be scaled into the comparison range
// epsilon for numeric_limits is valid when abs(x) <1.0, scaling only needs to be upwards
// in particular, this prevents a lhs of 0 from requiring an exact comparison
// additionally, a scale factor is applied.
common_t scaled_fuzz = epsilon_scale * epsilon * max(max(xlnt::detail::abs<common_t>(lhs),
xlnt::detail::abs<common_t>(rhs)), // |max| of parameters.
common_t{1}); // clamp
return ((lhs + scaled_fuzz) >= rhs) && ((rhs + scaled_fuzz) >= lhs);
}
} // namespace detail
} // namespace xlnt

View File

@ -33,12 +33,12 @@
#include <detail/serialization/vector_streambuf.hpp>
#include <detail/serialization/xlsx_producer.hpp>
#include <detail/serialization/zstream.hpp>
#include <detail/numeric_utils.hpp>
#include <xlnt/cell/cell.hpp>
#include <xlnt/cell/hyperlink.hpp>
#include <xlnt/packaging/manifest.hpp>
#include <xlnt/utils/path.hpp>
#include <xlnt/utils/scoped_enum_hash.hpp>
#include <xlnt/utils/serialisation_utils.hpp>
#include <xlnt/workbook/workbook.hpp>
#include <xlnt/workbook/workbook_view.hpp>
#include <xlnt/worksheet/header_footer.hpp>

View File

@ -507,12 +507,12 @@ bool izstream::read_central_header()
// Find the header
// NOTE: this assumes the zip file header is the last thing written to file...
source_stream_.seekg(0, std::ios_base::end);
auto end_position = static_cast<std::size_t>(source_stream_.tellg());
auto end_position = source_stream_.tellg();
auto max_comment_size = std::uint32_t(0xffff); // max size of header
auto read_size_before_comment = std::uint32_t(22);
std::uint32_t read_start = max_comment_size + read_size_before_comment;
std::streamoff read_start = max_comment_size + read_size_before_comment;
if (read_start > end_position)
{
@ -536,11 +536,14 @@ bool izstream::read_central_header()
}
auto found_header = false;
std::size_t header_index = 0;
std::streamoff header_index = 0;
for (std::size_t i = 0; i < static_cast<std::size_t>(read_start - 3); ++i)
for (std::streamoff i = 0; i < read_start - 3; ++i)
{
if (buf[i] == 0x50 && buf[i + 1] == 0x4b && buf[i + 2] == 0x05 && buf[i + 3] == 0x06)
if (buf[static_cast<std::size_t>(i)] == 0x50
&& buf[static_cast<std::size_t>(i) + 1] == 0x4b
&& buf[static_cast<std::size_t>(i) + 2] == 0x05
&& buf[static_cast<std::size_t>(i) + 3] == 0x06)
{
found_header = true;
header_index = i;
@ -554,7 +557,7 @@ bool izstream::read_central_header()
}
// seek to end of central header and read
source_stream_.seekg(end_position - (read_start - static_cast<std::ptrdiff_t>(header_index)));
source_stream_.seekg(end_position - (read_start - header_index));
/*auto word = */ read_int<std::uint32_t>(source_stream_);
auto disk_number1 = read_int<std::uint16_t>(source_stream_);

View File

@ -22,6 +22,7 @@
// @license: http://www.opensource.org/licenses/mit-license.php
// @author: see AUTHORS file
#include <xlnt/worksheet/page_margins.hpp>
#include "detail/numeric_utils.hpp"
namespace xlnt {
@ -91,11 +92,11 @@ void page_margins::footer(double footer)
bool page_margins::operator==(const page_margins &rhs) const
{
return top_ == rhs.top_
&& left_ == rhs.left_
&& right_ == rhs.right_
&& header_ == rhs.header_
&& footer_ == rhs.footer_;
return detail::float_equals(top_, rhs.top_)
&& detail::float_equals(left_,rhs.left_)
&& detail::float_equals(right_, rhs.right_)
&& detail::float_equals(header_, rhs.header_)
&& detail::float_equals(footer_, rhs.footer_);
}
} // namespace xlnt

View File

@ -22,6 +22,7 @@
// @license: http://www.opensource.org/licenses/mit-license.php
// @author: see AUTHORS file
#include <xlnt/worksheet/page_setup.hpp>
#include "detail/numeric_utils.hpp"
namespace xlnt {
@ -114,7 +115,7 @@ bool page_setup::operator==(const page_setup &rhs) const
&& fit_to_page_ == rhs.fit_to_page_
&& fit_to_height_ == rhs.fit_to_height_
&& fit_to_width_ == rhs.fit_to_width_
&& scale_ == rhs.scale_;
&& detail::float_equals(scale_, rhs.scale_);
}
} // namespace xlnt

View File

@ -46,10 +46,11 @@
#include <xlnt/worksheet/column_properties.hpp>
#include <detail/constants.hpp>
#include <detail/default_case.hpp>
#include <detail/numeric_utils.hpp>
#include <detail/unicode.hpp>
#include <detail/implementations/cell_impl.hpp>
#include <detail/implementations/workbook_impl.hpp>
#include <detail/implementations/worksheet_impl.hpp>
#include <detail/unicode.hpp>
namespace {
@ -102,7 +103,7 @@ void worksheet::create_named_range(const std::string &name, const range_referenc
throw invalid_parameter(); //("named range name must be outside the range A1-XFD1048576");
}
}
catch (xlnt::invalid_cell_reference&)
catch (xlnt::invalid_cell_reference &)
{
// name is not a valid reference, that's good
}
@ -732,7 +733,8 @@ void worksheet::clear_row(row_t row)
{
it = d_->cell_map_.erase(it);
}
else {
else
{
++it;
}
}
@ -930,29 +932,28 @@ bool worksheet::compare(const worksheet &other, bool reference) const
if (d_->parent_ != other.d_->parent_) return false;
for (auto &cell : d_->cell_map_)
for (auto &cell : d_->cell_map_)
{
if (other.d_->cell_map_.find(cell.first) == other.d_->cell_map_.end())
{
if (other.d_->cell_map_.find(cell.first) == other.d_->cell_map_.end())
{
return false;
}
xlnt::cell this_cell(&cell.second);
xlnt::cell other_cell(&other.d_->cell_map_[cell.first]);
if (this_cell.data_type() != other_cell.data_type())
{
return false;
}
if (this_cell.data_type() == xlnt::cell::type::number
&& std::fabs(this_cell.value<double>() - other_cell.value<double>()) > 0.0)
{
return false;
}
return false;
}
xlnt::cell this_cell(&cell.second);
xlnt::cell other_cell(&other.d_->cell_map_[cell.first]);
if (this_cell.data_type() != other_cell.data_type())
{
return false;
}
if (this_cell.data_type() == xlnt::cell::type::number
&& !detail::float_equals(this_cell.value<double>(), other_cell.value<double>()))
{
return false;
}
}
// todo: missing some comparisons
if (d_->auto_filter_ == other.d_->auto_filter_ && d_->views_ == other.d_->views_
@ -1186,12 +1187,12 @@ bool worksheet::has_phonetic_properties() const
return d_->phonetic_properties_.is_set();
}
const phonetic_pr& worksheet::phonetic_properties() const
const phonetic_pr &worksheet::phonetic_properties() const
{
return d_->phonetic_properties_.get();
}
void worksheet::phonetic_properties(const phonetic_pr& phonetic_props)
void worksheet::phonetic_properties(const phonetic_pr &phonetic_props)
{
d_->phonetic_properties_.set(phonetic_props);
}

View File

@ -16,6 +16,7 @@ endif()
file(GLOB CELL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/cell/*.cpp)
file(GLOB DRAWING_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/drawing/*.cpp)
file(GLOB DETAIL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/detail/*.cpp)
file(GLOB PACKAGING_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/packaging/*.cpp)
file(GLOB STYLES_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/styles/*.cpp)
file(GLOB UTILS_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/utils/*.cpp)
@ -25,6 +26,7 @@ file(GLOB WORKSHEET_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/worksheet/*.cpp)
set(TESTS
${CELL_TESTS}
${DRAWING_TESTS}
${DETAIL_TESTS}
${PACKAGING_TESTS}
${STYLES_TESTS}
${UTILS_TESTS}
@ -61,6 +63,7 @@ endif()
source_group(helpers FILES ${HELPERS})
source_group(runner FILES ${RUNNER})
source_group(tests\\cell FILES ${CELL_TESTS})
source_group(tests\\detail FILES ${DETAIL_TESTS})
source_group(tests\\packaging FILES ${PACKAGING_TESTS})
source_group(tests\\serialization FILES ${SERIALIZATION_TESTS})
source_group(tests\\styles FILES ${STYLES_TESTS})

View File

@ -0,0 +1,222 @@
// Copyright (c) 2014-2018 Thomas Fussell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, WRISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE
//
// @license: http://www.opensource.org/licenses/mit-license.php
// @author: see AUTHORS file
#include "../../source/detail/numeric_utils.hpp"
#include <helpers/test_suite.hpp>
class numeric_test_suite : public test_suite
{
public:
numeric_test_suite()
{
register_test(test_serialise_number);
register_test(test_float_equals_zero);
register_test(test_float_equals_large);
register_test(test_float_equals_fairness);
register_test(test_min);
register_test(test_max);
register_test(test_abs);
}
void test_serialise_number()
{
// excel serialises numbers as floating point values with <= 15 digits of precision
xlnt_assert(xlnt::detail::serialize_number_to_string(1) == "1");
// trailing zeroes are ignored
xlnt_assert(xlnt::detail::serialize_number_to_string(1.0) == "1");
xlnt_assert(xlnt::detail::serialize_number_to_string(1.0f) == "1");
// one to 1 relation
xlnt_assert(xlnt::detail::serialize_number_to_string(1.23456) == "1.23456");
xlnt_assert(xlnt::detail::serialize_number_to_string(1.23456789012345) == "1.23456789012345");
xlnt_assert(xlnt::detail::serialize_number_to_string(123456.789012345) == "123456.789012345");
xlnt_assert(xlnt::detail::serialize_number_to_string(1.23456789012345e+67) == "1.23456789012345e+67");
xlnt_assert(xlnt::detail::serialize_number_to_string(1.23456789012345e-67) == "1.23456789012345e-67");
}
void test_float_equals_zero()
{
// comparing relatively small numbers (2.3e-6) with 0 will be true by default
const float comp_val = 2.3e-6; // about the largest difference allowed by default
xlnt_assert(0.f != comp_val); // fail because not exactly equal
xlnt_assert(xlnt::detail::float_equals(0.0, comp_val));
xlnt_assert(xlnt::detail::float_equals(0.0, -comp_val));
// fail because diff is out of bounds for fuzzy equality
xlnt_assert(!xlnt::detail::float_equals(0.0, comp_val + 0.1e-6));
xlnt_assert(!xlnt::detail::float_equals(0.0, -(comp_val + 0.1e-6)));
// if the bounds of comparison are too loose, there are two tweakable knobs to tighten the comparison up
//==========================================================
// #1: reduce the epsilon_scale (default is 20)
// This can bring the range down to FLT_EPSILON (scale factor of 1)
xlnt_assert(!xlnt::detail::float_equals(0.0, comp_val, 10));
const float closer_comp_val = 1.1e-6;
xlnt_assert(xlnt::detail::float_equals(0.0, closer_comp_val, 10));
xlnt_assert(!xlnt::detail::float_equals(0.0, closer_comp_val + 0.1e-6, 10));
xlnt_assert(xlnt::detail::float_equals(0.0, -closer_comp_val, 10));
xlnt_assert(!xlnt::detail::float_equals(0.0, -(closer_comp_val + 0.1e-6), 10));
//==========================================================
// #2: specify the epsilon source as a higher precision type (e.g. double)
// This makes the epsilon range quite significantly less
xlnt_assert(!xlnt::detail::float_equals<double>(0.0, comp_val));
xlnt_assert(!xlnt::detail::float_equals<double>(0.0, closer_comp_val));
const float tiny_comp_val = 4.4e-15;
xlnt_assert(xlnt::detail::float_equals<double>(0.0, tiny_comp_val));
xlnt_assert(!xlnt::detail::float_equals<double>(0.0, tiny_comp_val + 0.1e-15));
xlnt_assert(xlnt::detail::float_equals<double>(0.0, -tiny_comp_val));
xlnt_assert(!xlnt::detail::float_equals<double>(0.0, -(tiny_comp_val + 0.1e-15)));
//==========================================================
// #3: combine #1 & #2
// for the tightest default precision, double with a scale of 1
xlnt_assert(!xlnt::detail::float_equals<double>(0.0, comp_val, 1));
xlnt_assert(!xlnt::detail::float_equals<double>(0.0, closer_comp_val, 1));
xlnt_assert(!xlnt::detail::float_equals<double>(0.0, tiny_comp_val, 1));
const float really_tiny_comp_val = 2.2e-16; // the limit is +/- std::numeric_limits<double>::epsilon()
xlnt_assert(xlnt::detail::float_equals<double>(0.0, really_tiny_comp_val, 1));
xlnt_assert(!xlnt::detail::float_equals<double>(0.0, really_tiny_comp_val + 0.1e-16, 1));
xlnt_assert(xlnt::detail::float_equals<double>(0.0, -really_tiny_comp_val, 1));
xlnt_assert(!xlnt::detail::float_equals<double>(0.0, -(really_tiny_comp_val + 0.1e-16), 1));
//==========================================================
// in the world of floats, 2.2e-16 is still significantly different to 0.f (smallest representable float is around 1e-38)
// if comparisons are known to involve extremely small numbers (such that +/- 2.2e-16 is too large a band),
// a type that specialises std::numeric_limits::epsilon may be passed as the first template parameter
// the type itself doesn't actually need to have any behaviour as it is only used as the source for epsilon
// struct super_precise{};
// namespace std {
// template<> numeric_limits<super_precise> {
// double epsilon() {
// return 1e-30;
// }
// }
// }
// float_equals<double>(0.0, 2e-30, 1); // returns true
// float_equals<super_precise>(0.0, 2e-30, 1); // returns false
}
void test_float_equals_large()
{
const float compare_to = 20e6;
// fp math with arguments of different magnitudes is wierd
xlnt_assert(compare_to == compare_to + 1); // x == x + 1 ...
xlnt_assert(compare_to != compare_to + 10); // x != x + 10
xlnt_assert(compare_to != compare_to - 10); // x != x - 10
// if the same epsilon was used for comparison of large values as the values around one
// we'd have all the issues around zero again
xlnt_assert(xlnt::detail::float_equals(compare_to, compare_to + 49));
xlnt_assert(!xlnt::detail::float_equals(compare_to, compare_to + 50));
xlnt_assert(xlnt::detail::float_equals(compare_to, compare_to - 49));
xlnt_assert(!xlnt::detail::float_equals(compare_to, compare_to - 50));
// float_equals also scales epsilon up to match the magnitude of its arguments
// all the same options are available for increasing/decreasing the precision of the comparison
// however the the epsilon source should always be of equal or lesser precision than the arguments when away from zero
}
void test_float_equals_nan()
{
const float nan = std::nan("");
// nans always compare false
xlnt_assert(!xlnt::detail::float_equals(nan, 0.f));
xlnt_assert(!xlnt::detail::float_equals(nan, nan));
xlnt_assert(!xlnt::detail::float_equals(nan, 1000.f));
}
void test_float_equals_fairness()
{
// tests for parameter ordering dependency
// (lhs ~= rhs) == (rhs ~= lhs)
const double test_val = 1.0;
const double test_diff_pass = 1.192092e-07; // should all pass with this
const double test_diff = 1.192093e-07; // difference enough to provide different results if the comparison is not "fair"
const double test_diff_fails = 1.192094e-07; // should all fail with this
// test_diff_pass
xlnt_assert(xlnt::detail::float_equals<float>((test_val + test_diff_pass), test_val, 1));
xlnt_assert(xlnt::detail::float_equals<float>(test_val, (test_val + test_diff_pass), 1));
xlnt_assert(xlnt::detail::float_equals<float>(-(test_val + test_diff_pass), -test_val, 1));
xlnt_assert(xlnt::detail::float_equals<float>(-test_val, -(test_val + test_diff_pass), 1));
// test_diff
xlnt_assert(xlnt::detail::float_equals<float>((test_val + test_diff), test_val, 1));
xlnt_assert(xlnt::detail::float_equals<float>(test_val, (test_val + test_diff), 1));
xlnt_assert(xlnt::detail::float_equals<float>(-(test_val + test_diff), -test_val, 1));
xlnt_assert(xlnt::detail::float_equals<float>(-test_val, -(test_val + test_diff), 1));
// test_diff_fails
xlnt_assert(!xlnt::detail::float_equals<float>((test_val + test_diff_fails), test_val, 1));
xlnt_assert(!xlnt::detail::float_equals<float>(test_val, (test_val + test_diff_fails), 1));
xlnt_assert(!xlnt::detail::float_equals<float>(-(test_val + test_diff_fails), -test_val, 1));
xlnt_assert(!xlnt::detail::float_equals<float>(-test_val, -(test_val + test_diff_fails), 1));
}
void test_min()
{
// simple
xlnt_assert(xlnt::detail::min(0, 1) == 0);
xlnt_assert(xlnt::detail::min(1, 0) == 0);
xlnt_assert(xlnt::detail::min(0.0, 1) == 0.0); // comparisons between different types just work
xlnt_assert(xlnt::detail::min(1, 0.0) == 0.0);
// negative numbers
xlnt_assert(xlnt::detail::min(0, -1) == -1.0);
xlnt_assert(xlnt::detail::min(-1, 0) == -1.0);
xlnt_assert(xlnt::detail::min(0.0, -1) == -1.0);
xlnt_assert(xlnt::detail::min(-1, 0.0) == -1.0);
// no zeroes
xlnt_assert(xlnt::detail::min(10, -10) == -10.0);
xlnt_assert(xlnt::detail::min(-10, 10) == -10.0);
xlnt_assert(xlnt::detail::min(10.0, -10) == -10.0);
xlnt_assert(xlnt::detail::min(-10, 10.0) == -10.0);
static_assert(xlnt::detail::min(-10, 10.0) == -10.0, "constexpr");
}
void test_max()
{
// simple
xlnt_assert(xlnt::detail::max(0, 1) == 1);
xlnt_assert(xlnt::detail::max(1, 0) == 1);
xlnt_assert(xlnt::detail::max(0.0, 1) == 1.0); // comparisons between different types just work
xlnt_assert(xlnt::detail::max(1, 0.0) == 1.0);
// negative numbers
xlnt_assert(xlnt::detail::max(0, -1) == 0.0);
xlnt_assert(xlnt::detail::max(-1, 0) == 0.0);
xlnt_assert(xlnt::detail::max(0.0, -1) == 0.0);
xlnt_assert(xlnt::detail::max(-1, 0.0) == 0.0);
// no zeroes
xlnt_assert(xlnt::detail::max(10, -10) == 10.0);
xlnt_assert(xlnt::detail::max(-10, 10) == 10.0);
xlnt_assert(xlnt::detail::max(10.0, -10) == 10.0);
xlnt_assert(xlnt::detail::max(-10, 10.0) == 10.0);
static_assert(xlnt::detail::max(-10, 10.0) == 10.0, "constexpr");
}
void test_abs()
{
xlnt_assert(xlnt::detail::abs(0) == 0);
xlnt_assert(xlnt::detail::abs(1) == 1);
xlnt_assert(xlnt::detail::abs(-1) == 1);
xlnt_assert(xlnt::detail::abs(0.0) == 0.0);
xlnt_assert(xlnt::detail::abs(1.5) == 1.5);
xlnt_assert(xlnt::detail::abs(-1.5) == 1.5);
static_assert(xlnt::detail::abs(-1.23) == 1.23, "constexpr");
}
};
static numeric_test_suite x;