blob: 897c000ec2dea5060da4c2ce92a3f933d2bc7a7b [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.
*/
#include "paimon/common/data/variant/variant_json_utils.h"
#include <charconv>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include "arrow/vendored/datetime.h"
#include "fmt/format.h"
namespace paimon {
namespace {
constexpr int64_t kMicrosPerSecond = 1000000LL;
constexpr int64_t kMicrosPerDay = 86400LL * kMicrosPerSecond;
// Formats the shortest round-trip decimal digits + decimal exponent into the textual form of
// `java.lang.Double.toString` / `java.lang.Float.toString`: plain notation when
// `1e-3 <= |value| < 1e7`, computerized scientific notation otherwise.
std::string JavaFloatingToString(std::string_view digits, int32_t exp, bool negative) {
std::string result;
if (negative) {
result.push_back('-');
}
if (exp >= -3 && exp < 7) {
if (exp >= 0) {
size_t int_digits = static_cast<size_t>(exp) + 1;
if (digits.size() > int_digits) {
result.append(digits, 0, int_digits);
result.push_back('.');
result.append(digits.substr(int_digits));
} else {
result.append(digits);
result.append(int_digits - digits.size(), '0');
result.append(".0");
}
} else {
result.append("0.");
result.append(static_cast<size_t>(-exp) - 1, '0');
result.append(digits);
}
} else {
result.push_back(digits[0]);
result.push_back('.');
if (digits.size() > 1) {
result.append(digits.substr(1));
} else {
result.push_back('0');
}
result.push_back('E');
result.append(std::to_string(exp));
}
return result;
}
template <typename T>
std::string FloatingToJavaString(T value) {
if (std::isnan(value)) {
return "NaN";
}
if (std::isinf(value)) {
return value > 0 ? "Infinity" : "-Infinity";
}
if (value == 0) {
return std::signbit(value) ? "-0.0" : "0.0";
}
// The shortest round-trip representation in scientific form, e.g. `1.234e+05`. gcc 8's
// <charconv> lacks floating-point to_chars, so probe increasing precision until the value
// round-trips; dropping a trailing zero digit is an exact rounding, so the first precision
// that round-trips carries no trailing zeros.
constexpr int kMaxFractionDigits = std::is_same_v<T, float> ? 8 : 16;
char buf[64];
int len = 0;
for (int precision = 0; precision <= kMaxFractionDigits; ++precision) {
len = std::snprintf(buf, sizeof(buf), "%.*e", precision, static_cast<double>(value));
T parsed;
if constexpr (std::is_same_v<T, float>) {
parsed = std::strtof(buf, nullptr);
} else {
parsed = std::strtod(buf, nullptr);
}
if (parsed == value) {
break;
}
}
auto parse = [](std::string_view repr, std::string* digits, int32_t* exp, bool* negative) {
*negative = repr[0] == '-';
if (*negative) {
repr.remove_prefix(1);
}
size_t e_pos = repr.find('e');
std::string_view mantissa = repr.substr(0, e_pos);
size_t exp_start = e_pos + 1;
// `std::from_chars` does not accept a leading '+'.
if (repr[exp_start] == '+') {
++exp_start;
}
*exp = 0;
std::from_chars(repr.data() + exp_start, repr.data() + repr.size(), *exp);
digits->clear();
digits->push_back(mantissa[0]);
if (mantissa.size() > 2) {
digits->append(mantissa.substr(2));
}
};
std::string digits;
int32_t exp = 0;
bool negative = false;
parse(std::string_view(buf, static_cast<size_t>(len)), &digits, &exp, &negative);
if (digits.size() == 1 && (exp < -3 || exp >= 8)) {
// Java's FloatingDecimal emits at least two significant digits when the first digit
// alone would terminate in scientific form; re-render the correctly rounded two-digit
// representation (e.g. `Double.MIN_VALUE` is `4.9E-324`, not `5.0E-324`).
len = std::snprintf(buf, sizeof(buf), "%.1e", static_cast<double>(value));
parse(std::string_view(buf, static_cast<size_t>(len)), &digits, &exp, &negative);
}
return JavaFloatingToString(digits, exp, negative);
}
// Appends a year like `java.time.LocalDate.toString`: absolute value padded to at least 4
// digits; years above 9999 get a `+` prefix.
void AppendJavaYear(int64_t year, std::string* out) {
if (year < 0) {
out->push_back('-');
year = -year;
out->append(fmt::format("{:04}", year));
} else {
if (year > 9999) {
out->push_back('+');
}
out->append(fmt::format("{:04}", year));
}
}
void AppendDate(int64_t days_since_epoch, std::string* out) {
using arrow_vendored::date::days;
using arrow_vendored::date::sys_days;
using arrow_vendored::date::year_month_day;
year_month_day ymd{sys_days{days{days_since_epoch}}};
AppendJavaYear(static_cast<int32_t>(ymd.year()), out);
out->append(fmt::format("-{:02}-{:02}", static_cast<uint32_t>(ymd.month()),
static_cast<uint32_t>(ymd.day())));
}
int64_t FloorDiv(int64_t x, int64_t y) {
int64_t quotient = x / y;
if ((x % y != 0) && ((x < 0) != (y < 0))) {
--quotient;
}
return quotient;
}
} // namespace
void VariantJsonUtils::AppendEscapedJson(std::string_view str, std::string* out) {
out->push_back('"');
for (char c : str) {
switch (c) {
case '"':
out->append("\\\"");
break;
case '\\':
out->append("\\\\");
break;
case '\b':
out->append("\\b");
break;
case '\f':
out->append("\\f");
break;
case '\n':
out->append("\\n");
break;
case '\r':
out->append("\\r");
break;
case '\t':
out->append("\\t");
break;
default:
if (static_cast<uint8_t>(c) < 0x20) {
out->append(fmt::format("\\u{:04x}", static_cast<uint8_t>(c)));
} else {
out->push_back(c);
}
}
}
out->push_back('"');
}
std::string VariantJsonUtils::JavaDoubleToString(double value) {
return FloatingToJavaString(value);
}
std::string VariantJsonUtils::JavaFloatToString(float value) {
return FloatingToJavaString(value);
}
std::string VariantJsonUtils::DateToString(int32_t days_since_epoch) {
std::string result;
AppendDate(days_since_epoch, &result);
return result;
}
std::string VariantJsonUtils::TimestampToString(int64_t micros_since_epoch, int32_t offset_seconds,
bool with_offset) {
int64_t local_micros = micros_since_epoch + static_cast<int64_t>(offset_seconds) * 1000000;
int64_t days = FloorDiv(local_micros, kMicrosPerDay);
int64_t micros_of_day = local_micros - days * kMicrosPerDay;
std::string result;
AppendDate(days, &result);
int64_t seconds_of_day = micros_of_day / kMicrosPerSecond;
int64_t micros_of_second = micros_of_day % kMicrosPerSecond;
result.append(fmt::format(" {:02}:{:02}:{:02}", seconds_of_day / 3600,
(seconds_of_day / 60) % 60, seconds_of_day % 60));
if (micros_of_second != 0) {
std::string fraction = fmt::format("{:06}", micros_of_second);
while (fraction.back() == '0') {
fraction.pop_back();
}
result.push_back('.');
result.append(fraction);
}
if (with_offset) {
int32_t abs_offset = offset_seconds >= 0 ? offset_seconds : -offset_seconds;
result.append(fmt::format("{}{:02}:{:02}", offset_seconds >= 0 ? '+' : '-',
abs_offset / 3600, (abs_offset / 60) % 60));
}
return result;
}
Result<int32_t> VariantJsonUtils::GetZoneOffsetSeconds(const std::string& zone_id,
int64_t micros_since_epoch) {
std::string_view id = zone_id;
if (id == "Z" || id == "UTC" || id == "GMT" || id == "UT") {
return 0;
}
// `UTC+08:00` style ids: strip the prefix and parse the remaining fixed offset.
if (id.size() > 3 && (id.substr(0, 3) == "UTC" || id.substr(0, 3) == "GMT")) {
id.remove_prefix(3);
} else if (id.size() > 2 && id.substr(0, 2) == "UT" && (id[2] == '+' || id[2] == '-')) {
id.remove_prefix(2);
}
if (!id.empty() && (id[0] == '+' || id[0] == '-')) {
bool negative = id[0] == '-';
id.remove_prefix(1);
// Accepted forms: H, HH, HH:MM, HHMM, HH:MM:SS, HHMMSS.
std::string digits;
for (char c : id) {
if (c >= '0' && c <= '9') {
digits.push_back(c);
} else if (c != ':') {
return Status::Invalid(fmt::format("Invalid zone offset: {}", zone_id));
}
}
int32_t hours = 0;
int32_t minutes = 0;
int32_t seconds = 0;
if (digits.size() == 1 || digits.size() == 2) {
hours = std::stoi(digits);
} else if (digits.size() == 4) {
hours = std::stoi(digits.substr(0, 2));
minutes = std::stoi(digits.substr(2, 2));
} else if (digits.size() == 6) {
hours = std::stoi(digits.substr(0, 2));
minutes = std::stoi(digits.substr(2, 2));
seconds = std::stoi(digits.substr(4, 2));
} else {
return Status::Invalid(fmt::format("Invalid zone offset: {}", zone_id));
}
if (hours > 18 || minutes > 59 || seconds > 59) {
return Status::Invalid(fmt::format("Invalid zone offset: {}", zone_id));
}
int32_t total = hours * 3600 + minutes * 60 + seconds;
return negative ? -total : total;
}
// IANA region id, resolved at the given instant (honoring DST).
try {
const auto* zone = arrow_vendored::date::locate_zone(zone_id);
std::chrono::time_point<std::chrono::system_clock, std::chrono::microseconds> tp{
std::chrono::microseconds(micros_since_epoch)};
auto info = zone->get_info(tp);
return static_cast<int32_t>(info.offset.count());
} catch (const std::exception& e) {
return Status::Invalid(fmt::format("Invalid zone id: {}, {}", zone_id, e.what()));
}
}
} // namespace paimon