Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cpp/src/arrow/compute/api_scalar.cc
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,11 @@ SCALAR_EAGER_UNARY(USWeek, "us_week")
SCALAR_EAGER_UNARY(USYear, "us_year")
SCALAR_EAGER_UNARY(Year, "year")

Result<Datum> MakeDate(const Datum& year, const Datum& month, const Datum& day,
ExecContext* ctx) {
return CallFunction("make_date", {year, month, day}, ctx);
}

Result<Datum> AssumeTimezone(const Datum& arg, AssumeTimezoneOptions options,
ExecContext* ctx) {
return CallFunction("assume_timezone", {arg}, &options, ctx);
Expand Down
14 changes: 14 additions & 0 deletions cpp/src/arrow/compute/api_scalar.h
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,20 @@ Result<Datum> Month(const Datum& values, ExecContext* ctx = NULLPTR);
ARROW_EXPORT
Result<Datum> Day(const Datum& values, ExecContext* ctx = NULLPTR);

/// \brief MakeDate composes date32 values from year, month and day components.
///
/// \param[in] year input year component
/// \param[in] month input month component
/// \param[in] day input day component
/// \param[in] ctx the function execution context, optional
/// \return the resulting datum
///
/// \since 22.0.0
/// \note API not yet finalized
ARROW_EXPORT
Result<Datum> MakeDate(const Datum& year, const Datum& month, const Datum& day,
ExecContext* ctx = NULLPTR);

/// \brief YearMonthDay returns a struct containing the Year, Month and Day value for
/// each element of `values`.
///
Expand Down
89 changes: 89 additions & 0 deletions cpp/src/arrow/compute/kernels/scalar_temporal_binary.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@
// specific language governing permissions and limitations
// under the License.

#include <array>
#include <cmath>
#include <initializer_list>
#include <limits>
#include <sstream>

#include "arrow/builder.h"
#include "arrow/compute/api_scalar.h"
#include "arrow/compute/kernels/common_internal.h"
#include "arrow/compute/kernels/temporal_internal.h"
#include "arrow/compute/registry_internal.h"
#include "arrow/scalar.h"
#include "arrow/util/bit_util.h"
#include "arrow/util/checked_cast.h"
#include "arrow/util/logging_internal.h"
#include "arrow/util/time.h"
Expand All @@ -48,15 +52,18 @@ using chrono::jan;
using chrono::last;
using chrono::local_days;
using chrono::local_time;
using chrono::month;
using chrono::mon;
using chrono::sun;
using chrono::day;
using chrono::sys_days;
using chrono::sys_time;
using chrono::thu;
using chrono::trunc;
using chrono::wed;
using chrono::weekday;
using chrono::weeks;
using chrono::year;
using chrono::year_month_day;
using chrono::year_month_weekday;
using chrono::years;
Expand Down Expand Up @@ -311,6 +318,73 @@ using MicrosecondsBetween = UnitsBetween<std::chrono::microseconds, Duration, Lo
template <typename Duration, typename Localizer>
using NanosecondsBetween = UnitsBetween<std::chrono::nanoseconds, Duration, Localizer>;

bool IsValueValid(const ArraySpan& array, int64_t i) {
return !array.MayHaveNulls() ||
bit_util::GetBit(array.buffers[0].data, array.offset + i);
}

Result<Date32Type::c_type> ComposeDate32(int64_t year_component, int64_t month_component,
int64_t day_component) {
const int64_t year_min = static_cast<int>(year::min());
const int64_t year_max = static_cast<int>(year::max());
if (year_component < year_min || year_component > year_max) {
return Status::Invalid("Year component out of range for date32: ", year_component);
}

const auto ymd = year{static_cast<int>(year_component)} /
month{static_cast<unsigned>(month_component)} /
day{static_cast<unsigned>(day_component)};
if (!ymd.ok()) {
return Status::Invalid("Invalid date components: year=", year_component,
", month=", month_component, ", day=", day_component);
}

const auto days_since_epoch = sys_days{ymd}.time_since_epoch().count();
if (days_since_epoch < std::numeric_limits<Date32Type::c_type>::min() ||
days_since_epoch > std::numeric_limits<Date32Type::c_type>::max()) {
return Status::Invalid("Composed date is out of range for date32: year=",
year_component, ", month=", month_component,
", day=", day_component);
}
return static_cast<Date32Type::c_type>(days_since_epoch);
}

struct MakeDate {
static Status Exec(KernelContext*, const ExecSpan& batch, ExecResult* out) {
std::array<const ArraySpan*, 3> arrays = {nullptr, nullptr, nullptr};
std::array<const int64_t*, 3> data = {nullptr, nullptr, nullptr};
std::array<int64_t, 3> scalars = {0, 0, 0};

for (int i = 0; i < batch.num_values(); ++i) {
if (batch[i].is_array()) {
arrays[i] = &batch[i].array;
data[i] = batch[i].array.GetValues<int64_t>(1);
} else {
const auto& scalar = *batch[i].scalar;
if (!scalar.is_valid) {
return Status::OK();
}
scalars[i] = checked_cast<const Int64Scalar&>(scalar).value;
}
}

auto* out_values = out->array_span_mutable()->GetValues<Date32Type::c_type>(1);
for (int64_t i = 0; i < batch.length; ++i) {
if ((arrays[0] && !IsValueValid(*arrays[0], i)) ||
(arrays[1] && !IsValueValid(*arrays[1], i)) ||
(arrays[2] && !IsValueValid(*arrays[2], i))) {
continue;
}

const int64_t years = arrays[0] ? data[0][i] : scalars[0];
const int64_t months = arrays[1] ? data[1][i] : scalars[1];
const int64_t days = arrays[2] ? data[2][i] : scalars[2];
ARROW_ASSIGN_OR_RAISE(out_values[i], ComposeDate32(years, months, days));
}
return Status::OK();
}
};

// ----------------------------------------------------------------------
// Registration helpers

Expand Down Expand Up @@ -453,9 +527,24 @@ const FunctionDoc nanoseconds_between_doc{
"Null values emit null."),
{"start", "end"}};

