diff --git a/matlab/src/matlab/+arrow/+array/Date32Array.m b/matlab/src/matlab/+arrow/+array/Date32Array.m index 545db51b56988..7ecdd6d2a6350 100644 --- a/matlab/src/matlab/+arrow/+array/Date32Array.m +++ b/matlab/src/matlab/+arrow/+array/Date32Array.m @@ -65,8 +65,14 @@ arrow.internal.validate.shape(data); validElements = arrow.internal.validate.parseValidElements(data, opts); + % UNIX Epoch (January 1st, 1970) - unixEpoch = datetime(0, ConvertFrom="posixtime"); + if ~isempty(data.TimeZone) + unixEpoch = datetime(0, ConvertFrom="posixtime", TimeZone="UTC"); + else + unixEpoch = datetime(0, ConvertFrom="posixtime"); + end + numDays = int32(floor(days(data - unixEpoch))); args = struct(MatlabArray=numDays, Valid=validElements); proxy = arrow.internal.proxy.create("arrow.array.proxy.Date32Array", args); diff --git a/matlab/test/arrow/array/tDate32Array.m b/matlab/test/arrow/array/tDate32Array.m index 66e3baa4d52f8..2016bdc5df458 100644 --- a/matlab/test/arrow/array/tDate32Array.m +++ b/matlab/test/arrow/array/tDate32Array.m @@ -22,20 +22,20 @@ end properties (Constant) - unixEpoch = datetime(0, ConvertFrom="posixtime"); - missingDates = [datetime(2023, 1, 1), NaT, NaT, datetime(2022, 1, 1), NaT]; + 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); + dates = testCase.UnixEpoch + days(1:10); array = testCase.ArrowArrayConstructorFcn(dates); testCase.verifyInstanceOf(array, "arrow.array.Date32Array"); end function TestTypeIsDate32(testCase) - dates = testCase.unixEpoch + days(1:10); + dates = testCase.UnixEpoch + days(1:10); array = testCase.ArrowArrayConstructorFcn(dates); testCase.verifyDate32Type(array.Type); end @@ -49,14 +49,14 @@ function TestLength(testCase) array = testCase.ArrowArrayConstructorFcn(dates); testCase.verifyEqual(array.Length, int64(1)); - dates = testCase.unixEpoch + days(1:10); + 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); + dates = testCase.UnixEpoch + days(1:10); array = testCase.ArrowArrayConstructorFcn(dates); values = toMATLAB(array); testCase.verifyEqual(values, dates'); @@ -64,7 +64,7 @@ function TestToMATLAB(testCase) function TestDatetime(testCase) % Verify datetime() round-trips the original datetime array. - dates = testCase.unixEpoch + days(1:10); + dates = testCase.UnixEpoch + days(1:10); array = testCase.ArrowArrayConstructorFcn(dates); values = datetime(array); testCase.verifyEqual(values, dates'); @@ -72,7 +72,7 @@ function TestDatetime(testCase) function TestValid(testCase) % Verify the Valid property returns the expected logical vector. - dates = testCase.missingDates; + dates = testCase.MissingDates; array = testCase.ArrowArrayConstructorFcn(dates); testCase.verifyEqual(array.Valid, [true; false; false; true; false]); testCase.verifyEqual(toMATLAB(array), dates'); @@ -82,7 +82,7 @@ function TestValid(testCase) function InferNullsTrueNVPair(testCase) % Verify arrow.array.Date32Array.fromMATLAB() behaves as % expected when InferNulls=true is provided. - dates = testCase.missingDates; + dates = testCase.MissingDates; array = testCase.ArrowArrayConstructorFcn(dates, InferNulls=true); expectedValid = [true; false; false; true; false]; testCase.verifyEqual(array.Valid, expectedValid); @@ -93,7 +93,7 @@ function InferNullsTrueNVPair(testCase) function InferNullsFalseNVPair(testCase) % Verify arrow.array.Date32Array.fromMATLAB() behaves as % expected when InferNulls=false is provided. - dates = testCase.missingDates; + dates = testCase.MissingDates; array = testCase.ArrowArrayConstructorFcn(dates, InferNulls=false); expectedValid = [true; true; true; true; true]; testCase.verifyEqual(array.Valid, expectedValid); @@ -101,7 +101,7 @@ function InferNullsFalseNVPair(testCase) % If NaT datetimes were not considered null values, then they % are treated like int32(0) - i.e. the Unix epoch. expectedDates = dates'; - expectedDates([2, 3, 5]) = testCase.unixEpoch; + expectedDates([2, 3, 5]) = testCase.UnixEpoch; testCase.verifyEqual(toMATLAB(array), expectedDates); testCase.verifyEqual(datetime(array), expectedDates); end @@ -109,13 +109,13 @@ function InferNullsFalseNVPair(testCase) function TestValidNVPair(testCase) % Verify arrow.array.Date32Array.fromMATLAB() accepts the Valid % nv-pair, and it behaves as expected. - dates = testCase.missingDates; + 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([2, 3]) = testCase.UnixEpoch; expectedDates([4, 5]) = NaT; testCase.verifyEqual(toMATLAB(array), expectedDates); @@ -171,6 +171,106 @@ function ErrorIfNonDatetime(testCase) testCase.verifyError(fcn, "arrow:array:InvalidType"); end + function Int32MaxDays(testCase) + % Verify that no precision is lost when trying to round-trip a + % datetime value that is within intmax("int32") - 1 days before + % and intmax("int32") days after the UNIX epoch. + numDaysBefore = uint64(intmax("int32")) + 1; + numDaysAfter = uint64(intmax("int32")); + + expectedBefore = testCase.UnixEpoch - days(numDaysBefore); + expectedAfter = testCase.UnixEpoch + days(numDaysAfter); + + array = testCase.ArrowArrayConstructorFcn(expectedBefore); + actualBefore = array.toMATLAB(); + testCase.verifyEqual(actualBefore, expectedBefore); + + array = testCase.ArrowArrayConstructorFcn(expectedAfter); + actualAfter = array.toMATLAB(); + testCase.verifyEqual(actualAfter, expectedAfter); + end + + function GreaterThanInt32MaxDays(testCase) + % Verify that precision is lost when trying to round-trip a + % datetime that is greater than intmax("int32") + 1 days before + % or greater than intmax("int32") after after the UNIX epoch. + numDaysBefore = uint64(intmax("int32")) + 2; + numDaysAfter = uint64(intmax("int32")) + 1; + + expectedBefore = testCase.UnixEpoch - days(numDaysBefore); + expectedAfter = testCase.UnixEpoch + days(numDaysAfter); + + array = testCase.ArrowArrayConstructorFcn(expectedBefore); + actualBefore = array.toMATLAB(); + testCase.verifyNotEqual(actualBefore, expectedBefore); + + array = testCase.ArrowArrayConstructorFcn(expectedAfter); + actualAfter = array.toMATLAB(); + testCase.verifyNotEqual(actualAfter, expectedAfter); + end + + function ZonedDatetime(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. + expectedZoned = testCase.UnixEpoch + days(10); + expectedZoned.TimeZone = "America/New_York"; + expected = expectedZoned; + expected.TimeZone = char.empty(0, 0); + + array = testCase.ArrowArrayConstructorFcn(expectedZoned); + actual = array.toMATLAB(); + testCase.verifyEqual(actual, expected); + end + + function Int32MaxDaysZoned(testCase) + % Verify that zoned datetimes which are within intmax("int32") + 1 days + % before and intmax("int32") days after the UNIX epoch are round-tripped + % (not including the TimeZone). + numDaysBefore = uint64(intmax("int32")) + 1; + numDaysAfter = uint64(intmax("int32")); + + expectedZonedBefore = testCase.UnixEpoch - days(numDaysBefore); + expectedZonedAfter = testCase.UnixEpoch + days(numDaysAfter); + + expectedZonedBefore.TimeZone = "UTC"; + expectedZonedAfter.TimeZone = "UTC"; + + expectedUnzonedBefore = expectedZonedBefore; + expectedUnzonedBefore.TimeZone = char.empty(0, 0); + + expectedUnzonedAfter = expectedZonedAfter; + expectedUnzonedAfter.TimeZone = char.empty(0, 0); + + array = testCase.ArrowArrayConstructorFcn(expectedZonedBefore); + actualBefore = array.toMATLAB(); + testCase.verifyEqual(actualBefore, expectedUnzonedBefore); + + array = testCase.ArrowArrayConstructorFcn(expectedUnzonedAfter); + actualAfter = array.toMATLAB(); + testCase.verifyEqual(actualAfter, expectedUnzonedAfter); + end + + function NonWholeDaysRoundDown(testCase) + % Verify that datetimes which are not whole days (i.e. are not datetimes + % with zero hours, zero minutes, and zero seconds), round down + % to the nearest whole day when round-tripping with + % Date32Array. + dates = testCase.UnixEpoch + days(10) + hours(20) + minutes(30) + seconds(40) + milliseconds(50); + % Round down to the nearest whole day when round-tripping. + expected = testCase.UnixEpoch + days(10); + array = testCase.ArrowArrayConstructorFcn(dates); + actual = array.toMATLAB(); + testCase.verifyEqual(actual, expected); + + dates = testCase.UnixEpoch + days(10) + hours(10) + minutes(20) + seconds(30) + milliseconds(20); + % Round down to the nearest whole day when round-tripping. + expected = testCase.UnixEpoch + days(10); + array = testCase.ArrowArrayConstructorFcn(dates); + actual = array.toMATLAB(); + testCase.verifyEqual(actual, expected); + end + end methods