Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
30 changes: 29 additions & 1 deletion 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,11 @@ 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,
which enables round-trip save/load of `Lazy_exact_nt` values without loss of
precision: `operator>>` then reconstructs the same exact number.
\relates Lazy_exact_nt
*/
std::ostream& operator<<(std::ostream& out, const Lazy_exact_nt<NT>& m);
Expand All @@ -122,5 +126,29 @@ 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);

namespace IO {

/*!
makes `operator<<` write `Lazy_exact_nt` values on the stream `s` through their
exact value, so that `operator>>` reconstructs the same number without loss of
precision. Returns the previous state. Mirrors `CGAL::IO::set_pretty_mode()`.
\relates Lazy_exact_nt
*/
bool set_exact_mode(std::ios& s);

/*!
makes `operator<<` write `Lazy_exact_nt` values on the stream `s` through
`to_double()`, which is the default. Returns the previous state.
\relates Lazy_exact_nt
*/
bool set_lossy_mode(std::ios& s);

/*!
returns `true` if the stream `s` is in exact-output mode for `Lazy_exact_nt`.
\relates Lazy_exact_nt
*/
bool is_exact_mode(std::ios& s);

} /* end namespace IO */

} /* end namespace CGAL */
52 changes: 51 additions & 1 deletion Number_types/include/CGAL/Lazy_exact_nt.h
Original file line number Diff line number Diff line change
Expand Up @@ -1300,10 +1300,60 @@ const Lazy_exact_nt<ET> & y){
return CGAL::Max<Lazy_exact_nt<ET> > ()(x,y);
}

namespace IO {

// Per-stream flag controlling how Lazy_exact_nt is written by operator<<. By
// default (flag unset) the value is written via to_double(), the historical
// behaviour. After set_exact_mode(s), operator<< writes the exact() value, so
// that operator>> reconstructs the same number without precision loss (issue
// #135). This mirrors set_pretty_mode() / is_pretty().
namespace internal {
inline int lazy_exact_nt_exact_output_index()
{
static const int index = std::ios_base::xalloc();
return index;
}
} // namespace internal

// Sets the stream s so that operator<< writes Lazy_exact_nt values through their
// exact value (lossless, so operator>> reconstructs the same number). Returns the
// previous state.
inline bool set_exact_mode(std::ios& s)
{
const bool old = s.iword(internal::lazy_exact_nt_exact_output_index()) != 0;
s.iword(internal::lazy_exact_nt_exact_output_index()) = 1;
return old;
}

// Sets the stream s so that operator<< writes Lazy_exact_nt values through
// to_double() (the default). Returns the previous state.
inline bool set_lossy_mode(std::ios& s)
{
const bool old = s.iword(internal::lazy_exact_nt_exact_output_index()) != 0;
s.iword(internal::lazy_exact_nt_exact_output_index()) = 0;
return old;
}

// Returns true if s is in exact-output mode for Lazy_exact_nt.
inline bool is_exact_mode(std::ios& s)
{
return s.iword(internal::lazy_exact_nt_exact_output_index()) != 0;
}

} // namespace IO

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 instead, so that
// operator>> reconstructs the same number without precision loss (issue #135).
// Keeping the default keeps this non-breaking.
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
133 changes: 133 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,133 @@
// 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, which round-trips losslessly through
// operator>>. This test sets that mode and checks the round-trip is exact.

#include <CGAL/Lazy_exact_nt.h>
#include <CGAL/Exact_rational.h>

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

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;
}

std::cout << "All tests passed." << std::endl;
return 0;
}

Loading