Category : C Source Code
Archive   : RCS55SRC.ZIP
Filename : PARTIME.C

 
Output of file : PARTIME.C contained in archive : RCS55SRC.ZIP
/*
* PARTIME parse date/time string into a TM structure
*
* Returns:
* 0 if parsing failed
* else time values in specified TM structure and zone (unspecified values
* set to TMNULL)
* Notes:
* This code is quasi-public; it may be used freely in like software.
* It is not to be sold, nor used in licensed software without
* permission of the author.
* For everyone's benefit, please report bugs and improvements!
* Copyright 1980 by Ken Harrenstien, SRI International.
* (ARPANET: KLH @ SRI)
*/

/* Hacknotes:
* If parsing changed so that no backup needed, could perhaps modify
* to use a FILE input stream. Need terminator, though.
* Perhaps should return 0 on success, else a non-zero error val?
*/

/* $Log: partime.c%v $
* Revision 1.2 1991/08/23 13:31:42 SGP
* Ported to MSDOS using Borland C++
*
* Revision 5.4 1990/10/04 06:30:15 eggert
* Remove date vs time heuristics that fail between 2000 and 2400.
* Check for overflow when lexing an integer.
* Parse 'Jan 10 LT' as 'Jan 10, LT', not 'Jan, 10 LT'.
*
* Revision 5.3 1990/09/24 18:56:31 eggert
* Update timezones.
*
* Revision 5.2 1990/09/04 08:02:16 eggert
* Don't parse two-digit years, because it won't work after 1999/12/31.
* Don't permit 'Aug Aug'.
*
* Revision 5.1 1990/08/29 07:13:49 eggert
* Be able to parse our own date format. Don't assume year<10000.
*
* Revision 5.0 1990/08/22 08:12:40 eggert
* Switch to GMT and fix the bugs exposed thereby. Update timezones.
* Ansify and Posixate. Fix peekahead and int-size bugs.
*
* Revision 1.4 89/05/01 14:48:46 narten
* fixed #ifdef DEBUG construct
*
* Revision 1.3 88/08/28 14:53:40 eggert
* Remove unportable "#endif XXX"s.
*
* Revision 1.2 87/03/27 14:21:53 jenkins
* Port to suns
*
* Revision 1.1 82/05/06 11:38:26 wft
* Initial revision
*
*/

#include "rcsbase.h"

libId(partId, "$Id: partime.c%v 1.2 1991/08/23 13:31:42 SGP Exp $")

#define given(v) (0 <= (v))
#define TMNULL (-1) /* Items not given are given this value */
#define TZ_OFFSET (24*60) /* TMNULL < zone_offset - TZ_OFFSET */

struct tmwent {
const char *went;
short wval;
char wflgs;
char wtype;
};
/* wflgs */
#define TWTIME 02 /* Word is a time value (absence implies date) */
#define TWDST 04 /* Word is a DST-type timezone */
/* wtype */
#define TM_MON 1 /* month name */
#define TM_WDAY 2 /* weekday name */
#define TM_ZON 3 /* time zone name */
#define TM_LT 4 /* local time */
#define TM_DST 5 /* daylight savings time */
#define TM_12 6 /* AM, PM, NOON, or MIDNIGHT */
/* wval (for wtype==TM_12) */
#define T12_AM 1
#define T12_PM 2
#define T12_NOON 12
#define T12_MIDNIGHT 0

