Code covered by the BSD License  

Highlights from
Date Vector or Serial Date Number to ISO 8601 Date String

image thumbnail
from Date Vector or Serial Date Number to ISO 8601 Date String by Stephen Cobeldick
Convert a Date Vector/Number to an ISO 8601 Date String. Tokens control the date/time notation.

datestr8601(DVN,varargin)
function varargout = datestr8601(DVN,varargin)
% Convert a Date Vector/Serial Date Number to an ISO 8601 formatted Date String (timestamp).
%
% (c) 2013 Stephen Cobeldick
%
% ### Function ###
%
% Syntax:
%  String = datestr8601
%  String = datestr8601(DateVN)
%  String = datestr8601(DateVN,Token)
%  [String1,String2,...] = datestr8601(DateVN,Token1,Token2,...)
%
% Fast conversion of a Date Vector or Serial Date Number to a string, the
% style of which is controlled by (optional) input token/s. The string may
% be an ISO 8601 timestamp or a single date/time value: multiple tokens may
% be used to output multiple strings (faster than multiple function calls).
%
% By default the function uses the current time and returns the basic
% ISO 8601 calendar timestamp: this is very useful for naming files that
% sort alphabetically into chronological order!
%
% The ISO 8601 timestamp style options are:
%
% - Date in ordinal, calendar or week-numbering notation.
% - Basic or Extended format (without/with unit separation characters).
% - Any date-time separator character (with a few exceptions).
% - Full or lower precision (fewer trailing date/time units).
% - Decimal fraction of the trailing unit (decimal places).
%
% These style options are explained in the tables below (see "Timestamps").
%
% Note 1: Some Strings use the ISO 8601 week-numbering year, where the first
%  week of the year includes the first Thursday of the year: check the tables!
% Note 2: Out-of-range values are permitted in the Date Vector.
% Note 3: Calls undocumented MATLAB functions "datevecmx", "datenummx" & "ismembc".
%
% See also DATENUM8601 CLOCK NOW DATESTR DATENUM DATEVEC DATEROUND
%
% ### Examples ###
%
% Examples use the date+time described by the vector [1999,1,3,15,6,48.0568].
%
% datestr8601(datenum8601('19990103T150648'),'ymdHMS4')
%  ans = '19990103T150648.0568'
%
% datestr8601
%  ans = '19990103T150648'
%
% datestr8601([],'yn_HM')
%  ans = '1999003_1506'
%
% datestr8601(now-1,'DDDD')
%  ans = 'Saturday'
%
% datestr8601(clock,'*ymdHMS')
%  ans = '1999-01-03T15:06:48'
%
% [A,B,C,D] = datestr8601(clock,'ddd','mmmm','yyyy','*YWD')
% sprintf('The %s of %s %s has the ISO week-date "%s".',A,B,C,D)
%  ans = 'The 3rd of January 1999 has the ISO week-date "1998-W53-7".'
%
% ### Single Value Tokens ###
%
% For date values the case of the Token determines the output String's
% year-type: lowercase = calendar year, UPPERCASE = week-numbering year.
%
% 'W' = the standard ISO 8601 week number (this is probably what you want).
% 'w' = the weeks (rows) shown on an actual printed calendar (very esoteric).
%
% 'Q' = each quarter is 13 weeks (the last may be 14). Uses week-numbering year.
% 'q' = each quarter is three months long: Jan-Mar, Apr-Jun, Jul-Sep, Oct-Dec.
%
% 'N'+'R' = 7*52 or 7*53 (year dependent).
% 'n'+'r' = 365 (or 366 if a leap year).
%
% Input | Output                                      | Output
% Token | String Date/Time Representation             | Example
% ------|---------------------------------------------|---------
% Calendar year:                                      |
% 'yyyy'| year, four digit                            |'1999'
% 'n'   | day of the year, variable digits            |'3'
% 'nnn' | day of the year, three digit, zero padded   |'003'
% 'r'   | days remaining in year, variable digits     |'362'
% 'rrr' | days remaining in year, three digit, padded |'362'
% 'q'   | year quarter, 3-month                       |'1'
% 'qq'  | year quarter, 3-month, abbreviation         |'Q1'
% 'qqq' | year quarter, 3-month, ordinal and suffix   |'1st'
% 'w'   | week of the year, one or two digit          |'1'
% 'ww'  | week of the year, two digit, zero padded    |'01'
% 'www' | week of the year, ordinal and suffix        |'1st'
% 'm'   | month of the year, one or two digit         |'1'
% 'mm'  | month of the year, two digit, zero padded   |'01'
% 'mmm' | month name, three letter abbreviation       |'Jan'
% 'mmmm'| month name, in full                         |'January'
% 'd'   | day of the month, one or two digit          |'3'
% 'dd'  | day of the month, two digit, zero padded    |'03'
% 'ddd' | day of the month, ordinal and suffix        |'3rd'
% ------|---------------------------------------------|---------
% Week-numbering year:                                |
% 'YYYY'| year, four digit,                           |'1998'
% 'N'   | day of the year, variable digits            |'371'
% 'NNN' | day of the year, three digit, zero padded   |'371'
% 'R'   | days remaining in year, variable digits     |'0'
% 'RRR' | days remaining in year, three digit, padded |'000'
% 'Q'   | year quarter, 13-week                       |'4'
% 'QQ'  | year quarter, 13-week, abbreviation         |'Q4'
% 'QQQ' | year quarter, 13-week, ordinal and suffix   |'4th'
% 'W'   | week of the year, one or two digit          |'53'
% 'WW'  | week of the year, two digit, zero padded    |'53'
% 'WWW' | week of the year, ordinal and suffix        |'53rd'
% ------|---------------------------------------------|---------
% Weekday:                                            |
% 'D'   | weekday number (Monday=1)                   |'7'
% 'DD'  | weekday name, two letter abbreviation       |'Su'
% 'DDD' | weekday name, three letter abbreviation     |'Sun'
% 'DDDD'| weekday name, in full                       |'Sunday'
% ------|---------------------------------------------|---------
% Time of day:                                        |
% 'H'   | hour of the day, one or two digit           |'15'
% 'HH'  | hour of the day, two digit, zero padded     |'15'
% 'M'   | minute of the hour, one or two digit        |'6'
% 'MM'  | minute of the hour, two digit, zero padded  |'06'
% 'S'   | second of the minute, one or two digit      |'48'
% 'SS'  | second of the minute, two digit, zero padded|'48'
% 'F'   | deci-second of the second, zero padded      |'0'
% 'FF'  | centisecond of the second, zero padded      |'05'
% 'FFF' | millisecond of the second, zero padded      |'056'
% ------|---------------------------------------------|---------
% 'MANP'| Midnight/AM/Noon/PM (+-0.0005s)             |'PM'
% ------|---------------------------------------------|---------
%
% ### ISO 8601 Timestamps ###
%
% Output    | Basic Format             | Extended Format (token prefix '*')
% Date      | Input  | Output Timestamp| Input   | Output Timestamp
% Notation: | Token: | Example:        | Token:  | Example:
% ----------|--------|-----------------|---------|-------------------------
% Ordinal   |'ynHMS' |'1999003T150648' |'*ynHMS' |'1999-003T15:06:48'
% ----------|--------|-----------------|---------|-------------------------
% Calendar  |'ymdHMS'|'19990103T150648'|'*ymdHMS'|'1999-01-03T15:06:48'
% ----------|--------|-----------------|---------|-------------------------
% Week      |'YWDHMS'|'1998W537T150648'|'*YWDHMS'|'1998-W53-7T15:06:48'
% ----------|--------|-----------------|---------|-------------------------
%
% Timestamp can omit leading or trailing units (reduced precision), eg:
% ----------|--------|-----------------|---------|-------------------------
%           |'DHMS'  |'7T150648'       |'*DHMS'  |'7T15:06:48'
% ----------|--------|-----------------|---------|-------------------------
%           |'mdH'   |'0103T15'        |'*mdH'   |'01-03T15'
% ----------|--------|-----------------|---------|-------------------------
% Date-time separator character can be specified (default='T'), eg:
% ----------|--------|-----------------|---------|-------------------------
%           |'n_HMS' |'003_150648'     |'*n_HMS' |'003_15:06:48'
% ----------|--------|-----------------|---------|-------------------------
%           |'YWD@H' |'1998W537@15'    |'*YWD@H' |'1998-W53-7@15'
% ----------|--------|-----------------|---------|-------------------------
% Trailing date/time value can have decimal digits (fraction), eg:
% ----------|--------|-----------------|---------|-------------------------
%           |'HMS4'  |'150648.0568'    |'*HMS4'  |'15:06:48.0568'
% ----------|--------|-----------------|---------|-------------------------
%           |'YW7'   |'1998W53.9471032'|'*YW7'   |'1998-W53.9471032'
% ----------|--------|-----------------|---------|-------------------------
%           |'y10'   |'1999.0072047202'|'*y10'   |'1999.0072047202'
% ----------|--------|-----------------|---------|-------------------------
%
% Note 4: Token parsing matches Single Value Tokens before ISO 8601 Tokens.
% Note 5: Function does not check for ISO 8601 compliance: user beware!
% Note 6: Date-time separator must not be any of [+-./0123456789:DFHMPRSWYZdmny].
%
% ### Inputs & Outputs ###
%
% Inputs:
%  DateVN = Date Vector, [year,month,day,hour,minute,second.millisecond].
%         = Serial Date Number, where 1 = start of 1st January of the year 0000.
%         = []*, uses current time (default).
%  Token  = String token, chosen from the above tables (default is 'ymdHMS').
%
% Outputs:
%  String = String date-value, whose representation is controlled by Token.
%
% Inputs  = (DateVN,Token1,Token2,...)
% Outputs = [String1,String2,...]

