############################################################################
#
#	File:     conman.icn
#
#	Subject:  Program to convert units
#
#	Author:   William E. Drissel
#
#	Date:     May 2, 2001
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#  Conman is a toy I used to teach myself elementary Icon.  I 
#  once vaguely heard of a program which could respond to queries 
#  like "? Volume of the earth in tbsp".  
#
#  The keywords of the language (which are not reserved) are:
#
#          load
#          save
#          print
#          ? (same as print)
#          list
#          is and are which have the same effect
#
#  "Load" followed by an optional filename loads definitions of 
#  units from a file.  If filename is not supplied, it defaults to 
#  "conman.sav"  
#
#  "Save" makes a file for "load".  Filename defaults to 
#  "conman.sav".  "Save" appends to an existing file so a user 
#  needs to periodically edit his save file to prune it back.
#
#  "Print" and "?" are used in phrases like:
#
#          ? 5 minutes in seconds
#
#  Conman replies:
#
#          5 minutes in seconds  equals  300
#
#  List puts up on the screen all the defined units and the 
#  corresponding values.  Format is same as load/store format.
#
#  "Is" and "are" are used like this:
#
#          100 cm are 1 meter
#
#  The discovery of is or are causes the preceding token (in 
#  this case "cm") to be defined.  The load/store format is:
#
#         unitname "is" value
#
#  Examples:
#
#       8 furlongs is 1 mile
#       furlong is 1 / 8 mile
#
#  These last two are equivalent.  Note spaces before and after 
#  "/".  Continuing examples:
#
#       1 fortnight is 14 days
#       furlong/fortnight is furlong / fortnight
#       inches/hour is inch / hour
#
#  After this a user might type:
#
#       ? 1 furlong/fortnight in inches/hour
#       
#  Conman will reply:
#
#       1 furlong/fortnight in inches/hour equals 23.57
#
#  Note: the following feature of Conman:  his operators have no 
#  precedence so the line above gets the right answer but 
#
#        1 furlong/fortnight in inches / hour 
#
#  gets the wrong answer.  (One definition of a feature is a flaw we're 
#  not going to fix).
#
############################################################################
#
#  Program Notes:
#
#  The procedure, process, parses the user's string to see if it 
#  begins with a keyword.  If so, it acts accordingly.  If not, 
#  the user string is fed to isare.
#
#  Isare attempts to find "is" or "are" in the users string.  
#  Failing to, isare feeds the string to conman which can 
#  interpret anything.  If "is" or "are" is found, the tokens 
#  (delimited by blanks) before the "is" or "are" are stacked in 
#  foregoing; those after are stacked in subsequent.  Then the 
#  name to be defined is popped off the foregoing and used as 
#  the "index" into a table named values.  The corresponding 
#  number is computed as eval(subsequent) / eval(foregoing).
#
#  The procedure, stack, is based on Griswold and Griswold, "The 
#  Icon Programming Language", p122.
#
#  The procedure, eval, unstacks the tokens from a stack one by 
#  one until all have been considered.  First, the tokens which 
#  signify division by the next token are considered and used to 
#  set a switch named action.  Then depending on action, the 
#  token is used to multiply the accumulator or divide it.  If 
#  eval can make the token into a number, the number is used, 
#  failing that the token is looked up in the table named values 
#  and the corresponding number is used.  Failing both of those, 
#  conman gripes to the user and does nothing (in effect 
#  multiplying or dividing by 1).  Finally, eval returns the 
#  number accumulated by the operations with the tokens.
#
#  Load defaults the filename to conman.sav if the user didn't 
#  supply one.  Each line read is fed to isare.  We will see 
#  that save prepares the lines so isare can define the units.
#
#  Save uses Icon's sort to go thru the table "values".  The 
#  unit name is the left of a pair and the number stored is the 
#  right of the pair.  The word " is " is stuck between them so 
#  isare will work.
#
#  Finally, we consider the procedure conman.  During initial 
#  design, this was perceived to be the largest part of the 
#  effort of conman.  It is a real tribute to the power of Icon 
#  that only one non-trivial line of code is required.  The 
#  user's string is reproduced then the word "equals" followed 
#  the result produced by eval after the user's string is 
#  stacked.
#
############################################################################
#
# Requires:  conman.sav
#
############################################################################
#
#  Links:  io
#
############################################################################

link io

global values, blank, nonblank

procedure main (args)
    local line

    if map(args[1]) == "-t" then &trace := -1

     init()

     while line := prompt() do {
         process(line || " ")      # pad with a blank to make life easy
     }
     windup()
end
############################################################################
#
# windup
#
procedure windup()
     write(&errout,"windup")
end
############################################################################
#
# process
#
procedure process(str)

     case parse(str) of {
     "load"        : load(str)
     "save"        : save(str)
     "print"       : conman(butfirst(str))          # strip first token
     "list"        : zlist()
     default       : isare(str) # didn't start with a kw, try is or are
     }
end
############################################################################
#
# parse
#
procedure parse(str)
    local token

     token := first(str)
     case token of  {
         "?"       : return "print"             # only special case at present
         default   : return token
     }
end
############################################################################
#
# conman
#
# compute and write result - During initial design, this was perceived to
#                            require 50 lines of complicated lookup etc.!
#
procedure conman(strn) 

     write (strn , " equals ", eval(stack(strn, 1, *strn)))
end
############################################################################
#
# isare - routine to define values - tries to evaluate if not a definition
#
# locate is,are - delete
# backup one word - save, delete
# compute foregoing
# compute subsequent
# store word, subsequent/foregoing in values
#
procedure isare(str)
    local after, before, foregoing, subsequent

