Friday, November 09, 2007

Shifting

Dos Date Time
Two's Compliment

I recently encountered this strange but simple bug in some code that i had written to convert from posix standard to dos standard time. It was strange becuase it only shows up when you use dates and times which are earlier than a certain magic number.

Dos uses a strange bit packed representation of date and time in two 16 bit words. Within each word the various values occupy a set number of bits as follows.


Date Format

0-4 Day of the month (1–31)
5-8 Month (1 = January, 2 = February, and so on)
9-15 Year offset from 1980 (add 1980 to get actual year)

Time Format
0-4 Second divided by 2
5-10 Minute (0–59)
11-15 Hour (0–23 on a 24-hour clock)


So assuming you have some simple integer representation of time and date, lets call it a Calendar you might use the following code to convert to and from the Dos format.



// Conversion to dos date and time
Calendar calendar;
short date = ((calendar.Year - 1980)<<9)
+ ((calendar.Month)<<5) + calendar.Day;
short time = ((calendar.Hour)<<11)
+ ((calendar.Minute)<<5) + (calendar.Second/2);

// Conversion from dos date and time
Calendar calendar;
calendar.Year = ((date>>9)&0x007F)+1980; // 7 bits
calendar.Month = ((date>>5)&0x000F); // 4 bits
calendar.Day = (date&0x001F); // 5 bits

calendar.Hour = ((time>>11)&0x001F); // 5 bits
calendar.Minute = ((time>>5)&0x003F); // 6 bits
calendar.Second = (time&0x001F)*2; // 5 bits




So wheres the bug ?

Well, the bug occurs when you use a year earlier than 1980, for example 1970. What happens is that thanks to the subtraction of 1980 from the year you end up with a negative number which is left shifted 9 bits into the 16 bit word.

Now usually when you do bit packing, its standard practice to mask the bits as you extract them by using an binary and after the right shift operation. This ensures that the value you get is isolated from the other bits, especially those further to the left since usually you right shift until zero.

In this case though the year is different from the other values in the date word. Becuase the year occupies the left most bits, its two's compliment representation as a negative number is preserved all the way through the right shift operation until the binary and, where it is suddenly forced into being a positive integer with the same 7 bits, changing it from a small negative to a large positive number.

The solution is to remove the binary and mask on the year. For all other values the mask is still necessary, but due to the weird although perhaps purposeful design of the dos date and time format, the year is special.