DfAr = {'ymdHMS'}; % {Token}
DfAr(1:numel(varargin)) = varargin;
%
% Calculate date-vector:
if nargin==0||isempty(DVN) % Default = now
    DtV = clock;
elseif isscalar(DVN)       % Serial Date Number
    DtV = datevecmx(DVN);
elseif isrow(DVN)          % Date Vector
    DtV = datevecmx(datenummx(DVN));
else
    error('Invalid Date Vector or Date Number. Check array dimensions.');
end
% Calculate serial date-number:
DtN = datenummx(DtV);
% Weekday index (Mon=0):
DtD = mod(floor(DtN(1))-3,7);
% Adjust date to suit week-numbering:
DtN(2,1) = DtN(1)+3-DtD;
DtV(2,:) = datevecmx(floor(DtN(2)));
DtV(2,4:6) = DtV(1,4:6);
% Separate milliseconds from seconds:
DtV(:,7) = round(rem(DtV(1,6),1)*10000);
DtV(:,6) = floor(DtV(1,6));
% Date at the end of the year [last,this]:
DtE(1,:) = datenummx([DtV(1)-1,12,31;DtV(1),12,31]);
DtE(2,:) = datenummx([DtV(2)-1,12,31;DtV(2),12,31]);
DtO = 3-mod(DtE(2,:)+1,7);
DtE(2,:) = DtE(2,:)+DtO;
%
varargout = DfAr;
%
ChO = ['00000000001111111111222222222233333333334444444444555555555566';...
       '01234567890123456789012345678901234567890123456789012345678901';...
       'tsnrtttttttttttttttttsnrtttttttsnrtttttttsnrtttttttsnrttttttts';...
       'htddhhhhhhhhhhhhhhhhhtddhhhhhhhtddhhhhhhhtddhhhhhhhtddhhhhhhht'].';
