From 33b714ec4e0c0f30236dc83223ff831517e0165d Mon Sep 17 00:00:00 2001 From: Kevin Gurney Date: Wed, 6 Sep 2023 11:37:58 -0400 Subject: [PATCH] GH-37572: [MATLAB] Add `arrow.array.Date64Array` class (#37581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Rationale for this change Now that `arrow.type.Date64Type` class has been added to the MATLAB Interface (#37578), we can add the `arrow.array.Date64Array` class. `Date64Array`s can be created from MATLAB [`datetime`](https://www.mathworks.com/help/matlab/ref/datetime.html) values. ### What changes are included in this PR? 1. Added a new `arrow.array.Date64Array` class. 2. Added a new `arrow.type.traits.Date64Traits` class. 3. Added `arrow.type.Date64Type` support to `arrow.type.traits.traits` function. 4. Factored out `convertToEpochTime` method on `TimestampArray` into internal helper function `arrow.array.internal.temporal.convertDatetimeToEpochTime`. 5. Updated `arrow.internal.test.tabular.createAllSupportedArrayTypes` to include `Date64Array`. `Date64Array`s can be created from MATLAB [`datetime`](https://www.mathworks.com/help/matlab/ref/datetime.html) values using the `fromMATLAB` method. `Date64Array`s can be converted to MATLAB `datetime` values using the `toMATLAB` method. **Example** ```matlab >> dates = datetime + milliseconds(1:5)' dates = 5×1 datetime array 05-Sep-2023 16:47:12 05-Sep-2023 16:47:12 05-Sep-2023 16:47:12 05-Sep-2023 16:47:12 05-Sep-2023 16:47:12 % "SSS" displays fractional seconds (i.e. milliseconds) >> dates.Format = "MMM dd, yyyy HH:mm:ss SSS" dates = 5×1 datetime array Sep 05, 2023 16:47:12 933 Sep 05, 2023 16:47:12 934 Sep 05, 2023 16:47:12 935 Sep 05, 2023 16:47:12 936 Sep 05, 2023 16:47:12 937 >> array = arrow.array.Date64Array.fromMATLAB(dates) array = [ 2023-09-05, 2023-09-05, 2023-09-05, 2023-09-05, 2023-09-05 ] >> array.toMATLAB ans = 5×1 datetime array 05-Sep-2023 16:47:12 05-Sep-2023 16:47:12 05-Sep-2023 16:47:12 05-Sep-2023 16:47:12 05-Sep-2023 16:47:12 % Milliseconds are preserved on round-trip >> ans.Format = "MMM dd, yyyy HH:mm:ss SSS" ans = 5×1 datetime array Sep 05, 2023 16:47:12 933 Sep 05, 2023 16:47:12 934 Sep 05, 2023 16:47:12 935 Sep 05, 2023 16:47:12 936 Sep 05, 2023 16:47:12 937 ``` ### Are these changes tested? 1. Added a new `tDate64Array` test class. 2. Added `Date64` related test to `ttraits.m`. 3. Added a new `tDate64Traits.m` test class. ### Are there any user-facing changes? Yes. 1. Users can now create `arrow.array.Date64Array`s from MATLAB `datetime`s. ### Future Directions 1. Add round-trip precision tests for `TimestampArray` (i.e. similar to the test case `TestInt64MaxMilliseconds`). 2. Add a way to extract the raw `int64` values from an `arrow.array.Date64Array` without converting to a MATLAB `datetime` using `toMATLAB`. ### Notes 1. Thank you @ sgilmore10 for your help with this pull request! * Closes: #37572 Lead-authored-by: Kevin Gurney Co-authored-by: Sarah Gilmore Signed-off-by: Kevin Gurney --- .../src/cpp/arrow/matlab/array/proxy/wrap.cc | 2 + matlab/src/cpp/arrow/matlab/proxy/factory.cc | 1 + .../src/cpp/arrow/matlab/type/proxy/traits.h | 6 + .../src/cpp/arrow/matlab/type/proxy/wrap.cc | 3 + .../+temporal/convertDatetimeToEpochTime.m | 31 ++ matlab/src/matlab/+arrow/+array/Date64Array.m | 77 +++++ .../src/matlab/+arrow/+array/TimestampArray.m | 32 +- .../+tabular/createAllSupportedArrayTypes.m | 2 +- .../+arrow/+type/+traits/Date64Traits.m | 30 ++ .../src/matlab/+arrow/+type/+traits/traits.m | 2 + matlab/src/matlab/+arrow/+type/DateUnit.m | 15 + matlab/test/arrow/array/tDate64Array.m | 316 ++++++++++++++++++ matlab/test/arrow/type/traits/tDate64Traits.m | 33 ++ matlab/test/arrow/type/traits/ttraits.m | 12 + 14 files changed, 537 insertions(+), 25 deletions(-) create mode 100644 matlab/src/matlab/+arrow/+array/+internal/+temporal/convertDatetimeToEpochTime.m create mode 100644 matlab/src/matlab/+arrow/+array/Date64Array.m create mode 100644 matlab/src/matlab/+arrow/+type/+traits/Date64Traits.m create mode 100644 matlab/test/arrow/array/tDate64Array.m create mode 100644 matlab/test/arrow/type/traits/tDate64Traits.m diff --git a/matlab/src/cpp/arrow/matlab/array/proxy/wrap.cc b/matlab/src/cpp/arrow/matlab/array/proxy/wrap.cc index 104eecbb4f266..a8e3f239919cc 100644 --- a/matlab/src/cpp/arrow/matlab/array/proxy/wrap.cc +++ b/matlab/src/cpp/arrow/matlab/array/proxy/wrap.cc @@ -57,6 +57,8 @@ namespace arrow::matlab::array::proxy { return std::make_shared>(std::static_pointer_cast(array)); case ID::DATE32: return std::make_shared>(std::static_pointer_cast(array)); + case ID::DATE64: + return std::make_shared>(std::static_pointer_cast(array)); case ID::STRING: return std::make_shared(std::static_pointer_cast(array)); default: diff --git a/matlab/src/cpp/arrow/matlab/proxy/factory.cc b/matlab/src/cpp/arrow/matlab/proxy/factory.cc index 5a7d284c0b68c..593e8ffbb6f01 100644 --- a/matlab/src/cpp/arrow/matlab/proxy/factory.cc +++ b/matlab/src/cpp/arrow/matlab/proxy/factory.cc @@ -57,6 +57,7 @@ libmexclass::proxy::MakeResult Factory::make_proxy(const ClassName& class_name, REGISTER_PROXY(arrow.array.proxy.Time32Array , arrow::matlab::array::proxy::NumericArray); REGISTER_PROXY(arrow.array.proxy.Time64Array , arrow::matlab::array::proxy::NumericArray); REGISTER_PROXY(arrow.array.proxy.Date32Array , arrow::matlab::array::proxy::NumericArray); + REGISTER_PROXY(arrow.array.proxy.Date64Array , arrow::matlab::array::proxy::NumericArray); REGISTER_PROXY(arrow.array.proxy.ChunkedArray , arrow::matlab::array::proxy::ChunkedArray); REGISTER_PROXY(arrow.tabular.proxy.RecordBatch , arrow::matlab::tabular::proxy::RecordBatch); REGISTER_PROXY(arrow.tabular.proxy.Schema , arrow::matlab::tabular::proxy::Schema); diff --git a/matlab/src/cpp/arrow/matlab/type/proxy/traits.h b/matlab/src/cpp/arrow/matlab/type/proxy/traits.h index a4c5d06f8ff22..799a40cd67450 100644 --- a/matlab/src/cpp/arrow/matlab/type/proxy/traits.h +++ b/matlab/src/cpp/arrow/matlab/type/proxy/traits.h @@ -24,6 +24,7 @@ #include "arrow/matlab/type/proxy/time32_type.h" #include "arrow/matlab/type/proxy/time64_type.h" #include "arrow/matlab/type/proxy/date32_type.h" +#include "arrow/matlab/type/proxy/date64_type.h" #include "arrow/matlab/type/proxy/string_type.h" namespace arrow::matlab::type::proxy { @@ -105,4 +106,9 @@ namespace arrow::matlab::type::proxy { struct Traits { using TypeProxy = Date32Type; }; + + template <> + struct Traits { + using TypeProxy = Date64Type; + }; } diff --git a/matlab/src/cpp/arrow/matlab/type/proxy/wrap.cc b/matlab/src/cpp/arrow/matlab/type/proxy/wrap.cc index 7dfdf58e1dd21..91a1e353496c7 100644 --- a/matlab/src/cpp/arrow/matlab/type/proxy/wrap.cc +++ b/matlab/src/cpp/arrow/matlab/type/proxy/wrap.cc @@ -22,6 +22,7 @@ #include "arrow/matlab/type/proxy/time32_type.h" #include "arrow/matlab/type/proxy/time64_type.h" #include "arrow/matlab/type/proxy/date32_type.h" +#include "arrow/matlab/type/proxy/date64_type.h" #include "arrow/matlab/type/proxy/string_type.h" namespace arrow::matlab::type::proxy { @@ -59,6 +60,8 @@ namespace arrow::matlab::type::proxy { return std::make_shared(std::static_pointer_cast(type)); case ID::DATE32: return std::make_shared(std::static_pointer_cast(type)); + case ID::DATE64: + return std::make_shared(std::static_pointer_cast(type)); case ID::STRING: return std::make_shared(std::static_pointer_cast(type)); default: diff --git a/matlab/src/matlab/+arrow/+array/+internal/+temporal/convertDatetimeToEpochTime.m b/matlab/src/matlab/+arrow/+array/+internal/+temporal/convertDatetimeToEpochTime.m new file mode 100644 index 0000000000000..f38de844693df --- /dev/null +++ b/matlab/src/matlab/+arrow/+array/+internal/+temporal/convertDatetimeToEpochTime.m @@ -0,0 +1,31 @@ +% Converts MATLAB datetime values to integer "Epoch time" values which +% represent the number of "ticks" since the UNIX Epoch (Jan-1-1970) with +% respect to the specified TimeUnit / DateUnit. + +% 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. + +function epochTime = convertDatetimeToEpochTime(datetimes, unit) + epochTime = zeros(size(datetimes), "int64"); + indices = ~isnat(datetimes); + + % convertTo uses the Unzoned UNIX Epoch Jan-1-1970 as the default Epoch. + % If the input datetime array has a TimeZone, then a Zoned UNIX Epoch + % of Jan-1-1970 UTC is used instead. + % + % TODO: convertTo may error if the datetime is 2^63-1 before or + % after the epoch. We should throw a custom error in this case. + epochTime(indices) = convertTo(datetimes(indices), "epochtime", TicksPerSecond=ticksPerSecond(unit)); +end diff --git a/matlab/src/matlab/+arrow/+array/Date64Array.m b/matlab/src/matlab/+arrow/+array/Date64Array.m new file mode 100644 index 0000000000000..f5da26bbb5594 --- /dev/null +++ b/matlab/src/matlab/+arrow/+array/Date64Array.m @@ -0,0 +1,77 @@ +% arrow.array.Date64Array + +% 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. + +classdef Date64Array < arrow.array.Array + + properties(Access=private) + NullSubstitutionValue = NaT + end + + methods + + function obj = Date64Array(proxy) + arguments + proxy(1, 1) libmexclass.proxy.Proxy {validate(proxy, "arrow.array.proxy.Date64Array")} + end + import arrow.internal.proxy.validate + obj@arrow.array.Array(proxy); + end + + function dates = toMATLAB(obj) + epochTime = obj.Proxy.toMATLAB(); + + ticksPerSecond = obj.Type.DateUnit.ticksPerSecond(); + + % UNIX Epoch (January 1st, 1970). + unixEpoch = datetime(0, ConvertFrom="posixtime", TimeZone="UTC"); + dates = datetime(epochTime, ConvertFrom="epochtime", Epoch=unixEpoch, .... + TicksPerSecond=ticksPerSecond); + + dates(~obj.Valid) = obj.NullSubstitutionValue; + end + + function dates = datetime(obj) + dates = obj.toMATLAB(); + end + + end + + methods(Static) + + function array = fromMATLAB(data, opts) + arguments + data + opts.InferNulls(1, 1) logical = true + opts.Valid + end + + import arrow.array.Date64Array + + arrow.internal.validate.type(data, "datetime"); + arrow.internal.validate.shape(data); + + validElements = arrow.internal.validate.parseValidElements(data, opts); + epochTime = arrow.array.internal.temporal.convertDatetimeToEpochTime(data, arrow.type.DateUnit.Millisecond); + + args = struct(MatlabArray=epochTime, Valid=validElements); + proxy = arrow.internal.proxy.create("arrow.array.proxy.Date64Array", args); + array = Date64Array(proxy); + end + + end + +end diff --git a/matlab/src/matlab/+arrow/+array/TimestampArray.m b/matlab/src/matlab/+arrow/+array/TimestampArray.m index 3b05903bb13ea..80198f965fe92 100644 --- a/matlab/src/matlab/+arrow/+array/TimestampArray.m +++ b/matlab/src/matlab/+arrow/+array/TimestampArray.m @@ -30,15 +30,15 @@ end function dates = toMATLAB(obj) - time = obj.Proxy.toMATLAB(); + epochTime = obj.Proxy.toMATLAB(); - epoch = datetime(1970, 1, 1, TimeZone="UTC"); + timeZone = obj.Type.TimeZone; + ticksPerSecond = obj.Type.TimeUnit.ticksPerSecond(); - tz = obj.Type.TimeZone; - ticsPerSecond = ticksPerSecond(obj.Type.TimeUnit); - - dates = datetime(time, ConvertFrom="epochtime", Epoch=epoch, ... - TimeZone=tz, TicksPerSecond=ticsPerSecond); + % UNIX Epoch (January 1st, 1970). + unixEpoch = datetime(0, ConvertFrom="posixtime", TimeZone="UTC"); + dates = datetime(epochTime, ConvertFrom="epochtime", Epoch=unixEpoch, ... + TimeZone=timeZone, TicksPerSecond=ticksPerSecond); dates(~obj.Valid) = obj.NullSubstitutionValue; end @@ -48,21 +48,6 @@ end end - methods (Static, Access = private) - function time = convertToEpochTime(dates, units) - - time = zeros(size(dates), "int64"); - indices = ~isnat(dates); - - % convertTo uses Jan-1-1970 as the default epoch. If the input - % datetime array has a TimeZone, the epoch is Jan-1-1970 UTC. - % - % TODO: convertTo may error if the datetime is 2^63-1 before or - % after the epoch. We should throw a custom error in this case. - time(indices) = convertTo(dates(indices), "epochtime", TicksPerSecond=ticksPerSecond(units)); - end - end - methods(Static) function array = fromMATLAB(data, opts) arguments @@ -74,9 +59,8 @@ arrow.internal.validate.type(data, "datetime"); arrow.internal.validate.shape(data); - validElements = arrow.internal.validate.parseValidElements(data, opts); - epochTime = arrow.array.TimestampArray.convertToEpochTime(data, opts.TimeUnit); + epochTime = arrow.array.internal.temporal.convertDatetimeToEpochTime(data, opts.TimeUnit); timezone = string(data.TimeZone); args = struct(MatlabArray=epochTime, Valid=validElements, TimeZone=timezone, TimeUnit=string(opts.TimeUnit)); diff --git a/matlab/src/matlab/+arrow/+internal/+test/+tabular/createAllSupportedArrayTypes.m b/matlab/src/matlab/+arrow/+internal/+test/+tabular/createAllSupportedArrayTypes.m index f7a9bda927075..c0bedaf2faf39 100644 --- a/matlab/src/matlab/+arrow/+internal/+test/+tabular/createAllSupportedArrayTypes.m +++ b/matlab/src/matlab/+arrow/+internal/+test/+tabular/createAllSupportedArrayTypes.m @@ -102,7 +102,7 @@ end function dateClasses = getDateArrayClasses() - dateClasses = compose("arrow.array.Date%dArray", 32); + dateClasses = compose("arrow.array.Date%dArray", [32 64]); end function number = randomNumbers(numberType, numElements) diff --git a/matlab/src/matlab/+arrow/+type/+traits/Date64Traits.m b/matlab/src/matlab/+arrow/+type/+traits/Date64Traits.m new file mode 100644 index 0000000000000..c7ff4ac2f8245 --- /dev/null +++ b/matlab/src/matlab/+arrow/+type/+traits/Date64Traits.m @@ -0,0 +1,30 @@ +% 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. + +classdef Date64Traits < arrow.type.traits.TypeTraits + + properties (Constant) + ArrayConstructor = @arrow.array.Date64Array + ArrayClassName = "arrow.array.Date64Array" + ArrayProxyClassName = "arrow.array.proxy.Date64Array" + ArrayStaticConstructor = @arrow.array.Date64Array.fromMATLAB + TypeConstructor = @arrow.type.Date64Type; + TypeClassName = "arrow.type.Date64Type" + TypeProxyClassName = "arrow.type.proxy.Date64Type" + MatlabConstructor = @datetime + MatlabClassName = "datetime" + end + +end diff --git a/matlab/src/matlab/+arrow/+type/+traits/traits.m b/matlab/src/matlab/+arrow/+type/+traits/traits.m index b1f6193a3e7c1..78804fdccb3f0 100644 --- a/matlab/src/matlab/+arrow/+type/+traits/traits.m +++ b/matlab/src/matlab/+arrow/+type/+traits/traits.m @@ -54,6 +54,8 @@ typeTraits = Time64Traits(); case ID.Date32 typeTraits = Date32Traits(); + case ID.Date64 + typeTraits = Date64Traits(); otherwise error("arrow:type:traits:UnsupportedArrowTypeID", "Unsupported Arrow type ID: " + type); end diff --git a/matlab/src/matlab/+arrow/+type/DateUnit.m b/matlab/src/matlab/+arrow/+type/DateUnit.m index bb82de5b02ab3..45354be62f59a 100644 --- a/matlab/src/matlab/+arrow/+type/DateUnit.m +++ b/matlab/src/matlab/+arrow/+type/DateUnit.m @@ -21,4 +21,19 @@ Millisecond (1) end + methods (Hidden) + + function ticks = ticksPerSecond(obj) + import arrow.type.DateUnit + switch obj + case DateUnit.Millisecond + ticks = 1e3; + otherwise + error("arrow:dateunit:UnsupportedTicksPerSecond", ... + "The ticksPerSecond method can only be called on a DateUnit of type Millisecond."); + end + end + + end + end diff --git a/matlab/test/arrow/array/tDate64Array.m b/matlab/test/arrow/array/tDate64Array.m new file mode 100644 index 0000000000000..57eb3b0ca7d95 --- /dev/null +++ b/matlab/test/arrow/array/tDate64Array.m @@ -0,0 +1,316 @@ +%TDATE64ARRAY Unit tests for arrow.array.Date64Array + +% 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. + +classdef tDate64Array < matlab.unittest.TestCase + + properties + ArrowArrayConstructorFcn = @arrow.array.Date64Array.fromMATLAB + end + + properties (Constant) + UnixEpoch = datetime(0, ConvertFrom="posixtime"); + MissingDates = [datetime(2023, 1, 1), NaT, NaT, datetime(2022, 1, 1), NaT]; + end + + methods (Test) + + function TestBasic(testCase) + dates = testCase.UnixEpoch + days(1:10); + array = testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyInstanceOf(array, "arrow.array.Date64Array"); + end + + function TestTypeIsDate64(testCase) + dates = testCase.UnixEpoch + days(1:10); + array = testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyDate64Type(array.Type); + end + + function TestLength(testCase) + dates = datetime.empty(0, 1); + array = testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyEqual(array.Length, int64(0)); + + dates = datetime(2023, 1, 1); + array = testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyEqual(array.Length, int64(1)); + + dates = testCase.UnixEpoch + days(1:10); + array = testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyEqual(array.Length, int64(10)); + end + + function TestToMATLAB(testCase) + % Verify toMATLAB() round-trips the original datetime array. + dates = testCase.UnixEpoch + days(1:10); + array = testCase.ArrowArrayConstructorFcn(dates); + values = toMATLAB(array); + testCase.verifyEqual(values, dates'); + end + + function TestDatetime(testCase) + % Verify datetime() round-trips the original datetime array. + dates = testCase.UnixEpoch + days(1:10); + array = testCase.ArrowArrayConstructorFcn(dates); + values = datetime(array); + testCase.verifyEqual(values, dates'); + end + + function TestValid(testCase) + % Verify the Valid property returns the expected logical vector. + dates = testCase.MissingDates; + array = testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyEqual(array.Valid, [true; false; false; true; false]); + testCase.verifyEqual(toMATLAB(array), dates'); + testCase.verifyEqual(datetime(array), dates'); + end + + function TestInferNullsTrueNVPair(testCase) + % Verify arrow.array.Date64Array.fromMATLAB() behaves as + % expected when InferNulls=true is provided. + dates = testCase.MissingDates; + array = testCase.ArrowArrayConstructorFcn(dates, InferNulls=true); + expectedValid = [true; false; false; true; false]; + testCase.verifyEqual(array.Valid, expectedValid); + testCase.verifyEqual(toMATLAB(array), dates'); + testCase.verifyEqual(datetime(array), dates'); + end + + function TestInferNullsFalseNVPair(testCase) + % Verify arrow.array.Date64Array.fromMATLAB() behaves as + % expected when InferNulls=false is provided. + dates = testCase.MissingDates; + array = testCase.ArrowArrayConstructorFcn(dates, InferNulls=false); + expectedValid = [true; true; true; true; true]; + testCase.verifyEqual(array.Valid, expectedValid); + + % If NaT datetimes were not considered null values, then they + % are treated like int64(0) - i.e. the Unix epoch. + expectedDates = dates'; + expectedDates([2, 3, 5]) = testCase.UnixEpoch; + testCase.verifyEqual(toMATLAB(array), expectedDates); + testCase.verifyEqual(datetime(array), expectedDates); + end + + function TestValidNVPair(testCase) + % Verify arrow.array.Date64Array.fromMATLAB() accepts the Valid + % nv-pair, and it behaves as expected. + dates = testCase.MissingDates; + + % Supply the Valid name-value pair as vector of indices. + array = testCase.ArrowArrayConstructorFcn(dates, Valid=[1, 2, 3]); + testCase.verifyEqual(array.Valid, [true; true; true; false; false]); + expectedDates = dates'; + expectedDates([2, 3]) = testCase.UnixEpoch; + expectedDates([4, 5]) = NaT; + testCase.verifyEqual(toMATLAB(array), expectedDates); + + % Supply the Valid name-value pair as a logical scalar. + array = testCase.ArrowArrayConstructorFcn(dates, Valid=false); + testCase.verifyEqual(array.Valid, [false; false; false; false; false]); + expectedDates(:) = NaT; + testCase.verifyEqual(toMATLAB(array), expectedDates); + end + + function TestEmptyDatetimeVector(testCase) + % Verify arrow.array.Date64Array.fromMATLAB() accepts any + % empty-shaped datetime as input. + + dates = datetime.empty(0, 0); + array = testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyEqual(array.Length, int64(0)); + testCase.verifyEqual(array.Valid, logical.empty(0, 1)); + testCase.verifyEqual(toMATLAB(array), datetime.empty(0, 1)); + + % Test with an N-Dimensional empty array + dates = datetime.empty(0, 1, 0); + array = testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyEqual(array.Length, int64(0)); + testCase.verifyEqual(array.Valid, logical.empty(0, 1)); + testCase.verifyEqual(toMATLAB(array), datetime.empty(0, 1)); + end + + function TestErrorIfNonVector(testCase) + % Verify arrow.array.Date64Array.fromMATLAB() throws an error + % if the input provided is not a vector. + + dates = datetime(2023, 1, 1) + days(1:12); + dates = reshape(dates, 2, 6); + fcn = @() testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyError(fcn, "arrow:array:InvalidShape"); + + dates = reshape(dates, 3, 2, 2); + fcn = @() testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyError(fcn, "arrow:array:InvalidShape"); + end + + function TestErrorIfNonDatetime(testCase) + % Verify arrow.array.Date64Array.fromMATLAB() throws an error + % if not given a datetime as input. + + dates = duration(1, 2, 3); + fcn = @() testCase.ArrowArrayConstructorFcn(dates); + testCase.verifyError(fcn, "arrow:array:InvalidType"); + + numbers = [1; 2; 3; 4]; + fcn = @() testCase.ArrowArrayConstructorFcn(numbers); + testCase.verifyError(fcn, "arrow:array:InvalidType"); + end + + function TestInt64MaxMilliseconds(testCase) + % Verify that no precision is lost when trying to round-trip a + % datetime value that is within abs(intmin("int64")) milliseconds before + % and intmax("int64") days after the UNIX epoch. + + ticksPerSecond = 1e3; + + numMillisecondsBefore = intmin("int64"); + numMillisecondsAfter = intmax("int64"); + + expectedBefore = datetime(numMillisecondsBefore, ConvertFrom="epochtime", ... + Epoch=testCase.UnixEpoch, TicksPerSecond=ticksPerSecond); + expectedAfter = datetime(numMillisecondsAfter, ConvertFrom="epochtime", ... + Epoch=testCase.UnixEpoch, TicksPerSecond=ticksPerSecond); + + array = testCase.ArrowArrayConstructorFcn(expectedBefore); + actualBefore = array.toMATLAB(); + testCase.verifyEqual(actualBefore, expectedBefore); + + array = testCase.ArrowArrayConstructorFcn(expectedAfter); + actualAfter = array.toMATLAB(); + testCase.verifyEqual(actualAfter, expectedAfter); + end + + function TestZonedDatetime(testCase) + % Verify that zoned datetimes are supported as inputs to the + % fromMATLAB method and that the output datetime returned by + % the toMATLAB method is unzoned. + zoned = testCase.UnixEpoch + days(10); + zoned.TimeZone = "America/New_York"; + + expected = zoned; + % Set the TimeZone of the expected datetime value to "UTC" + % to trigger a time adjustment to UTC-relative time. + % Date64Array values are implicitly relative to UTC time, so + % adjusting the expected datetime value to UTC will ensure that + % the actual and expected datetimes represent the same "instant-in-time". + expected.TimeZone = "UTC"; + % datetimes returned by the toMATLAB method of Date64Array are unzoned. + % Set the TimeZone of the expected datetime to a 0x0 empty char array + % in order to make the datetime unzoned. + expected.TimeZone = char.empty(0, 0); + + array = testCase.ArrowArrayConstructorFcn(zoned); + actual = array.toMATLAB(); + testCase.verifyEqual(actual, expected); + end + + function TestInt64MaxMillisecondsZoned(testCase) + % Verify that zoned datetimes which are within abs(intmin("int64")) days + % before and intmax("int64") days after the UNIX epoch are round-tripped + % (not including the TimeZone). + + ticksPerSecond = 1e3; + + numMillisecondsBefore = intmin("int64"); + numMillisecondsAfter = intmax("int64"); + + expectedBefore = datetime(numMillisecondsBefore, ConvertFrom="epochtime", ... + Epoch=testCase.UnixEpoch, TicksPerSecond=ticksPerSecond); + expectedAfter = datetime(numMillisecondsAfter, ConvertFrom="epochtime", ... + Epoch=testCase.UnixEpoch, TicksPerSecond=ticksPerSecond); + + expectedZonedBefore = expectedBefore; + expectedZonedBefore.TimeZone = "UTC"; + + expectedZonedAfter = expectedAfter; + expectedZonedAfter.TimeZone = "UTC"; + + array = testCase.ArrowArrayConstructorFcn(expectedZonedBefore); + actualBefore = array.toMATLAB(); + testCase.verifyEqual(actualBefore, expectedBefore); + + array = testCase.ArrowArrayConstructorFcn(expectedZonedAfter); + actualAfter = array.toMATLAB(); + testCase.verifyEqual(actualAfter, expectedAfter); + end + + function TestIsEqualTrue(tc) + % Verifies arrays are considered equal if: + % + % 1. Their Type properties are equal + % 2. They have the same length (i.e. their Length properties are equal) + % 3. They have the same validity bitmap (i.e. their Valid properties are equal) + % 4. All corresponding valid elements have the same values + + dates1 = datetime(2023, 6, 22) + days(0:4); + dates2 = datetime(2023, 6, 22) + days([0 1 5 3 4]); + + array1 = tc.ArrowArrayConstructorFcn(dates1, Valid=[1 2 4]); + array2 = tc.ArrowArrayConstructorFcn(dates1, Valid=[1 2 4]); + array3 = tc.ArrowArrayConstructorFcn(dates2, Valid=[1 2 4]); + + tc.verifyTrue(isequal(array1, array2)); + tc.verifyTrue(isequal(array1, array3)); + + % Test supplying more than two arrays to isequal + tc.verifyTrue(isequal(array1, array2, array3)); + end + + function TestIsEqualFalse(tc) + % Verify isequal returns false when expected. + dates1 = datetime(2023, 6, 22) + days(0:4); + dates2 = datetime(2023, 6, 22) + days([1 1 2 3 4]); + dates3 = datetime(2023, 6, 22) + days(0:5); + array1 = tc.ArrowArrayConstructorFcn(dates1, Valid=[1 2 4]); + array2 = tc.ArrowArrayConstructorFcn(dates1, Valid=[1 4]); + array3 = tc.ArrowArrayConstructorFcn(dates2, Valid=[1 2 4]); + array4 = arrow.array([true false true false]); + array5 = tc.ArrowArrayConstructorFcn(dates3, Valid=[1 2 4]); + + % Their validity bitmaps are not equal + tc.verifyFalse(isequal(array1, array2)); + + % Not all corresponding valid elements are equal + tc.verifyFalse(isequal(array1, array3)); + + % Their Type properties are not equal + tc.verifyFalse(isequal(array1, array4)); + + % Their Length properties are not equal + tc.verifyFalse(isequal(array1, array5)); + + % Comparing an arrow.array.Array to a MATLAB double + tc.verifyFalse(isequal(array1, 1)); + + % Test supplying more than two arrays to isequal + tc.verifyFalse(isequal(array1, array1, array3, array4, array5)); + end + + end + + methods + + function verifyDate64Type(testCase, actual) + testCase.verifyInstanceOf(actual, "arrow.type.Date64Type"); + testCase.verifyEqual(actual.ID, arrow.type.ID.Date64); + testCase.verifyEqual(actual.DateUnit, arrow.type.DateUnit.Millisecond); + end + + end + +end diff --git a/matlab/test/arrow/type/traits/tDate64Traits.m b/matlab/test/arrow/type/traits/tDate64Traits.m new file mode 100644 index 0000000000000..1dc3b38b7f755 --- /dev/null +++ b/matlab/test/arrow/type/traits/tDate64Traits.m @@ -0,0 +1,33 @@ +%TDATE64TRAITS Unit tests for arrow.type.traits.Date64Traits + +% 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. + +classdef tDate64Traits < hTypeTraits + + properties + TraitsConstructor = @arrow.type.traits.Date64Traits + ArrayConstructor = @arrow.array.Date64Array + ArrayClassName = "arrow.array.Date64Array" + ArrayProxyClassName = "arrow.array.proxy.Date64Array" + ArrayStaticConstructor = @arrow.array.Date64Array.fromMATLAB + TypeConstructor = @arrow.type.Date64Type + TypeClassName = "arrow.type.Date64Type" + TypeProxyClassName = "arrow.type.proxy.Date64Type" + MatlabConstructor = @datetime + MatlabClassName = "datetime" + end + +end diff --git a/matlab/test/arrow/type/traits/ttraits.m b/matlab/test/arrow/type/traits/ttraits.m index 70508a5e7cfbf..cdc5990ed03ba 100644 --- a/matlab/test/arrow/type/traits/ttraits.m +++ b/matlab/test/arrow/type/traits/ttraits.m @@ -187,6 +187,18 @@ function TestDate32(testCase) testCase.verifyEqual(actualTraits, expectedTraits); end + function TestDate64(testCase) + import arrow.type.traits.* + import arrow.type.* + + type = ID.Date64; + expectedTraits = Date64Traits(); + + actualTraits = traits(type); + + testCase.verifyEqual(actualTraits, expectedTraits); + end + function TestMatlabUInt8(testCase) import arrow.type.traits.*