Free Web Hosting Provider - Web Hosting - E-commerce - High Speed Internet - Free Web Page
Search the Web


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Getting the current date (reverse engineering system date format)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


There are many cases when it is desirable to get the system date
into some environment variables, for example a flexible routine
would be one that returned;

DD=2 digit day
MM=2 digit month
YH=2 digit century
YL=2 digit year (last 2 digits of year)

A general method is frustrated somewhat by the problem of
locale. It is not good enough to consider getting the date
into a variable, in such manner (though of course this
gets the day as well);

ECHO @PROMPT SET DATE=$D$_>$.BAT
%COMSPEC% /C $.BAT>$$.BAT
CALL $$.BAT

And then parsing the string extracting the digits from
the date;

MM/DD/YYYY
^  ^  ^ ^
|  |  | |
|  |  | |
|  |  | Write to %YL%
|  |  |
|  |  Write to %YH%
|  |
|  Write to %DD%
|
Write to %MM%


Because for example in the UK we have date in this format;

DD/MM/YYYY

In Hungary they use;

YYYY-MM-DD

Looking through the country code tables you find that all
dates follow one of the formats;

  (1) YYYY MM DD
  (2) DD MM YYYY
  (3) MM DD YYYY

With one of the seperators "." "/" or "-".

There is no documented way in DOS batch to either;
 (i) get the date in some unambiguous format
(ii) return the country settings

Creation of the function that now follows is certainly
academic, the problem is easily solved in a few lines with
the aid of a little DEBUG machine code script (that will
get the date in an unambiguous format regardless of the locale).
The DEBUG solution is the most practical but it is interesting
to see how this can be done in "pure" batch. Not only that
but also because the routine extracts certain information
that could be used for other things (for example to set
the date), and so may find a use elsewhere.


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                Determine system date format                         
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


This problem hinges on one key routine that deduces the
format (1,2 or 3 above) of the system date. Laurence Soucy
came up with the idea that if one successively feeds dates
to the DATE command, each one being valid in only one of the
formats, once the date set had succeeded the program would
be able to infer the system date format.

*FYI: See Laurence Soucy's extensive batch web pages at
*     http://bigfoot.com/~batfiles/

date1= 2000-01-31
date2= 31-01-2000
date3= 01-31-2000

If the system date format is MM-DD-YYYY, then only date3 will
pass, date1 failing because the first and last fields will be
out of range. The "31" in what should be the day field here
ensures that if it falls in the month field of the system
date, it will be rejected.

Notice that DATE will accept all 3 delimitors "/" "-" "."
regardless of language settings. Discovering this was an
important factor in developing the algorithm.

I deviated from Laurence Soucy's idea slightly by instead of
using DATE, using XCOPY /D:date. Both refer to language settings
for validation/parsing, but I never like resetting the system
date or time.

Using XCOPY /D to validate dates proved to be tricky, especially
since the routine was intended to work in DOS 5+.
But the basic idea is to use each date in turn (in the /D: switch)
to perform a dummy copy operation. If the date is invalid
then XCOPY returns an ERRORLEVEL >= 1, if the date is valid
then XCOPY performs the dummy copy operation and returns ERRORLEVEL 0.
DOS5 and DOS6 both output errors from XCOPY to stderr, which
means that CTTY NUL needs to be used to silence it.

Apart from that the rest should be now straightforward;

----------------------------------------------------------------------
SET .=GOTO:LOP0>$.BAT
MD @@
CTTY NUL
%F% 2000-01-31 31-01-2000 01-31-2000 1 2 3
:LOP0
SHIFT
XCOPY /Y /D:%0 $.BAT @@
IF NOT !==!%4 IF ERRORLEVEL 1 GOTO LOP0
DEL @@\$.BAT
CTTY CON
RD @@
SET C=%3
----------------------------------------------------------------------

Originally the dummy copy operation was to copy $.BAT to $$.BAT,
however a problem occurred with this. When copying like this
with XCOPY you get a prompt asking whether the target is
a (d)irectory or a (f)ile. The original code went something like;

ECHO F|XCOPY /D:%0 $.BAT $$.BAT