%
for m = 1:numel(DfAr)
    tok = DfAr{m};
    tkl = numel(tok);
    tkw = strcmp(upper(tok),tok);
    switch tok
        case {'S','SS','M','MM','H','HH','d','dd','ddd','m','mm'}
            % seconds, minutes, hours, day of the month, month
            val = DtV(1,strfind('ymdHMS',tok(1)));
            varargout{m} = ChO(1+val,1+(tkl~=2&&val<10):max(2,2*(tkl-1))); % (also week)
        case {'D','DD','DDD','DDDD'}
            % weekday
            varargout{m} = d8601Day(tkl,DtD);
        case {'mmm','mmmm'}
            % month of the year
            varargout{m} = d8601Mon(tkl,DtV(1,2));
        case {'F','FF','FFF'}
            % deci/centi/milliseconds
            str = sprintf('%04.0f',DtV(1,7));
            varargout{m} = str(1:tkl);
        case {'n','nnn','r','rrr','N','NNN','R','RRR'}
            % day of the year, days remaining in the year
            varargout{m} = sprintf('%0*.0f',tkl,abs(floor(DtN(1))-...
                DtE(1+tkw,1+strncmpi('r',tok,1))));
        case {'y','yyyy','Y','YYYY'}
            % year
            varargout{m} = sprintf('%04.0f',DtV(1+tkw));
        case {'w','ww','www','W','WW','WWW'}
            % week of the year
            val = floor(max(0,(DtN(1+tkw)-DtE(1+tkw)+DtO(1)*~tkw))/7);
            varargout{m} = ChO(2+val,1+(tkl~=2&&val<10):max(2,2*(tkl-1))); % (also S/M/H/d/m)
        case {'q','qq','qqq','Q','QQ','QQQ'}
            % year quarter
            val = [ceil(DtV(1,2)/3),min(4,1+floor((DtN(2)-DtE(2))/91))];
            varargout{m} = d8601Qtr(tkl,val(1+tkw));
        case 'MANP'
            % midnight/am/noon/pm
            apc = {'Midnight','AM','Noon','PM'};
            ind = 2+2*(DtV(1,4)>=12)-(all(DtV(1,5:7)==0)&&any(DtV(1,4)==[0,12]));
            varargout{m} = apc{ind};