static const struct tmwent tmwords [] = {
{"january", 0, 0, TM_MON},
{"february", 1, 0, TM_MON},
{"march", 2, 0, TM_MON},
{"april", 3, 0, TM_MON},
{"may", 4, 0, TM_MON},
{"june", 5, 0, TM_MON},
{"july", 6, 0, TM_MON},
{"august", 7, 0, TM_MON},
{"september", 8, 0, TM_MON},
{"october", 9, 0, TM_MON},
{"november", 10, 0, TM_MON},
{"december", 11, 0, TM_MON},

{"sunday", 0, 0, TM_WDAY},
{"monday", 1, 0, TM_WDAY},
{"tuesday", 2, 0, TM_WDAY},
{"wednesday", 3, 0, TM_WDAY},
{"thursday", 4, 0, TM_WDAY},
{"friday", 5, 0, TM_WDAY},
{"saturday", 6, 0, TM_WDAY},

{"gmt", 0*60, TWTIME, TM_ZON}, /* Greenwich */
{"utc", 0*60, TWTIME, TM_ZON},
{"ut", 0*60, TWTIME, TM_ZON},

{"nzst", -12*60, TWTIME, TM_ZON}, /* New Zealand */
{"jst", -9*60, TWTIME, TM_ZON}, /* Japan */
{"kst", -9*60, TWTIME, TM_ZON}, /* Korea */
{"ist", -5*60-30, TWTIME, TM_ZON},/* India */
{"eet", -2*60, TWTIME, TM_ZON}, /* Eastern Europe */
{"cet", -1*60, TWTIME, TM_ZON}, /* Central Europe */
{"met", -1*60, TWTIME, TM_ZON}, /* Middle Europe */
{"wet", 0*60, TWTIME, TM_ZON}, /* Western Europe */
{"nst", 3*60+30, TWTIME, TM_ZON},/* Newfoundland */
{"ast", 4*60, TWTIME, TM_ZON}, /* Atlantic */
{"est", 5*60, TWTIME, TM_ZON}, /* Eastern */
{"cst", 6*60, TWTIME, TM_ZON}, /* Central */
{"mst", 7*60, TWTIME, TM_ZON}, /* Mountain */
{"pst", 8*60, TWTIME, TM_ZON}, /* Pacific */
{"akst", 9*60, TWTIME, TM_ZON}, /* Alaska */
{"hast", 10*60, TWTIME, TM_ZON}, /* Hawaii-Aleutian */
{"hst", 10*60, TWTIME, TM_ZON}, /* Hawaii */
{"sst", 11*60, TWTIME, TM_ZON}, /* Samoa */

{"nzdt", -12*60, TWTIME+TWDST, TM_ZON}, /* New Zealand */
{"kdt", -9*60, TWTIME+TWDST, TM_ZON}, /* Korea */
{"bst", 0*60, TWTIME+TWDST, TM_ZON}, /* Britain */
{"ndt", 2*60+30, TWTIME+TWDST, TM_ZON}, /*Newfoundland (DDST)*/
{"adt", 4*60, TWTIME+TWDST, TM_ZON}, /* Atlantic */
{"edt", 5*60, TWTIME+TWDST, TM_ZON}, /* Eastern */
{"cdt", 6*60, TWTIME+TWDST, TM_ZON}, /* Central */
{"mdt", 7*60, TWTIME+TWDST, TM_ZON}, /* Mountain */
{"pdt", 8*60, TWTIME+TWDST, TM_ZON}, /* Pacific */
{"akdt", 9*60, TWTIME+TWDST, TM_ZON}, /* Alaska */
{"hadt", 10*60, TWTIME+TWDST, TM_ZON}, /* Hawaii-Aleutian */

#if 0
/*
* The following names are duplicates or are not well attested.
* A standard is needed.
*/
{"?st", -13*60, TWTIME, TM_ZON}, /* Uelen */
{"?st", -11*60, TWTIME, TM_ZON}, /* Magadan */
{"east", -10*60, TWTIME, TM_ZON}, /* Eastern Australia */
{"cast", -9*60-30, TWTIME, TM_ZON},/* Central Australia */
{"cst", -8*60, TWTIME, TM_ZON}, /* China */
{"hkt", -8*60, TWTIME, TM_ZON}, /* Hong Kong */
{"sst", -8*60, TWTIME, TM_ZON}, /* Singapore */
{"wast", -8*60, TWTIME, TM_ZON}, /* Western Australia */
{"?st", -7*60, TWTIME, TM_ZON}, /* Novosibirsk */
{"jt", -7*60-30, TWTIME, TM_ZON},/* Java */
{"nst", -6*60-30, TWTIME, TM_ZON},/* North Sumatra */
{"?st", -6*60, TWTIME, TM_ZON}, /* Tashkent */
{"?st", -5*60, TWTIME, TM_ZON}, /* Sverdlovsk */
{"?", -4*60-30, TWTIME, TM_ZON},/* Afghanistan */
{"?st", -4*60, TWTIME, TM_ZON}, /* Rostov */
{"it", -3*60-30, TWTIME, TM_ZON},/* Iran */
{"?st", -3*60, TWTIME, TM_ZON}, /* Moscow */
{"ist", -2*60, TWTIME, TM_ZON}, /* Israel */
{"ast", 1*60, TWTIME, TM_ZON}, /* Azores */
{"fst", 2*60, TWTIME, TM_ZON}, /* Fernando de Noronha */
{"bst", 3*60, TWTIME, TM_ZON}, /* Brazil */
{"wst", 4*60, TWTIME, TM_ZON}, /* Western Brazil */
{"ast", 5*60, TWTIME, TM_ZON}, /* Acre Brazil */
{"?", 9*60+30, TWTIME, TM_ZON},/* Marquesas */
{"?st", 12*60, TWTIME, TM_ZON}, /* Kwajalein */

{"?dt", -13*60, TWTIME+TWDST, TM_ZON}, /* Uelen */
{"?dt", -11*60, TWTIME+TWDST, TM_ZON}, /* Magadan */
{"eadt", -10*60, TWTIME+TWDST, TM_ZON}, /* Eastern Australia */
{"cadt", -9*60-30, TWTIME+TWDST, TM_ZON}, /* Central Australia */
{"cdt", -8*60, TWTIME+TWDST, TM_ZON}, /* China */
{"wadt", -8*60, TWTIME+TWDST, TM_ZON}, /* Western Australia */
{"?dt", -7*60, TWTIME+TWDST, TM_ZON}, /* Novosibirsk */
{"?dt", -6*60, TWTIME+TWDST, TM_ZON}, /* Tashkent */
{"?dt", -5*60, TWTIME+TWDST, TM_ZON}, /* Sverdlovsk */
{"?dt", -4*60, TWTIME+TWDST, TM_ZON}, /* Rostov */
{"?dt", -3*60, TWTIME+TWDST, TM_ZON}, /* Moscow */
{"idt", -2*60, TWTIME+TWDST, TM_ZON}, /* Israel */
{"eest", -2*60, TWTIME+TWDST, TM_ZON}, /* Eastern Europe */
{"cest", -1*60, TWTIME+TWDST, TM_ZON}, /* Central Europe */
{"mest", -1*60, TWTIME+TWDST, TM_ZON}, /* Middle Europe */
{"west", 0*60, TWTIME+TWDST, TM_ZON}, /* Western Europe */
{"adt", 1*60, TWTIME+TWDST, TM_ZON}, /* Azores */
{"fdt", 2*60, TWTIME+TWDST, TM_ZON}, /* Fernando de Noronha */
{"edt", 3*60, TWTIME+TWDST, TM_ZON}, /* Eastern Brazil */
{"wdt", 4*60, TWTIME+TWDST, TM_ZON}, /* Western Brazil */
{"adt", 5*60, TWTIME+TWDST, TM_ZON}, /* Acre Brazil */
#endif

{"lt", 0, TWTIME, TM_LT}, /* local time */
{"dst", 1*60, TWTIME, TM_DST}, /* daylight savings time */
{"ddst", 2*60, TWTIME, TM_DST}, /* double dst */

{"am", T12_AM, TWTIME, TM_12},
{"pm", T12_PM, TWTIME, TM_12},
{"noon", T12_NOON, TWTIME, TM_12},
{"midnight", T12_MIDNIGHT, TWTIME, TM_12},

{0, 0, 0, 0}, /* Zero entry to terminate searches */
};

