blob: 0599afa4f21d6e87e6979ee6c47a865f400b0bcb [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/// \file iceberg/util/decimal.cc
/// \brief 128-bit fixed-point decimal numbers.
/// Adapted from Apache Arrow with only Decimal128 support.
/// https://github.com/apache/arrow/blob/main/cpp/src/arrow/util/decimal.cc
#include "iceberg/util/decimal.h"
#include <algorithm>
#include <bit>
#include <charconv>
#include <climits>
#include <cmath>
#include <cstring>
#include <iomanip>
#include <limits>
#include <sstream>
#include <utility>
#include "iceberg/exception.h"
#include "iceberg/result.h"
#include "iceberg/util/int128.h"
#include "iceberg/util/macros.h"
#include "iceberg/util/string_util.h"
namespace iceberg {
namespace {
constexpr int32_t kMinDecimalBytes = 1;
constexpr int32_t kMaxDecimalBytes = 16;
// The maximum decimal value that can be represented with kMaxPrecision digits.
// 10^38 - 1
constexpr Decimal kMaxDecimalValue(5421010862427522170LL, 687399551400673279ULL);
// The mininum decimal value that can be represented with kMaxPrecision digits.
// - (10^38 - 1)
constexpr Decimal kMinDecimalValue(-5421010862427522171LL, 17759344522308878337ULL);
struct DecimalComponents {
std::string_view whole_digits;
std::string_view fractional_digits;
int32_t exponent{0};
char sign{0};
bool has_exponent{false};
};
inline bool IsSign(char c) { return c == '+' || c == '-'; }
inline bool IsDigit(char c) { return c >= '0' && c <= '9'; }
inline bool IsDot(char c) { return c == '.'; }
inline bool StartsExponent(char c) { return c == 'e' || c == 'E'; }
inline size_t ParseDigitsRun(std::string_view str, size_t pos, std::string_view* out) {
size_t start = pos;
while (pos < str.size() && IsDigit(str[pos])) {
++pos;
}
*out = str.substr(start, pos - start);
return pos;
}
bool ParseDecimalComponents(std::string_view str, DecimalComponents* out) {
size_t pos = 0;
if (str.empty()) {
return false;
}
// Sign of the number
if (IsSign(str[pos])) {
out->sign = str[pos++];
}
// First run of digits
pos = ParseDigitsRun(str, pos, &out->whole_digits);
if (pos == str.size()) {
return !out->whole_digits.empty();
}
// Optional dot
if (IsDot(str[pos])) {
// Second run of digits after the dot
pos = ParseDigitsRun(str, ++pos, &out->fractional_digits);
}
if (out->fractional_digits.empty() && out->whole_digits.empty()) {
// Need at least some digits (whole or fractional)
return false;
}
if (pos == str.size()) {
return true;
}
// Optional exponent part
if (StartsExponent(str[pos])) {
++pos;
// Skip '+' sign, '-' sign will be handled by from_chars
if (pos < str.size() && str[pos] == '+') {
++pos;
}
out->has_exponent = true;
auto [ptr, ec] =
std::from_chars(str.data() + pos, str.data() + str.size(), out->exponent);
if (ec != std::errc()) {
return false; // Failed to parse exponent
}
pos = ptr - str.data();
}
return pos == str.size();
}
constexpr auto kInt64DecimalDigits =
static_cast<size_t>(std::numeric_limits<int64_t>::digits10);
constexpr std::array<uint64_t, kInt64DecimalDigits + 1> kUInt64PowersOfTen = {
// clang-format off
1ULL,
10ULL,
100ULL,
1000ULL,
10000ULL,
100000ULL,
1000000ULL,
10000000ULL,
100000000ULL,
1000000000ULL,
10000000000ULL,
100000000000ULL,
1000000000000ULL,
10000000000000ULL,
100000000000000ULL,
1000000000000000ULL,
10000000000000000ULL,
100000000000000000ULL,
1000000000000000000ULL
// clang-format on
};
/// \brief Powers of ten for Decimal with scale from 0 to 38.
constexpr std::array<Decimal, Decimal::kMaxScale + 1> kDecimal128PowersOfTen = {
Decimal(1LL),
Decimal(10LL),
Decimal(100LL),
Decimal(1000LL),
Decimal(10000LL),
Decimal(100000LL),
Decimal(1000000LL),
Decimal(10000000LL),
Decimal(100000000LL),
Decimal(1000000000LL),
Decimal(10000000000LL),
Decimal(100000000000LL),
Decimal(1000000000000LL),
Decimal(10000000000000LL),
Decimal(100000000000000LL),
Decimal(1000000000000000LL),
Decimal(10000000000000000LL),
Decimal(100000000000000000LL),
Decimal(1000000000000000000LL),
Decimal(0LL, 10000000000000000000ULL),
Decimal(5LL, 7766279631452241920ULL),
Decimal(54LL, 3875820019684212736ULL),
Decimal(542LL, 1864712049423024128ULL),
Decimal(5421LL, 200376420520689664ULL),
Decimal(54210LL, 2003764205206896640ULL),
Decimal(542101LL, 1590897978359414784ULL),
Decimal(5421010LL, 15908979783594147840ULL),
Decimal(54210108LL, 11515845246265065472ULL),
Decimal(542101086LL, 4477988020393345024ULL),
Decimal(5421010862LL, 7886392056514347008ULL),
Decimal(54210108624LL, 5076944270305263616ULL),
Decimal(542101086242LL, 13875954555633532928ULL),
Decimal(5421010862427LL, 9632337040368467968ULL),
Decimal(54210108624275LL, 4089650035136921600ULL),
Decimal(542101086242752LL, 4003012203950112768ULL),
Decimal(5421010862427522LL, 3136633892082024448ULL),
Decimal(54210108624275221LL, 12919594847110692864ULL),
Decimal(542101086242752217LL, 68739955140067328ULL),
Decimal(5421010862427522170LL, 687399551400673280ULL)};
inline void ShiftAndAdd(std::string_view input, uint128_t& out) {
for (size_t pos = 0; pos < input.size();) {
const size_t group_size = std::min(kInt64DecimalDigits, input.size() - pos);
const uint64_t multiple = kUInt64PowersOfTen[group_size];
auto res = StringUtils::ParseNumber<uint64_t>(input.substr(pos, group_size));
ICEBERG_DCHECK(res.has_value(), "Failed to parse digits in ShiftAndAdd");
out = out * multiple + res.value();
pos += group_size;
}
}
void AdjustIntegerStringWithScale(int32_t scale, std::string* str) {
if (scale == 0) {
return;
}
ICEBERG_DCHECK(str != nullptr && !str->empty(), "str must not be null or empty");
const bool is_negative = str->front() == '-';
const auto is_negative_offset = static_cast<int32_t>(is_negative);
const auto len = static_cast<int32_t>(str->size());
const int32_t num_digits = len - is_negative_offset;
const int32_t adjusted_exponent = num_digits - 1 - scale;
// Note that the -6 is taken from the Java BigDecimal documentation.
if (scale < 0 || adjusted_exponent < -6) {
// Example 1:
// Precondition: *str = "123", is_negative_offset = 0, num_digits = 3, scale = -2,
// adjusted_exponent = 4
// After inserting decimal point: *str = "1.23"
// After appending exponent: *str = "1.23E+4"
// Example 2:
// Precondition: *str = "-123", is_negative_offset = 1, num_digits = 3, scale = 9,
// adjusted_exponent = -7
// After inserting decimal point: *str = "-1.23"
// After appending exponent: *str = "-1.23E-7"
// Example 3:
// Precondition: *str = "0", is_negative_offset = 0, num_digits = 1, scale = -1,
// adjusted_exponent = 1
// After inserting decimal point: *str = "0" // Not inserted
// After appending exponent: *str = "0E+1"
if (num_digits > 1) {
str->insert(str->begin() + 1 + is_negative_offset, '.');
}
str->push_back('E');
if (adjusted_exponent >= 0) {
str->push_back('+');
}
// Append the adjusted exponent as a string.
str->append(std::to_string(adjusted_exponent));
return;
}
if (num_digits > scale) {
const auto n = static_cast<size_t>(len - scale);
// Example 1:
// Precondition: *str = "123", len = num_digits = 3, scale = 1, n = 2
// After inserting decimal point: *str = "12.3"
// Example 2:
// Precondition: *str = "-123", len = 4, num_digits = 3, scale = 1, n = 3
// After inserting decimal point: *str = "-12.3"
str->insert(str->begin() + n, '.');
return;
}
// Example 1:
// Precondition: *str = "123", is_negative_offset = 0, num_digits = 3, scale = 4
// After insert: *str = "000123"
// After setting decimal point: *str = "0.0123"
// Example 2:
// Precondition: *str = "-123", is_negative_offset = 1, num_digits = 3, scale = 4
// After insert: *str = "-000123"
// After setting decimal point: *str = "-0.0123"
str->insert(is_negative_offset, scale - num_digits + 2, '0');
str->at(is_negative_offset + 1) = '.';
}
bool RescaleWouldCauseDataLoss(const Decimal& value, int32_t delta_scale,
const Decimal& multiplier, Decimal* result) {
if (delta_scale < 0) {
auto res = value.Divide(multiplier);
ICEBERG_DCHECK(res, "Decimal::Divide failed");
*result = res->first;
return res->second != 0;
}
auto max_safe_value = kMaxDecimalValue / multiplier;
auto min_safe_value = kMinDecimalValue / multiplier;
if (value > max_safe_value || value < min_safe_value) {
// Overflow would happen — treat as data loss
return true;
}
*result = value * multiplier;
return false;
}
} // namespace
Decimal::Decimal(std::string_view str) {
auto result = Decimal::FromString(str);
ICEBERG_CHECK_OR_DIE(result, "Failed to parse Decimal from string: {}, error: {}", str,
result.error().message);
*this = std::move(result.value());
}
Decimal& Decimal::Negate() {
uint128_t u = ~static_cast<uint128_t>(data_) + 1;
data_ = static_cast<int128_t>(u);
return *this;
}
Decimal& Decimal::Abs() { return *this < 0 ? Negate() : *this; }
Decimal Decimal::Abs(const Decimal& value) {
Decimal result(value);
return result.Abs();
}
Decimal& Decimal::operator+=(const Decimal& other) {
data_ += other.data_;
return *this;
}
Decimal& Decimal::operator-=(const Decimal& other) {
data_ -= other.data_;
return *this;
}
Decimal& Decimal::operator*=(const Decimal& other) {
data_ *= other.data_;
return *this;
}
Result<std::pair<Decimal, Decimal>> Decimal::Divide(const Decimal& divisor) const {
std::pair<Decimal, Decimal> result;
if (divisor == 0) {
return Invalid("Cannot divide by zero in Decimal::Divide");
}
return std::make_pair(*this / divisor, *this % divisor);
}
Decimal& Decimal::operator/=(const Decimal& other) {
data_ /= other.data_;
return *this;
}
Decimal& Decimal::operator|=(const Decimal& other) {
data_ |= other.data_;
return *this;
}
Decimal& Decimal::operator&=(const Decimal& other) {
data_ &= other.data_;
return *this;
}
Decimal& Decimal::operator<<=(uint32_t bits) {
if (bits != 0) {
data_ = static_cast<int128_t>(static_cast<uint128_t>(data_) << bits);
}
return *this;
}
Decimal& Decimal::operator>>=(uint32_t bits) {
if (bits != 0) {
data_ >>= bits;
}
return *this;
}
Result<std::string> Decimal::ToString(int32_t scale) const {
if (scale < -kMaxScale || scale > kMaxScale) {
return InvalidArgument(
"Decimal::ToString: scale must be in the range [-{}, {}], was {}", kMaxScale,
kMaxScale, scale);
}
std::string str(ToIntegerString());
AdjustIntegerStringWithScale(scale, &str);
return str;
}
std::string Decimal::ToIntegerString() const {
if (data_ == 0) {
return "0";
}
bool negative = data_ < 0;
uint128_t uval =
negative ? -static_cast<uint128_t>(data_) : static_cast<uint128_t>(data_);
constexpr uint32_t k1e9 = 1000000000U;
constexpr size_t kNumBits = 128;
// Segments will contain the array split into groups that map to decimal digits, in
// little endian order. Each segment will hold at most 9 decimal digits. For example, if
// the input represents 9876543210123456789, then segments will be [123456789,
// 876543210, 9].
// The max number of segments needed = ceil(kNumBits * log(2) / log(1e9))
// = ceil(kNumBits / 29.897352854) <= ceil(kNumBits / 29).
std::array<uint32_t, (kNumBits + 28) / 29> segments;
size_t num_segments = 0;
while (uval > 0) {
// Compute remainder = uval % 1e9 and uval = uval / 1e9.
auto remainder = static_cast<uint32_t>(uval % k1e9);
uval /= k1e9;
segments[num_segments++] = remainder;
}
std::ostringstream oss;
if (negative) {
oss << '-';
}
// First segment is formatted as-is.
oss << segments[num_segments - 1];
// Remaining segments are formatted with leading zeros to fill 9 digits. e.g. 123 is
// formatted as "000000123"
for (size_t i = num_segments - 1; i-- > 0;) {
oss << std::setw(9) << std::setfill('0') << segments[i];
}
return oss.str();
}
Result<Decimal> Decimal::FromString(std::string_view str, int32_t* precision,
int32_t* scale) {
if (str.empty()) {
return InvalidArgument("Empty string is not a valid Decimal");
}
DecimalComponents dec;
if (!ParseDecimalComponents(str, &dec)) {
return InvalidArgument("Invalid decimal string '{}'", str);
}
// Count number of significant digits (without leading zeros)
size_t first_non_zero = dec.whole_digits.find_first_not_of('0');
size_t significant_digits = dec.fractional_digits.size();
if (first_non_zero != std::string_view::npos) {
significant_digits += dec.whole_digits.size() - first_non_zero;
}
auto parsed_precision = static_cast<int32_t>(significant_digits);
int32_t parsed_scale = 0;
if (dec.has_exponent) {
auto adjusted_exponent = dec.exponent;
parsed_scale = static_cast<int32_t>(dec.fractional_digits.size()) - adjusted_exponent;
} else {
parsed_scale = static_cast<int32_t>(dec.fractional_digits.size());
}
uint128_t value = 0;
ShiftAndAdd(dec.whole_digits, value);
ShiftAndAdd(dec.fractional_digits, value);
Decimal result(static_cast<int128_t>(value));
if (dec.sign == '-') {
result.Negate();
}
if (parsed_scale < 0) {
// For the scale to 0, to avoid negative scales (due to compatibility issues with
// external systems such as databases)
if (parsed_scale < -kMaxScale) {
return InvalidArgument("scale must be in the range [-{}, {}], was {}", kMaxScale,
kMaxScale, parsed_scale);
}
result *= kDecimal128PowersOfTen[-parsed_scale];
parsed_precision -= parsed_scale;
parsed_scale = 0;
}
if (precision != nullptr) {
*precision = parsed_precision;
}
if (scale != nullptr) {
*scale = parsed_scale;
}
return result;
}
Result<Decimal> Decimal::FromBigEndian(const uint8_t* bytes, int32_t length) {
if (length < kMinDecimalBytes || length > kMaxDecimalBytes) {
return InvalidArgument(
"Decimal::FromBigEndian: length must be in the range [{}, {}], was {}",
kMinDecimalBytes, kMaxDecimalBytes, length);
}
// Bytes are coming in big-endian, so the first byte is the MSB and therefore holds the
// sign bit.
const bool is_negative = static_cast<int8_t>(bytes[0]) < 0;
uint128_t result = 0;
std::memcpy(reinterpret_cast<uint8_t*>(&result) + kMaxDecimalBytes - length, bytes,
length);
if constexpr (std::endian::native == std::endian::little) {
auto high = static_cast<uint64_t>(result >> 64);
auto low = static_cast<uint64_t>(result);
high = std::byteswap(high);
low = std::byteswap(low);
// also need to swap the two halves
result = (static_cast<uint128_t>(low) << 64) | high;
}
if (is_negative && length < kMaxDecimalBytes) {
// Sign extend the high bits
result |= (static_cast<uint128_t>(-1) << (length * CHAR_BIT));
}
return Decimal(static_cast<int128_t>(result));
}
std::vector<uint8_t> Decimal::ToBigEndian() const {
std::vector<uint8_t> bytes(kMaxDecimalBytes);
auto uvalue = static_cast<uint128_t>(data_);
std::memcpy(bytes.data(), &uvalue, kMaxDecimalBytes);
if constexpr (std::endian::native == std::endian::little) {
std::ranges::reverse(bytes);
}
auto is_negative = data_ < 0;
int keep = kMaxDecimalBytes;
for (int32_t i = 0; i < kMaxDecimalBytes - 1; ++i) {
uint8_t byte = bytes[i];
uint8_t next = bytes[i + 1];
// For negative numbers, keep the leading 0xff byte if the next byte has its sign bit
// unset. For positive numbers, keep the leading 0x00 byte if the next byte has its
// sign bit set.
if ((is_negative && byte == 0xff && (next & 0x80)) ||
(!is_negative && byte == 0x00 && !(next & 0x80))) {
--keep;
} else {
break;
}
}
bytes.erase(bytes.begin(), bytes.begin() + (kMaxDecimalBytes - keep));
return bytes;
}
Result<Decimal> Decimal::Rescale(int32_t orig_scale, int32_t new_scale) const {
if (orig_scale == new_scale) {
return *this;
}
const int32_t delta_scale = new_scale - orig_scale;
const int32_t abs_delta_scale = std::abs(delta_scale);
Decimal out;
ICEBERG_DCHECK(abs_delta_scale <= kMaxScale, "");
auto& multiplier = kDecimal128PowersOfTen[abs_delta_scale];
if (RescaleWouldCauseDataLoss(*this, delta_scale, multiplier, &out)) [[unlikely]] {
return Invalid("Rescale {} from {} to {} would cause data loss", ToIntegerString(),
orig_scale, new_scale);
}
return out;
}
bool Decimal::FitsInPrecision(int32_t precision) const {
ICEBERG_DCHECK(precision >= 1 && precision <= kMaxPrecision, "");
return Decimal::Abs(*this) < kDecimal128PowersOfTen[precision];
}
std::partial_ordering Decimal::Compare(const Decimal& lhs, const Decimal& rhs,
int32_t lhs_scale, int32_t rhs_scale) {
if (lhs_scale == rhs_scale || lhs.data_ == 0 || rhs.data_ == 0) {
return lhs <=> rhs;
}
// If one is negative and the other is positive, the positive is greater.
if (lhs.data_ < 0 && rhs.data_ > 0) {
return std::partial_ordering::less;
}
if (lhs.data_ > 0 && rhs.data_ < 0) {
return std::partial_ordering::greater;
}
// Both are negative
bool negative = lhs.data_ < 0 && rhs.data_ < 0;
const int32_t delta_scale = lhs_scale - rhs_scale;
const int32_t abs_delta_scale = std::abs(delta_scale);
ICEBERG_DCHECK(abs_delta_scale <= kMaxScale, "");
const auto& multiplier = kDecimal128PowersOfTen[abs_delta_scale];
Decimal adjusted_lhs;
Decimal adjusted_rhs;
if (delta_scale < 0) {
// lhs_scale < rhs_scale
if (RescaleWouldCauseDataLoss(lhs, -delta_scale, multiplier, &adjusted_lhs))
[[unlikely]] {
return negative ? std::partial_ordering::less : std::partial_ordering::greater;
}
adjusted_rhs = rhs;
} else {
// lhs_scale > rhs_scale
if (RescaleWouldCauseDataLoss(rhs, delta_scale, multiplier, &adjusted_rhs))
[[unlikely]] {
return negative ? std::partial_ordering::greater : std::partial_ordering::less;
}
adjusted_lhs = lhs;
}
return adjusted_lhs <=> adjusted_rhs;
}
std::array<uint8_t, Decimal::kByteWidth> Decimal::ToBytes() const {
std::array<uint8_t, kByteWidth> out{{0}};
std::memcpy(out.data(), &data_, kByteWidth);
return out;
}
std::ostream& operator<<(std::ostream& os, const Decimal& decimal) {
os << decimal.ToIntegerString();
return os;
}
// Unary operators
Decimal operator-(const Decimal& operand) {
Decimal result(operand.data_);
return result.Negate();
}
Decimal operator~(const Decimal& operand) { return {~operand.data_}; }
// Binary operators
Decimal operator+(const Decimal& lhs, const Decimal& rhs) {
Decimal result(lhs);
result += rhs;
return result;
}
Decimal operator-(const Decimal& lhs, const Decimal& rhs) {
Decimal result(lhs);
result -= rhs;
return result;
}
Decimal operator*(const Decimal& lhs, const Decimal& rhs) {
Decimal result(lhs);
result *= rhs;
return result;
}
Decimal operator/(const Decimal& lhs, const Decimal& rhs) {
return lhs.data_ / rhs.data_;
}
Decimal operator%(const Decimal& lhs, const Decimal& rhs) {
return lhs.data_ % rhs.data_;
}
} // namespace iceberg