Monday, May 27, 2013

DETERMINING TIME DELTAS IN DOS SHELL

I ran into a bit of a tricky problem the other day and I wanted to share it with you.  I am using a program called ATRT (Automated Test and Re-Test) to test our software remotely, and we ran into a problem where two OCR (optical character recognition) extracted time stamp needs to be compared to find a time delta.  Unfortunately, in this version of software we are unable to convert the text time stamp back into a time object, and the software does not currently have a full math library (ex. Modulus function) or token splitting capabilities to determine the delta in-house.  Since it will be a at least a few weeks before we could get these features we decided to find a work-a-round; and, since ATRT can spawn child processes with custom arguments and read in the results we figured a simple Dos Shell Script was the way to go (note - powershell, unix tools, etc... will not be available on the target systems).

Simple idea, of course it's been a few years since I've been forced to use the Windows Command Prompt and the idiosyncracies can definitely lead to some frustrations.  It took about a minute to double check some syntax and write up the script and then to do the testing... first let's look at some code:

   REM > time_delta.bat <start> <stop>
   REM > time_delta.bat 2:03:04 2:05:06
   REM 122

   set MY_START=%1%
   set MY_STOP=%2%

   REM grab the hours, minutes and seconds from the start time fields 1 through 3
   REM grab the hours, minutes and seconds from the stop time  fields 4 through 6
   FOR /f "tokens=1-6 delims=:" %%a IN ( "%MY_START%:%MY_STOP%" ) DO (
     set H1=%%a
     set M1=%%b
     set S1=%%c
     set H2=%%d
     set M2=%%e
     set S2=%%f
     echo "%H2% %M2% %S2% - %H1% %M1% %S1%"
   )


I'm going to stop right here... I ran this from the command line about a dozen times with different arguments, and it didn't work.  The idea is really simple, place the timestamp into a string of colon delimited tokens and extract each of the fields and assign them sequentially to variables starting with %a% through %f% (six fields).  If I gave it the same arguments twice in a row it would be wrong the first time, and right each subsequent time.  If I ran from the command line the arguments would not be set.

Once I understood what was going on it was easy enough to fix.  Moving my echo outside of the loop caused it to behave properly... it took about an hour to finish up the script that I thought would take five minutes because of this behavior.  Convinced that there was a pointless bug in Dosshell I went off to consult the inter-webs to find out the logic behind this, what I discovered is a bit disturbing.

Dos Shell has something called "Delayed vs. Immediate Variable Expansion".  The default behavior is for the Shell interpreter to read in all of the lines between "(" and ")" at once and apply any variable expansion prior to executing any of these lines.  So if you are doing an IF/ELSE construct and have a series of assignments they will be expanded prior to evaluating the first expression:

   IF %H1% GTR %H2% (
      REM we have had a clock rollover since we started, add 24 hours
      set /a TEMP=%H2% + 24
      set /a HOUR2=%TEMP% * 3600
   )


In this example the TEMP variable will not be equal to "%H2% + 24", it will either be set to a previous value or not at all.  The disturbing thing is that Microsoft considers this a "feature" and not a "bug"; the documentation gives an example of how you could use a value stored in a variable and reset it to the original value all in one fell swoop... but since the value would be the same for both actions I don't see how this could ever be useful, or even do what they are saying cleanly.  The good news is that there are work-a-rounds... the bad news is that they are ugly.  The simplest method is to enable delayed expansion by setting an environment variable and then use alternate variable syntax (!VARIABLE!) in the instances where you want delayed expansion.  For me, I decided to leave my code as-is, and took a silent vow to avoid using Dos Shell in the future if any other alternative existed.

For completeness the code for the time delta is included below:

   @ECHO OFF
   REM > time_delta.bat <start> <stop>
   REM > time_delta.bat 2:03:04 2:05:06
   REM 122

   set MY_START=%1%
   set MY_STOP=%2%

   REM grab the hours, minutes and seconds from the start time fields 1 through 3
   REM grab the hours, minutes and seconds from the stop time  fields 4 through 6
   FOR /f "tokens=1-6 delims=:" %%a IN ( "%MY_START%:%MY_STOP%" ) DO (
     set H1=%%a
     set M1=%%b
     set S1=%%c
     set H2=%%d
     set M2=%%e
     set S2=%%f
   )

   REM convert the start time into raw seconds
   set /a MIN1=%M1% * 60
   set /a HOUR1=%H1% * 3600
   set /a TOTAL_1=%HOUR1% + %MIN1% + %S1%

   REM convert the stop time into raw seconds
   REM NOTE: the next line needs to be outside of the IF block in order to work due to
   REM       immediate variable expansion - variables are expanded when read, but the
   REM       commands are not executed until the ending brace is reached ")".  There
   REM       are other work-arounds for this, but none of them are clean.
   set /a TEMP=%H2% + 24

   IF %H1% GTR %H2% (
      REM we have had a clock rollover since we started, add 24 hours
      set /a MIN2=%M2% * 60
      set /a HOUR2=%TEMP% * 3600
   ) ELSE (
      REM no clock rollover, so convert directly into seconds
      set /a MIN2=%M2% * 60
      set /a HOUR2=%H2% * 3600
   )

   set /a TOTAL_2=%HOUR2% + %MIN2% + %S2%
   set /a DELTA=%TOTAL_2% - %TOTAL_1%
   echo %DELTA%



No comments:

Post a Comment