struct token {
const char *tcp;/* pointer to string */
int tcnt; /* # chars */
char tbrk; /* "break" char */
char tbrkl; /* last break char */
char tflg; /* 0 = alpha, 1 = numeric */
union { /* Resulting value; */
int tnum;/* either a #, or */
const struct tmwent *ttmw;/* ptr to a tmwent. */
} tval;
};

static const struct tmwent*ptmatchstr P((const char*,int,const struct tmwent*));
static int pt12hack P((struct tm *,int));
static int ptitoken P((struct token *));
static int ptstash P((int *,int));
static int pttoken P((struct token *));

static int
goodzone(register const struct token *t, int offset, int *am)
{
register int m;
if (
t->tflg &&
t->tcnt == 4+offset &&
(m = t->tval.tnum) <= 2400 &&
isdigit(t->tcp[offset]) &&
(m%=100) < 60
) {
m += t->tval.tnum/100 * 60;
if (t->tcp[offset-1]=='+')
m = -m;
*am = m;
return 1;
}
return 0;
}

int
partime(const char *astr, register struct tm *atm, int *zone)
{
register int i;
struct token btoken, atoken;
int zone_offset; /* minutes west of GMT, plus TZ_OFFSET */
register const char *cp;
register char ch;
int ord, midnoon;
int *atmfield, dst, m;
int got1 = 0;

atm->tm_sec = TMNULL;
atm->tm_min = TMNULL;
atm->tm_hour = TMNULL;
atm->tm_mday = TMNULL;
atm->tm_mon = TMNULL;
atm->tm_year = TMNULL;
atm->tm_wday = TMNULL;
atm->tm_yday = TMNULL;
midnoon = TMNULL; /* and our own temp stuff */
zone_offset = TMNULL;
dst = TMNULL;
btoken.tcnt = btoken.tbrk = 0;
btoken.tcp = astr;

for (;; got1=1) {
if (!ptitoken(&btoken)) /* Get a token */
{ if(btoken.tval.tnum) return(0); /* Read error? */
if (given(midnoon)) /* EOF, wrap up */
if (!pt12hack(atm, midnoon))
return 0;
if (!given(atm->tm_min))
atm->tm_min = 0;
*zone =
(given(zone_offset) ? zone_offset-TZ_OFFSET : 0)
- (given(dst) ? dst : 0);
return got1;
}
if(btoken.tflg == 0) /* Alpha? */
{ i = btoken.tval.ttmw->wval;
switch (btoken.tval.ttmw->wtype) {
default:
return 0;
case TM_MON:
atmfield = &atm->tm_mon;
break;
case TM_WDAY:
atmfield = &atm->tm_wday;
break;
case TM_DST:
atmfield = &dst;
break;
case TM_LT:
if (ptstash(&dst, 0))
return 0;
i = 48*60; /* local time magic number -- see maketime() */
/* fall into */
case TM_ZON:
i += TZ_OFFSET;
if (btoken.tval.ttmw->wflgs & TWDST)
if (ptstash(&dst, 60))
return 0;
/* Peek ahead for offset immediately afterwards. */
if (
(btoken.tbrk=='-' || btoken.tbrk=='+') &&
(atoken=btoken, ++atoken.tcnt, ptitoken(&atoken)) &&
goodzone(&atoken, 0, &m)
) {
i += m;
btoken = atoken;
}
atmfield = &zone_offset;
break;
case TM_12:
atmfield = &midnoon;
}
if (ptstash(atmfield, i))
return(0); /* ERR: val already set */
continue;
}

/* Token is number. Lots of hairy heuristics. */
if (!isdigit(*btoken.tcp)) {
if (!goodzone(&btoken, 1, &m))
return 0;
zone_offset = TZ_OFFSET + m;
continue;
}

i = btoken.tval.tnum; /* Value now known to be valid; get it. */

if (btoken.tcnt == 3) /* 3 digits = HMM */
{
hhmm4: if (ptstash(&atm->tm_min, i%100))
return(0); /* ERR: min conflict */
i /= 100;
hh2: if (ptstash(&atm->tm_hour, i))
return(0); /* ERR: hour conflict */
continue;
}

if (4 < btoken.tcnt)
goto year4; /* far in the future */
if(btoken.tcnt == 4) /* 4 digits = YEAR or HHMM */
{ if (given(atm->tm_year)) goto hhmm4; /* Already got yr? */
if (given(atm->tm_hour)) goto year4; /* Already got hr? */
if(btoken.tbrk == ':') /* HHMM:SS ? */
if ( ptstash(&atm->tm_hour, i/100)
|| ptstash(&atm->tm_min, i%100))
return(0); /* ERR: hr/min clash */
else goto coltm2; /* Go handle SS */
if(btoken.tbrk != ',' && btoken.tbrk != '/'
&& (atoken=btoken, ptitoken(&atoken)) /* Peek */
&& ( atoken.tflg
? !isdigit(*atoken.tcp)
: atoken.tval.ttmw->wflgs & TWTIME)) /* HHMM-ZON */
goto hhmm4;
goto year4; /* Give up, assume year. */
}

/* From this point on, assume tcnt == 1 or 2 */
/* 2 digits = MM, DD, or HH (MM and SS caught at coltime) */
if(btoken.tbrk == ':') /* HH:MM[:SS] */
goto coltime; /* must be part of time. */
if (31 < i)
return 0;

/* Check for numerical-format date */
for (cp = "/-."; ch = *cp++;)
{ ord = (ch == '.' ? 0 : 1); /* n/m = D/M or M/D */
if(btoken.tbrk == ch) /* "NN-" */
{ if(btoken.tbrkl != ch)
{
atoken = btoken;
atoken.tcnt++;
if (ptitoken(&atoken)
&& atoken.tflg == 0
&& atoken.tval.ttmw->wtype == TM_MON)
goto dd2;
if(ord)goto mm2; else goto dd2; /* "NN-" */
} /* "-NN-" */
if (!given(atm->tm_mday)
&& given(atm->tm_year)) /* If "YYYY-NN-" */
goto mm2; /* then always MM */
if(ord)goto dd2; else goto mm2;
}
if(btoken.tbrkl == ch /* "-NN" */
&& given(ord ? atm->tm_mon : atm->tm_mday))
if (!given(ord ? atm->tm_mday : atm->tm_mon)) /* MM/DD */
if(ord)goto dd2; else goto mm2;
}

/* Now reduced to choice between HH and DD */
if (given(atm->tm_hour)) goto dd2; /* Have hour? Assume day. */
if (given(atm->tm_mday)) goto hh2; /* Have day? Assume hour. */
if (given(atm->tm_mon)) goto dd2; /* Have month? Assume day. */
if(i > 24) goto dd2; /* Impossible HH means DD */
atoken = btoken;
if (!ptitoken(&atoken)) /* Read ahead! */
if(atoken.tval.tnum) return(0); /* ERR: bad token */
else goto dd2; /* EOF, assume day. */
if ( atoken.tflg
? !isdigit(*atoken.tcp)
: atoken.tval.ttmw->wflgs & TWTIME)
/* If next token is a time spec, assume hour */
goto hh2; /* e.g. "3 PM", "11-EDT" */

dd2: if (ptstash(&atm->tm_mday, i)) /* Store day (1 based) */
return(0);
continue;

mm2: if (ptstash(&atm->tm_mon, i-1)) /* Store month (make zero based) */
return(0);
continue;

year4: if ((i-=1900) < 0 || ptstash(&atm->tm_year, i)) /* Store year-1900 */
return(0); /* ERR: year conflict */
continue;

/* Hack HH:MM[[:]SS] */
coltime:
if (ptstash(&atm->tm_hour, i)) return 0;
if (!ptitoken(&btoken))
return(!btoken.tval.tnum);
if(!btoken.tflg) return(0); /* ERR: HH: */
if(btoken.tcnt == 4) /* MMSS */
if (ptstash(&atm->tm_min, btoken.tval.tnum/100)
|| ptstash(&atm->tm_sec, btoken.tval.tnum%100))
return(0);
else continue;
if(btoken.tcnt != 2
|| ptstash(&atm->tm_min, btoken.tval.tnum))
return(0); /* ERR: MM bad */
if (btoken.tbrk != ':') continue; /* Seconds follow? */
coltm2: if (!ptitoken(&btoken))
return(!btoken.tval.tnum);
if(!btoken.tflg || btoken.tcnt != 2 /* Verify SS */
|| ptstash(&atm->tm_sec, btoken.tval.tnum))
return(0); /* ERR: SS bad */
}
}

