/*
 * units -- do multiplicative unit conversions
 * td 80.09.04
 * Modified to keep the intermediate format in a file and
 * update it automatically when it has changed.
 *
 * Modified for Decus C 82-05-12 MM.
 */

/*)BUILD	$(PROGRAM)	= units
		$(FILES)	= { units atof }
		$(DTOA)		= 1
		$(TKBOPTIONS) = {
			TASK	= ...UNI
		}
*/

#ifdef	DOCUMENTATION

title	units	Units Conversion
index		Convert between various units

synopsis

	units [-u[d]]

description

	Units converts between various scales to their equivalents in
	other scales.  It works interactively:
	.s.nf
		You have: inches
		You want: cm
		* 2.54
		/ 3.93701
	.s.f
	Quantities are specified as multiplicative combinations, optionally
	preceeded by a numeric multiplier.  Powers are indicated by suffixed
	positive integers, division by '/'.
	.s
	Units does only multiplicative conversions;  it cannot convert,
	e.g., Farenheit to Celsius.
	.s
	The units conversion table is stored in binary format.  If it
	cannot be found, or if units is invoked using the '-u' switch,
	a binary table will be built, slowly, from the source table.
	"-ud" causes each input line to be dumped when building tables.

	The tables are found using a search list.  Be sure to copy the
	units.txt file from the distribution kit to a place where it
	can be found by the search list.

diagnostics

	.lm +8
	.s.i -8;Conformability
	.s
	The measures are not interconvertable, for example, you cannot
	convert inches to pounds.
	.lm -8

author

	David Conroy

bugs

	This version does not understand about file dates.  The only
	way to rebuild the binary data file is to use the -u switch
	(or to delete all versions of units.dat).

	It would be nice to have additive conversions, such as
	Farenheit to Celsius.

#endif


#include <stdio.h>
#define	FALSE	0
#define	TRUE	1
#define	EOS	0

#ifdef vms
#include		<ssdef.h>
#include		<stsdef.h>
#define	IO_SUCCESS	(SS$_NORMAL | STS$M_INHIB_MSG)
#define	IO_ERROR	SS$_ABORT
#endif
/*
 * Note: IO_SUCCESS and IO_ERROR are defined in the Decus C stdio.h file
 */
#ifndef	IO_SUCCESS
#define	IO_SUCCESS	0
#endif
#ifndef	IO_ERROR
#define	IO_ERROR	1
#endif

#ifndef unix
#define unix
#endif

#ifdef	vms
#undef	unix
#endif
#ifdef	decus
int	$$narg = 1;
#define	BINARY_READ	"rn"
#define	BINARY_WRITE	"wn"
#undef	unix
#else
#define	BINARY_READ	"r"
#define	BINARY_WRITE	"w"
#endif

#define NDIM    12
#define NUNITS  900
#define NBUF    256             /* Length of longest line */
#define UMAGIC  0123456         /* Magic number written in binary header */

/*
 * Header for the units file.
 * This  tells that it is the real
 * thing and how much to read in.
 */

typedef struct  HDR {
        unsigned h_magic;
        unsigned h_nunits;
        unsigned h_ssize;               /* String space size */
}       HDR;

HDR     hdr;

typedef struct  UNIT {
        char    *u_name;
        double  u_val;
        char    u_dim[NDIM];
}       UNIT;

UNIT    *units;

#ifdef	unix
#define	UNITS_FILE	"/usr/lib/units"
#define BINARY_FILE	"/usr/lib/binunits"
#else
#define	UNITS_FILE	"units.txt"	/* Source file			*/
#define	BINARY_FILE	"units.dat"	/* Compiled file		*/
char    buf[NBUF];
#endif
#ifndef decus
char    inbuf[BUFSIZ];
#endif

int nunits;
int     uflag;                  /* Update information only */
int	debug;
FILE *fd;
int peekc = EOF;
int lastc = EOF;
int lineno = 1;

#ifndef unix
char	*searchlist[] = {
	"",				/* Current directory -- first	*/
	"bin:",
	"pub:",
	"games:",
	"c:",
	"lb:",
	"lb:[1,1]",			/* RSX				*/
	"lb:[1,5]",			/* P/OS				*/
	"appl$dir:",			/* P/OS				*/
	NULL,
};
#endif

extern char    *get_unitname();
extern char    *prefix();
extern char    *myalloc();
extern double	atof();

