Skip to content

Commit c281308

Browse files
committed
[treeplayer] Fix precision loss in TTree::Scan for 64-bit integers
The `"lld"` column format passed without an embedded size (e.g. via `"colsize=N col=lld"`) was not recognized as a `"long long"` modifier in **TTreeFormula::PrintValue**, so 64-bit integer branches (e.g. `ULong64_t`) were evaluated as double and rounded above 2^53. This commit fixes a off-by-one problem in the length-modifier detection to address the issue, adding also a regression test. Closes #7844. 🤖 Done with the help of [Claude Code](https://claude.com/claude-code) (Claude Opus 4.8)
1 parent 06bd32f commit c281308

2 files changed

Lines changed: 77 additions & 2 deletions

File tree

tree/treeplayer/src/TTreeFormula.cxx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5052,12 +5052,12 @@ char *TTreeFormula::PrintValue(Int_t mode, Int_t instance, const char *decform)
50525052
Ssiz_t len = strlen(decform);
50535053
Char_t outputSizeLevel = 1;
50545054
char *expo = nullptr;
5055-
if (len>2) {
5055+
if (len > 1) {
50565056
switch (decform[len-2]) {
50575057
case 'l':
50585058
case 'L': {
50595059
outputSizeLevel = 2;
5060-
if (len>3 && tolower(decform[len-3])=='l') {
5060+
if (len > 2 && tolower(decform[len - 3]) == 'l') {
50615061
outputSizeLevel = 3;
50625062
}
50635063
break;

tree/treeplayer/test/regressions.cxx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include "gtest/gtest.h"
2121

22+
#include <limits>
2223
#include <string>
2324
#include <vector>
2425
#include <fstream>
@@ -585,3 +586,77 @@ TEST(TTreeScan, TTreeGetBranchOfFriendTChain)
585586
throw std::runtime_error("Could not retrieve TTreePlayer from main tree!");
586587
}
587588
}
589+
590+
// https://github.com/root-project/root/issues/7844
591+
// TTree::Scan() used to lose precision when printing 64-bit integer branches
592+
// (e.g. ULong64_t): the "lld" column format, when given without an embedded
593+
// column size (i.e. via "colsize=N col=lld"), was not recognized as a
594+
// "long long" modifier because of an off-by-one in the length-modifier
595+
// detection in TTreeFormula::PrintValue. As a consequence the value was
596+
// evaluated and printed as a double, rounding anything above 2^53.
597+
TEST(TTreeScan, ULong64Precision)
598+
{
599+
// The "long long" Scan/Draw column format is evaluated through `long double`
600+
// (see TTreeFormula::PrintValue), so exact 64-bit integer output is only
601+
// possible where `long double` has more mantissa bits than `double`. That is
602+
// the case on x86-64 (80-bit, 64-bit mantissa) but not, e.g., on macOS ARM
603+
// where `long double` is just a 64-bit `double` (53-bit mantissa). Skip the
604+
// exactness check there, since the value genuinely cannot be represented.
605+
if (std::numeric_limits<long double>::digits <= std::numeric_limits<double>::digits)
606+
GTEST_SKIP() << "long double is not wider than double on this platform";
607+
608+
// 1617047019150033926 needs 61 bits, so it cannot be represented exactly
609+
// by a double (53-bit mantissa).
610+
constexpr ULong64_t value{1617047019150033926ULL};
611+
612+
struct DatasetRAII {
613+
const char *fTreeName{"tree_7844"};
614+
const char *fFileName{"tree_7844.root"};
615+
DatasetRAII(ULong64_t v)
616+
{
617+
auto file = std::make_unique<TFile>(fFileName, "recreate");
618+
auto tree = std::make_unique<TTree>(fTreeName, fTreeName);
619+
620+
ULong64_t x = v;
621+
tree->Branch("x", &x, "x/l");
622+
tree->Fill();
623+
file->Write();
624+
}
625+
626+
~DatasetRAII() { std::remove(fFileName); }
627+
} dataset{value};
628+
629+
auto file = std::make_unique<TFile>(dataset.fFileName);
630+
std::unique_ptr<TTree> tree{file->Get<TTree>(dataset.fTreeName)};
631+
632+
std::ostringstream strCout;
633+
{
634+
if (auto *treePlayer = static_cast<TTreePlayer *>(tree->GetPlayer())) {
635+
struct FileRAII {
636+
const char *fPath;
637+
FileRAII(const char *name) : fPath(name) {}
638+
~FileRAII() { std::remove(fPath); }
639+
} redirectFile{"tree_7844_regression_redirect.txt"};
640+
treePlayer->SetScanRedirect(true);
641+
treePlayer->SetScanFileName(redirectFile.fPath);
642+
// The "lld" format without an embedded size (the size is given
643+
// separately via "colsize=") must print the exact 64-bit value, as
644+
// well as the exact result of integer arithmetic with large constants.
645+
tree->Scan("x:x-1617047019150033925:x-1617047019150033000", "", "colsize=21 col=lld:lld:lld");
646+
647+
std::ifstream redirectStream(redirectFile.fPath);
648+
std::stringstream redirectOutput;
649+
redirectOutput << redirectStream.rdbuf();
650+
651+
const static std::string expectedScanOut{
652+
R"Scan(************************************************************************************
653+
* Row * x * x-1617047019150033925 * x-1617047019150033000 *
654+
************************************************************************************
655+
* 0 * 1617047019150033926 * 1 * 926 *
656+
************************************************************************************
657+
)Scan"};
658+
EXPECT_EQ(redirectOutput.str(), expectedScanOut);
659+
} else
660+
throw std::runtime_error("Could not retrieve TTreePlayer from main tree!");
661+
}
662+
}

0 commit comments

Comments
 (0)