;;; db-convert.el --- part of EDB, the Emacs database

;; See database.el for copyright notice, distribution conditions, etc.

;; Author: Michael Ernst <mernst@theory.lcs.mit.edu>
;; Keywords: EDB

;;; Commentary:

;; Functions for changing the field structure of a database.

;; This takes a database and rearranges the field order, converts fields
;; from one type to another, adds or removes fields, etc.  There are two
;; parts to the following code:  the user interface and the actual
;; conversion.

;;; Code:


;;; Variables used dynamically; avoid compiler messages about free variables.
;; Should this be?  Perhaps get rid of it.
(defvar computed-functions)


(provide 'db-convert)


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Creation methods
;;;

;; To create a new database from an existing one, the user must specify a
;; creation method for each field of the new database.  This can be done
;; from a program by supplying a fieldnames-creation list, which is a list
;; of two-element lists of (fieldname creation-method).  It can also be
;; done interactively.

;; A creation method (for field F, say) is one of the following:
;; * A field reference.  Field F is set to the value of the specified field
;;   in the corresponding old record.
;; * A literal value.  Field F of every record is set to that value.
;; * Result of a function call.  The user must specify the function, what
;;   arguments should be passed to it, and which of the results is desired.
;;   Each argument specifier (one for each argument accepted by the
;;   function) specifies a field of the old record, the entire old record,
;;   or a literal.  The result specifier indicates that the function's
;;   result should be used as is or that the function returns a list, one
;;   element of which should be used as the value for field F.  This
;;   permits one function to do the computation for several fields in the
;;   new record; since the results are memoized, the function need only be
;;   called once per record.  For instance, suppose the original database
;;   stored addresses as a single field, but the new one stored street,
;;   city, state, and ZIP code separately.  The parsing of the address
;;   would only have to be done once.

;;; Represenation:
;; * Field reference:  a number (the field number in the old record) or a
;;   symbol (the field name in the old database).
;; * Literal value:  list of the symbol 'literal and the value; for
;;   strings, just the string itself.  Nil means the empty string.
;; * Function call:  The function is a symbol whose function cell should be
;;   bound.  Each argument specifier is a fieldnumber (or -1, for the
;;   entire record) or fieldname (or 'original-record, for the entire
;;   record), or a literal as above.  The result specifier is nil (for the
;;   entire result) or an integer (in which case the value of interest is
;;   obtained via (nth result-number result).)  (Should this be displayed
;;   in base 1 for the naive user's benefit?  No, because the naive user
;;   will be told by the function writer exactly which argument to
;;   specify.)  The full specification is a list of two elements, the first
;;   of which is a list of the function and the argument specifiers and the
;;   second of which is the result number.  Just the first element -- the
;;   list of the function and argument specifiers -- may be used if the
;;   result number is nil.

;;; Canonical representation:
;; The user is encouraged to use the fieldname representations, because
;; they are easier to read and write, but internally those are all
;; converted to the canonical representation, which is always the first one
;; listed in the descriptions above.  Thus, each creation method is a
;; number, a list whose car is 'literal, or a function specification.  This
;; permits the code which manipulates creation methods internally to be
;; more efficient.


(defun creation-method-literal-p (creation-method)
  (and (consp creation-method)
       (eq (car creation-method) 'literal)))
(proclaim-inline creation-method-literal-p)


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; User interface
;;;

;; The screen is split into two columns (not necessarily two windows), with
;; the old database structure on the left and the new database structure on
;; the right.  From each record in the original database, one record is
;; created in the new database; by default, the new record is identical to
;; the old.

;; The user interface will be most useful for small changes, like adding a
;; single field; for major changes, I suspect that users will construct
;; fieldname-creation lists by hand.


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Conversion
;;;

;; Currently this doesn't create a new database; it destructively changes
;; the format of the old one.  I suspect this is the desired behavior.

;;;###autoload
(defun db-convert (database fieldnames-creation)
  "Convert DATABASE's field structure according to FIELDNAMES-CREATION.
This function rearranges the field order, converts fields from one type to
another, adds or removes fields, and so forth.

FIELDNAMES-CREATION is a list of two-element lists of \(fieldnames
creation-method\); if creation-method would be nil, then fieldname alone
may be used in place of the two-element list."
  (let ((cmethods (db-canonicalize-creation-methods
		   (mapcar (function (lambda (fieldname-creation)
				       (if (symbolp fieldname-creation)
					   ""
					 (car (cdr fieldname-creation)))))
			   fieldnames-creation)
		   database))
	(no-of-new-fields (length fieldnames-creation)))
    (maplinks-macro
     (link-set-record maplinks-link
		      (db-convert-record (link-record maplinks-link)
					 cmethods no-of-new-fields))
     database)

    ;; Change like fieldlist, accessor functions, etc.
 
    ;; changed from database-set-fieldnames per Henry Thompson
    (db-set-fieldname-vars database
			   (mapcar (function car) fieldnames-creation))))

;; Create and return a new record from RECORD using CREATION-METHODS, a list
;; of canonical creation methods.  Optional argument NO-OF-FIELDS gives the
;; length of CREATION-METHODS and the number of fields in the result record.
;; Dynamically binds computed-functions.
(defun db-convert-record (record creation-methods &optional no-of-fields)
  (let ((new-record (make-vector (or no-of-fields
				     (length creation-methods))
				 nil))
	(this-field-no 0)
	computed-functions)
    (while creation-methods
      (aset new-record this-field-no
	    (db-convert-compute-field-value (car creation-methods) record))

      ;; ...

      (setq creation-methods (cdr creation-methods)
	    this-field-no (1+ this-field-no)))
    new-record
    ))

;; Uses and sets the "computed-functions" dynamic variable.
;; Perhaps get rid of the "result" variable.
(defun db-convert-compute-field-value (creation-method old-record)
  (cond ((numberp creation-method)
	 (aref old-record creation-method))
	((creation-method-literal-p creation-method)
	 (car (cdr creation-method)))
	((consp creation-method)
	 (let* ((function-spec (car creation-method))
		(result-no (car (cdr creation-method)))
		(result-found (if result-no
				  (assoc function-spec computed-functions))))
	   (if result-found
	       (nth result-no (cdr result-found))
	     (let* ((args (mapcar (function
				   (lambda (arg-spec)
				     (cond ((numberp arg-spec)
					    (aref old-record arg-spec))
					   ((creation-method-literal-p arg-spec)
					    (car (cdr arg-spec)))
					   (t (error "Bad argument specification %s." arg-spec)))))
				  (cdr function-spec)))
		    (result (apply (car function-spec) args)))
	       (if result-no
		   (progn
		     (setq computed-functions
			   (cons (cons function-spec result) computed-functions))
		     (nth result-no result))
		 result)))))
	(t (error "Unknown creation method %s" creation-method))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Canonicalizing creation methods
;;;

(defun db-canonicalize-creation-methods (creation-methods database)
  (mapcar (function (lambda (creation-method)
	     (db-canonicalize-creation-method creation-method database)))
	  creation-methods))

(defun db-canonicalize-creation-method (creation-method database)
  (cond
   ;; Literals
   ((null creation-method)
    ;; I don't think this clause is ever entered, but just in case...
    ;; The null test must precede the symbolp test, below.
    '(literal ""))
   ((stringp creation-method)
    (list 'literal creation-method))
   ((creation-method-literal-p creation-method)
    creation-method)
   ;; Field references
   ((symbolp creation-method)
    (or (fieldname->fieldnumber creation-method database)
	(error "%s isn't a fieldname in database %s."
	       creation-method (database-print-name database))))
   ((numberp creation-method)
    (if (and (> creation-method 0)
	     (< creation-method (database-no-of-fields database)))
	creation-method
      (error "%d isn't a valid field number in database %s."
	     creation-method (database-print-name database))))
   ;; Functions
   ((not (consp creation-method))
    (error "Ill-formed creation method %s." creation-method))
   (t
    (let ((function-spec (car creation-method))
	  function args result-no)
      (if (consp function-spec)
	  (setq function (car function-spec)
		args (cdr function-spec)
		result-no (car (cdr creation-method)))
	(setq function function-spec
	      args (cdr creation-method)
	      result-no nil))
      (if (not (fboundp function))
	  (error "%s has no function definition." function))
      (setq args (mapcar (function (lambda (name-or-number)
				     (if (numberp name-or-number)
					 name-or-number
				       fieldname->fieldnumber name-or-number)))
			 args))
      (cons (cons function args) result-no)))))

;;; db-convert.el ends here