main(argc, argv)
int argc;
char *argv[];
{
        UNIT have, want;
        register int i;
	char		*ap;

#ifdef vms
	argc = getredirection(argc, argv);
#endif
#ifndef	decus
	if (isatty(fileno(stdout)))
	    setbuf(stdout, NULL);
	if (isatty(fileno(stderr)))
	    setbuf(stderr, NULL);
#endif
        if (argc > 1 && argv[1][0] == '-') {
	    ap = &argv[1][1];
	    while (*ap != 0) {
		switch (*ap++) {
		case 'u': case 'U':
		    uflag++;
		    fprintf(stderr, "Rebuild requested\n");
		    break;
		case 'd': case 'D':
		    debug++;
		    fprintf(stderr, "Debug requested\n");
		    break;
		default:
		    fprintf(stderr, "Funny arg: '%c'\n", ap[-1]);
		    break;
		}
	    }
	}
        init();
#ifndef	decus
        setbuf(fd = stdin, inbuf);
#else
	fd = stdin;
#endif
Again:
        if (!getunit(&have, "You have: ")
        || !getunit(&want, "You want: "))
                exit(IO_SUCCESS);
        for (i=0; i!=NDIM; i++)
                if (have.u_dim[i] != want.u_dim[i]) {
                        printf("Conformability\n");
                        punit(&have);
                        punit(&want);
                        goto Again;
                }
	printf("* %g\n", have.u_val / want.u_val);
        printf("/ %g\n", want.u_val / have.u_val);
        goto Again;
}

/*
 * Initialise by reading in units information
 * either in binary or ascii form and updating
 * the binary information.
 */
init()
{
        if (uflag || !binary())
                update();
        printf("%d units\n", nunits);
        peekc = EOF;

#ifdef	unix
        /*
         * Throw away super-user information
         */
        setuid(getuid());
#endif
}

/*
 * Attempt to read in the already-stored
 * binary information.  Return non-zero if
 * successful.
 */
binary()
{
#ifdef unix
        register char *sstart;
        register int n;
        register int bfd;
        time_t timeasc;

        if (stat(UNITS_FILE, &sb) < 0)
                return (0);
        timeasc = sb.st_mtime;
        if ((bfd = open(BINARY_FILE, 0))<0 || fstat(bfd, &sb)<0)
                return (0);
        if (timeasc > sb.st_mtime)      /* Out of date? */
                goto bad1;
        if (read(bfd, &hdr, sizeof(hdr)) != sizeof(hdr))
                goto bad1;
        if (hdr.h_magic != UMAGIC)
                goto bad1;
        nunits = hdr.h_nunits;
        sstart = myalloc(hdr.h_ssize);
        if (read(bfd, sstart, hdr.h_ssize) != hdr.h_ssize)
                goto bad;
        units = (UNIT *)myalloc(n = nunits*sizeof(UNIT));
        if (read(bfd, units, n) != n)
                goto bad;
        for (n=0; n!=nunits; n++)
                units[n].u_name += (long)sstart;
        close(bfd);
        return (1);
bad:
        brk(sstart);
bad1:
        close(bfd);
        return (0);
#else
	/*
	 * Binary readin for Decus C or VMS
	 */
        register char		*sstart;
        register int		n;
        register FILE		*bfd;

	for (n = 0; searchlist[n] != NULL; n++) {
		strcpy(buf, searchlist[n]);
		strcat(buf, BINARY_FILE);
		if ((bfd = fopen(buf, BINARY_READ)) != NULL)
			break;
	}
	if (bfd == NULL)
		return (FALSE);
	printf("(reading pre-compiled units table from \"%s\")\n", buf);
	if ((n = fread(&hdr, sizeof(hdr), 1, bfd)) != 1) {
		perror(buf);
		fprintf(stderr, "Can't read %d bytes header\n", sizeof (hdr));
		goto bad1;
	}
        if (hdr.h_magic != UMAGIC) {
		fprintf(stderr, "Header flag 0x%X should be 0x%X\n",
			hdr.h_magic, UMAGIC);
                goto bad1;
	}
        nunits = hdr.h_nunits;
        sstart = malloc(hdr.h_ssize);
	units = NULL;
        if (fread(sstart, hdr.h_ssize, 1, bfd) != 1) {
		perror(buf);
		fprintf(stderr, "Can't read %d bytes units headers\n",
			hdr.h_ssize);
                goto bad;
	}
        units = (UNIT *)malloc(n = nunits*sizeof(UNIT));
        if (fread(units, n, 1, bfd) != 1) {
		perror(buf);
		fprintf(stderr, "Can't read %d bytes units data\n", n);
                goto bad;
	}
        for (n=0; n!=nunits; n++)
                units[n].u_name += (long)sstart;
        fclose(bfd);
        return (TRUE);
bad:
	free(sstart);
	if (units != NULL)
		free(units);
bad1:
        fclose(bfd);
        return (FALSE);
#endif
}
/*
 * Update units information by reading the
 * units file.
 */
