datetime sometimes returns NaT and sometimes throws an error - what is the expected behaviour?

Sometimes datetime throws an error if it can't return a valid datetime object from a string input:
dt = datetime(["2026_06" "2026_06_13"], Format = "yyyy_MM_dd")
Error using datetime (line 261)
Unable to convert the text to datetime using the format 'yyyy_MM_dd'.
and sometimes it returns NaT:
dt = datetime(["2026_06_13" "2026_06"], Format = "yyyy_MM_dd")
dt =
1×2 datetime array
2026_06_13 NaT
The second result is what I'd expect from the documentation. I'm having difficulty figuring out what the underlying rule is, since the two examples have the same input apart from the order and the same format. It seems to be to do with whether the first element of the array can be interpreted according to the format.
It looks like I have to put every call to datetime in a try-catch block and also test the result result for NaT, if I'm to detect any non-conforming input. This seems excessive - can anyone suggest a better way, or whether this is actually a bug I should try to report?

23 Comments

Report it as a bug. This behavior (giving up on the first element) kinda defeats the point of having NaTs in the first place.
"I'm having difficulty figuring out what the underlying rule is..."
The rule is it's an error if the first element in the input array doesn't match the specified format while the second case does match and a subsequent doesn't.
Now, as @Stephen23 says, that's not particularly user-friendly behavior and while it probably doesn't match the formal definition of a bug, it is, difficult to handle generically and is at least worthy of a strong request for an enhancement for consistent behavior.
The doc notes all elements in the input array must have the same format; for a case where didn't have control of the input I resorted to scanning the input array first to ensure it was consistent so could fix up bum ones before converting. I found that simpler than having to fix up after the fact or having an error handler.
One approach that would make it much more deterministic would be to offer a user-option, e.g. "ParseFailureAction" with possible values of "error" or "NaT". That would let the user decide what response they require from malformed date formats. The current behavior is not predictable or obvious.
Yeah, that would be a relatively easy fix that shouldn't require much, if any, restructuring of the code itself. I guess having that also would end up with one of the two as the default behavior which would also be one or the other for all cases.
There's been a lot of effort towards improving performance of the parsing of string input over last several releases but the above behavior is consistent back at least as far as R2017b although the syntax has to use the name,parameter comma-separated pairs instead of assignment.
The behavior is predictable; just that one has to test first to know which is going to be the result. That doesn't alleviate writing code to deal with the two possibilities, though, granted....
Thank you all for the helpful comments. I will put in an enhancement request, and suggest that one solution would be an additional name-value argument.
On reflection, one possible issue and peraps why it behaves as it does is, what is it to do in the case if user asked for NaT and it's the first one that fails instead of error? Return an array of NaT the input size instead, I guess?
It would seem it might require a (significant?) change in logic to switch from the present behavior that the first errors to then continue to try all subsequent to only return NaT by position; the presupposition may be that if the first fails it's likely the input format description is simply wrong for the given input array, not that just one or a few may be inconsistent but others may be ok...which condition violates the documented requirement that all be the same format.
When InputFormat is specified, the doc states: "All values in the input argument DateStrings must have the same format." as @dpb noted. It seems to me that neither case in the question meets that requirement, so both should error.
But is that not also the requirement for the case where InputFormat is not specified for which the "same format" requirement is absent? Apparently not (and the doc page does not say that it is):
s1 = "24-Oct-2014 12:45:07";
s2 = "2026-06-13";
datetime([s1,s2])
ans = 1×2 datetime array
24-Oct-2014 12:45:07 NaT
datetime([s2,s1])
ans = 1×2 datetime array
13-Jun-2026 NaT
AFAICT, returning NaT is not documented at datetime. But it is over at NaT, which states:
"The datetime function creates a NaT value automatically when it cannot convert text to a datetime value, ..."
By the plain language (or any other frankly) reading of that statement, neither case of the OP should error and both should return NaT for any entry that doesn't match the InputFormat (and if actually implemented would obviate the "same format" requirement in datetime because anything that doesn't match InputFormat would just return NaT anyway).
Seems like the datetime behavior and relevant doc pages are all inconsistent with each other.
@Paul -- True that.
The same format requirement was imposed from the beginning clear back with the introduction in R2014 (I don't recall now if it made a or b was first). I just stumbled over a ten year old Answers of essentially the same issue in which I responded showing how to test and then use @cellfun to process the two or more separate formats.
@Peter Perkins was involved in the thread from Mathworks side; however, the idea of an additional paramater to control whether an error would or would not be generated didn't come up; it concentrated on workarounds to deal with multiple input formats.
"It seems to me that neither case in the question meets that requirement, so both should error."
The function STR2DOUBLE does not throw an error for such cases, it attempts all and returns NaN for those that it cannot convert. So there is precedent for the behavior of DATETIME returning NaT for those elements. Simply throwing an error is obstructive.
So str2double behaves as datetime should behave based on the statement at NaT.
Throwing an error might be obstructive, but it's a reasonable outcome given the doc statement "... must have the same format. (emphasis added)"
Perhaps that statement should be removed and datetime should be corrected so that it returns NaT for any entry that doesn't match the required format, whether that format be explicitly specified by InputFormat or implicitly specified by the first entry in an array input. Adding a name-value pair to give the user control over what happens if an input entry is mismatched with the format could be helpful.
Keep in mind that the user can explicitly request a NaT output
[datetime("NaT"),datetime("NaT",'InputFormat','yyyy-MM-dd')]
ans = 1×2 datetime array
NaT NaT
Is the "NaT" input always to be considered as matching the format or not matching the format?
Like a NaN, the display format is immaterial to the internal representation so asking for a NaT will always produce one. I guess that can be interpreted as "always matches".
nat=[datetime("NaT"),datetime("NaT",'InputFormat','yyyy-MM-dd')]
nat = 1×2 datetime array
NaT NaT
[nat(1).Format; nat(2).Format]
ans = 2×20 char array
'dd-MMM-uuuu HH:mm:ss' 'dd-MMM-uuuu HH:mm:ss'
shows there's only one format property for the array, not by element in the array and that the first kid on the block controls...
nat=[datetime("NaT",'InputFormat','yyyy-MM-dd','Format','yyyy-MM-dd') datetime("NaT")]
nat = 1×2 datetime array
NaT NaT
nat.Format
ans = 'yyyy-MM-dd'
While not explicitly documented as is with NaN, the same comparisons hold
[NaT==NaT NaT~=NaT]
ans = 1×2 logical array
0 1
Amplifying some on the above example of implicit format string interpretation--
s1 = "24-Oct-2014 12:45:07";
s2 = "2026-06-13";
datetime([s1 s2])
ans = 1×2 datetime array
24-Oct-2014 12:45:07 NaT
ans.Format
ans = 'dd-MMM-uuuu HH:mm:ss'
datetime([s2 s1])
ans = 1×2 datetime array
13-Jun-2026 NaT
ans.Format
ans = 'dd-MMM-uuuu'
illustrates that without the explicit format, datetime tries to figure it out on its own and then sets the 'InputFormat' property to whatever it decides is the one to use -- and then fails it a subsequent string doesn't match. So, the requirement that all are the same format still holds, and the behavior is the same as if the 'InputFormat' string were given and matched the first element correctly. Hence as in @Paul's example, that case is NOT going to error but return the NaT for elements not matching that format. The requirement is there even if not explicitly documented, yes.
The following examples show another wart in that behavior is different for the same possible format ambiguity depending upon which format is given
s = "2026-06-01";
datetime(s)
ans = datetime
01-Jun-2026
s = "06/01/2026";
datetime(s)
Warning: Successfully converted the text to datetime using the format 'MM/dd/uuuu', but the format is ambiguous and could also be 'dd/MM/uuuu'. To create datetimes from text with a specific format, call:

datetime(textinput,'InputFormat',infmt)
ans = datetime
01-Jun-2026
Both end up presuming the month is June, but both are ambiguous although it's undoubtadly more common to write yyyy/mm/dd than yyyy/dd/mm or mm/dd/yyyy over dd/mm/yyyy, why warn one but not the other?
What should the output be for the following line, assuming the proposed name-value pair is in effect
[datetime("NaT",ParseFailureAction="error"),datetime("NaT",'InputFormat','yyyy-MM-dd',ParseFailureAction="error")]
On the prior assumption that "NaT" is format invariant, there is no error so both are NaT.
One could probably argue for another interpretation, but to me would be inconsistent in expecting a string representation of "NaT" to not match any format because there is no associated format, just a value.
Now, if one were to do something like
%datetime(NaT,'Format','default','InputFormat','yyyy-MM-dd',ParseFailureAction="error")
one might think it should error as the display format isn't the same as the default format --
n=NaT;
n.Format='default';
n.Format
ans = 'dd-MMM-uuuu HH:mm:ss'
and those don't match and there is an associated format with the object. But, it isn't a string input so
However, that would also be inconsistent with datetime current behavior with datetime variables that; since they're already datetime, get passed through even if their display formats don't match an 'InputFormat' string; only if one converts back to the string representation will it then fail. But, all there is in a string representation of a NaT is "NaT"; there is no other formatting info available once the variable is lost.
Of course, to further complicate things, datetime returns a single array output variable that has one and only one Format property for all its members so it either has to be specified or it will be the current in effect default (and, otomh, I'm not sure one can set a user preference default or not).
Like NaN, a NaT is a peculiar animal and it's easy to dream up strange things with them; this is, I think, especially true with NaT since it's a derived class specific to MATLAB as opposed to an IEEE Standard and there is a defined bit pattern for it.
That's an initial thinking; deeper diving might reveal other issues...
Separate from the issue of what datetime( ) should do for invalid/irregular input, note that both datetime and duration class objects store the data as double precision variables, so there is an actual IEEE NaN bit pattern in the background. E.g.,
snan = seconds(nan)
snan = duration
NaN sec
struct(snan)
Warning: Calling STRUCT on an object prevents the object from hiding its implementation details and should thus be avoided. Use DISP or DISPLAY to see the visible public details of an object. See 'help struct' for more information.
ans = struct with fields:
millis: NaN fmt: 's'
class(ans.millis)
ans = 'double'
dnan = datetime(2000,1,1) + snan
dnan = datetime
NaT
struct(dnan)
Warning: Calling STRUCT on an object prevents the object from hiding its implementation details and should thus be avoided. Use DISP or DISPLAY to see the visible public details of an object. See 'help struct' for more information.
ans = struct with fields:
UTCZoneID: 'UTC' UTCLeapSecsZoneID: "UTCLeapSeconds" ISO8601Format: 'uuuu-MM-dd'T'HH:mm:ss.SSS'Z'' ISO8601FormatPattern: regexpPattern("uuuu-MM-dd'T'HH:mm:ss(\.S{1,9})?'Z'") epochDN: 719529 MonthsOfYear: [1×1 struct] DaysOfWeek: [1×1 struct] data: NaN fmt: '' tz: '' dfltPivot: 1976 dateFields: [1×1 struct] noConstructorParamsSupplied: [1×1 struct]
class(ans.data)
ans = 'double'
You can see the data areas of both variables contain doubles, so there is in fact an IEEE double defined bit pattern in the background for these NaN and NaT values for what it's worth.
The real difference between datetime and duration class objects is that datetime variables contain an imaginary correction factor to keep sub-nanosecond accuracy, whereas duration class variables do not. A big oversight IMO, so you have to be very careful and write special code to process differences if you want to maintain that accuracy. E.g.,
format longg
dt0 = datetime(1970,1,1)
dt0 = datetime
01-Jan-1970
dt1 = datetime(2000,1,1,1,1,12.1234567891234)
dt1 = datetime
01-Jan-2000 01:01:12
dur = dt1 - dt0
dur = duration
262969:01:12
dt2 = dt0 + dur
dt2 = datetime
01-Jan-2000 01:01:12
dt1.Second
ans =
12.1234567891234
dt2.Second
ans =
12.1234567871094
You can see that the seconds precision has been lost in the arithmetic. The reason is the storage scheme is different between datetime and duration. E.g.,
struct(dt1)
Warning: Calling STRUCT on an object prevents the object from hiding its implementation details and should thus be avoided. Use DISP or DISPLAY to see the visible public details of an object. See 'help struct' for more information.
ans = struct with fields:
UTCZoneID: 'UTC' UTCLeapSecsZoneID: "UTCLeapSeconds" ISO8601Format: 'uuuu-MM-dd'T'HH:mm:ss.SSS'Z'' ISO8601FormatPattern: regexpPattern("uuuu-MM-dd'T'HH:mm:ss(\.S{1,9})?'Z'") epochDN: 719529 MonthsOfYear: [1×1 struct] DaysOfWeek: [1×1 struct] data: 946688472123.457 + 2.01402508537285e-06i fmt: '' tz: '' dfltPivot: 1976 dateFields: [1×1 struct] noConstructorParamsSupplied: [1×1 struct]
struct(dur)
Warning: Calling STRUCT on an object prevents the object from hiding its implementation details and should thus be avoided. Use DISP or DISPLAY to see the visible public details of an object. See 'help struct' for more information.
ans = struct with fields:
millis: 946688472123.457 fmt: 'hh:mm:ss'
struct(dt2)
Warning: Calling STRUCT on an object prevents the object from hiding its implementation details and should thus be avoided. Use DISP or DISPLAY to see the visible public details of an object. See 'help struct' for more information.
ans = struct with fields:
UTCZoneID: 'UTC' UTCLeapSecsZoneID: "UTCLeapSeconds" ISO8601Format: 'uuuu-MM-dd'T'HH:mm:ss.SSS'Z'' ISO8601FormatPattern: regexpPattern("uuuu-MM-dd'T'HH:mm:ss(\.S{1,9})?'Z'") epochDN: 719529 MonthsOfYear: [1×1 struct] DaysOfWeek: [1×1 struct] data: 946688472123.457 fmt: '' tz: '' dfltPivot: 1976 dateFields: [1×1 struct] noConstructorParamsSupplied: [1×1 struct]
That imaginary part of the dt1 data is a correction factor to maintain sub-nanosecond accuracy in storage. But the dur storage doesn't have that, so that level of precision is lost and doesn't appear in the follow on dt2 construction. If you need to have sub millisecond accuracy, you generally need to write your own code for datetime arithmetic. E.g., pick off the Second manually and do your own datetime arithmetic w/o seconds as usual and then do the Seconds arithmetic off to the side and added in after the fact. E.g.,
s0 = dt0.Second; % pick off dt0 seconds
s1 = dt1.Second; % pick off dt1 seconds
temp0 = dt0; temp0.Second = 0; % temp datetime w/o seconds
temp1 = dt1; temp1.Second = 0; % temp datetime w/o seconds
dur = temp1 - temp0; sdur = seconds(s1 - s0); % difference temp datetimes and seconds
dt3 = dt0 + dur + sdur % final result with seconds result added in manually
dt3 = datetime
01-Jan-2000 01:01:12
dt3.Second
ans =
12.1234567891234
Now you get the full precision result, but it is messy to say the least.
(Hey, Mathworks ... it's not too late to enhance duration for this!)
This example also demonstrates that the data value in datetime objects happens to be the number of milliseconds since 1970-01-01.
I should have figured the NaT was NaN in sheep's clothing...good digging, that's good supplementary data for the thread, indeed.
That imaginary part of the dt1 data is a correction factor to maintain sub-nanosecond accuracy in storage.
See this post on Cleve's blog. "I am pleased to reveal some details about the arithmetic in datetime. The object is using a form of quadruple precision known as double-double. In fact, the second half of the 112-bit format is stored as the imaginary part of the data field, as you see in the following."
I recall at least one Answers thread that the poster was bit by the loss of precision of the duration class. Was looking at recordeded timestamps to nanosecond precison but wanted the diff() to see consistency. The simple-minded solution didn't work, leaving him/her(?, I forget) puzzled. Fortunately, it was easy in his case to simply scale up first, but is inconvenient and seems strange to not have the two be consistent in internal precision.
There is also a discrepancy on string input parsing. E.g.,
format longg
dt = datetime(2000,1,1,1,1,12.1234567891234)
dt = datetime
01-Jan-2000 01:01:12
dt.Second
ans =
12.1234567891234
dt = datetime("2000-01-01 01:01:12.1234567891234")
dt = datetime
01-Jan-2000 01:01:12
dt.Second
ans =
12.123456789
dt = datetime("2000-01-01T01:01:12.1234567891234Z",'TimeZone','UTCLeapSeconds')
dt = datetime
2000-01-01T01:01:12.123Z
dt.Second
ans =
12.123456789
Apparently when processing input strings, the datetime parser cuts off the input at nanoseconds (I seem to recall this may be noted in the documentation). If you want to retain full input precision, you have to manually parse at least the seconds part of the string yourself and add it in. E.g.,
dt = datetime("2000-01-01T01:01:00Z",'TimeZone','UTCLeapSeconds') + seconds(12.1234567891234)
dt = datetime
2000-01-01T01:01:12.123Z
dt.Second
ans =
12.1234567891234
datetime does mention that limitation in the input description for infmt for the InputFormat parameter...
"For input text that represents fractional seconds, you can specify infmt with up to nine S characters to indicate fractional second digits."
The Description also states that a datetime provides
  • Storage for fractional seconds out to nanosecond precision
so there's no repreesentation of being able to actually use the full available storage even though implemented internally.
It and duration do still seem like a work in progress that has lost attention in the push for fancy gewgaws instead.
Another wart with duration and precision...
s=[0:9]/1E9; % a sequence of nsec values
fmt='HH:mm:ss.SSSSSSSSS'; % show just time fields
dt=datetime(2026,1,1,0,0,s*2,'Format',fmt).'
dt = 10×1 datetime array
00:00:00.000000000 00:00:00.000000002 00:00:00.000000004 00:00:00.000000006 00:00:00.000000008 00:00:00.000000010 00:00:00.000000012 00:00:00.000000014 00:00:00.000000016 00:00:00.000000018
du=diff(dt);
fmt='hh:mm:ss.SSSSSSSSS'; du.Format=fmt
du = 9×1 duration array
00:00:00.000000002 00:00:00.000000002 00:00:00.000000001 00:00:00.000000002 00:00:00.000000001 00:00:00.000000001 00:00:00.000000001 00:00:00.000000002 00:00:00.000000001
There's still a FP rounding issue that the difference doesn't show a 2 nsec difference between all elements. I don't know if this is solvable or not?
You'll also notice that the format string for the duration is all lower case except for the capital S for fractional seconds. This is an inconsistency/inconvenience in comparison to the datetime and two conventions for related functions that have to keep in mind.
"There's still a FP rounding issue that the difference doesn't show a 2 nsec difference between all elements."
So, this is a display feature. I believe it happens for both datetime and duration objects. The display is truncated instead of rounded, and I am not aware of any way to change that. For this particular example, this is not an issue with the duration class per se. However, it does mean that if you want to output a datetime or duration as a string and be able to read it back in and get the original, you will need to write your own string conversion routine (at least for the sub-seconds part).
E.g.,
s=[0:9]/1E9; % a sequence of nsec values
fmt='HH:mm:ss.SSSSSSSSS'; % show just time fields
dt=datetime(2026,1,1,0,0,s*2,'Format',fmt).';
du=diff(dt);
fmt='hh:mm:ss.SSSSSSSSS'; du.Format=fmt
du = 9×1 duration array
00:00:00.000000002 00:00:00.000000002 00:00:00.000000001 00:00:00.000000002 00:00:00.000000001 00:00:00.000000001 00:00:00.000000001 00:00:00.000000002 00:00:00.000000001
format longg
for k=1:numel(du)
sprintf('%35.30f',seconds(du(k)))
end
ans = ' 0.000000002000000000000000538153'
ans = ' 0.000000002000000000000000538153'
ans = ' 0.000000001999999999999999297383'
ans = ' 0.000000002000000000000001365334'
ans = ' 0.000000001999999999999999297383'
ans = ' 0.000000001999999999999999297383'
ans = ' 0.000000001999999999999999297383'
ans = ' 0.000000002000000000000003019695'
ans = ' 0.000000001999999999999997643021'
dt = datetime(2000,1,1,1,1,12.6)
dt = datetime
01-Jan-2000 01:01:12
Good catch, @James Tursa....was fixated on the display formatting available for the duration being limited to only the nine decimal places which also seems to be an unnecessary limitation.
ADDENDUM
" The display is truncated instead of rounded,..."
That is inconsistent with and in contradiction to the behavior of all other format specifiers for decimal precision. Should that not be considered an implementation shortfall (if not an actual bug since there isn't a comparable standard C library function spec to compare to as with the others)?
s=[0:9]/1E9; % a sequence of nsec values
fmt='HH:mm:ss.SSSSSSSSS'; % show just time fields
dt=datetime(2026,1,1,0,0,s*2,'Format',fmt).';
du=diff(dt);
fmt0='hh:mm:ss.';
nDec=9; % start at nosec
fmt=[fmt0 repmat('S',1,nDec)];
du.Format=fmt;
disp('Nine digit format succeeded')
Nine digit format succeeded
try
nDec=nDec+1; % ask for an extra digit
fmt=[fmt0 repmat('S',1,10)];
disp('Trying one additional digit precision')
du.Format=fmt;
catch ME
disp(ME.message)
end
Trying one additional digit precision
The format 'hh:mm:ss.SSSSSSSSSS' is not valid.
disp(num2str(seconds(du),'%0.10f'))
0.0000000020 0.0000000020 0.0000000020 0.0000000020 0.0000000020 0.0000000020 0.0000000020 0.0000000020 0.0000000020
The above produces the expected result; it "just seems wrong" that
  1. You can't set an arbitrary precision for output on a duration variable (or datetime, either, I presume, although I didn't test it)
  2. It truncates the displayed value at the number of digits of precision instead of rounding as per C Standard i/o library formatting functions are required to do.

Sign in to comment.

Answers (1)

The short anser is "Both!"
Sorry, couldn't help myself <very big grin> )

Categories

Products

Release

R2025b

Asked:

on 13 Jun 2026

Edited:

dpb
on 25 Jun 2026 at 13:24

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!