/*
 * 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]

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.
	The tables are found using a 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).

#endif


#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

#define	FALSE	0
#define	TRUE	1
#define	EOS	0

#define	BINARY_READ	"r"
#define	BINARY_WRITE	"w"

#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	decus
char	ufile[]		= "units.txt";	/* Source file			*/
char	binufile[]	= "units.dat";	/* Compiled file		*/
char    buf[NBUF];
#else
char	ufile[]		= "units.text";
char	binufile[]	= "units.bin";
char    buf[BUFSIZ];
#endif

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

#ifdef	decus
char	*searchlist[] = {
	"",				/* Current directory -- first	*/
	"bin:",
	"pub:",
	"games:",
	"c:",
	"lb:",
	NULL,
};
#endif

extern char    *getname();
extern char    *prefix();
extern char    *alloc();
extern double	atof();

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

#ifndef	decus
        setbuf(stdout, NULL);
        setbuf(stderr, NULL);
#endif
        if (argc>1 && strcmp(argv[1], "-u")==0)
                uflag++;
        init();
#ifndef	decus
        setbuf(fd = stdin, buf);
#else
	fd = stdin;
#endif
Again:
        if (!getunit(&have, "You have: ")
        || !getunit(&want, "You want: "))
                exit(0);
        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()
{
        register char *sstart;
        register int n;
        register int bfd;
        time_t timeasc;
	struct stat sb;

        if (stat(ufile, &sb) < 0)
                return (0);
        timeasc = sb.st_mtime;
        if ((bfd = open(binufile, 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 = alloc(hdr.h_ssize);
        if (read(bfd, sstart, hdr.h_ssize) != hdr.h_ssize)
                goto bad;
        units = (UNIT *)alloc(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);
}
/*
 * Update units information by reading the
 * units file.
 */
update()
{
        register char *name;
        register int i;
        register char *sstart, *send;
        int		bfd;

        if ((fd = fopen(ufile, "r")) == NULL)
                cerr("can't open unit file `%s'", ufile);
	printf("Building units table from \"%s\"\n", ufile);

        units = (UNIT *)alloc(NUNITS*sizeof(UNIT));
        sstart = (char *)sbrk(0);
        for (nunits=0; nunits!=NUNITS; nunits++) {
                name = getname();
                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;
/* printf("%s = ", name); printf("%g\n", units[nunits].u_val); */
        }
        send = (char *)sbrk(0);
        if (!feof(fd))
                cerr("too many units");
        fclose(fd);
        /*
         * Write out, if possible, binary
         * information for faster response next time.
         */
#ifdef	decus
	if ((bfd = fopen(binufile, BINARY_WRITE)) != NULL) {
		printf("Creating compiled units table on \"%s\"\n", binufile);
		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", binufile);
		fclose(bfd);
	}
#else
        if ((bfd = creat(binufile, 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);
        if (lastc == '#') {     /* Eat a comment */
                do {
                        lastc = getc(fd);
                } while(lastc!='\n' && lastc!=EOF);
        }
        return (lastc);
}

char *
getname()
{
        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 = alloc(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)
                printf("%s", prompt);
        u->u_val = 1.;
        for (i=0; i != NDIM; i++)
                u->u_dim[i] = 0;
        div = 0;
        pow = 1;
	for(;;){
	 c = nextc();
	 if (c < 0) return 0;
         switch(c){
        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);
/* printf("[%s == ", buf); printf("%g]\n", factor); */
                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'));
}

/*
 * Diagnostics
 */
/* VARARGS */
cerr(x)
{
        fprintf(stderr, "units: %r\n", &x);
        exit(1);
}

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

        if ((rp = (char *)sbrk(nb)) == NULL)
                cerr("out of memory");
        return (rp);
}