/* Store date/time value, return 0 if successful.
* Fail if entry is already set.
*/
static int
ptstash(int *adr,int val)
{ register int *a;
if (given(*(a=adr)))
return 1;
*a = val;
return(0);
}

/* This subroutine is invoked for AM, PM, NOON and MIDNIGHT when wrapping up
* just prior to returning from partime.
*/
static int
pt12hack(register struct tm *tm, register int aval)
{ register int h = tm->tm_hour;
switch (aval) {
case T12_AM:
case T12_PM:
if (h > 12)
return 0;
if (h == 12)
tm->tm_hour = 0;
if (aval == T12_PM)
tm->tm_hour += 12;
break;
default:
if (0 < tm->tm_min || 0 < tm->tm_sec)
return 0;
if (!given(h) || h==12)
tm->tm_hour = aval;
else if (aval==T12_MIDNIGHT && (h==0 || h==24))
return 0;
}
return 1;
}

/* Get a token and identify it to some degree.
* Returns 0 on failure; token.tval will be 0 for normal EOF, otherwise
* hit error of some sort
*/

static int
ptitoken(register struct token *tkp)
{
register const char *cp;
register int i, j, k;

if (!pttoken(tkp))
#ifdef DEBUG
{
VOID printf("EOF\n");
return(0);
}
#else
return(0);
#endif
cp = tkp->tcp;

#ifdef DEBUG
VOID printf("Token: \"%.*s\" ", tkp->tcnt, cp);
#endif

if (tkp->tflg) {
i = tkp->tcnt;
if (*cp == '+' || *cp == '-') {
cp++;
i--;
}
while (0 <= --i) {
j = tkp->tval.tnum*10;
k = j + (*cp++ - '0');
if (j/10 != tkp->tval.tnum || k < j) {
/* arithmetic overflow */
tkp->tval.tnum = 1;
return 0;
}
tkp->tval.tnum = k;
}
} else if (!(tkp->tval.ttmw = ptmatchstr(cp, tkp->tcnt, tmwords)))
{
#ifdef DEBUG
VOID printf("Not found!\n");
#endif
tkp->tval.tnum = 1;
return 0;
}

#ifdef DEBUG
if(tkp->tflg)
VOID printf("Val: %d.\n",tkp->tval.tnum);
else VOID printf("Found: \"%s\", val: %d, type %d\n",
tkp->tval.ttmw->went,tkp->tval.ttmw->wval,tkp->tval.ttmw->wtype);
#endif

return(1);
}