update()
{
        register char *name;
        register int i;
        register char *sstart, *send;
#ifndef unix
	FILE		*bfd;
	int		temp;
#else
        int		bfd;
#endif


#ifdef	decus
	for (i = 0; searchlist[i] != NULL; i++) {
		strcpy(buf, searchlist[i]);
		strcat(buf, UNITS_FILE);
		if ((fd = fopen(buf, "r")) != NULL)
			break;
	}
	if (fd == NULL) {
		perror(UNITS_FILE);
		exit(IO_ERROR);
	}
	printf("Building units table from \"%s\"\n", buf);
#else
        if ((fd = fopen(UNITS_FILE, "r")) == NULL) {
		perror(UNITS_FILE);
		exit(IO_ERROR);
	}
	printf("Building units table from \"%s\"\n", UNITS_FILE);
	setbuf(fd, inbuf);
#endif
        units = (UNIT *)myalloc(NUNITS*sizeof(UNIT));
        sstart = (char *)sbrk(0);
        for (nunits=0; nunits!=NUNITS; nunits++) {
                name = get_unitname();
		if (debug)
		    printf("%s = ", name);
                for (i=0; i!=nunits; i++)
                        if (strcmp(units[i].u_name, name) == 0)
                                fprintf(stderr, "`%s' redefined, line %d\n",
                                        name, lineno);
                units[nunits].u_name = name;
                if (!getunit(&units[nunits], NULL))
                        break;
		if (debug)
		    printf("%g\n", units[nunits].u_val);
        }
        send = (char *)sbrk(0);
        if (!feof(fd)) {
		fprintf(stderr, "Too many units\n");
		exit(IO_ERROR);
	}
        fclose(fd);
        /*
         * Write out, if possible, binary
         * information for faster response next time.
         */
#ifndef unix
	if ((bfd = fopen(BINARY_FILE, BINARY_WRITE)) != NULL) {
		printf("Creating compiled units table on \"%s\"\n",
		    BINARY_FILE);
		hdr.h_magic = UMAGIC;
		hdr.h_nunits = nunits;
		hdr.h_ssize = send - sstart;
		if (fwrite(&hdr, sizeof (hdr), 1, bfd) != 1)
			goto bad;
		if (fwrite(sstart, hdr.h_ssize, 1, bfd) != 1)
			goto bad;
                for (i = 0; i!=nunits; i++)
                        units[i].u_name -= (long)sstart;/* Rel. address */
                temp = fwrite(units, nunits*sizeof(UNIT), 1, bfd);
                for (i=0; i!=nunits; i++)
                        units[i].u_name += (long)sstart;
		if (temp == 1)
			return;
bad:		printf("Couldn't create \"%s\"\n", BINARY_FILE);
		fclose(bfd);
	}
#else
        if ((bfd = creat(BINARY_FILE, 0644)) >= 0) {
                hdr.h_magic = UMAGIC;
                hdr.h_nunits = nunits;
                hdr.h_ssize = send-sstart;
                if (write(bfd, &hdr, sizeof(hdr)) != sizeof(hdr))
                        goto bad;
                if (write(bfd, sstart, hdr.h_ssize) != hdr.h_ssize)
                        goto bad;
                for (i=0; i!=nunits; i++)
                        units[i].u_name -= (long)sstart;/* Rel. address */
                write(bfd, units, nunits*sizeof(UNIT));
                for (i=0; i!=nunits; i++)
                        units[i].u_name += (long)sstart;
        bad:
                close(bfd);
        }
#endif
}