This is all normal technique, however there is a big problem
that was until recently overlooked; it is only true in
US DOS that the prompt requires the letter "F" (for file),
in for example German DOS the letter is a "D" (for Datei).

At some time I may add a small piece on the history of the
XCOPY method itself, for the development has been interesting
(initial problems were caused by variations in different versions
of DOS). Anyhow, the dummy copy to a directory does not give a
prompt so this language problem is banished.

The %F%/%.% co-operation is my standard method for recurring a batch
program. If you've never seen it before you should be able
to understand it by simply tracing the program code. Until I have
a page dealing with this I shall put a brief explanation here.

+++++++++++++++++++

At the very beginning of the program is the sequence;

@ECHO OFF
%.%
SET F=%0

%F% now carries the filename of the batch program itself. This is
so that it can continue to call itself on demand even when
the name has been SHIFTed out of %0. The %.% on the second line
will be replaced with the contents of the variable %.% before
DOS tries to execute that line. The dot was very deliberately chosen
to be most unlikely to exist when the program starts, and it is
cleared upon exit.

Later, if the program wants to get "a" "b" "c" into the positional
parameters, it can do it very simply;

SET .=GOTO:NEXT1
%F% a b c
:NEXT1
ECHO %1 %2 %3

On hitting the %F% the program calls itself (this is a non-returning
call in DOS) and the %.% variable is set to GOTO:NEXT1.

So when it gets to the second line DOS replaces the variable;

GOTO:NEXT1          <---   %.% becomes

And continues on the next line from the original call. I really
_love_ doing this one; see how effortlessly it allows us to
set the position parameters and continue the program! The real
beauty of this technique is that it can be used for subroutines
as well.

The only real problem that occurs with this method is if the
program is terminated by the user, or by some error occurring.
In this case the %.% may remain set to "GOTO:somearbitarylabel",
if the user forgets to unset it (by typing SET .=) then the
next invocation of the program is going to go astray, control
flow immediately diverted to the middle of the program instead
of the initialisation. The only solution to this problem is
to simply remember to unset %.% in these cases (after it has
happened around 3 times one soon develops that habit !).

+++++++++++++++++++

After this key piece of code the worst of it is over,
the only thing to check before we continue is in case something
went wrong;

----------------------------------------------------------------------
IF ERRORLEVEL 1 ECHO ERROR: Unable to determine system date format
IF ERRORLEVEL 1 GOTO END
----------------------------------------------------------------------

If no date was passed then ERRORLEVEL will be >=1, so in this case
the program will terminate here with an error message. It should never
happen, but it's a reasonable safety belt.



~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                   Get system date into %_%                          
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Anyhow, the next thing to do is to actually get the system date.
We want the 8 important characters which are YYYY MM DD (in some
order, which we now know) with 2 seperators. This can be done as
follows;

----------------------------------------------------------------------
ECHO @PROMPT %F% $D$_>$.BAT
%COMSPEC% /C $.BAT>$$.BAT
SET .=GOTO:LOP1
$$.BAT
:LOP1
SHIFT
IF NOT !==!%1 GOTO LOP1
SET _=%0
----------------------------------------------------------------------