# locate blank-delimited is or are - early (?clumsy) Icon code replaced at
# the suggestion of one of REG's students

     if (str ? (before := tab(find(" is ")) & move(4) & 
                 after := \tab(0)))  then {  }                    # is

     else if (str ? (before := tab(find(" are ")) & move(5) & 
                 after := \tab(0)))  then {  }                    # are

     else {                          # found nothing - try to eval anyhow
         conman(str)
         return 
     } 
#
# here if is or are
#
     foregoing  := stack(before)    # so we can look back one token
     subsequent := stack(after)     # might as well stack this too

     name := singular(pop(foregoing))     # define token before is or are
#
# next line so we can support "100 cms are 1 meter"
#
     values[name] := eval(subsequent) / eval(foregoing) 
     return
end
############################################################################
#
# stack - stack tokens - based on IPL section 12.1 p122
#
#  stack the "words" in str - needs cset nonblank
#
procedure stack(str)
    local i, j, words

    words := [] ; i := 1

    while j := upto(nonblank, str, i) do {
         i := many(nonblank, str, j)
         push(words, str[i:j])
         }
    return words
end
############################################################################
#
# eval - evaluate a stack
#
#  while more remain
#    unstack a token
#    if "in" or "over" or "/", set to divide next time
#    else if number multiply/divide it
#    else if in values, multiply/divide value
#    else gripe and leave accum alone
#
procedure eval(stk)
    local accum, action, token

    accum := 1.0  ;  action := "multiply"

    while token := singular(pull(stk)) do {          

         if token == ("in" | "over" | "/" )then action := "divide"
         else if action == "multiply" then {

#             write("multiplying by ", token, " ", (real(token) | 
 #                                                  real(values[token]) |
  #                                                 "unknown"))

              if not (accum *:= \(real(token) | real(values[token]))) then
                   write (&errout,
                         "Can't evaluate ", token, " - using 1.0 instead")
         }
         else if action == "divide" then {
              action := "multiply"
              if not (accum /:= \(real(token) | real(values[token]))) then
                   write (&errout,
                         "Can't evaluate ", token, " - using 1.0 instead")
         }
     }#........................................ # end of while more tokens
    return accum       
end
############################################################################
#
# init
#
procedure init()
     write(&errout, "Conman version 1.1, 7/24/87")
     values := table(&null)
     nonblank := &ascii -- ' '
     blank := ' '
     values["times"] := 1.0
     values["by"]    := 1.0
     values["of"]    := 1.0
     values["at"]    := 1.0
     values["print"] := 1.0
     values["?"]     := 1.0
     values["meter"] := 1.0
     values["kilogram"] := 1.0
     values["second"]   := 1.0

end
############################################################################
#
# prompt
#
procedure prompt()
    return read()
end
############################################################################
#
# load - loads table from a file - assumes save format compatible 
#        with isare
#
procedure load(str)
    local intext, line, filnam

    filnam := (\second(str) | "conman.sav")
    write (&errout, "Load from ", filnam, ".  May take a minute or so.")
    intext := dopen(filnam,"r") | { write(&errout, "can't open ", filnam)
                                    fail}
     while line := read(intext)  do {
         isare(line || " ")  # pad with a blank to make life easy
     }
    close(intext)
    return
end
############################################################################
#
# save - saves table to file in format compatible with isare
#
procedure save(str)
    local i, outtext, pair, wlist, filnam
 
    filnam := (\second(str) | "conman.sav")
    write (&errout, "Save into ", filnam)
    outtext := open(filnam,"a") | { write(&errout, "can't save to ", filnam)
                                    fail}
    wlist := sort(values)
    i := 0
    every pair := !wlist do {
         write(outtext, pair[1], " is ", pair[2])
    }
    close(outtext)
end
############################################################################
#
# zlist - lists the table
#
procedure zlist()
    local i, pair, wlist

    i := 0
    wlist := sort(values)
    every pair := !wlist do {
         write(&errout, pair[1], " is ", pair[2])
    }
end
############################################################################
#
# first - returns first token in a string - needs cset nonblank
#
procedure first(s)
    local stuff

    s? (tab(upto(nonblank)) , (stuff := tab(many(nonblank))))
    return \stuff 
end
############################################################################
#
# second - returns second token in a string - needs cset nonblank
#
procedure second(s)
    local stuff

    s? (tab(upto(nonblank)) , (tab(many(nonblank)) & tab(upto(nonblank)) &
         (stuff := tab(many(nonblank)))))
    return \stuff 
end
############################################################################
#
# butfirst - returns all butfirst token in a string - needs cset nonblank
#
procedure butfirst(s)
    local stuff

    s? (tab(upto(nonblank)) , tab(many(nonblank)) & tab(upto(nonblank)) &
         (stuff := tab(0)))
    return \stuff 
end
############################################################################
#
# singular - returns singular of a unit of measure - add special cases in 
#            an obvious way.  Note: singulars ending in "e" should be handled
#            here also "per second" units which end in "s".
#
procedure singular(str)
    local s

    s := str 
    if s == "fps" then return "fps"
    if s == "feet" then return "foot"
    if s == "minutes" then return "minute"
    if s == "miles" then return "mile"
#
## otherwise strip "es" or "s".  Slick code by Icon grad student
#
    return s? (1(tab(-2), ="es") | 1(tab(-1), ="s" ) | tab(0))
end
############################################################################
