| /* |
| * 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 |