Geoff Chappell - Software Analyst

This function provides very particular assistance to a kernel extension for breaking a count of days since 1980 to a year, month and day. It is implemented in version 3.0 and higher.

The function uses registers for its input and output. As with any subfunction of int 2Fh function 12h, it also reads a word from above the interrupt frame as if to access an argument. This particular subfunction, however, does not interpret the word.

At its most general, the function works with three units of measure: large, medium and small, but with a catch. Every large unit is X medium units and is also Y small units, but the successive medium units in a large unit contain varying numbers of small units. The numbers of small units in the successive medium units in any large unit are represented by a conversion table.

ax | 121Dh |

cx | number of medium units |

dx | number < Y of small units in excess of whole large units |

ds:si | address of conversion table of small units in each of X successive medium units of a large unit |

The conversion table is an array of X bytes whose sum is Y.

The function normalises its inputs, adding to cx as many whole medium units ≤ X as can be extracted in sequence from the small units given as dx, and reducing dx either to zero or to the number of small units on the last, partial, medium unit:

ax | corrupt |

cx | number of medium units |

dx | number of small units in excess of whole medium units |

si | corrupt |

Before version 3.0, the kernel does not implement int 2Fh and has nothing to do with whatever happens from trying to call this function.

This function has its origins as an internal routine in the kernel. It is not quite ancient, being not in PC DOS 1.0. It is, however, in PC DOS 1.10 and it is named DSLIDE in the source code for MS-DOS 1.25 that Microsoft published in 2014 at the Computer History Museum and in 2018 on GitHub, There, in the kernel, called only from another internal routine named READTIME, the DSLIDE routine is an arguably efficient avoidance of repetition in a multi-part algorithm that reads from a device driver to get a count of days which it then inteprets as a date in the sense of year, month and day.

In the function’s expected use, the count of days is specifically since 1980 and is known not to extend as far as 2100. Conveniently, the intervening end-of-century year 2000 is a leap year for being a multiple of not just 100 but also of 400. The years from 1980 can therefore be reckoned in 4-year cycles of 1461 days each. A first call to the function thus has as its units:

- the large unit is a cycle of four years beginning with a leap year;
- the medium unit is an irregular half-year, so that the number of days in each half-year fits a byte;
- the small unit is the day.

Thus X is 8 and Y is 1461. For a second call, the units are:

- the large unit is the year;
- the medium unit is the month;
- the small unit is the day.

Now X is 12 and Y is either 365 or 366, depending on whether the year is ordinary or leap.

The expected sequence for use starts with dividing the count of days since 1980 by 1461 to get a quotient and remainder as preparation for calling int 2Fh function 121Dh. The remainder is immediately good for dx. Multiplying the quotient by eight gives a count of half-years to pass in cx. For ds:si pass the address of any eight bytes whose successive pairs total 366, 365, 365 and 365 such that the first byte in each pair is F. On return, divide cx by two to get the 0-based year since 1980. If there’s a remainder, add F to the count of excess days in dx. This is then ready for dx in a second call to int 2Fh function 121Dh. For this second call, pass 0 or 1 in cx to compute a 0-based or 1-based month. For ds:si pass the address of any twelve bytes that define the days in successive months, remembering to have 29 or 28 days for the second month depending on whether the computed year is or is not divisible by four. The second call then returns the month in cx and a 0-based day in dx.

Not actually required but almost certainly expected as the usual practice is that the tables addressed by ds:si will be in the kernel. The kernel has a days-per-half-year table in which F is 200. Published source code shows that this table is labelled YRTAB. The kernel has a days-per-month table labelled MONTAB. It’s the one table for both leap years and not. To adjust its second byte for the year, call int 2Fh function 111Bh. Note that MONTAB is not in the swappable data area whose address and size are returned through int 21h function 5D06h: this use of the kernel’s tables, with adjustment for leap years, assumes execution in a critical section.

That the interface allows, if not expects, to be fed the addresses of tables in the kernel has in practice required that these addresses were stablised long ago. Starting with version 3.10 and its introduction of a data version at offset 04h in the kernel’s data segment, the two tables are reliably at the following offsets into the kernel’s data:

Name | Offset If Data Version 0 | Offset If Data Version 1 |
---|---|---|

YRTAB | 0C7Ah | 0D14h |

MONTAB | 0C82h | 0D1Ch |