/* Read token from input string into token structure */
static int
pttoken(register struct token *tkp)
{
register const char *cp;
register int c;
const char *astr;

tkp->tcp = astr = cp = tkp->tcp + tkp->tcnt;
tkp->tbrkl = tkp->tbrk; /* Set "last break" */
tkp->tcnt = tkp->tbrk = tkp->tflg = 0;
tkp->tval.tnum = 0;

while(c = *cp++)
{ switch(c)
{ case ' ': case '\t': /* Flush all whitespace */
case '\r': case '\n':
case '\v': case '\f':
if (!tkp->tcnt) { /* If no token yet */
tkp->tcp = cp; /* ignore the brk */
continue; /* and go on. */
}
/* fall into */
case '(': case ')': /* Perhaps any non-alphanum */
case '-': case ',': /* shd qualify as break? */
case '+':
case '/': case ':': case '.': /* Break chars */
if(tkp->tcnt == 0) /* If no token yet */
{ tkp->tcp = cp; /* ignore the brk */
tkp->tbrkl = c;
continue; /* and go on. */
}
tkp->tbrk = c;
return(tkp->tcnt);
}
if (!tkp->tcnt++) { /* If first char of token, */
if (isdigit(c)) {
tkp->tflg = 1;
if (astr /* timezone is break+sign+digit */
tkp->tcp--;
tkp->tcnt++;
}
}
} else if ((isdigit(c)!=0) != tkp->tflg) { /* else check type */
tkp->tbrk = c;
return --tkp->tcnt; /* Wrong type, back up */
}
}
return(tkp->tcnt); /* When hit EOF */
}


static const struct tmwent *
ptmatchstr(const char *astr,int cnt,const struct tmwent *astruc)
{
register const char *cp, *mp;
register int c;
const struct tmwent *lastptr;
int i;

lastptr = 0;
for(;mp = astruc->went; astruc += 1)
{ cp = astr;
for(i = cnt; i > 0; i--)
{
switch (*cp++ - (c = *mp++))
{ case 0: continue; /* Exact match */
case 'A'-'a':
if (ctab[c] == Letter)
continue;
}
break;
}
if(i==0)
if (!*mp) return astruc; /* Exact match */
else if(lastptr) return(0); /* Ambiguous */
else lastptr = astruc; /* 1st ambig */
}
return lastptr;
}


  3 Responses to “Category : C Source Code
Archive   : RCS55SRC.ZIP
Filename : PARTIME.C

  1. Very nice! Thank you for this wonderful archive. I wonder why I found it only now. Long live the BBS file archives!

  2. This is so awesome! 😀 I’d be cool if you could download an entire archive of this at once, though.

  3. But one thing that puzzles me is the “mtswslnkmcjklsdlsbdmMICROSOFT” string. There is an article about it here. It is definitely worth a read: http://www.os2museum.com/wp/mtswslnk/