%        case 'test'
%            varargout{m} = d8601Test(DtN(1));
        otherwise % All ISO 8601 timestamps
            % Check if extended or basic format, identify any decimal digits:
            Ext = strncmp('*',tok,1);
            DcP = find(~isstrprop(tok,'digit'),1,'last');
            Dgt = sscanf(tok(DcP+1:end),'%d');
            tok = tok(1+Ext:DcP);
            % Identify date-time separator and start of timestamp:
            IsT = ismembc(tok,'+-./0123456789:DFHMPRSWYZdmny'); % (presorted)
            tkl = sum(IsT);
            BeI = strfind('YWDHMSymdHMSynHMS',tok(IsT));
            assert(any(BeI)&&tkl<(7-rem(BeI(1)-1,6)),'Input token is not recognized.')
            switch sum(~IsT)
                case 0 % Standard 'T' separator.
                    varargout{m} = d8601ISO(tkl,BeI(1),DtV,DtN,DtE,DtD,ChO,Ext,Dgt,'T');
                case 1 % User supplied separator.
                    nxt = strcmp('H',tok([false,~IsT(1:end-1)]));
                    assert(nxt,'Input token date-time separator position incorrect.')
                    varargout{m} = d8601ISO(tkl,BeI(1),DtV,DtN,DtE,DtD,ChO,Ext,Dgt,tok(~IsT));
                otherwise
                    error('Input token is not recognized: too many separator chars.')
            end
    end
end
%
end
%--------------------------------------------------------------------------
function DtS = d8601Day(tkl,ind)
% weekday
%
if tkl==1
    StT = '1234567';
    DtS = StT(1+ind);
else
    ind = 1+ind;
    StT = ['Monday   ';'Tuesday  ';'Wednesday';...
           'Thursday ';'Friday   ';'Saturday ';'Sunday   '];
    StE = [6,7,9,8,6,8,6]; % Weekday name lengths
    DtS = StT(ind,1:max(tkl,StE(ind)*(tkl-3))); % (also month)
end
%
end
%--------------------------------------------------------------------------
function DtS = d8601Mon(tkl,ind)
% month
%
StT = ['January  ';'February ';'March    ';'April    ';...
       'May      ';'June     ';'July     ';'August   ';...
       'September';'October  ';'November ';'December '];
StE = [7,8,5,5,3,4,4,6,9,7,8,8]; % Month name lengths
DtS = StT(ind,1:max(tkl,StE(ind)*(tkl-3))); % (also weekday)
%
end
%--------------------------------------------------------------------------
function DtS = d8601Qtr(tkl,ind)
% year quarter
%
QT = ['Q1st';'Q2nd';'Q3rd';'Q4th'];
QI = 1+abs(tkl-2):max(2,2*(tkl-1));
DtS = QT(ind,QI);
%
end
%--------------------------------------------------------------------------
function DtS = d8601ISO(tkl,ind,DtV,DtN,DtE,DtD,ChO,Ext,Dgt,sep)
% ISO 8601 timestamp
%
% Determine value indices within the token:
BeM = [1,2,3,4,5,6,1,2,3,4,5,6,1,3,4,5,6];
BeR = BeM(ind:ind+tkl-1);
% For calculating decimal fraction of date/time values:
BeE = BeR(end);
DtK = 1;
DtW = DtV(1,:);
DtZ = [0,1,1,0,0,0,0];
%
% {separators;values}:
if Ext % Extended-format
    DtC = {'','-','-',sep,':',':';'','','','','',''};
