Skip to content

Commit

Permalink
apacheGH-38420: [MATLAB] Implement a DatetimeValidator class that v…
Browse files Browse the repository at this point in the history
…alidates a MATLAB `cell` array contains only values of zoned or unzoned `datetime`s (apache#38533)

### Rationale for this change

This is a followup to apache#38419.

Adding this `DatetimeTypeValidator` class is a step towards implementing the `arrow.array.ListArray.fromMATLAB()` method for creating `ListArray`s whose `ValueType`s is a timestamp array from a MATLAB `cell` array.

This validator will ensure the cell array contain only `datetime`s or unzoned `datetime`s. This is a requirement when creating a `List` of `Timestamp`s because two MATLAB `datetime`s can only be concatenated together if they are either both zoned or both unzoned:

```matlab
>> d1 = datetime(2023, 10, 31, TimeZone="America/New_York");
>> d2 =datetime(2023, 11, 1);
>> [d1; d2]
Error using datetime/vertcat
Unable to concatenate a datetime array that has a time zone with one that does not have a time
zone.
```

### What changes are included in this PR?

Added a new MATLAB class called `arrow.array.internal.list.DatetimeValidator`, which inherits from `arrow.array.internal.list.ClassTypeValidator`.

 This new class defines one property called `HasTimeZone`, which is a scalar `logical` indicating if the validator expects all `datetime`s to be zoned or not. 

Additionally, `DatetimeValidator` overrides the `validateElement` method. It first call's `ClassTypeValidator`'s implementation of `validateElement` to verify the input element is a `datetime`. If so, it then confirms that the input `datetime`'s TimeZone property is empty or nonempty, based on the validator's `HasTimeZone`  property value.

### Are these changes tested?

Yes, I added a new test class called `tDatetimeValidator.m`.

### Are there any user-facing changes?

No.

### Future Directions

1. apache#38417 
2. apache#38354 
* Closes: apache#38420

Authored-by: Sarah Gilmore <sgilmore@mathworks.com>
Signed-off-by: Kevin Gurney <kgurney@mathworks.com>
  • Loading branch information
sgilmore10 authored and dgreiss committed Feb 17, 2024
1 parent 8310ffd commit 18335ec
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
% 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 DatetimeValidator < arrow.array.internal.list.ClassTypeValidator

properties (GetAccess=public, SetAccess=private)
Zoned (1, 1) logical = false
end

methods
function obj = DatetimeValidator(date)
arguments
date(:, :) datetime
end
obj@arrow.array.internal.list.ClassTypeValidator(date);
obj.Zoned = ~isempty(date.TimeZone);
end

function validateElement(obj, element)
validateElement@arrow.array.internal.list.ClassTypeValidator(obj, element);
% zoned and obj.Zoned must be equal because zoned
% and unzoned datetimes cannot be concatenated together.
zoned = ~isempty(element.TimeZone);
if obj.Zoned && ~zoned
errorID = "arrow:array:list:ExpectedZonedDatetime";
msg = "Expected all datetime elements in the cell array to " + ...
"have a time zone but encountered a datetime array without a time zone";
error(errorID, msg);
elseif ~obj.Zoned && zoned
errorID = "arrow:array:list:ExpectedUnzonedDatetime";
msg = "Expected all datetime elements in the cell array to " + ...
"not have a time zone but encountered a datetime array with a time zone";
error(errorID, msg);
end
end
end
end
181 changes: 181 additions & 0 deletions matlab/test/arrow/array/list/tDatetimeValidator.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
%TDATETIMEVALIDATOR Unit tests for
%arrow.array.internal.list.DatetimeValidator

% 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 tDatetimeValidator < matlab.unittest.TestCase

methods (Test)
function Smoke(testCase)
import arrow.array.internal.list.DatetimeValidator
validator = DatetimeValidator(datetime(2023, 10, 31));
testCase.verifyInstanceOf(validator, "arrow.array.internal.list.DatetimeValidator");
end

function ClassNameGetter(testCase)
% Verify the ClassName getter returns the expected scalar
% string.
import arrow.array.internal.list.DatetimeValidator

validator = DatetimeValidator(datetime(2023, 10, 31));
testCase.verifyEqual(validator.ClassName, "datetime");
end

function ClassNameNoSetter(testCase)
% Verify ClassName property is not settable.
import arrow.array.internal.list.DatetimeValidator

validator = DatetimeValidator(datetime(2023, 10, 31));
fcn = @() setfield(validator, "ClassName", "duration");
testCase.verifyError(fcn, "MATLAB:class:SetProhibited");
end

function ZonedGetter(testCase)
% Verify the Zoned getter returns the expected scalar
% logical.

import arrow.array.internal.list.DatetimeValidator
validator = DatetimeValidator(datetime(2023, 10, 31));
testCase.verifyEqual(validator.Zoned, false);

validator = DatetimeValidator(datetime(2023, 10, 31, TimeZone="UTC"));
testCase.verifyEqual(validator.Zoned, true);
end

function ZonedNoSetter(testCase)
% Verify Zoned property is not settable.
import arrow.array.internal.list.DatetimeValidator

validator = DatetimeValidator(datetime(2023, 10, 31));
fcn = @() setfield(validator, "Zoned", true);
testCase.verifyError(fcn, "MATLAB:class:SetProhibited");

validator = DatetimeValidator(datetime(2023, 10, 31, TimeZone="UTC"));
fcn = @() setfield(validator, "Zoned", false);
testCase.verifyError(fcn, "MATLAB:class:SetProhibited");
end

function ValidateElementNoThrow(testCase) %#ok<MANU>
% Verify validateElement does not throw an exception if:
% 1. the input element is a datetime
% 2. its TimeZone property is '' and Zoned = false
% 3. its TimeZone property is not empty and Zoned = true

import arrow.array.internal.list.DatetimeValidator

validator = DatetimeValidator(datetime(2023, 10, 31));
validator.validateElement(datetime(2023, 11, 1));
validator.validateElement(datetime(2023, 11, 1) + days(0:2));
validator.validateElement(datetime(2023, 11, 1) + days(0:2)');
validator.validateElement(datetime.empty(0, 1));

validator = DatetimeValidator(datetime(2023, 10, 31, TimeZone="UTC"));
validator.validateElement(datetime(2023, 11, 1, TimeZone="UTC"));
validator.validateElement(datetime(2023, 11, 1, TimeZone="America/New_York") + days(0:2));
validator.validateElement(datetime(2023, 11, 1, TimeZone="Pacific/Fiji") + days(0:2)');
emptyDatetime = datetime.empty(0, 1);
emptyDatetime.TimeZone = "Asia/Dubai";
validator.validateElement(emptyDatetime);
end

function ValidateElementExpectedZonedDatetimeError(testCase)
% Verify validateElement throws an exception whose identifier
% is "arrow:array:list:ExpectedZonedDatetime" if the input
% datetime is unzoned, but the validator expected all
% datetimes to zoned.
import arrow.array.internal.list.DatetimeValidator

% validator will expect all elements to be zoned datetimes
% because the input datetime is zoned.
validator = DatetimeValidator(datetime(2023, 10, 31, TimeZone="UTC"));
errorID = "arrow:array:list:ExpectedZonedDatetime";
fcn = @() validator.validateElement(datetime(2023, 11, 1));
testCase.verifyError(fcn, errorID);
end

function ValidateElementExpectedUnzonedDatetimeError(testCase)
% Verify validateElement throws an exception whose identifier
% is "arrow:array:list:ExpectedUnzonedDatetime" if the input
% datetime has a time zone, but the validator expected all
% datetimes to be unzoned.
import arrow.array.internal.list.DatetimeValidator

% validator will expect all elements to be unzoned datetimes
% because the input datetime is not zoned.
validator = DatetimeValidator(datetime(2023, 10, 31));
errorID = "arrow:array:list:ExpectedUnzonedDatetime";
fcn = @() validator.validateElement(datetime(2023, 11, 1, TimeZone="America/New_York"));
testCase.verifyError(fcn, errorID);
end

function ValidateElementClassTypeMismatchError(testCase)
% Verify validateElement throws an exception whose identifier
% is "arrow:array:list:ClassTypeMismatch" if the input
% element is not a datetime.
import arrow.array.internal.list.DatetimeValidator

validator = DatetimeValidator(datetime(2023, 10, 31));
errorID = "arrow:array:list:ClassTypeMismatch";
fcn = @() validator.validateElement(1);
testCase.verifyError(fcn, errorID);
fcn = @() validator.validateElement("A");
testCase.verifyError(fcn, errorID);
fcn = @() validator.validateElement(seconds(1));
testCase.verifyError(fcn, errorID);
end

function GetElementLength(testCase)
% Verify getElementLength returns the expected length values
% for the given input arrays.
import arrow.array.internal.list.DatetimeValidator

validator = DatetimeValidator(datetime(2023, 10, 31));
length = validator.getElementLength(datetime.empty(0, 1));
testCase.verifyEqual(length, 0);
length = validator.getElementLength(datetime(2023, 11, 1));
testCase.verifyEqual(length, 1);
length = validator.getElementLength(datetime(2023, 11, 1) + days(0:2));
testCase.verifyEqual(length, 3);
length = validator.getElementLength(datetime(2023, 11, 1) + days([0 1; 2 3]));
testCase.verifyEqual(length, 4);
end

function ReshapeCellElements(testCase)
% Verify reshapeCellElements reshapes all elements in the input
% cell array into column vectors.
import arrow.array.internal.list.DatetimeValidator

validator = DatetimeValidator(datetime(2023, 10, 31));
date = datetime(2023, 10, 31);

C = {date + days(0:2), ...
date + days(3:4)', ...
date + days([5 6; 7 8]), ...
datetime.empty(1, 0)};

act = validator.reshapeCellElements(C);

exp = {date + days(0:2)', ...
date + days(3:4)', ...
date + days([5; 7; 6; 8]), ...
datetime.empty(0, 1)};

testCase.verifyEqual(act, exp);
end

end

end

0 comments on commit 18335ec

Please sign in to comment.