Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<NT>& m);
Expand All @@ -122,5 +130,4 @@ reads a `NT` from `in`, then converts it to a `Lazy_exact_nt<NT>`.
*/
std::istream& operator>>(std::istream& in, Lazy_exact_nt<NT>& m);


} /* end namespace CGAL */
13 changes: 12 additions & 1 deletion Number_types/include/CGAL/Lazy_exact_nt.h
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,18 @@ const Lazy_exact_nt<ET> & y){
template <typename ET>
std::ostream &
operator<< (std::ostream & os, const Lazy_exact_nt<ET> & 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 <typename ET>
std::istream &
Expand Down
1 change: 1 addition & 0 deletions Number_types/test/Number_types/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
179 changes: 179 additions & 0 deletions Number_types/test/Number_types/Lazy_exact_nt_io.cpp
Original file line number Diff line number Diff line change
@@ -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 <CGAL/Lazy_exact_nt.h>
#include <CGAL/Exact_rational.h>

#include <CGAL/CORE_Expr.h>

#include <cassert>
#include <iostream>
#include <sstream>
#include <string>

typedef CGAL::Lazy_exact_nt<CGAL::Exact_rational> 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);
Comment on lines +32 to +36

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test doesn’t verify that extraction succeeded (stream state) before comparing values. If parsing fails, b can remain at its default value (which is 0 for Lazy_exact_nt), potentially making the test pass spuriously (notably in the zero case). Add an assertion like assert(iss) / assert(!iss.fail()) after iss >> b (and similarly in the other test blocks).

Copilot uses AI. Check for mistakes.
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);
Comment on lines +47 to +51

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: the test should assert the stream extraction succeeded after iss >> b to avoid false positives if parsing fails.

Copilot uses AI. Check for mistakes.
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);
Comment on lines +62 to +66

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: please assert the stream is still good after iss >> b so the test can’t pass if parsing fails.

Copilot uses AI. Check for mistakes.
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;

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: add a stream-state assertion after iss >> d to ensure the read succeeded before comparing c == d.

Suggested change
iss >> d;
iss >> d;
assert(iss);

Copilot uses AI. Check for mistakes.
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);
Comment on lines +99 to +103

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: assert successful extraction after iss >> b so the test can’t pass if parsing fails.

Copilot uses AI. Check for mistakes.
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;

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: because the expected value is 0 here, a failed parse could leave b at the default value and make the assertion pass. Add an assert(iss) / assert(!iss.fail()) after iss >> b.

Suggested change
iss >> b;
iss >> b;
assert(iss);

Copilot uses AI. Check for mistakes.
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<CORE::Expr> 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;
}

3 changes: 3 additions & 0 deletions Stream_support/doc/Stream_support/PackageDescription.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Stream_support/doc/Stream_support/dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Manual
Mesh_2
Mesh_3
Nef_3
Number_types
Point_set_3
Point_set_processing_3
Polygon
Expand Down
65 changes: 65 additions & 0 deletions Stream_support/include/CGAL/IO/io.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};

/*!
Expand Down Expand Up @@ -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 >
Expand Down
Loading