else % Basic-format
    DtC = {'', '', '',sep, '', '';'','','','','',''};
end
%
% hours, minutes, seconds:
for m = 4:max(BeR)
    DtC{2,m} = ChO(1+DtW(m),1:2);
end
%
ind = ceil(ind/6);
switch ind
    case 1 % Week-numbering
        DtW = DtV(2,:);
        % Decimal fraction of weeks, not days:
        if BeR(end)==2
            BeE = 3;
            DtK = 7;
            DtZ(3) = DtW(3)-DtD;
        end
        % weekday:
        if any(BeR==3)
            DtC{2,3} = ChO(2+DtD,2);
        end
        % week of the year:
        if any(BeR==2)
            DtC{2,2} = ['W',ChO(2+floor((DtN(2)-DtE(2))/7),1:2)];
        end
    case 2 % Calendar.
        % month, day of the month:
        for m = max(2,min(BeR)):3
            DtC{2,m} = ChO(1+DtW(m),1:2);
        end
    case 3 % Ordinal.
        % day of the year:
        DtC{2,3} = sprintf('%03.0f',floor(DtN(1))-DtE(1));
end
%
if BeR(1)==1
    % year:
    DtC{2,1} = sprintf('%04.0f',DtW(1));
end
%
% Concatenate separator and value strings:
BeN = [BeR*2-1;BeR*2];
DtS = [DtC{BeN(2:end)}];
%
% Decimal fraction (decimal places):
if 0<Dgt
    prc = 0;
    if BeR(end)==6
        % second
        prc = 4;
        str = sprintf('%0*.0f',prc,DtW(7));
    elseif BeR(end)==3
        % day
        prc = 10;
        str = sprintf('%.*f',prc,rem(DtN(1),1));
        str(1:2) = [];
    elseif any(DtW(BeR(end)+1:7)>DtZ(BeR(end)+1:7));
        % year/month/week/hour/minute
        prc = 16;
        % Floor all trailing units:
        DtW(7) = [];
        DtW(BeR(end)+1:6) = DtZ(BeR(end)+1:6);
        DtF = datenummx(DtW);
        % Increment the chosen unit:
        DtW(BeE) = DtW(BeE)+DtK;
        % Decimal fraction of the chosen unit:
        dcf = (DtN(1+(ind==1))-DtF)/(datenummx(DtW)-DtF);
        str = sprintf('%.*f',prc,dcf);
        str(1:2) = [];
    end
    str(1+prc:Dgt) = '0';
    DtS = [DtS,'.',str(1:Dgt)];
end
%
end
%--------------------------------------------------------------------------
%{
function s = d8601Test(DtN)
% Compare runtimes of this function and "datestr".
%
MN = 10000;
%
MS = mfilename;
DS = 'datestr';
MF = str2func(MS);
DF = str2func(DS);
%
profile on
tic
for m = 1:MN
    %B1 = DF(DtN,'yyyy-mm-dd_HH:MM:SS');   % Test 1
    B1 = DF(DtN,'yyyy-mm-dddd');          % Test 2
end
T1 = toc;
%
tic
for m = 1:MN
    %B2 = MF(DtN,'*ymd_HMS');              % Test 1
    [A,B,C] = MF(DtN,'yyyy','mm','DDDD'); % Test 2
    B2 = [A,'-',B,'-',C];                 % Test 2
end
T2 = toc;
profile viewer
%
XS = char(['"',DS,'"'],['"',MS,'"']);
s = sprintf('%s (%05.2fs): ''%s''\n%s (%05.2fs): ''%s''',XS(1,:),T1,B1,XS(2,:),T2,B2);
%
end
%}
%----------------------------------------------------------------------End!

Contact us