nextc()
{
        register int c;

        if (peekc != EOF) {
                c = peekc;
                peekc = EOF;
                return (c);
        }
        if (lastc == '\n')
                lineno++;
        lastc = getc(fd);
#ifdef DEBUG
	putchar(lastc);
#endif
        if (lastc == '#') {     /* Eat a comment */
                do {
                        lastc = getc(fd);
#ifdef DEBUG
			if (lastc != EOF)
				putchar(lastc);
#endif
                } while(lastc!='\n' && lastc!=EOF);
        }
        return (lastc);
}

char *
get_unitname()
{
        register char *s, *t;
        register int c;
        register char *v;

        do {
                c = nextc();
        } while(c==' ' || c=='\n' || c=='\t');
        s = buf;
        while(c!=' ' && c!='\t' && c!='\n' && c!=EOF) {
                *s++ = c;
                c = nextc();
        }
        *s = '\0';
        peekc = c;
        v = t = myalloc(strlen(buf)+1);
        s = buf;
        while (*t++ = *s++)
                ;
        return (v);
}

punit(u)
register UNIT *u;
{
        register int i;

        printf("%g", u->u_val);
        for (i=0; i!=NDIM; i++)
                if (u->u_dim[i] == 1)
                        printf(" %s", units[i].u_name);
                else if (u->u_dim[i] > 0)
                        printf(" %s+%d", units[i].u_name, u->u_dim[i]);
                else if (u->u_dim[i] < 0)
                        printf(" %s-%d", units[i].u_name, -u->u_dim[i]);
        printf("\n");
}

double
ipow(d, n)
double d;
{
        double v;

        v = 1.;
        if (n < 0) {
                d = 1./d;
                n = -n;
        }
        while (n) {
                v *= d;
                --n;
        }
        return (v);
}

struct{
        char *prefix;
        double factor;
}pre[]={
"femto",        1e-15,
"pico",         1e-12,
"nano",         1e-9,
"micro",        1e-6,
"milli",        1e-3,
"centi",        1e-2,
"deci",         1e-1,
"hemi",         .5,
"demi",         .5,
"semi",         .5,
"sesqui",       1.5,
"deka",         1e1,
"hecto",        1e2,
"hekto",        1e2,    /* common (?) misspelling */
"kilo",         1e3,
"myria",        1e5,
"mega",         1e6,
"giga",         1e9,
"tera",         1e12,
NULL,           1.
};
/*
 * Return the string stripped of its
 * prefix (if any).  Set factor
 * to the multiplicative factor indicated by
 * the prefix found.
 */
char *
prefix(str, factor)
char *str;
double *factor;
{
        register char *s, *t;
        register int i;

        for (i=0; pre[i].prefix!=NULL; i++) {
                s = pre[i].prefix;
                t = str;
                while (*s != '\0')
                        if (*s++ != *t++)
                                break;
                if (*s == '\0') {
                        *factor = *factor * pre[i].factor;
                        return (t);
                }
        }
        return(NULL);
}

