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