const FunctionDoc make_date_doc{
"Compose date32 values from year, month, and day components",
("For each set of input components, compose a date32 value.\n"
"Null values emit null.\n"
"An error is returned if any component set does not form a valid date."),
{"year", "month", "day"}};

} // namespace

void RegisterScalarTemporalBinary(FunctionRegistry* registry) {
// Temporal composition functions
auto make_date =
std::make_shared<ScalarFunction>("make_date", Arity::Ternary(), make_date_doc);
ScalarKernel make_date_kernel({int64(), int64(), int64()}, date32(), MakeDate::Exec);
make_date_kernel.null_handling = NullHandling::INTERSECTION;
DCHECK_OK(make_date->AddKernel(std::move(make_date_kernel)));
DCHECK_OK(registry->AddFunction(std::move(make_date)));

// Temporal difference functions
auto years_between =
BinaryTemporalFactory<YearsBetween, TemporalBinary, Int64Type>::Make<
Expand Down
30 changes: 30 additions & 0 deletions cpp/src/arrow/compute/kernels/scalar_temporal_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,36 @@ TEST_F(ScalarTemporalTest, TestTemporalComponentExtractionWithDifferentUnits) {
}
}

TEST_F(ScalarTemporalTest, TestMakeDate) {
auto years = ArrayFromJSON(int64(), year);
auto months = ArrayFromJSON(int64(), month);
auto days = ArrayFromJSON(int64(), day);
auto expected = ArrayFromJSON(date32(), date32s);

ASSERT_OK_AND_ASSIGN(auto actual, CallFunction("make_date", {years, months, days}));
AssertDatumsEqual(expected, actual);

ASSERT_OK_AND_ASSIGN(
auto from_wrapper,
MakeDate(ScalarFromJSON(int64(), "2024"), ArrayFromJSON(int8(), "[1, 2, null]"),
ScalarFromJSON(int16(), "1")));
AssertDatumsEqual(ArrayFromJSON(date32(), "[19723, 19754, null]"), from_wrapper);

ASSERT_OK_AND_ASSIGN(
auto null_scalar,
CallFunction("make_date",
{ScalarFromJSON(int64(), "null"),
ArrayFromJSON(int64(), "[1, 2, 3]"),
ScalarFromJSON(int64(), "1")}));
AssertDatumsEqual(ArrayFromJSON(date32(), "[null, null, null]"), null_scalar);

EXPECT_RAISES_WITH_MESSAGE_THAT(
Invalid, ::testing::HasSubstr("Invalid date components"),
CallFunction("make_date",
{ArrayFromJSON(int64(), "[2021]"), ArrayFromJSON(int64(), "[2]"),
ArrayFromJSON(int64(), "[29]")}));
}

TEST_F(ScalarTemporalTest, TestOutsideNanosecondRange) {
const char* times = R"(["1677-09-20T00:00:59.123456", "2262-04-13T23:23:23.999999"])";
auto unit = timestamp(TimeUnit::MICRO);
Expand Down
20 changes: 20 additions & 0 deletions python/pyarrow/tests/test_compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -2548,6 +2548,26 @@ def _check_datetime_components(timestamps, timezone=None):
assert pc.week(tsa, options=week_options).equals(pa.array(iso_week))


def test_make_date():
year = pa.array([1970, 2000, 2019, None], type=pa.int32())
month = pa.array([1, 2, 12, 1], type=pa.int8())
day = pa.array([1, 29, 31, 1], type=pa.uint8())
expected = pa.array([datetime.date(1970, 1, 1),
datetime.date(2000, 2, 29),
datetime.date(2019, 12, 31),
None], type=pa.date32())
assert pc.make_date(year, month, day).equals(expected)

broadcasted = pc.make_date(2024, pa.array([1, 2, None], type=pa.int16()), 1)
expected_broadcasted = pa.array([datetime.date(2024, 1, 1),
datetime.date(2024, 2, 1),
None], type=pa.date32())
assert broadcasted.equals(expected_broadcasted)

with pytest.raises(pa.ArrowInvalid, match="Invalid date components"):
pc.make_date([2021], [2], [29])


@pytest.mark.pandas
def test_extract_datetime_components(request):
timestamps = ["1970-01-01T00:00:59.123456789",
Expand Down
Loading