diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt index d7086773a1fd..c7f3d928f8da 100644 --- a/cpp/src/arrow/CMakeLists.txt +++ b/cpp/src/arrow/CMakeLists.txt @@ -550,7 +550,8 @@ set(ARROW_VENDORED_SRCS vendored/uriparser/UriResolve.c vendored/uriparser/UriShorten.c) if(APPLE) - list(APPEND ARROW_VENDORED_SRCS vendored/datetime/ios.mm) + # This wrapper excludes the iOS timezone implementation from standard-backend builds. + list(APPEND ARROW_VENDORED_SRCS vendored/datetime_ios.mm) endif() set_source_files_properties(vendored/datetime.cpp PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON) diff --git a/cpp/src/arrow/array/diff.cc b/cpp/src/arrow/array/diff.cc index fd907e3c7b29..9deeac949926 100644 --- a/cpp/src/arrow/array/diff.cc +++ b/cpp/src/arrow/array/diff.cc @@ -43,17 +43,19 @@ #include "arrow/type_traits.h" #include "arrow/util/bit_util.h" #include "arrow/util/checked_cast.h" +#include "arrow/util/chrono_internal.h" #include "arrow/util/float16.h" #include "arrow/util/logging_internal.h" #include "arrow/util/range.h" #include "arrow/util/ree_util.h" #include "arrow/util/string.h" #include "arrow/util/unreachable.h" -#include "arrow/vendored/datetime.h" #include "arrow/visit_type_inline.h" namespace arrow { +namespace chrono = internal::chrono; + using internal::checked_cast; using internal::checked_pointer_cast; using internal::MakeLazyRange; @@ -631,14 +633,13 @@ class MakeFormatterImpl { template enable_if_date Visit(const T&) { using unit = typename std::conditional::value, - arrow_vendored::date::days, - std::chrono::milliseconds>::type; + chrono::days, std::chrono::milliseconds>::type; - static arrow_vendored::date::sys_days epoch{arrow_vendored::date::jan / 1 / 1970}; + static chrono::sys_days epoch{chrono::jan / 1 / 1970}; impl_ = [](const Array& array, int64_t index, std::ostream* os) { unit value(checked_cast&>(array).Value(index)); - *os << arrow_vendored::date::format("%F", value + epoch); + *os << chrono::format("%F", value + epoch); }; return Status::OK(); } @@ -854,42 +855,41 @@ class MakeFormatterImpl { auto value = checked_cast&>(array).Value(index); // Using unqualified `format` directly would produce ambiguous // lookup because of `std::format` (ARROW-15520). - namespace avd = arrow_vendored::date; using std::chrono::nanoseconds; using std::chrono::microseconds; using std::chrono::milliseconds; using std::chrono::seconds; if (AddEpoch) { - static avd::sys_days epoch{avd::jan / 1 / 1970}; + static chrono::sys_days epoch{chrono::jan / 1 / 1970}; switch (unit) { case TimeUnit::NANO: - *os << avd::format(fmt, static_cast(value) + epoch); + *os << chrono::format(fmt, static_cast(value) + epoch); break; case TimeUnit::MICRO: - *os << avd::format(fmt, static_cast(value) + epoch); + *os << chrono::format(fmt, static_cast(value) + epoch); break; case TimeUnit::MILLI: - *os << avd::format(fmt, static_cast(value) + epoch); + *os << chrono::format(fmt, static_cast(value) + epoch); break; case TimeUnit::SECOND: - *os << avd::format(fmt, static_cast(value) + epoch); + *os << chrono::format(fmt, static_cast(value) + epoch); break; } return; } switch (unit) { case TimeUnit::NANO: - *os << avd::format(fmt, static_cast(value)); + *os << chrono::format(fmt, static_cast(value)); break; case TimeUnit::MICRO: - *os << avd::format(fmt, static_cast(value)); + *os << chrono::format(fmt, static_cast(value)); break; case TimeUnit::MILLI: - *os << avd::format(fmt, static_cast(value)); + *os << chrono::format(fmt, static_cast(value)); break; case TimeUnit::SECOND: - *os << avd::format(fmt, static_cast(value)); + *os << chrono::format(fmt, static_cast(value)); break; } }; diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_temporal.cc b/cpp/src/arrow/compute/kernels/scalar_cast_temporal.cc index d076186e5635..a35c8e3d42e2 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_temporal.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_temporal.cc @@ -462,8 +462,7 @@ struct ParseDate { using value_type = typename DateType::c_type; using duration_type = - typename std::conditional::value, - arrow_vendored::date::days, + typename std::conditional::value, chrono::days, std::chrono::milliseconds>::type; template diff --git a/cpp/src/arrow/compute/kernels/scalar_temporal_test.cc b/cpp/src/arrow/compute/kernels/scalar_temporal_test.cc index 86a81ffdd384..aaa4aefa6c54 100644 --- a/cpp/src/arrow/compute/kernels/scalar_temporal_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_temporal_test.cc @@ -2003,6 +2003,28 @@ TEST_F(ScalarTemporalTest, TestAssumeTimezoneNonexistent) { &options_earliest); } +TEST_F(ScalarTemporalTest, StrftimeFormatSyntax) { + const auto type = timestamp(TimeUnit::MILLI, "UTC"); + const char* input = R"(["1970-01-01T00:00:00.123", null])"; + for (const auto& [format, expected] : + {std::pair{"", R"(["", null])"}, + std::pair{"literal {%Y}", R"(["literal {1970}", null])"}, + std::pair{"unmatched }%Y{", R"(["unmatched }1970{", null])"}, + std::pair{"%Y}", R"(["1970}", null])"}, + std::pair{"%Q %q %J %z %Z", R"(["123 ms %J +0000 UTC", null])"}, + std::pair{"%% %n%t %Ez %Oz %OV %EJ end%", + R"(["% \n\t +00:00 +00:00 01 %EJ end%", null])"}}) { + SCOPED_TRACE(format); + const auto options = StrftimeOptions(format); + CheckScalarUnary("strftime", type, input, utf8(), expected, &options); + } + + const auto options = StrftimeOptions("%Q %q"); + CheckScalarUnary("strftime", timestamp(TimeUnit::MICRO, "UTC"), + R"(["1970-01-01T00:00:00.000001", null])", utf8(), + R"(["1 \u00b5s", null])", &options); +} + TEST_F(ScalarTemporalTest, StrftimeOffsetTimezone) { auto options_ymdhms = StrftimeOptions("%Y-%m-%dT%H:%M:%S"); diff --git a/cpp/src/arrow/config.cc b/cpp/src/arrow/config.cc index 41cc6decc6bf..290c0db2f447 100644 --- a/cpp/src/arrow/config.cc +++ b/cpp/src/arrow/config.cc @@ -19,14 +19,15 @@ #include +#include "arrow/util/chrono_internal.h" #include "arrow/util/config.h" #include "arrow/util/config_internal.h" #include "arrow/util/cpu_info.h" -#include "arrow/vendored/datetime.h" namespace arrow { using internal::CpuInfo; +namespace chrono = internal::chrono; namespace { @@ -77,8 +78,8 @@ RuntimeInfo GetRuntimeInfo() { MakeSimdLevelString([&](int64_t flags) { return cpu_info->IsSupported(flags); }); info.detected_simd_level = MakeSimdLevelString([&](int64_t flags) { return cpu_info->IsDetected(flags); }); - info.using_os_timezone_db = USE_OS_TZDB; -#if !USE_OS_TZDB + info.using_os_timezone_db = ARROW_CHRONO_USE_OS_TZDB; +#if !ARROW_CHRONO_USE_OS_TZDB info.timezone_db_path = timezone_db_path; #else info.timezone_db_path = std::optional(); @@ -91,10 +92,10 @@ RuntimeInfo GetRuntimeInfo() { Status Initialize(const GlobalOptions& options) noexcept { ARROW_SUPPRESS_DEPRECATION_WARNING if (options.timezone_db_path.has_value()) { -#if !USE_OS_TZDB +#if !ARROW_CHRONO_USE_OS_TZDB try { - arrow_vendored::date::set_install(options.timezone_db_path.value()); - arrow_vendored::date::reload_tzdb(); + chrono::set_install(options.timezone_db_path.value()); + chrono::reload_tzdb(); } catch (const std::runtime_error& e) { return Status::IOError(e.what()); } @@ -103,7 +104,7 @@ Status Initialize(const GlobalOptions& options) noexcept { return Status::Invalid( "Arrow was set to use OS timezone database at compile time, " "so a downloaded database cannot be provided at runtime."); -#endif // !USE_OS_TZDB +#endif // !ARROW_CHRONO_USE_OS_TZDB } ARROW_UNSUPPRESS_DEPRECATION_WARNING return Status::OK(); diff --git a/cpp/src/arrow/config.h b/cpp/src/arrow/config.h index cbb29c84ae77..fe5a7937dd6a 100644 --- a/cpp/src/arrow/config.h +++ b/cpp/src/arrow/config.h @@ -66,8 +66,8 @@ struct RuntimeInfo { /// The SIMD level available on the OS and CPU std::string detected_simd_level; - /// Whether using the OS-based timezone database - /// This is set at compile-time. + /// Whether the timezone database is managed by the OS or standard library, + /// rather than Arrow's configurable text database. This is set at compile-time. bool using_os_timezone_db; /// The path to the timezone database; by default None. diff --git a/cpp/src/arrow/pretty_print.cc b/cpp/src/arrow/pretty_print.cc index 723449928592..a311247e53c4 100644 --- a/cpp/src/arrow/pretty_print.cc +++ b/cpp/src/arrow/pretty_print.cc @@ -42,7 +42,6 @@ #include "arrow/util/int_util_overflow.h" #include "arrow/util/key_value_metadata.h" #include "arrow/util/string.h" -#include "arrow/vendored/datetime.h" #include "arrow/visit_array_inline.h" namespace arrow { diff --git a/cpp/src/arrow/public_api_test.cc b/cpp/src/arrow/public_api_test.cc index 12c703f120f6..30a148e239f7 100644 --- a/cpp/src/arrow/public_api_test.cc +++ b/cpp/src/arrow/public_api_test.cc @@ -125,6 +125,19 @@ TEST(Misc, BuildInfo) { // TODO(GH-48593): Remove when libc++ supports std::chrono timezones. ARROW_SUPPRESS_DEPRECATION_WARNING TEST(Misc, SetTimezoneConfig) { + ASSERT_OK(Initialize(GlobalOptions{})); + if (GetRuntimeInfo().using_os_timezone_db) { + ASSERT_FALSE(GetRuntimeInfo().timezone_db_path.has_value()); + // Standard-library backends must reject even an existing path rather than + // silently configuring an unused vendored database. + GlobalOptions options; + options.timezone_db_path = "."; + ASSERT_RAISES(Invalid, Initialize(options)); + ASSERT_FALSE(GetRuntimeInfo().timezone_db_path.has_value()); + EnvVarGuard tzdata("ARROW_TIMEZONE_DATABASE", "."); + ASSERT_OK(InitTestTimezoneDatabase()); + return; + } #ifndef _WIN32 GTEST_SKIP() << "Can only set the Timezone database on Windows"; #elif !defined(ARROW_FILESYSTEM) @@ -164,6 +177,7 @@ TEST(Misc, SetTimezoneConfig) { // Validate that tzdb is working ASSERT_OK(arrow::Initialize(options)); + ASSERT_EQ(GetRuntimeInfo().timezone_db_path, options.timezone_db_path); #endif } ARROW_UNSUPPRESS_DEPRECATION_WARNING diff --git a/cpp/src/arrow/testing/util.cc b/cpp/src/arrow/testing/util.cc index 5edec7cd21fe..2b2b130b2ee0 100644 --- a/cpp/src/arrow/testing/util.cc +++ b/cpp/src/arrow/testing/util.cc @@ -141,6 +141,8 @@ std::optional GetTestTimezoneDatabaseRoot() { // TODO(GH-48593): Remove when libc++ supports std::chrono timezones. ARROW_SUPPRESS_DEPRECATION_WARNING Status InitTestTimezoneDatabase() { + if (GetRuntimeInfo().using_os_timezone_db) return Status::OK(); + auto maybe_tzdata = GetTestTimezoneDatabaseRoot(); // If missing, timezone database will default to %USERPROFILE%\Downloads\tzdata if (!maybe_tzdata.has_value()) return Status::OK(); diff --git a/cpp/src/arrow/util/CMakeLists.txt b/cpp/src/arrow/util/CMakeLists.txt index c67abf55a251..806f9585c154 100644 --- a/cpp/src/arrow/util/CMakeLists.txt +++ b/cpp/src/arrow/util/CMakeLists.txt @@ -22,6 +22,11 @@ # Headers: top level arrow_install_all_headers("arrow/util") +# The automatic rule excludes internal headers, but these are dependencies of +# the installed formatting.h and value_parsing.h headers. +install(FILES chrono_config_internal.h chrono_internal.h + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/arrow/util") + # # arrow_test_main # diff --git a/cpp/src/arrow/util/chrono_config_internal.h b/cpp/src/arrow/util/chrono_config_internal.h new file mode 100644 index 000000000000..731f21065003 --- /dev/null +++ b/cpp/src/arrow/util/chrono_config_internal.h @@ -0,0 +1,56 @@ +// 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. + +#pragma once + +#include + +// Share backend selection with the vendored implementation without including its +// headers. datetime.h undefines macros needed when compiling the implementation. +// +// On Windows, MSVC's standard library uses the system timezone database, while +// libstdc++ reads tzdata files (using TZDIR). Libraries without the C++20 timezone +// APIs, including older libc++, still require the vendored date library. +// +// Use the standard backend by default. Builds may explicitly define +// ARROW_USE_STD_CHRONO to 0 or 1 when they need to select a backend. +// +// Automatically disable the default for libraries without the C++20 timezone APIs. +// On non-Windows, older libstdc++ versions also need the fallback because of +// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=116110 (fully fixed in GCC 16.2). +// Check library macros, not __GNUC__, so Clang using libstdc++ agrees with GCC. +// The datestamp distinguishes 16.2 (2026-08-07) from 16.1 and early snapshots. +// Keep the existing Windows backend selection unchanged. +#ifndef ARROW_USE_STD_CHRONO +# define ARROW_USE_STD_CHRONO 1 +# if !defined(__cpp_lib_chrono) || __cpp_lib_chrono < 201907L +# undef ARROW_USE_STD_CHRONO +# define ARROW_USE_STD_CHRONO 0 +# elif !defined(_WIN32) && defined(__GLIBCXX__) && \ + (!defined(_GLIBCXX_RELEASE) || _GLIBCXX_RELEASE < 16 || __GLIBCXX__ < 20260807) +# undef ARROW_USE_STD_CHRONO +# define ARROW_USE_STD_CHRONO 0 +# endif +#endif + +// Only the vendored Windows text database supports setting its path via Arrow. +// The non-Windows vendored backend uses USE_OS_TZDB (see datetime/visibility.h). +#if ARROW_USE_STD_CHRONO || !defined(_WIN32) +# define ARROW_CHRONO_USE_OS_TZDB 1 +#else +# define ARROW_CHRONO_USE_OS_TZDB 0 +#endif diff --git a/cpp/src/arrow/util/chrono_internal.h b/cpp/src/arrow/util/chrono_internal.h index ea4051bccf59..fbb086028e08 100644 --- a/cpp/src/arrow/util/chrono_internal.h +++ b/cpp/src/arrow/util/chrono_internal.h @@ -21,51 +21,27 @@ /// \brief Abstraction layer for C++20 chrono calendar/timezone APIs /// /// This header provides a unified interface for chrono calendar and timezone -/// functionality. On compilers with full C++20 chrono support, it uses -/// std::chrono. On other compilers, it falls back to the vendored Howard Hinnant +/// functionality. It uses std::chrono with supported C++20 timezone +/// implementations, otherwise falling back to the vendored Howard Hinnant /// date library. +/// See chrono_config_internal.h for backend selection. /// -/// The main benefit is on Windows where std::chrono uses the system timezone -/// database, eliminating the need for users to install IANA tzdata separately. +/// On Windows with MSVC, std::chrono uses the system timezone database, +/// eliminating the need for users to install IANA tzdata separately. #include #include #include -// Feature detection for C++20 chrono timezone support -// https://en.cppreference.com/w/cpp/compiler_support/20.html#cpp_lib_chrono_201907L -// -// On Windows with MSVC: std::chrono uses Windows' internal timezone database, -// eliminating the need for users to install IANA tzdata separately. -// -// On Windows with MinGW/GCC: libstdc++ reads tzdata files via TZDIR env var. -// Set TZDIR=/usr/share/zoneinfo to use the system tzdata. -// -// On non-Windows: GCC libstdc++ has a bug where DST state is incorrectly reset when -// a timezone transitions between rule sets (e.g., Australia/Broken_Hill around -// 2000-02-29). Until this is fixed, we use the vendored date.h library. -// See: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=116110 - -// Use std::chrono on Windows when C++20 chrono timezone support is available. -// The __cpp_lib_chrono >= 201907L feature test macro indicates full support: -// - MSVC: Uses Windows' internal timezone database (no IANA tzdata needed) -// - GCC/libstdc++: Requires TZDIR environment variable to locate tzdata -// - Clang/libc++: Does not define 201907L (no timezone support), so falls back -// -// On non-Windows, we use the vendored date library due to a GCC libstdc++ bug -// where DST state is incorrectly reset during timezone rule transitions. -// See: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=116110 -#if defined(_WIN32) && defined(__cpp_lib_chrono) && __cpp_lib_chrono >= 201907L -# define ARROW_USE_STD_CHRONO 1 -#else -# define ARROW_USE_STD_CHRONO 0 -#endif +#include "arrow/util/chrono_config_internal.h" #if ARROW_USE_STD_CHRONO // Use C++20 standard library chrono # include -# include +# include # include +# include +# include #else // Use vendored Howard Hinnant date library # include "arrow/vendored/datetime.h" @@ -151,22 +127,172 @@ inline const time_zone* locate_zone(std::string_view tz_name) { inline const time_zone* current_zone() { return std::chrono::current_zone(); } -// Formatting support - streams directly using C++20 std::vformat_to -// Provides: direct streaming, stream state preservation, chaining, rich format specifiers +namespace detail { + +// Argument positions passed to std::vformat by to_stream below. +enum class FormatArgument : char { + ZonedTime = '0', + TimeOfDay = '1', + TimeOfDayCount = '2', +}; + +template +void AppendEscapedLiteral(std::basic_string* out, CharT value) { + out->push_back(value); + if (value == CharT{'{'} || value == CharT{'}'}) { + out->push_back(value); + } +} + +// These are the directives accepted by Arrow's existing strftime syntax. Treat +// all others as literals to preserve compatibility. +template +bool IsSupportedStrftimeSpecifier(CharT modifier, CharT specifier) { + const auto contains = [specifier](const char* candidates) { + for (; *candidates != '\0'; ++candidates) { + if (specifier == static_cast(*candidates)) return true; + } + return false; + }; + if (modifier == CharT{}) { + return contains("aAbBhcCxdeDFgGHIjmMprRSTuUVWwXyYzZ"); + } + if (modifier == CharT{'E'}) { + return contains("cCxXyYz"); + } + if (modifier == CharT{'O'}) { + return contains("deHImMSuUVwWyz"); + } + return false; +} + +template +void AppendChronoField(std::basic_string* out, FormatArgument argument, + CharT specifier, CharT modifier = CharT{}) { + *out += {CharT{'{'}, static_cast(argument), CharT{':'}, CharT{'L'}, CharT{'%'}}; + if (modifier != CharT{}) out->push_back(modifier); + *out += {specifier, CharT{'}'}}; +} + +template +void AppendLocalizedField(std::basic_string* out, FormatArgument argument) { + *out += {CharT{'{'}, static_cast(argument), CharT{':'}, CharT{'L'}, CharT{'}'}}; +} + +template +std::basic_string ToChronoFormat(const CharT* fmt, bool use_microseconds_suffix) { + std::basic_string out; + while (*fmt != CharT{}) { + if (*fmt != CharT{'%'}) { + AppendEscapedLiteral(&out, *fmt++); + continue; + } + + ++fmt; + if (*fmt == CharT{}) { + AppendEscapedLiteral(&out, CharT{'%'}); + break; + } + + CharT modifier{}; + if (*fmt == CharT{'E'} || *fmt == CharT{'O'}) { + modifier = *fmt++; + if (*fmt == CharT{}) { + AppendEscapedLiteral(&out, CharT{'%'}); + AppendEscapedLiteral(&out, modifier); + break; + } + } + const CharT specifier = *fmt++; + + if (modifier == CharT{}) { + switch (specifier) { + case CharT{'%'}: + AppendEscapedLiteral(&out, CharT{'%'}); + continue; + case CharT{'n'}: + AppendEscapedLiteral(&out, CharT{'\n'}); + continue; + case CharT{'t'}: + AppendEscapedLiteral(&out, CharT{'\t'}); + continue; + case CharT{'Q'}: + // Formatting a duration's %Q does not consistently apply the numeric locale. + AppendLocalizedField(&out, FormatArgument::TimeOfDayCount); + continue; + case CharT{'q'}: + if (use_microseconds_suffix) { + // Some standard libraries use "us"; Arrow uses the micro sign. + if constexpr (std::is_same_v) { + AppendEscapedLiteral(&out, CharT{'\xC2'}); + AppendEscapedLiteral(&out, CharT{'\xB5'}); + } else { + AppendEscapedLiteral(&out, static_cast(0xB5)); + } + AppendEscapedLiteral(&out, CharT{'s'}); + } else { + AppendChronoField(&out, FormatArgument::TimeOfDay, specifier); + } + continue; + default: + break; + } + } + +# if defined(__GLIBCXX__) + if (modifier == CharT{'O'} && specifier == CharT{'V'}) { + // libstdc++ does not yet accept %OV; use its equivalent base representation. + AppendChronoField(&out, FormatArgument::ZonedTime, specifier); + continue; + } +# endif + + if (IsSupportedStrftimeSpecifier(modifier, specifier)) { + AppendChronoField(&out, FormatArgument::ZonedTime, specifier, modifier); + } else { + AppendEscapedLiteral(&out, CharT{'%'}); + if (modifier != CharT{}) AppendEscapedLiteral(&out, modifier); + AppendEscapedLiteral(&out, specifier); + } + } + return out; +} + +} // namespace detail + +// Convert Arrow's strftime syntax to C++20 replacement fields. Literal braces and +// unsupported directives remain literal, and %Q/%q use local time of day. template std::basic_ostream& to_stream( std::basic_ostream& os, const CharT* fmt, const std::chrono::zoned_time& zt) { - std::vformat_to(std::ostreambuf_iterator(os), std::string("{:") + fmt + "}", - std::make_format_args(zt)); + static_assert(std::is_same_v || std::is_same_v); + using Precision = typename std::chrono::zoned_time::duration; + const auto standard_format = detail::ToChronoFormat( + fmt, std::ratio_equal_v); + const auto local_time = zt.get_local_time(); + const auto local_day = std::chrono::floor(local_time); + const auto time_of_day = local_time - local_day; + const auto time_of_day_count = time_of_day.count(); + + std::basic_string formatted; + if constexpr (std::is_same_v) { + formatted = std::vformat(os.getloc(), standard_format, + std::make_format_args(zt, time_of_day, time_of_day_count)); + } else { + formatted = std::vformat(os.getloc(), standard_format, + std::make_wformat_args(zt, time_of_day, time_of_day_count)); + } + os.write(formatted.data(), static_cast(formatted.size())); return os; } -// Format a duration using strftime-like format specifiers -// Converts "%H%M" style to C++20's "{:%H%M}" style and uses std::vformat -template -std::string format(const char* fmt, const Duration& d) { - return std::vformat(std::string("{:") + fmt + "}", std::make_format_args(d)); +// Format a duration or time point using strftime-like format specifiers. +// Converts "%H%M" style to C++20's "{:L%H%M}" style and uses std::vformat. +template +std::string format(const char* fmt, const Temporal& value) { + return std::vformat(std::locale{}, std::string("{:L") + fmt + "}", + std::make_format_args(value)); } inline constexpr std::chrono::month jan = std::chrono::January; @@ -248,6 +374,11 @@ inline const time_zone* locate_zone(std::string_view tz_name) { inline const time_zone* current_zone() { return vendored::current_zone(); } +# if !ARROW_CHRONO_USE_OS_TZDB +using vendored::reload_tzdb; +using vendored::set_install; +# endif + // Formatting support using vendored::format; diff --git a/cpp/src/arrow/util/formatting.h b/cpp/src/arrow/util/formatting.h index 844b6fb91a8d..3447513526e9 100644 --- a/cpp/src/arrow/util/formatting.h +++ b/cpp/src/arrow/util/formatting.h @@ -32,11 +32,11 @@ #include "arrow/status.h" #include "arrow/type_fwd.h" #include "arrow/type_traits.h" +#include "arrow/util/chrono_internal.h" #include "arrow/util/macros.h" #include "arrow/util/string.h" #include "arrow/util/time.h" #include "arrow/util/visibility.h" -#include "arrow/vendored/datetime.h" namespace arrow { namespace internal { @@ -344,7 +344,7 @@ constexpr size_t BufferSizeYYYY_MM_DD() { detail::Digits10(31); } -inline void FormatYYYY_MM_DD(arrow_vendored::date::year_month_day ymd, char** cursor) { +inline void FormatYYYY_MM_DD(chrono::year_month_day ymd, char** cursor) { FormatTwoDigits(static_cast(ymd.day()), cursor); FormatOneChar('-', cursor); FormatTwoDigits(static_cast(ymd.month()), cursor); @@ -372,7 +372,7 @@ constexpr size_t BufferSizeHH_MM_SS() { } template -void FormatHH_MM_SS(arrow_vendored::date::hh_mm_ss hms, char** cursor) { +void FormatHH_MM_SS(chrono::hh_mm_ss hms, char** cursor) { constexpr size_t subsecond_digits = Digits10(Duration::period::den) - 1; if (subsecond_digits != 0) { FormatAllDigitsLeftPadded(hms.subseconds().count(), subsecond_digits, '0', cursor); @@ -386,20 +386,18 @@ void FormatHH_MM_SS(arrow_vendored::date::hh_mm_ss hms, char** cursor) } // Some out-of-bound datetime values would result in erroneous printing -// because of silent integer wraparound in the `arrow_vendored::date` library. +// because calendar conversions outside the supported year range can wrap around. // // To avoid such misprinting, we must therefore check the bounds explicitly. // The bounds correspond to start of year -32767 and end of year 32767, -// respectively (-32768 is an invalid year value in `arrow_vendored::date`). +// respectively (-32768 is an invalid year value in both chrono backends). // // Note these values are the same as documented for C++20: // https://en.cppreference.com/w/cpp/chrono/year_month_day/operator_days template bool IsDateTimeInRange(Unit duration) { - constexpr Unit kMinIncl = - std::chrono::duration_cast(arrow_vendored::date::days{-12687428}); - constexpr Unit kMaxExcl = - std::chrono::duration_cast(arrow_vendored::date::days{11248738}); + constexpr Unit kMinIncl = std::chrono::duration_cast(chrono::days{-12687428}); + constexpr Unit kMaxExcl = std::chrono::duration_cast(chrono::days{11248738}); return duration >= kMinIncl && duration < kMaxExcl; } @@ -422,7 +420,7 @@ Return FormatOutOfRange(RawValue&& raw_value, Appender&& append) { return append(std::move(formatted)); } -const auto kEpoch = arrow_vendored::date::sys_days{arrow_vendored::date::jan / 1 / 1970}; +const auto kEpoch = chrono::sys_days{chrono::jan / 1 / 1970}; } // namespace detail @@ -437,16 +435,15 @@ class DateToStringFormatterMixin { protected: template - Return FormatDays(arrow_vendored::date::days since_epoch, Appender&& append) { - arrow_vendored::date::sys_days timepoint_days{since_epoch}; + Return FormatDays(chrono::days since_epoch, Appender&& append) { + chrono::sys_days timepoint_days{since_epoch}; constexpr size_t buffer_size = detail::BufferSizeYYYY_MM_DD(); std::array buffer; char* cursor = buffer.data() + buffer_size; - detail::FormatYYYY_MM_DD(arrow_vendored::date::year_month_day{timepoint_days}, - &cursor); + detail::FormatYYYY_MM_DD(chrono::year_month_day{timepoint_days}, &cursor); return append(detail::ViewDigitBuffer(buffer, cursor)); } }; @@ -460,7 +457,7 @@ class StringFormatter : public DateToStringFormatterMixin { template Return operator()(value_type value, Appender&& append) { - const auto since_epoch = arrow_vendored::date::days{value}; + const auto since_epoch = chrono::days{value}; if (!ARROW_PREDICT_TRUE(detail::IsDateTimeInRange(since_epoch))) { return detail::FormatOutOfRange(value, append); } @@ -481,7 +478,7 @@ class StringFormatter : public DateToStringFormatterMixin { if (!ARROW_PREDICT_TRUE(detail::IsDateTimeInRange(since_epoch))) { return detail::FormatOutOfRange(value, append); } - return FormatDays(std::chrono::duration_cast(since_epoch), + return FormatDays(std::chrono::duration_cast(since_epoch), std::forward(append)); } }; @@ -497,7 +494,7 @@ class StringFormatter { template Return operator()(Duration, value_type value, Appender&& append) { - using arrow_vendored::date::days; + using chrono::days; const Duration since_epoch{value}; if (!ARROW_PREDICT_TRUE(detail::IsDateTimeInRange(since_epoch))) { @@ -506,7 +503,7 @@ class StringFormatter { const auto timepoint = detail::kEpoch + since_epoch; // Round days towards zero - // (the naive approach of using arrow_vendored::date::floor() would + // (the naive approach of using chrono::floor() would // result in UB for very large negative timestamps, similarly as // https://github.com/HowardHinnant/date/issues/696) auto timepoint_days = std::chrono::time_point_cast(timepoint); @@ -530,7 +527,7 @@ class StringFormatter { if (timezone_.size() > 0) { detail::FormatOneChar('Z', &cursor); } - detail::FormatHH_MM_SS(arrow_vendored::date::make_time(since_midnight), &cursor); + detail::FormatHH_MM_SS(chrono::hh_mm_ss{since_midnight}, &cursor); detail::FormatOneChar(' ', &cursor); detail::FormatYYYY_MM_DD(timepoint_days, &cursor); return append(detail::ViewDigitBuffer(buffer, cursor)); @@ -566,7 +563,7 @@ class StringFormatter> { std::array buffer; char* cursor = buffer.data() + buffer_size; - detail::FormatHH_MM_SS(arrow_vendored::date::make_time(since_midnight), &cursor); + detail::FormatHH_MM_SS(chrono::hh_mm_ss{since_midnight}, &cursor); return append(detail::ViewDigitBuffer(buffer, cursor)); } diff --git a/cpp/src/arrow/util/logger_test.cc b/cpp/src/arrow/util/logger_test.cc index 0faea81a598f..786d99e157e4 100644 --- a/cpp/src/arrow/util/logger_test.cc +++ b/cpp/src/arrow/util/logger_test.cc @@ -23,8 +23,9 @@ #include "arrow/testing/gtest_util.h" #include "arrow/util/logger.h" -// Emit log via the default logger -#define DO_LOG(LEVEL, ...) ARROW_LOGGER_CALL("", LEVEL, __VA_ARGS__) +// Emit log via the default logger. Token-paste here to prevent Windows' ERROR +// macro from expanding before the logger macro is selected. +#define DO_LOG(LEVEL, ...) ARROW_LOGGER_##LEVEL("", __VA_ARGS__) namespace arrow { namespace util { diff --git a/cpp/src/arrow/util/meson.build b/cpp/src/arrow/util/meson.build index 729cfba47222..52c19fa60079 100644 --- a/cpp/src/arrow/util/meson.build +++ b/cpp/src/arrow/util/meson.build @@ -121,6 +121,8 @@ install_headers( 'byte_size.h', 'cancel.h', 'checked_cast.h', + 'chrono_config_internal.h', + 'chrono_internal.h', 'compare.h', 'compression.h', 'concurrent_map.h', diff --git a/cpp/src/arrow/util/value_parsing.h b/cpp/src/arrow/util/value_parsing.h index 195cdc843ac6..750970a33fee 100644 --- a/cpp/src/arrow/util/value_parsing.h +++ b/cpp/src/arrow/util/value_parsing.h @@ -31,13 +31,13 @@ #include "arrow/type.h" #include "arrow/type_traits.h" #include "arrow/util/checked_cast.h" +#include "arrow/util/chrono_internal.h" #include "arrow/util/config.h" #include "arrow/util/float16.h" #include "arrow/util/int_util_overflow.h" #include "arrow/util/macros.h" #include "arrow/util/time.h" #include "arrow/util/visibility.h" -#include "arrow/vendored/datetime.h" #include "arrow/vendored/strptime.h" namespace arrow { @@ -651,13 +651,11 @@ static inline bool ParseYYYY_MM_DD(const char* s, Duration* since_epoch) { if (ARROW_PREDICT_FALSE(!ParseUnsigned(s + 8, 2, &day))) { return false; } - arrow_vendored::date::year_month_day ymd{arrow_vendored::date::year{year}, - arrow_vendored::date::month{month}, - arrow_vendored::date::day{day}}; + chrono::year_month_day ymd{chrono::year{year}, chrono::month{month}, chrono::day{day}}; if (ARROW_PREDICT_FALSE(!ymd.ok())) return false; - *since_epoch = std::chrono::duration_cast( - arrow_vendored::date::sys_days{ymd}.time_since_epoch()); + *since_epoch = + std::chrono::duration_cast(chrono::sys_days{ymd}.time_since_epoch()); return true; } @@ -810,7 +808,7 @@ static inline bool ParseTimestampStrptime(const char* buf, size_t length, const char* format, bool ignore_time_in_day, bool allow_trailing_chars, TimeUnit::type unit, int64_t* out) { - // NOTE: strptime() is more than 10x faster than arrow_vendored::date::parse(). + // Keep strptime(): benchmarks found it more than 10x faster than date::parse(). // The buffer may not be nul-terminated std::string clean_copy(buf, length); struct tm result; @@ -827,9 +825,9 @@ static inline bool ParseTimestampStrptime(const char* buf, size_t length, return false; } // ignore the time part - arrow_vendored::date::sys_seconds secs = - arrow_vendored::date::sys_days(arrow_vendored::date::year(result.tm_year + 1900) / - (result.tm_mon + 1) / std::max(result.tm_mday, 1)); + chrono::sys_seconds secs = + chrono::sys_days(chrono::year(result.tm_year + 1900) / (result.tm_mon + 1) / + std::max(result.tm_mday, 1)); if (!ignore_time_in_day) { secs += (std::chrono::hours(result.tm_hour) + std::chrono::minutes(result.tm_min) + std::chrono::seconds(result.tm_sec)); @@ -860,8 +858,7 @@ struct StringConverter> { using value_type = typename DATE_TYPE::c_type; using duration_type = - typename std::conditional::value, - arrow_vendored::date::days, + typename std::conditional::value, chrono::days, std::chrono::milliseconds>::type; bool Convert(const DATE_TYPE& type, const char* s, size_t length, value_type* out) { diff --git a/cpp/src/arrow/vendored/datetime.cpp b/cpp/src/arrow/vendored/datetime.cpp index 0f0bd12c7e16..b4f9dd368300 100644 --- a/cpp/src/arrow/vendored/datetime.cpp +++ b/cpp/src/arrow/vendored/datetime.cpp @@ -15,5 +15,12 @@ // specific language governing permissions and limitations // under the License. -#include "datetime/visibility.h" -#include "datetime/tz.cpp" +#include "arrow/util/chrono_config_internal.h" + +// Keep backend selection identical to the callers, including in Gandiva tests. +// Standard-library builds must not compile a second timezone implementation. +#if !ARROW_USE_STD_CHRONO +# include "datetime/visibility.h" + +# include "datetime/tz.cpp" +#endif diff --git a/cpp/src/arrow/vendored/datetime_ios.mm b/cpp/src/arrow/vendored/datetime_ios.mm new file mode 100644 index 000000000000..35f0fe08d729 --- /dev/null +++ b/cpp/src/arrow/vendored/datetime_ios.mm @@ -0,0 +1,25 @@ +// 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. + +// Evaluate automatic backend selection only when the build has not selected one. +#ifndef ARROW_USE_STD_CHRONO +# include "arrow/util/chrono_config_internal.h" +#endif + +#if !ARROW_USE_STD_CHRONO +# include "datetime/ios.mm" +#endif diff --git a/cpp/src/gandiva/cast_time.cc b/cpp/src/gandiva/cast_time.cc index f170375298b5..3effeeab36ab 100644 --- a/cpp/src/gandiva/cast_time.cc +++ b/cpp/src/gandiva/cast_time.cc @@ -17,7 +17,7 @@ #include -#include "arrow/vendored/datetime.h" +#include "arrow/util/chrono_internal.h" #include "gandiva/precompiled/time_fields.h" @@ -48,17 +48,19 @@ arrow::Status ExportedTimeFunctions::AddMappings(Engine* engine) const { } // namespace gandiva #endif // !GANDIVA_UNIT_TEST +namespace chrono = arrow::internal::chrono; + extern "C" { // TODO : Do input validation or make sure the callers do that ? int gdv_fn_time_with_zone(int* time_fields, const char* zone, int zone_len, int64_t* ret_time) { - using arrow_vendored::date::day; - using arrow_vendored::date::local_days; - using arrow_vendored::date::locate_zone; - using arrow_vendored::date::month; - using arrow_vendored::date::time_zone; - using arrow_vendored::date::year; + using chrono::day; + using chrono::local_days; + using chrono::locate_zone; + using chrono::month; + using chrono::time_zone; + using chrono::year; using std::chrono::hours; using std::chrono::milliseconds; using std::chrono::minutes; diff --git a/cpp/src/gandiva/gdv_function_stubs.cc b/cpp/src/gandiva/gdv_function_stubs.cc index 6b3e9935b017..13cbcff69b5c 100644 --- a/cpp/src/gandiva/gdv_function_stubs.cc +++ b/cpp/src/gandiva/gdv_function_stubs.cc @@ -27,6 +27,7 @@ #include "arrow/util/base64.h" #include "arrow/util/bit_util.h" +#include "arrow/util/chrono_internal.h" #include "arrow/util/double_conversion_internal.h" #include "arrow/util/value_parsing.h" @@ -38,6 +39,8 @@ #include "gandiva/random_generator_holder.h" #include "gandiva/to_date_holder.h" +namespace chrono = arrow::internal::chrono; + /// Stub functions that can be accessed from LLVM or the pre-compiled library. extern "C" { @@ -835,8 +838,8 @@ int32_t gdv_fn_cast_intervalyear_utf8_int32(int64_t context_ptr, int64_t holder_ GANDIVA_EXPORT gdv_timestamp to_utc_timezone_timestamp(int64_t context, gdv_timestamp time_milliseconds, const char* timezone, gdv_int32 length) { - using arrow_vendored::date::locate_zone; - using arrow_vendored::date::sys_time; + using chrono::locate_zone; + using chrono::sys_time; using std::chrono::milliseconds; sys_time tp{milliseconds{time_milliseconds}}; @@ -855,8 +858,8 @@ GANDIVA_EXPORT gdv_timestamp from_utc_timezone_timestamp(gdv_int64 context, gdv_timestamp time_milliseconds, const char* timezone, gdv_int32 length) { - using arrow_vendored::date::sys_time; - using arrow_vendored::date::zoned_time; + using chrono::sys_time; + using chrono::zoned_time; using std::chrono::milliseconds; const sys_time tp{milliseconds{time_milliseconds}}; diff --git a/cpp/src/gandiva/precompiled/epoch_time_point.h b/cpp/src/gandiva/precompiled/epoch_time_point.h index 45cfb28ca38c..781d588a51ac 100644 --- a/cpp/src/gandiva/precompiled/epoch_time_point.h +++ b/cpp/src/gandiva/precompiled/epoch_time_point.h @@ -17,11 +17,12 @@ #pragma once -// TODO(wesm): IR compilation does not have any include directories set -#include "../../arrow/vendored/datetime/date.h" +#include "arrow/util/chrono_internal.h" + +namespace chrono = arrow::internal::chrono; bool is_leap_year(int yy); -bool did_days_overflow(arrow_vendored::date::year_month_day ymd); +bool did_days_overflow(chrono::year_month_day ymd); int last_possible_day_in_month(int month, int year); // A point of time measured in millis since epoch. @@ -38,19 +39,16 @@ class EpochTimePoint { int TmMon() const { return static_cast(YearMonthDay().month()) - 1; } int TmYday() const { - auto to_days = arrow_vendored::date::floor(tp_); - auto first_day_in_year = arrow_vendored::date::sys_days{ - YearMonthDay().year() / arrow_vendored::date::jan / 1}; + auto to_days = chrono::floor(tp_); + auto first_day_in_year = chrono::sys_days{YearMonthDay().year() / chrono::jan / 1}; return (to_days - first_day_in_year).count(); } int TmMday() const { return static_cast(YearMonthDay().day()); } int TmWday() const { - auto to_days = arrow_vendored::date::floor(tp_); - return (arrow_vendored::date::weekday{to_days} - // NOLINT - arrow_vendored::date::Sunday) - .count(); + auto to_days = chrono::floor(tp_); + return (chrono::weekday{to_days} - chrono::Sunday).count(); } int TmHour() const { return static_cast(TimeOfDay().hours().count()); } @@ -63,16 +61,16 @@ class EpochTimePoint { } EpochTimePoint AddYears(int num_years) const { - auto ymd = YearMonthDay() + arrow_vendored::date::years(num_years); - return EpochTimePoint((arrow_vendored::date::sys_days{ymd} + // NOLINT + auto ymd = YearMonthDay() + chrono::years(num_years); + return EpochTimePoint((chrono::sys_days{ymd} + // NOLINT TimeOfDay().to_duration()) .time_since_epoch()); } EpochTimePoint AddMonths(int num_months) const { - auto ymd = YearMonthDay() + arrow_vendored::date::months(num_months); + auto ymd = YearMonthDay() + chrono::months(num_months); - EpochTimePoint tp = EpochTimePoint((arrow_vendored::date::sys_days{ymd} + // NOLINT + EpochTimePoint tp = EpochTimePoint((chrono::sys_days{ymd} + // NOLINT TimeOfDay().to_duration()) .time_since_epoch()); @@ -87,8 +85,8 @@ class EpochTimePoint { } EpochTimePoint AddDays(int num_days) const { - auto days_since_epoch = arrow_vendored::date::sys_days{YearMonthDay()} + // NOLINT - arrow_vendored::date::days(num_days); + auto days_since_epoch = chrono::sys_days{YearMonthDay()} + // NOLINT + chrono::days(num_days); return EpochTimePoint( (days_since_epoch + TimeOfDay().to_duration()).time_since_epoch()); } @@ -101,17 +99,14 @@ class EpochTimePoint { int64_t MillisSinceEpoch() const { return tp_.time_since_epoch().count(); } - arrow_vendored::date::time_of_day TimeOfDay() const { - auto millis_since_midnight = - tp_ - arrow_vendored::date::floor(tp_); - return arrow_vendored::date::time_of_day( - millis_since_midnight); + chrono::hh_mm_ss TimeOfDay() const { + auto millis_since_midnight = tp_ - chrono::floor(tp_); + return chrono::hh_mm_ss{millis_since_midnight}; } private: - arrow_vendored::date::year_month_day YearMonthDay() const { - return arrow_vendored::date::year_month_day{ - arrow_vendored::date::floor(tp_)}; // NOLINT + chrono::year_month_day YearMonthDay() const { + return chrono::year_month_day{chrono::floor(tp_)}; // NOLINT } std::chrono::time_point tp_; diff --git a/cpp/src/gandiva/precompiled/time.cc b/cpp/src/gandiva/precompiled/time.cc index 2b60f63651db..f1c3a189b0a3 100644 --- a/cpp/src/gandiva/precompiled/time.cc +++ b/cpp/src/gandiva/precompiled/time.cc @@ -637,11 +637,11 @@ void set_error_for_date(gdv_int32 length, const char* input, const char* msg, } gdv_date64 castDATE_utf8(int64_t context, const char* input, gdv_int32 length) { - using arrow_vendored::date::day; - using arrow_vendored::date::month; - using arrow_vendored::date::sys_days; - using arrow_vendored::date::year; - using arrow_vendored::date::year_month_day; + using chrono::day; + using chrono::month; + using chrono::sys_days; + using chrono::year; + using chrono::year_month_day; using gandiva::TimeFields; // format : 0 is year, 1 is month and 2 is day. int dateFields[3]; @@ -701,11 +701,11 @@ gdv_date64 castDATE_utf8(int64_t context, const char* input, gdv_int32 length) { * Format is [ hours:minutes:seconds][.millis][ displacement|zone] */ gdv_timestamp castTIMESTAMP_utf8(int64_t context, const char* input, gdv_int32 length) { - using arrow_vendored::date::day; - using arrow_vendored::date::month; - using arrow_vendored::date::sys_days; - using arrow_vendored::date::year; - using arrow_vendored::date::year_month_day; + using chrono::day; + using chrono::month; + using chrono::sys_days; + using chrono::year; + using chrono::year_month_day; using gandiva::TimeFields; using std::chrono::hours; using std::chrono::milliseconds; diff --git a/cpp/src/gandiva/precompiled/timestamp_arithmetic.cc b/cpp/src/gandiva/precompiled/timestamp_arithmetic.cc index 695605b3cc77..018af1d14af4 100644 --- a/cpp/src/gandiva/precompiled/timestamp_arithmetic.cc +++ b/cpp/src/gandiva/precompiled/timestamp_arithmetic.cc @@ -41,7 +41,7 @@ bool is_last_day_of_month(const EpochTimePoint& tp) { return (tp.TmMday() == days_in_a_month[matrix_index][tp.TmMon()]); } -bool did_days_overflow(arrow_vendored::date::year_month_day ymd) { +bool did_days_overflow(chrono::year_month_day ymd) { int year = static_cast(ymd.year()); int month = static_cast(ymd.month()); int days = static_cast(ymd.day()); diff --git a/cpp/src/gandiva/to_date_holder.cc b/cpp/src/gandiva/to_date_holder.cc index 76f16f0cb1b7..8b7220bb8107 100644 --- a/cpp/src/gandiva/to_date_holder.cc +++ b/cpp/src/gandiva/to_date_holder.cc @@ -21,7 +21,6 @@ #include #include "arrow/util/value_parsing.h" -#include "arrow/vendored/datetime.h" #include "gandiva/date_utils.h" #include "gandiva/execution_context.h" #include "gandiva/node.h" diff --git a/dev/tasks/tasks.yml b/dev/tasks/tasks.yml index 523e9a1fbf87..3591c02c0bdd 100644 --- a/dev/tasks/tasks.yml +++ b/dev/tasks/tasks.yml @@ -452,6 +452,17 @@ tasks: LLVM: "22" image: debian-cpp + test-debian-experimental-cpp-gcc-16: + ci: github + template: docker-tests/github.linux.yml + params: + env: + ARCH: "amd64" + DEBIAN: "experimental" + GCC: "16" + LLVM: "22" + image: debian-cpp + test-fedora-42-cpp: ci: github template: docker-tests/github.linux.yml