Re: proposal to include functions to read data from files
Subject: Re: proposal to include functions to read data from files
From: Barton Willis
Date: Fri, 9 Jan 2004 10:57:34 -0600
Here is a short demo of Maxima io.
(C1) load("l:/io.lisp");
(D1) l:/io.lisp
(C2) open_input_file("l:/maxima-io/fp.data")$
(C3) list_open_files();
(D3) [l:/maxima-io/fp.data]
(C4) read_matrix("l:/maxima-io/fp.data");
[ 0 1 2 ]
[ ]
(D4) [ 3 4 5 ]
[ ]
[ 6 7 8 ]
(C5) reset_file_position("l:/maxima-io/fp.data")$
(C6) read_list("l:/maxima-io/fp.data");
(D6) [0, 1, 2, 3, 4, 5, 6, 7, 8]
(C7) reset_file_position("l:/maxima-io/fp.data")$
(C8) read_matrix("l:/maxima-io/fp.data",2,2);
[ 0 1 ]
(D8) [ ]
[ 2 3 ]
(C9) close_all_files();
Closing file l:/maxima-io/fp.data
(D9) TRUE
(C10)
I have very little experience with io in general -- this was my first
time coding io in Common Lisp. Expect bugs (especially with reading CSV
files).
Barton
-------------------io.lisp--------------------------------------------------
;; Author: Barton Willis
;; Maxima io is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License,
;; http://www.gnu.org/copyleft/gpl.html.
;; Maxima io has NO WARRANTY, not even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
;; I have not tested some parts of this code, and the rest of it
;; has only been minimally tested. Exercise caution. At
;; least for now, I don't plan to debug this code; instead, I hope that
;; somebody adopts this project and finishes it (or throws it out and
;; starts from scratch).
(in-package "MAXIMA")
($put '$io '0.0 '$version)
(defmacro while (cond &rest body)
`(do ()
((not ,cond))
,@body))
;; Place all open files in a hash table open-files. For no particular
;; reason, initialize open-files to have 12 members.
(declaim (special open-files))
(setf open-files (make-hash-table :size 12 :test #'equal))
;; The structure io-stream holds a file stream, a function that
;; reads one object from the stream, and the character that
;; separates objects. When fs is an output stream, read-fun
;; isn't defined.
(defstruct io-stream (fs nil) read-fun ch)
;; If x is an io-stream structure, return its file stream; otherwise,
;; return nil.
(defun get-stream (x)
(if (io-stream-p x) (io-stream-fs x) nil))
;; Convert a Maxima string to a Lisp string. When str isn't a Maxima
;; string, signal an error.
(defun require-string (str)
(cond ((mstringp str)
(symbol-name (stripdollar str)))
(t
(merror "Needed a string, instead found ~:M" str))))
(defun $open_input_file (fn)
(let ((fs) (fp) (ft) (read-fun))
(setq fn (require-string fn))
(setq fp (pathname fn))
(setq ft (pathname-type fp))
(setq fs (gethash fn open-files))
(setq fs (get-stream fs))
(cond ((and (streamp fs) (output-stream-p fs))
(setq fn (namestring fp))
(merror "The file ~:S is open for output. It cannot be opened for
input" fn))
((not (and (streamp fs) (input-stream-p fs)))
(setq fs (open fp :direction :input
:if-does-not-exist nil))))
(setq read-fun (if (equal ft "csv") (intern "CSV-READ" 'MAXIMA)
(intern "READ" 'lisp)))
(if (and (streamp fs) (input-stream-p fs))
(setf (gethash fn open-files) (make-io-stream :fs fs :read-fun read-fun))
(merror "File ~:S not found" fn))))
(defun $open_output_file (fn)
(let ((fs) (fp) (ft) (ch))
(setq fn (require-string fn))
(setq fp (pathname fn))
(setq ft (pathname-type fp))
(setq fs (gethash fn open-files))
(setq fs (get-stream fs))
(cond ((and (streamp fs) (input-stream-p fs))
(setq fn (namestring fp))
(merror "The file ~:S is open for input. It cannot be opened for
output" fn))
((not (and (streamp fs) (input-stream-p fs)))
(setq fs (open fp :direction :output
:if-exists :append
:if-does-not-exist :create))))
(setq ch (cond ((equal ft "csv") #\,)
((equal ft "tsv") #\tab)
(t #\space)))
(if (and (streamp fs) (output-stream-p fs))
(setf (gethash fn open-files) (make-io-stream :fs fs :ch ch))
(merror "Unable to open file ~:S" fn))))
;; Close the file with name f and remove it from open-files.
(defun $closefile (f)
(let* ((fn (require-string f)) (fs (gethash fn open-files)))
(if (io-stream-p fs) (closefile fn fs)
(merror "No file with name ~:M" f))))
(defun closefile (fn fs)
(format t "Closing file ~A~%" (string-left-trim `(\#&) fn))
(remhash fn open-files)
(close (io-stream-fs fs)))
;; Reset the file position of fn to the start of the file.
(defun $reset_file_position (fn)
(setq fn (require-string fn))
(let ((fs (gethash fn open-files)))
(setq fs (get-stream fs))
(if (streamp fs) (file-position fs 0)
(merror "No file with name ~:S" fn))))
;; Close all files in open-files.
(defun $close_all_files ()
(maphash #'(lambda (fn fs) (closefile fn fs)) open-files)
t)
;; Return a Maxima list of all open files.
(defun $list_open_files ( )
(let ((acc))
(maphash #'(lambda (x y)
(declare (ignore y))
(push (string-left-trim `(\#&) x) acc)) open-files)
(cons '(mlist simp) acc)))
;; If read-delimited-list returns nil, backup to the original
;; file position and read the next object with read.
(defun csv-read (fs a b)
(declare (ignore a b))
(let ((pos (file-position fs))
(x (ignore-errors (read-delimited-list #\, fs))))
(cond ((null x)
(file-position fs pos)
(read fs nil nil))
(t (car x)))))
;; If x is an integer, a float, or a string, return x. If
;; x is a Lisp complex number or a Lisp rational number,
;; convert it to Maxima form; otherwise, signal an error.
(defun check-data (x)
(cond ((or (floatp x) (integerp x) (stringp x)) x)
((and (rationalp x) (not (integerp x)))
`((rat simp) ,(numerator x) ,(denominator x)))
((complexp x)
`((mplus simp) ,(check-data (realpart x))
((mtimes simp) $%i ,(check-data (imagpart x)))))
(t (merror "Bad data in input"))))
;; Read one line of the stream fs and return a list of the values and
;; the number of objects read.
(defun read-row (fs fread)
(read-data (make-string-input-stream (read-line fs nil "")) fread 0 nil))
;; Skip s objects then read objects from the stream fs until either
;; n have been read or fs has no more objects left to read.
(defun read-data (fs fread s n)
(let ((x) (acc) (k 0))
(dotimes (i s)
(funcall fread fs nil nil))
(while (and (or (not n) (< k n)) (setq x (funcall fread fs nil nil)))
(incf k)
(push (check-data x) acc))
(values (nreverse acc) k)))
;; Return true iff each member of the lisp list l is a Maxima number.
(defun check-num-data (l)
(every #'(lambda (x) ($numberp x)) l))
;; Return true iff m is a matrix and each matrix element is a Maxima
number.
(defun $numerical_matrixp (m)
(and ($matrixp m) (every #'$numerical_listp (margs m))))
;; Return true iff l is a Maxima list and each list element is a Maxima
number.
(defun $numerical_listp (l)
(and ($listp l) (check-num-data (margs l))))
;; When n isn't an integer that is >= m or nil, signal an error.
(defun require-nil-or-min-int (n m arg-pos fn)
(if (not (or (and (integerp n) (>= n m)) (null n)))
(merror "The ~:M argument to ~:M must be undefined or it must be a
integer that is >= ~:M; instead found ~:M" arg-pos fn m n)))
;; Start reading the s-th object from file fn. Read objects until
;; either n objects are read or there are no more objects left to read.
;; Return a Maxima list of the objects are read.
(defun $read_list (fn &optional s n)
(let ((fs ($open_input_file fn)))
(require-nil-or-min-int s 1 "$second" "$read_list")
(require-nil-or-min-int n 0 "$third" "$read_list")
(setq s (if (not (integerp s)) 0 (- s 1)))
(setq n (if (not (integerp n)) nil n))
(cons '(mlist simp) (read-data (io-stream-fs fs)
(io-stream-read-fun fs) s n))))
(defun $write_list (l fn)
(cond (($listp l)
(setq fn ($open_output_file fn))
(write-list (cdr l) (io-stream-fs fn) (io-stream-ch fn))
t)
(t (merror "First argument to write_list must be a list"))))
(defun my-mgrind (x fs)
(cond ((not ($freeof '$i x))
(let ((xr ($realpart x))
(xi ($imagpart x)))
(if (and ($numberp xr) ($numberp xi))
(setq x (complex xr xi))
(merror "Data must be a string or a number"))))
((and ($ratnump x) (not (integerp x)))
(setq x (/ (nth 1 x) (nth 2 x))))
((mstringp x)
(setq x (string-left-trim `(#\&) (symbol-name x)))))
(format fs (cond ((floatp x) "~F")
((stringp x) "~S")
(t "~W")) x))
(defun write-list (l fs ch)
(cond ((not (null l))
(mgrind (pop l) fs)
(while l
(write-char ch fs)
(my-mgrind (pop l) fs))))
(terpri fs)
(finish-output fs))
(defun $write_matrix (e fn)
(cond (($matrixp e)
(setq fn ($open_output_file fn))
(mapcar #'(lambda (x) (write-list (cdr x) (io-stream-fs fn)
(io-stream-ch fn))) (cdr e))
t)
(t (merror "First argument to write_list must be a matrix"))))
;; Write expression e to file or stream fn and append a newline to the
file.
;; Return true.
(defun $write (e fn)
(let ((fs (io-stream-fs ($open_output_file fn))))
(my-mgrind e fs)
(terpri fs)
(finish-output fs))
t)
;; Read a matrix from file fn. When c isn't defined, each line
;; of the file is a matrix row. When r is defined, read rows until
;; r rows have been read or until no rows are left to read.
(defun $read_matrix (fn &optional r c)
(let ((fn ($open_input_file fn)) (acc) (x) (i 0) (fs) (fr))
(require-nil-or-min-int r 0 "$second" "$read_matrix")
(require-nil-or-min-int c 0 "$third" "$read_matrix")
(setq fs (io-stream-fs fn))
(setq fr (io-stream-read-fun fn))
(cond ((null c)
(while (and (or (null r) (< i r)) (setq x (read-row fs fr)))
(incf i)
(push x acc)))
(t
(dotimes (i r)
(push (read-data fs fr 0 c) acc))))
(setq acc (mapcar #'(lambda (s) (cons '(mlist simp) s)) acc))
(cons '($matrix simp) (nreverse acc))))