getunit(u, prompt)
UNIT *u;
char *prompt;
{
        register int c;
        register char *s;
        register int i;
        int j, expon, digit, div, pow;
        double factor;

Again:
        if (prompt != NULL && isatty(fileno(fd))) {
		if (isatty(fileno(stdout)))
			fprintf(stdout, "%s", prompt);
		else
	                fprintf(stderr, "%s", prompt);
	}
        u->u_val = 1.;
        for (i=0; i != NDIM; i++)
                u->u_dim[i] = 0;
        div = 0;
        pow = 1;
        for(;;) switch(c=nextc()){
        case ' ':
        case '\t':
                break;
        case '\n':
                return (1);
        case EOF:
                return (0);
        case '0':case '1':case '2':case '3':case '4':
        case '5':case '6':case '7':case '8':case '9':
        case '.':case '-':case '+':
                /*
                 * a palpable number
                 */
                s = buf;
                if (c == '+')
                        c = nextc();
                digit = 0;
                while (c>='0' && c<='9') {
                        *s++ = c;
                        c = nextc();
                        digit++;
                }
                if (c == '.') {
                        *s++ = c;
                        while ((c=nextc())>='0' && c<='9') {
                                *s++ = c;
                                digit++;
                        }
                }
                if (!digit) {
                Badnumber:
                        *s = '\0';
                        fprintf(stderr, "Bad number `%s'\n", buf);
                        goto Bad;
                }
                if (c=='e' || c=='E') {
                        *s++ = 'e';
                        c = nextc();
                        if (c == '+')
                                c = nextc();
                        else if (c == '-') {
                                *s++ = c;
                                c = nextc();
                        }
                        if (c<'0' || '9'<c)
                                goto Badnumber;
                        do {
                                *s++ = c;
                                c = nextc();
                        } while('0'<=c && c<='9');
                }
                *s = '\0';
                peekc = c;
		factor = atof(buf);
#ifdef DEBUG
		printf("\natof(%s) == ", buf);
		printf("%g\n", factor);
#endif
		
                if (div) {
                        if (factor == 0.) {
                                fprintf(stderr, "Divide check\n");
                                goto Bad;
                        }
                        u->u_val /= factor;
                        div = 0;
                } else
                        u->u_val *= factor;
                break;

        case '/':       /* divide by next unit */
                if (div) {
                Baddiv:
                        fprintf(stderr, "Two division signs in a row\n");
                        goto Bad;
                }
                div++;
                break;

        case '!':       /* primitive unit */
                i = 0;
                if ((c = nextc())<'0' || c>'9') {
                        fprintf(stderr, "`!' must precede a number\n");
                        goto Bad;
                }
                do {
                        i = i* 10 + (c - '0');
                        c = nextc();
                } while('0'<=c && c<='9');
                peekc = c;
                if (i<0 || NDIM<=i) {
                        printf("Primitive unit out of range [0,%d]\n", NDIM-1);
                        goto Bad;
                }
                u->u_dim[i]++;
                break;

        default:
                s = buf;
                do {
                        *s++ = c;
                        c = nextc();
                } while(c!=EOF && !anyc(c, "/0123456789+-. \t\n"));
                *s = '\0';
                s = buf;
                if (strcmp(s, "per") == 0) {
                        if (div)
                                goto Baddiv;
                        div++;
                        break;
                }
                if (strcmp(s, "square")==0 || strcmp(s, "sq")==0) {
                        pow *= 2;
                        break;
                }
                if (strcmp(s, "cubic")==0 || strcmp(s, "cu")==0) {
                        pow *= 3;
                        break;
                }
                factor = 1.;
                do {
                        for (i=0; i!=nunits; i++)
                                if (eqplural(s, units[i].u_name))
                                        break;
                } while(i==nunits && (s=prefix(s, &factor))!=NULL);
                if (i == nunits) {
                        fprintf(stderr, "Unrecognised unit %s\n", buf);
                        goto Bad;
                }
                if (c=='+' || c=='-') {
                        if (c == '-')
                                div = !div;
                        expon = 0;
                        if ((c = nextc())<'0' || c>'9') {
                                printf("+ or - must be followed by digits\n");
                                goto Bad;
                        }
                        do {
                                expon = expon * 10 + (c - '0');
                                c = nextc();
                        } while('0'<=c && c<='9');
                } else
                        expon = 1;
                expon *= pow;
                pow = 1;
                peekc = c;
                if (div) {
                        expon = -expon;
                        div = 0;
                }
                u->u_val *= ipow(factor * units[i].u_val, expon);
                for (j=0; j!=NDIM; j++)
                        u->u_dim[j] += units[i].u_dim[j]*expon;
        }
Bad:
        while (c!='\n' && c!=EOF)
                c = nextc();
        if (prompt!=NULL)
                goto Again;
        fprintf(stderr, "line %d\n", lineno);
        return (1);
}

/*
 * Check for any occurrences of
 * the character `c' in string `s'.
 */
anyc(c, s)
register char c;
register char *s;
{
        while (*s != '\0')
                if (c == *s++)
                        return (1);
        return (0);
}

/*
 * Return non-zero if string `s' is the
 * same or plural as string `t'.
 */
eqplural(s, t)
register char *s, *t;
{
        while (*t != '\0')
                if (*s++ != *t++)
                        return (0);
        return (*s=='\0' || (*s++=='s' && *s=='\0'));
}

/*
 * Use (char *)sbrk for myalloc to get
 * contiguous memory.
 */
char *
myalloc(nb)
unsigned nb;
{
        register char *rp;

        if ((rp = (char *)sbrk(nb)) == NULL) {
		fprintf(stderr, "Out of memory\n");
		exit(IO_ERROR);
	}
        return (rp);
}