Once again you can see the program is calling itself with the
%F%/%.% mechanism (useful isn't it?). It uses this mechanism
to get the date from prompt into the positional parameters.
The code doesn't just assume that the day will be field 1, and
the date field 2, it SHIFTs until it finds the last field
and uses that as the date. The reason for this is that the author
has not tested under all different language versions of DOS, and
if one happens to use a delimitor character within the day name
(or the day name is 2 words) then the program would crash.

So now, with the date in %_% and the date format no. in %C%
the final step of extracting the information comes.


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
               Slashes in date hack                                  
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


The final step is frustrated by the particular quirks of the
slash "/" character. This character behaves differently in
DOS 6 and DOS 7 in FOR loops;

DOS 6;

FOR %%_ IN (01/01/2000) DO ECHO %%_

01
0
1
2
000

DOS 7;

FOR %%_ IN (01/01/2000) DO ECHO %%_

01
/01
/2000

It is also not possible to trim it from the first character
of a string using the double variable replacement method in
DOS 6 since;

FOR %%/ IN (BLAH) DO ECHO BLAH %%/

Simply produces a syntax error. The "/" character cannot be
used as a loop variable.

For these reasons the algorithm first translates any "/" in
the date string into a hyphen "-". Then processing will be
much easier in the final stage of the program.

---------------------------------------------------------------------
FOR %%_ IN (%_%) DO IF %%_==%_% GOTO K0                           (1)
SET .=GOTO:KU0                                                    (2)
FOR %%_ IN (%_%) DO CALL %F% %%_                                  (3)
:K0                                                               (4)
---------------------------------------------------------------------

Notice that the loop on line 1 will skip past the translation code in
the case that the date seperator is not a slash character. If it is
a dot or a hyphen the comparison will be TRUE, if it is a slash
the comparison will be FALSE and the code on lines (2) and (3) will
be executed; this is because in both DOS6 and DOS7 the slash character
will cause the date string %_% to be broken into a number of parts.

Now it is easiest if we look at what we are dealing with, this
is a "parallel" algorithm. For the 2 different functionalities
of DOS6 and DOS7 the same code finds the same result but in a
different manner.


The routine works in the same way irregardless of the actual
values in the date, but the format may be important, in general
you have;

    aa[aa]/bb/cc[cc]

Where a,b,c are digit groups, one of "a" or "c" being 4 digits
long, the others being 2 digits long.

The result of the loop at line (3) will be the following;

---------------------------------------------------------------------
DOS6                DOS7
---------------------------------------------------------------------
aa[aa]              aa[aa]          loop 1
b                   /bb             loop 2
b                   /cc[cc]         loop 3
c                                   loop 4
c[cc]                               loop 5
---------------------------------------------------------------------

"KU0" is set up to handle the first pass of the loop. In
both cases all that needs to be done is to initialise
the translated date variable and add the aa[aa] to it.

---------------------------------------------------------------------
:KU0  ---------- Auxillary code for slash reformat ------------------
SET _=
:KU1
SET _=%_%%1
SET .=GOTO:KU2
GOTO OUT
---------------------------------------------------------------------

To keep a low count on variables, and a good discipline in ones
mind regarding DOS syntax and grammar rules. I use %_% again
to create the translated date. Remember that after variable
substitution is performed on a line, the line is executed. In
the case of a FOR loop a further type of parameter replacement
happens on every iteration, however variable substitution only
occurs once before the loop has started. This is a common way
of keeping variable count down in DOS and also allows you to
do some interesting things.

After setting up %_% the handler routine is set up to "KU2" instead.
This is so that different rules can be used for the next pass of
the loop. Notice that entry at the point KU1 will cause a date
part to be appended to the date string. "OUT" is simply the exit
point at the very end of the program, allowing the handler to
return control to the calling loop.

---------------------------------------------------------------------
:KU2            (changes slashes to hyphens)
FOR %%_ IN (X%1) DO IF %%_==X GOTO KU3
SET _=%_%-%1
SET .=GOTO:KU1
GOTO OUT
:KU3
FOR %%"/" IN (-) DO SET _=%_%%%%1
GOTO OUT
---------------------------------------------------------------------

Now they have been together for a while but here is the time
for DOS7 to part from DOS6.

Notice that;

FOR %%_ IN (Xabcdef) DO IF %%_==X GOTO KU3

is always bound to fall through (to the DOS6 bit), this will
always happen in DOS6.

However;

FOR %%_ IN (X/abcdef) DO IF %%_==X GOTO KU3

Will jump immediately to "KU3", because in DOS7 the first iteration
will always be an X.

For the DOS7 part, the algorithm is quite clearly obvious.
For loop 2 and loop 3 we need to remove the "/" character
and append the digits to the date string with a hyphen seperator;

FOR %%"/" IN (-) DO SET _=%_%%%%1
GOTO OUT

For the DOS6 part we append the date part with a hyphen
seperator this time, however the handler is re-routed for the next
loop back to "KU1".  So on loop 3 the digit will be added with
no hyphen. Consequently "KU1" sets the handler back to "KU2".
You can see there is a little flip-flop going here. It will make
sure a hyphen is prepended right before loop 4 but not again
in loop 5, so that the date will be formatted properly but
now with hyphens instead of those dreaded slashes.


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
           Parse date string %_% into variables                      
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Now the final stage of the program ensues. We have the date
in %_% and it doesn't have slashes in it anymore. The format
of the date is in %C% so we know which digits belong to which
variables.

We are going to set these variables as indicated in the introduction;

YH=year high,
YL=year low,
MM=month,
DD=day

The method used to parse the string character by character is
overflowing the command line buffer to truncate the string and
get the first character. This method requires >1k of environment
space to work properly so the string parsing routine is run
in a sub-shell using the /E switch to COMMAND in order to enlarge
the environment approriately. (One always assumes that the system
is running the default environment size which is only 256 bytes).

A "mask" is created according to the date format that specifies
for each digit which variable it should be appended to. In the
mask the character "." means that the parser should skip that
character (it is a seperator). All variables YH,YL,MM and DD
are unset at the beginning in order that the first append operation
will work correctly.

---------------------------------------------------------------------
IF 1==%C% SET C=YH YH YL YL . MM MM . DD DD
IF 2==%C% SET C=DD DD . MM MM . YH YH YL YL
IF 3==%C% SET C=MM MM . DD DD . YH YH YL YL
FOR %%_ IN (%C%) DO SET %%_=
SET .=GOTO:K3>$.BAT
%COMSPEC% /E:4096 /C %F% %C%
CALL $.BAT
---------------------------------------------------------------------

You will notice that %C% is used doubly here. Once a match
of 1,2 or 3 is found with the date format no. the mask is set,
due to the nature of the mask it will fail to match subsequent
checks. Also notice that the order of the check #==%C% is important
otherwise when the mask is set in %C% a subsequent check %C%==#
would cause a syntax error due to the spaces now present in %C%.

Clearing the variables could I suppose have been done more clearly
with the line;

FOR %%_ IN (DD MM YH YL) DO SET %%_=

However it was done using the %C% in order that modification
of the variable names in the mask would not cause that line
to malfunction. Of course the %.% variable is reset here but
that doesn't matter because it is not in use and is set on
the very next line anyway (to the handler).

Because the string parser operates in a sub-shell it cannot
directly set the variables YH YL DD MM, so it creates a batch
file that will perform the SETting and is called upon completion
by the main program to do this.

The string parser;

---------------------------------------------------------------------
:K3   ---- Auxillary code for parsing date string into variables ----
SET T=                                                                %=%
SET  T=%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%
:LOP2
%T%SET C=%_%
FOR %%%C% IN (() DO SET _=%%%_%
FOR %%_ IN %_%) DO SET _=%%_
IF NOT !%1==!. ECHO SET %1=%%%1%%%C%>>$.BAT
SHIFT
IF NOT !%1==! GOTO LOP2
GOTO OUT
---------------------------------------------------------------------

%T% is set up with a large number of spaces. It is set in such a
way that the line;

%T%SET C=%_%

Will be truncated after variable substitution so that only the first
character of %_% will be in the line. Consequently %C% will be
SET with the first leftmost character of the date string %_%.

Now comes a trick;

FOR %%%C% IN (() DO SET _=%%%_%
FOR %%_ IN %_%) DO SET _=%%_

In both DOS6 and DOS7 this will delete the first character of
the date string %_%. Because the first character is %C% the
1st loop will evaluate to;

FOR %c IN (() DO SET _=%cMID(_,2)

Where 'c' is the first character of %_% and MID(_,2) are the rest.
So clearly the FOR loop itself will result in;

SET _=(MID(_,2)

Replacing the first character of the string %_% with a "(" bracket.

Now the next line simply removes that. Variable substitution gives;

FOR %_ IN (MID(_,2)) DO SET _=%_

Which will result in;

SET _=MID(_,2)

This trick has the benefit of working in both DOS6 and DOS7 (all of
the DOS6 material covered so far also applies to DOS5 incidently)
and only using internal commands and not requiring any temporary
files. Certainly a trick to remember.

Next the mask parameter is checked, if it is a dot "." then nothing
is done, the character was a seperator and we will just skip it
here. If it is not a dot then it must be a variable name. A line
is appended to the auxillary batch file being created that
will append the current character to the variable named in
the mask;

IF NOT !%1==!. ECHO SET %1=%%%1%%%C%>>$.BAT

The remaining lines are straightforward, a loop check that
stops looping when the mask runs out, then we return from
the sub-shell, the file $.BAT has now been created to set up
all the variables for the caller.

Well, apart from a little cleaning up that is it; the end.
The $.BAT file is called to set all the variables DD MM YH YL
then all working variables and files are deleted and the program
terminates. The program leaves the variables DD MM YH YL set
and they may be used by whatever other program called this one
(for it is basically intended as a utility batch program).


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Epilogue
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Final solution: 80 lines of code

DEBUG solution: 16 lines of code


Sure, we have just made a solution that is more than 5 times longer
than the sensible one. However, we have also covered a lot of
techniques, useful to batch programming, and there remain some
little problems which the material developed here will be able
to solve that the DEBUG solution can't (because it is specialized
to deal with the one task).

The DEBUG solution is presented in the article entitled;
"Getting the current date (DEBUG solution)".


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Get date into variables (DOS 5+, locale independent, pure batch)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@ECHO OFF
%.%
SET F=%0

:: ------------- Determine system date format -> %C% ----------------
SET .=GOTO:LOP0>$.BAT
MD @@
CTTY NUL
%F% 2000-01-31 31-01-2000 01-31-2000 1 2 3
:LOP0
SHIFT
XCOPY /Y /D:%0 $.BAT @@
IF NOT !==!%4 IF ERRORLEVEL 1 GOTO LOP0
DEL @@\$.BAT
CTTY CON
RD @@
SET C=%3
IF ERRORLEVEL 1 ECHO ERROR: Unable to determine system date format
IF ERRORLEVEL 1 GOTO END

:: --------------- Get system date -> %_% ---------------------------
ECHO @PROMPT %F% $D$_>$.BAT
%COMSPEC% /C $.BAT>$$.BAT
SET .=GOTO:LOP1
$$.BAT
:LOP1
SHIFT
IF NOT !==!%1 GOTO LOP1
SET _=%0

:: ------------ slashes in date hack => reformat --------------------
FOR %%_ IN (%_%) DO IF %%_==%_% GOTO K0
SET .=GOTO:KU0
FOR %%_ IN (%_%) DO CALL %F% %%_
:K0

:: --  Now parse date string %_% into variables:   ------------------
:: --  YH=year high, YL=year low, MM=month, DD=day ------------------
IF 1==%C% SET C=YH YH YL YL . MM MM . DD DD
IF 2==%C% SET C=DD DD . MM MM . YH YH YL YL
IF 3==%C% SET C=MM MM . DD DD . YH YH YL YL
FOR %%_ IN (%C%) DO SET %%_=
SET .=GOTO:K3>$.BAT
%COMSPEC% /E:4096 /C %F% %C%
CALL $.BAT

GOTO END

:K3   ---- Auxillary code for parsing date string into variables ----
SET T=                                                                %=%
SET  T=%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%
:LOP2
%T%SET C=%_%
FOR %%%C% IN (() DO SET _=%%%_%
FOR %%_ IN %_%) DO SET _=%%_
IF NOT !%1==!. ECHO SET %1=%%%1%%%C%>>$.BAT
SHIFT
IF NOT !%1==! GOTO LOP2
GOTO OUT

:KU0  ---------- Auxillary code for slash reformat ------------------
SET _=
:KU1
SET _=%_%%1
SET .=GOTO:KU2
GOTO OUT
:KU2            (changes slashes to hyphens)
FOR %%_ IN (X%1) DO IF %%_==X GOTO KU3
SET _=%_%-%1
SET .=GOTO:KU1
GOTO OUT
:KU3
FOR %%"/" IN (-) DO SET _=%_%%%%1
GOTO OUT

:END --------------- Clean-up and exit ------------------------------
FOR %%_ IN (. _ C F) DO SET %%_=
FOR %%_ IN ($.BAT $$.BAT) DO IF EXIST %%_ DEL %%_

:OUT --------------- Global exit point ------------------------------

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~