diff --git a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h index 8d225da3b088..b65a98462ac9 100644 --- a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h +++ b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h @@ -111,7 +111,15 @@ static double get_relative_precision_of_to_double(); }; /* end Lazy_exact_nt */ /*! -writes `m` to ostream `out` in an interval format. +writes `m` to ostream `out`. By default a `double` approximation +(`to_double(m)`) is written, as historically. After calling +`CGAL::IO::set_exact_mode(out)`, the exact value `m.exact()` is written instead, +using `NT`'s own output operator. When `NT` has an exact stream representation, +such as a rational type (`Exact_rational`, `Gmpq`), `operator>>` then reconstructs +the same exact number, so exact values can be saved and reloaded without loss. For +`NT` whose output operator writes an approximation (for example `CORE::Expr`, which +writes a decimal), exact mode writes that approximation and the round-trip is not +exact. \relates Lazy_exact_nt */ std::ostream& operator<<(std::ostream& out, const Lazy_exact_nt& m); @@ -122,5 +130,4 @@ reads a `NT` from `in`, then converts it to a `Lazy_exact_nt`. */ std::istream& operator>>(std::istream& in, Lazy_exact_nt& m); - } /* end namespace CGAL */ diff --git a/Number_types/include/CGAL/Lazy_exact_nt.h b/Number_types/include/CGAL/Lazy_exact_nt.h index 32736234fe14..a73c34772823 100644 --- a/Number_types/include/CGAL/Lazy_exact_nt.h +++ b/Number_types/include/CGAL/Lazy_exact_nt.h @@ -1303,7 +1303,18 @@ const Lazy_exact_nt & y){ template std::ostream & operator<< (std::ostream & os, const Lazy_exact_nt & a) -{ return os << CGAL_NTS to_double(a); } +{ + // By default output a double, the historical behaviour. After + // CGAL::IO::set_exact_mode(os), output the exact value via ET's own operator<<. + // The round-trip through operator>> is exact only when ET has an exact stream + // representation (e.g. a rational type such as Exact_rational or Gmpq); it is + // not exact for ET whose operator<< writes an approximation (e.g. CORE::Expr, + // which writes a decimal). Keeping the default keeps this non-breaking. See + // issue #135. + if (IO::is_exact_mode(os)) + return os << a.exact(); + return os << CGAL_NTS to_double(a); +} template std::istream & diff --git a/Number_types/test/Number_types/CMakeLists.txt b/Number_types/test/Number_types/CMakeLists.txt index edd7bd5959af..9a0ba807aec6 100644 --- a/Number_types/test/Number_types/CMakeLists.txt +++ b/Number_types/test/Number_types/CMakeLists.txt @@ -30,6 +30,7 @@ create_single_source_cgal_program("Interval_nt_new.cpp") create_single_source_cgal_program("ioformat.cpp") create_single_source_cgal_program("known_bit_size_integers.cpp") create_single_source_cgal_program("Lazy_exact_nt.cpp") +create_single_source_cgal_program("Lazy_exact_nt_io.cpp") create_single_source_cgal_program("Lazy_exact_nt_new.cpp") create_single_source_cgal_program("leda_bigfloat.cpp") create_single_source_cgal_program("leda_bigfloat_interval.cpp") diff --git a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp new file mode 100644 index 000000000000..4eb153cf802d --- /dev/null +++ b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp @@ -0,0 +1,179 @@ +// Regression test for issue #135: +// Lazy_exact_nt operator<< used to_double(), losing precision and breaking +// round-trip save/load of exact kernel coordinates. +// +// The default operator<< still writes to_double(). CGAL::IO::set_exact_mode(os) +// switches it to write the exact value. For a rational backend (Exact_rational) +// that round-trips losslessly through operator>>; for CORE::Expr, whose operator<< +// writes a decimal approximation, exact mode is not lossless (see Test 9). + +#include +#include + +#include + +#include +#include +#include +#include + +typedef CGAL::Lazy_exact_nt Lazy_nt; + +int main() +{ + // Test 1: exact rational round-trip + { + std::cout << "Test 1: round-trip of 1/3" << std::endl; + Lazy_nt a(CGAL::Exact_rational(1, 3)); + std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); + oss << a; + // Should output "1/3", not "0.333333..." + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\", read back equal" << std::endl; + } + + // Test 2: integer value + { + std::cout << "Test 2: round-trip of 42" << std::endl; + Lazy_nt a(42); + std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); + oss << a; + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + // Test 3: negative rational + { + std::cout << "Test 3: round-trip of -5/7" << std::endl; + Lazy_nt a(CGAL::Exact_rational(-5, 7)); + std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); + oss << a; + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + // Test 4: value that cannot be represented exactly as double + // 1/3 + 1/7 = 10/21, to_double() would give 0.476190476190... + { + std::cout << "Test 4: round-trip of computed 1/3 + 1/7 = 10/21" << std::endl; + Lazy_nt a(CGAL::Exact_rational(1, 3)); + Lazy_nt b(CGAL::Exact_rational(1, 7)); + Lazy_nt c = a + b; + std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); + oss << c; + std::istringstream iss(oss.str()); + Lazy_nt d; + iss >> d; + assert(iss); + assert(c == d); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + // Test 5: very large rational that would lose precision as double + { + std::cout << "Test 5: round-trip of large rational" << std::endl; + CGAL::Exact_rational big; + std::istringstream bigin("99999999999999999999/100000000000000000007"); + bigin >> big; + assert(bigin); + Lazy_nt a(big); + std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); + oss << a; + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + // Test 6: zero + { + std::cout << "Test 6: round-trip of 0" << std::endl; + Lazy_nt a(0); + std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); + oss << a; + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + // Test 7: the default (no set_exact_mode) still writes a double, so existing + // behaviour and user code are unchanged. Issue #135 must remain opt-in. + { + std::cout << "Test 7: default output is still to_double" << std::endl; + Lazy_nt a(CGAL::Exact_rational(1, 3)); + std::ostringstream oss; + oss << a; // no set_exact_mode: lossy, historical behaviour + const std::string s = oss.str(); + assert(s.find('/') == std::string::npos); // a double, not "1/3" + std::cout << " OK: default wrote \"" << s << "\" (double, unchanged)" << std::endl; + } + + // Test 8: set_lossy_mode reverts to the default double output, and the mode + // setters report the previous state (like set_pretty_mode). + { + std::cout << "Test 8: set_lossy_mode reverts, setters report previous state" << std::endl; + Lazy_nt a(CGAL::Exact_rational(1, 3)); + std::ostringstream oss; + const bool was_exact_before = CGAL::IO::set_exact_mode(oss); // default is lossy + assert(!was_exact_before); + assert(CGAL::IO::is_exact_mode(oss)); + const bool was_exact_after = CGAL::IO::set_lossy_mode(oss); // back to lossy + assert(was_exact_after); + assert(!CGAL::IO::is_exact_mode(oss)); + oss << a; // lossy again: a double, not "1/3" + assert(oss.str().find('/') == std::string::npos); + std::cout << " OK: reverted to \"" << oss.str() << "\"" << std::endl; + } + + // Test 9: CORE::Expr regression. Exact mode is lossless only for backends whose + // operator<< writes an exact representation. CORE::Expr::operator<< writes a + // decimal approximation, so exact mode is NOT lossless for it. We check the + // documented behaviour: the default still writes a double, and exact mode writes + // exact()'s (approximate) representation, which parses. + { + typedef CGAL::Lazy_exact_nt Lazy_core; + std::cout << "Test 9: CORE::Expr exact mode is approximate, not lossless" << std::endl; + Lazy_core a = Lazy_core(1) / Lazy_core(3); // 1/3 + + std::ostringstream def; + def << a; // default: a double, unchanged + assert(def.str().find('/') == std::string::npos); + + std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); + oss << a; // exact mode: CORE::Expr's decimal, parses but is not exact + std::istringstream iss(oss.str()); + Lazy_core b; + iss >> b; + assert(iss); + std::cout << " OK: default \"" << def.str() << "\", exact-mode \"" + << oss.str() << "\" (approximate for CORE::Expr)" << std::endl; + } + + std::cout << "All tests passed." << std::endl; + return 0; +} + diff --git a/Stream_support/doc/Stream_support/PackageDescription.txt b/Stream_support/doc/Stream_support/PackageDescription.txt index 7875657b1e57..717835144b7e 100644 --- a/Stream_support/doc/Stream_support/PackageDescription.txt +++ b/Stream_support/doc/Stream_support/PackageDescription.txt @@ -102,10 +102,13 @@ the printing mode. - `CGAL::IO::get_mode()` - `CGAL::IO::is_ascii()` - `CGAL::IO::is_binary()` +- `CGAL::IO::is_exact_mode()` - `CGAL::IO::is_pretty()` - `CGAL::IO::set_mode()` - `CGAL::IO::set_ascii_mode()` - `CGAL::IO::set_binary_mode()` +- `CGAL::IO::set_exact_mode()` +- `CGAL::IO::set_lossy_mode()` - `CGAL::IO::set_pretty_mode()` - \link IOstreamOperators `CGAL::operator>>()` \endlink - \link IOstreamOperators `CGAL::operator<<()` \endlink diff --git a/Stream_support/doc/Stream_support/dependencies b/Stream_support/doc/Stream_support/dependencies index 48f34988e106..45895f3a4955 100644 --- a/Stream_support/doc/Stream_support/dependencies +++ b/Stream_support/doc/Stream_support/dependencies @@ -8,6 +8,7 @@ Manual Mesh_2 Mesh_3 Nef_3 +Number_types Point_set_3 Point_set_processing_3 Polygon diff --git a/Stream_support/include/CGAL/IO/io.h b/Stream_support/include/CGAL/IO/io.h index 86041bfaec45..067ae9a2b4bc 100644 --- a/Stream_support/include/CGAL/IO/io.h +++ b/Stream_support/include/CGAL/IO/io.h @@ -47,6 +47,12 @@ class Static static const int mode = std::ios::xalloc(); return mode; } + + static int get_exact_mode() + { + static const int exact_mode = std::ios::xalloc(); + return exact_mode; + } }; /*! @@ -654,6 +660,65 @@ checks if the %IO stream `s` is in `BINARY` mode. */ inline bool is_binary(std::ios& s) { return s.iword(Static::get_mode()) == BINARY; } +/*! +\ingroup PkgStreamSupportRef + +sets the stream `s` so that a number type with a lazy exact representation, such as +`CGAL::Lazy_exact_nt`, is written through its exact value instead of through a +`double` approximation. Returns the previous state. + +This flag is only consulted by number types whose default output is an +approximation. At present that is `CGAL::Lazy_exact_nt`, which by default writes a +`double`. Exact number types that are not lazy, such as `CGAL::Gmpq` or +`CGAL::Exact_rational`, always write their exact representation and ignore this +flag. The default output is therefore exact for those types and approximate for +`CGAL::Lazy_exact_nt`. + +The round-trip through `operator>>` is exact when the exact type has an exact stream +representation, such as a rational type. It is not exact for an exact type whose +output operator writes an approximation, such as `CORE::Expr`. + +\sa `CGAL::IO::set_lossy_mode()` +\sa `CGAL::IO::is_exact_mode()` +\sa `CGAL::IO::set_pretty_mode()` +*/ +inline bool set_exact_mode(std::ios& s) +{ + const bool old = s.iword(Static::get_exact_mode()) != 0; + s.iword(Static::get_exact_mode()) = 1; + return old; +} + +/*! +\ingroup PkgStreamSupportRef + +sets the stream `s` so that a number type with a lazy exact representation, such as +`CGAL::Lazy_exact_nt`, is written through a `double` approximation. This is the +default for `CGAL::Lazy_exact_nt`. Number types that always write their exact +representation, such as `CGAL::Gmpq`, ignore this flag. Returns the previous state. + +\sa `CGAL::IO::set_exact_mode()` +\sa `CGAL::IO::is_exact_mode()` +\sa `CGAL::IO::set_pretty_mode()` +*/ +inline bool set_lossy_mode(std::ios& s) +{ + const bool old = s.iword(Static::get_exact_mode()) != 0; + s.iword(Static::get_exact_mode()) = 0; + return old; +} + +/*! +\ingroup PkgStreamSupportRef + +returns `true` if the stream `s` is in exact-output mode, that is if +`CGAL::IO::set_exact_mode()` was called on it. + +\sa `CGAL::IO::set_exact_mode()` +\sa `CGAL::IO::set_lossy_mode()` +*/ +inline bool is_exact_mode(std::ios& s) { return s.iword(Static::get_exact_mode()) != 0; } + } // namespace IO template < class T >