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
51 changes: 51 additions & 0 deletions src/iceberg/test/truncate_util_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

#include "iceberg/util/truncate_util.h"

#include <limits>
#include <vector>

#include <gtest/gtest.h>

#include "iceberg/expression/literal.h"
Expand Down Expand Up @@ -51,6 +54,54 @@ TEST(TruncateUtilTest, TruncateLiteral) {
Literal::Binary(std::vector<uint8_t>(expected.begin(), expected.end())));
}

TEST(TruncateUtilTest, TruncateIntegerBoundaryValues) {
auto check_boundaries = []<typename T>(auto make_literal) {
const T min = std::numeric_limits<T>::min();
const T max = std::numeric_limits<T>::max();
struct TestCase {
T value;
int32_t width;
T expected;
};
std::vector<TestCase> cases = {
{min, 1, min},
{-1, 1, -1},
{0, 1, 0},
{1, 1, 1},
{max, 1, max},
// The final subtraction wraps for the type minimum, matching Java.
{min, 10, max - 1},
{max, 10, max - max % 10},
// Values just above the type minimum truncate without wrapping.
{min + 8, 10, min + 8},
{min + 9, 10, min + 8},
// Java's truncateInt overflows (v % W) + W here; follow v - (v % W)
// instead, yielding 0 for both types, as Java's truncateLong also does.
{2147483645, 2147483646, 0},
};
for (int32_t width : {10, std::numeric_limits<int32_t>::max() - 1,
std::numeric_limits<int32_t>::max()}) {
const T w = width;
cases.insert(cases.end(), {{0, width, 0},
{1, width, 0},
{w - 1, width, 0},
{-1, width, -w},
{1 - w, width, -w},
{w, width, w},
{-w, width, -w}});
}

for (const auto& [value, width, expected] : cases) {
SCOPED_TRACE(::testing::Message() << "value=" << value << ", width=" << width);
EXPECT_EQ(TruncateUtils::TruncateInteger(value, width), expected);
EXPECT_EQ(TruncateUtils::TruncateLiteral(make_literal(value), width),
make_literal(expected));
}
};
check_boundaries.operator()<int32_t>(Literal::Int);
check_boundaries.operator()<int64_t>(Literal::Long);
}

TEST(TruncateUtilTest, TruncateLiteralRejectsInvalidWidth) {
std::vector<uint8_t> data{1, 2, 3};

Expand Down
15 changes: 12 additions & 3 deletions src/iceberg/util/truncate_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <cstdint>
#include <optional>
#include <string>
#include <type_traits>
#include <utility>

#include "iceberg/iceberg_export.h"
Expand Down Expand Up @@ -82,12 +83,20 @@ class ICEBERG_EXPORT TruncateUtils {

/// \brief Truncate an integer v, either int32_t or int64_t, to v - (v % W).
///
/// The remainder, v % W, must be positive. For languages where % can produce negative
/// values, the correct truncate function is: v - (((v % W) + W) % W)
/// W must be positive. Since C++ can produce a negative remainder, add W only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For widths above roughly 2^30 this returns 0 where Java's truncateInt wraps and returns -2147483646 (v = 2147483645, W = 2147483646) - the right call given Java's own int and long paths disagree there, but worth stating in this doc block rather than only in a test comment.

/// when v % W is negative to normalize it to [0, W). This addition cannot overflow.
/// The final subtraction uses unsigned arithmetic to avoid signed-overflow undefined
/// behavior when the truncated value is below T's minimum. Conversion back to T
/// preserves two's-complement wraparound (defined since C++20).
Comment on lines +89 to +90
template <typename T>
requires std::is_same_v<T, int32_t> || std::is_same_v<T, int64_t>
static inline T TruncateInteger(T v, int32_t W) {
return v - (((v % W) + W) % W);
using U = std::make_unsigned_t<T>;
T remainder = v % W;
if (remainder < 0) {
remainder += W;
}
return static_cast<T>(static_cast<U>(v) - static_cast<U>(remainder));
}

/// \brief Truncate a Decimal to a specified width.
Expand Down
Loading