Simple database: Difference between revisions
m rebol |
m →{{header|Rebol}}: Changed "Rebol" to "REBOL" |
||
Line 2,478: | Line 2,478: | ||
</pre> |
</pre> |
||
=={{header| |
=={{header|REBOL}}== |
||
<lang rebol>rebol [author: "Nick Antonaccio"] |
<lang rebol>rebol [author: "Nick Antonaccio"] |
||
write/append %rdb "" db: load %rdb |
write/append %rdb "" db: load %rdb |
Revision as of 12:21, 9 May 2014
![Task](http://static.miraheze.org/rosettacodewiki/thumb/b/ba/Rcode-button-task-crushed.png/64px-Rcode-button-task-crushed.png)
You are encouraged to solve this task according to the task description, using any language you may know.
Write a simple tool to track a small set of data. The tool should have a commandline interface to enter at least two different values. The entered data should be stored in a structured format and saved to disk.
It does not matter what kind of data is being tracked. It could be your CD collection, your friends birthdays, or diary.
You should track the following details:
- A description of the item. (e.g., title, name)
- A category or tag (genre, topic, relationship such as “friend” or “family”)
- A date (either the date when the entry was made or some other date that is meaningful, like the birthday); the date may be generated or entered manually
- Other optional fields
The command should support the following Command-line arguments to run:
- Add a new entry
- Print the latest entry
- Print the latest entry for each category
- Print all entries sorted by a date
The category may be realized as a tag or as structure (by making all entries in that category subitems)
The file format on disk should be human readable, but it need not be standardized. A natively available format that doesn't need an external library is preferred. Avoid developing your own format however if you can use an already existing one. If there is no existing format available pick one of: JSON, S-Expressions, YAML, or others.
See also Take notes on the command line for a related task.
C
A simple database in C with some error checking, even. A quick test with Valgrind revealed no obvious memory leaks. The following data was used for testing. -> database.csv
"Soon Rising","Dee","Lesace","10-12-2000","New Hat Press" "Brave Chicken","Tang","Owe","04-01-2008","Nowhere Press" "Aardvark Point","Dee","Lesace","5-24-2001","New Hat Press" "Bat Whisperer, The","Tang","Owe","01-03-2004","Nowhere Press" "Treasure Beach","Argus","Jemky","09-22-1999","Lancast"
<lang C>#include <stdio.h>
- include <stdlib.h> /* malloc */
- include <string.h> /* strlen */
- define _XOPEN_SOURCE /* requred for time functions */
- define __USE_XOPEN
- include <time.h>
- define DB "database.csv" /* database name */
- define TRY(a) if (!(a)) {perror(#a);exit(1);}
- define TRY2(a) if((a)<0) {perror(#a);exit(1);}
- define FREE(a) if(a) {free(a);a=NULL;}
- define sort_by(foo) \
static int by_##foo (const void*p1, const void*p2) { \
return strcmp ((*(const pdb_t*)p1)->foo, (*(const pdb_t*)p2)->foo); }
typedef struct db {
char title[26]; char first_name[26]; char last_name[26]; time_t date; char publ[100]; struct db *next;
} db_t,*pdb_t; typedef int (sort)(const void*, const void*); enum {CREATE,PRINT,TITLE,DATE,AUTH,READLINE,READ,SORT,DESTROY}; static pdb_t dao (int cmd, FILE *f, pdb_t db, sort sortby); static char *time2str (time_t *time); static time_t str2time (char *date); /* qsort callbacks */ sort_by(last_name); sort_by(title); static int by_date(pdb_t *p1, pdb_t *p2); /* main */ int main (int argc, char **argv) {
char buf[100]; const char *commands[]={"-c", "-p", "-t", "-d", "-a", NULL}; db_t db; db.next=NULL; pdb_t dblist; int i; FILE *f; TRY (f=fopen(DB,"a+")); if (argc<2) {
usage: printf ("Usage: %s [commands]\n"
"-c Create new entry.\n" "-p Print the latest entry.\n" "-t Print all entries sorted by title.\n" "-d Print all entries sorted by date.\n" "-a Print all entries sorted by author.\n",argv[0]); fclose (f); return 0; } for (i=0;commands[i]&&strcmp(argv[1],commands[i]);i++); switch (i) { case CREATE: printf("-c Create a new entry.\n"); printf("Title :");if((scanf(" %25[^\n]",db.title ))<0)break; printf("Author Firstname:");if((scanf(" %25[^\n]",db.first_name))<0)break; printf("Author Lastname :");if((scanf(" %25[^\n]",db.last_name ))<0)break; printf("Date 10-12-2000 :");if((scanf(" %10[^\n]",buf ))<0)break; printf("Publication :");if((scanf(" %99[^\n]",db.publ ))<0)break; db.date=str2time (buf); dao (CREATE,f,&db,NULL); break; case PRINT: printf ("-p Print the latest entry.\n"); while (!feof(f)) dao (READLINE,f,&db,NULL); dao (PRINT,f,&db,NULL); break; case TITLE: printf ("-t Print all entries sorted by title.\n"); dblist = dao (READ,f,&db,NULL); dblist = dao (SORT,f,dblist,by_title); dao (PRINT,f,dblist,NULL); dao (DESTROY,f,dblist,NULL); break; case DATE: printf ("-d Print all entries sorted by date.\n"); dblist = dao (READ,f,&db,NULL); dblist = dao (SORT,f,dblist,(int (*)(const void *,const void *)) by_date); dao (PRINT,f,dblist,NULL); dao (DESTROY,f,dblist,NULL); break; case AUTH: printf ("-a Print all entries sorted by author.\n"); dblist = dao (READ,f,&db,NULL); dblist = dao (SORT,f,dblist,by_last_name); dao (PRINT,f,dblist,NULL); dao (DESTROY,f,dblist,NULL); break; default: { printf ("Unknown command: %s.\n",strlen(argv[1])<10?argv[1]:""); goto usage; } } fclose (f); return 0;
} /* Data Access Object (DAO) */ static pdb_t dao (int cmd, FILE *f, pdb_t in_db, sort sortby) {
pdb_t *pdb=NULL,rec=NULL,hd=NULL; int i=0,ret; char buf[100]; switch (cmd) { case CREATE: fprintf (f,"\"%s\",",in_db->title); fprintf (f,"\"%s\",",in_db->first_name); fprintf (f,"\"%s\",",in_db->last_name); fprintf (f,"\"%s\",",time2str(&in_db->date)); fprintf (f,"\"%s\" \n",in_db->publ); break; case PRINT: for (;in_db;i++) { printf ("Title : %s\n", in_db->title); printf ("Author : %s %s\n", in_db->first_name, in_db->last_name); printf ("Date : %s\n", time2str(&in_db->date)); printf ("Publication : %s\n\n", in_db->publ); if (!((i+1)%3)) { printf ("Press Enter to continue.\n"); ret = scanf ("%*[^\n]"); if (ret<0) return rec; /* handle EOF */ else getchar(); } in_db=in_db->next; } break; case READLINE: if((fscanf(f," \"%[^\"]\",",in_db->title ))<0)break; if((fscanf(f," \"%[^\"]\",",in_db->first_name))<0)break; if((fscanf(f," \"%[^\"]\",",in_db->last_name ))<0)break; if((fscanf(f," \"%[^\"]\",",buf ))<0)break; if((fscanf(f," \"%[^\"]\" ",in_db->publ ))<0)break; in_db->date=str2time (buf); break; case READ: while (!feof(f)) { dao (READLINE,f,in_db,NULL); TRY (rec=malloc(sizeof(db_t))); *rec=*in_db; /* copy contents */ rec->next=hd;/* to linked list */ hd=rec;i++; } if (i<2) { puts ("Empty database. Please create some entries."); fclose (f); exit (0); } break; case SORT: rec=in_db; for (;in_db;i++) in_db=in_db->next; TRY (pdb=malloc(i*sizeof(pdb_t))); in_db=rec; for (i=0;in_db;i++) { pdb[i]=in_db; in_db=in_db->next; } qsort (pdb,i,sizeof in_db,sortby); pdb[i-1]->next=NULL; for (i=i-1;i;i--) { pdb[i-1]->next=pdb[i]; } rec=pdb[0]; FREE (pdb); pdb=NULL; break; case DESTROY: { while ((rec=in_db)) { in_db=in_db->next; FREE (rec); } } } return rec;
} /* convert numeric time to date string */ static char *time2str (time_t *time) {
static char buf[255]; struct tm *ptm; ptm=localtime (time); strftime(buf, 255, "%m-%d-%Y", ptm); return buf;
} /* convert date string to numeric time */ static time_t str2time (char *date) {
struct tm tm; memset (&tm, 0, sizeof(struct tm)); strptime(date, "%m-%d-%Y", &tm); return mktime(&tm);
} /* sort by date callback for qsort */ static int by_date (pdb_t *p1, pdb_t *p2) {
if ((*p1)->date < (*p2)->date) { return -1; } else return ((*p1)->date > (*p2)->date);
}</lang>
COBOL
This is a souped-up version of the task from Take notes on the command line. It stores the current date, a tag, a title and a note as an entry in a file. The database produced is not particularly human-readable or easy to modify, but it is in a well-structured format.
<lang cobol> IDENTIFICATION DIVISION.
PROGRAM-ID. simple-database.
ENVIRONMENT DIVISION. INPUT-OUTPUT SECTION. FILE-CONTROL. SELECT OPTIONAL database-file ASSIGN Database-Path ORGANIZATION INDEXED ACCESS SEQUENTIAL RECORD KEY data-title ALTERNATE RECORD KEY data-tag WITH DUPLICATES ALTERNATE RECORD KEY date-added WITH DUPLICATES FILE STATUS file-status . DATA DIVISION. FILE SECTION. FD database-file. 01 database-record. *> Date is in YYYYMMDD format. 03 date-added PIC 9(8). 03 data-tag PIC X(20). 03 data-title PIC X(50). 03 data-contents PIC X(200). *> Adding extra space is considered good style so the record *> can be expanded in the future. 03 FILLER PIC X(50).
WORKING-STORAGE SECTION. 78 Database-Path VALUE "database.dat".
01 file-status PIC XX. 88 file-ok VALUE "00". 88 duplicate-key VALUE "22". 88 key-not-found VALUE "23".
01 num-args PIC 99.
01 action PIC XX. 88 create-entry VALUE "-c". 88 remove-entry VALUE "-r". 88 find-entry VALUE "-f". 88 print-latest VALUE "-l". 88 print-database VALUES "-a", "-d", "-t". *> Printed by title. 88 print-by-title VALUE "-a". 88 print-by-date VALUE "-d". 88 print-by-tag VALUE "-t". 88 print-help VALUES "-h", SPACES.
01 read-direction-flag PIC X VALUE SPACE. 88 read-backwards VALUE "B".
01 edited-date PIC 9(4)/99/99. PROCEDURE DIVISION. DECLARATIVES. database-file-error SECTION. USE AFTER ERROR ON database-file
DISPLAY "An error has occurred while using " Database-Path ". Error no. " file-status DISPLAY "The program will terminate."
CLOSE database-file
GOBACK . END DECLARATIVES.
main-line. DISPLAY 1 UPON ARGUMENT-NUMBER ACCEPT action FROM ARGUMENT-VALUE
ACCEPT num-args FROM ARGUMENT-NUMBER
EVALUATE TRUE WHEN create-entry IF num-args >= 4 PERFORM write-entry ELSE DISPLAY "-a requires arguments to enter in the " "database. See help (-h) for details." END-IF
WHEN remove-entry IF num-args >= 2 PERFORM delete-entry ELSE DISPLAY "-r requires the title of the entry to " "delete." END-IF
WHEN find-entry IF num-args >= 2 PERFORM display-specified-entry ELSE DISPLAY "-f requires the title of the entry to " "find." END-IF
WHEN print-latest PERFORM show-latest
WHEN print-database PERFORM show-database
WHEN print-help PERFORM show-general-help
WHEN OTHER DISPLAY action " is not a valid option." END-EVALUATE GOBACK . write-entry. OPEN EXTEND database-file
DISPLAY 2 UPON ARGUMENT-NUMBER ACCEPT data-tag FROM ARGUMENT-VALUE DISPLAY 3 UPON ARGUMENT-NUMBER ACCEPT data-title FROM ARGUMENT-VALUE IF data-title = SPACES DISPLAY "The title cannot be blank." PERFORM close-and-terminate END-IF
DISPLAY 4 UPON ARGUMENT-NUMBER ACCEPT data-contents FROM ARGUMENT-VALUE
ACCEPT date-added FROM DATE YYYYMMDD
WRITE database-record INVALID KEY IF duplicate-key DISPLAY "An entry in the database already has " "that title. Please choose a different " "title or remove the entry." ELSE PERFORM database-file-error END-IF END-WRITE
PERFORM close-database . delete-entry. PERFORM get-title-arg OPEN I-O database-file PERFORM read-title
DELETE database-file
PERFORM close-database . display-specified-entry. PERFORM get-title-arg OPEN INPUT database-file PERFORM read-title
PERFORM show-record
PERFORM close-database . get-title-arg. DISPLAY 2 UPON ARGUMENT-NUMBER ACCEPT data-title FROM ARGUMENT-VALUE . read-title. START database-file KEY IS = data-title INVALID KEY IF key-not-found DISPLAY "An entry with that title was not found." PERFORM close-and-terminate ELSE PERFORM database-file-error END-IF END-START
READ database-file . close-and-terminate. PERFORM close-database GOBACK . show-latest. OPEN INPUT database-file
PERFORM start-at-last-date READ database-file PERFORM show-record
PERFORM close-database . show-database. OPEN INPUT database-file
EVALUATE TRUE WHEN print-by-title *> Primary key is the title. CONTINUE WHEN print-by-tag MOVE LOW-VALUES TO data-tag START database-file KEY IS > data-tag WHEN print-by-date PERFORM start-at-last-date SET read-backwards TO TRUE END-EVALUATE
PERFORM FOREVER *> The problem with statements instead of functions... IF NOT read-backwards READ database-file NEXT AT END EXIT PERFORM END-READ ELSE READ database-file PREVIOUS AT END EXIT PERFORM END-READ END-IF
PERFORM show-record DISPLAY SPACE END-PERFORM
PERFORM close-database . start-at-last-date. MOVE HIGH-VALUES TO date-added START database-file KEY IS < date-added . close-database. CLOSE database-file . show-record. MOVE date-added TO edited-date DISPLAY "Date added: " edited-date " Tag: " data-tag DISPLAY "Title: " data-title DISPLAY "Contents:" DISPLAY " " FUNCTION TRIM(data-contents) . show-general-help. DISPLAY "Help: Possible options are:" DISPLAY " -a - Show all the entries (sorted by title)." DISPLAY " -c - Create a new entry in the database. -c needs" " further arguments in this format:" DISPLAY ' "tag" "title" "content"' DISPLAY " Max argument sizes (in characters): tag - 20, " "title - 50, content - 200" DISPLAY " The title must be unique and not be blank." DISPLAY " -d - Show all the entries sorted by date added." DISPLAY " -f - Finds and displays entry with the title " "provided. The title should be specified as shown for " "-c." DISPLAY " -h - Show this help menu." DISPLAY " -l - Show the latest entry." DISPLAY " -r - Remove the entry with the title provided. " "The title should be specified as shown for -c." DISPLAY " -t - Show all the entries sorted by tag." .</lang>
Sample session:
$ ./database -c "Reminder" "Bob's Birthday" "Buy birthday present for Bob." $ ./database -c "Wishlist" "Beethoven" "Beethoven's Ode to Joy" $ ./database -c "Reminder" "Add to Simple Database" "Add brainf*** example for Simple Database on Rosetta Code." ... $ ./database -f "Mozart" An entry with that title was not found. $ ./database -t Date added: 2013/08/13 Tag: Reminder Title: Bob's Birthday Contents: Buy birthday present for Bob. Date added: 2013/08/13 Tag: Reminder Title: Add to Simple Database Contents: Add brainf*** example for Simple Database on Rosetta Code. Date added: 2013/08/13 Tag: Wishlist Title: Beethoven Contents: Beethoven's Ode to Joy $ ./database -r "Beethoven" $ ./database -l Date added: 2013/08/13 Tag: Reminder Title: Add to Simple Database Contents: Add brainf*** example for Simple Database on Rosetta Code. $
Common Lisp
A tool to track the episodes you have watched in a series. Tested with SBCL but should work with other implementations.
Run from the commandline as:
sbcl --script watch.lisp
Without arguments the function (watch-list)
is invoked to show the last episode of each series.
With the argument add
the function (watch-add)
will allow you to add a new episode with series name, episode title, episode number and date watched. If the series does not yet exist, you will be asked if you want to create it.
This code is also available under the GNU GPLv3. <lang lisp>(defvar *db* nil)
(defstruct series description tags episodes)
(defstruct (episode (:print-function print-episode-struct))
series title season episode part date tags)
(defun format-ymd (date)
(format nil "~{~a.~a.~a~}" date))
(defun print-episode-struct (ep stream level)
(let ((*print-pretty* nil)) (format stream (if *print-escape* "#s(episode~@{~*~@[ :~1:*~a ~s~]~})" "~32<~*~a~; ~*~@[~d-~]~*~d~> ~45<~*~@[~a ~]~*~@[(~a) ~]~;~*~@[(~a)~]~>~*~@[ (~{~a~^ ~})~]") :series (episode-series ep) :season (episode-season ep) :episode (episode-episode ep) :title (episode-title ep) :part (episode-part ep) :date (if *print-escape* (episode-date ep) (when (episode-date ep) (format-ymd (episode-date ep)))) :tags (episode-tags ep))))
(defun get-value (key alist)
(cdr (assoc key alist)))
(defun get-latest (database)
(when database (cons (car (series-episodes (cdar database))) (get-latest (cdr database)))))
(defun get-all (database)
(when database (append (series-episodes (cdar database)) (get-all (cdr database)))))
(defun compare-date (a b)
(cond ((not a) t) ((not b) nil) ((= (first a) (first b)) (compare-date (rest a) (rest b))) (t (< (first a) (first b)))))
(defun compare-by-date (a b)
(compare-date (episode-date a) (episode-date b)))
(defun prompt-read (prompt &optional default)
(format *query-io* "~a~@[ (~a)~]: " prompt default) (force-output *query-io*) (let ((answer (read-line *query-io*))) (if (string= answer "") default answer)))
(defun split (seperator string)
(loop for i = 0 then (1+ j) as j = (search seperator string :start2 i) collect (subseq string i j) while j))
(defun get-current-date ()
(multiple-value-bind (second minute hour date month year day-of-week dst-p tz) (get-decoded-time) (declare (ignore second minute hour day-of-week dst-p tz)) (list date month year)))
(defun parse-date (date)
(reverse (mapcar #'parse-integer (split "." date))))
(defun parse-tags (tags)
(when (and tags (string-not-equal "" tags)) (mapcar #'intern (split " " (string-upcase tags)))))
(defun parse-number (number)
(if (stringp number) (parse-integer number :junk-allowed t) number))
(defun prompt-for-episode (&optional last)
(when (not last) (setf last (make-episode))) (let* ((series (prompt-read "Series Title" (episode-series last))) (title (prompt-read "Title")) (season (parse-number (prompt-read "Season" (episode-season last)))) (episode (parse-number (prompt-read "Episode" (if (eq (episode-season last) season) (1+ (episode-episode last)) 1)))) (part (parse-number (prompt-read "Part" (when (and (episode-part last) (or (eq (episode-season last) season) (eq (episode-part last) 1))) (1+ (episode-part last)))))) (date (parse-date (prompt-read "Date watched" (format-ymd (get-current-date))))) (tags (parse-tags (prompt-read "Tags")))) (make-episode :series series :title title :season season :episode episode :part part :date date :tags tags)))
(defun parse-integer-quietly (&rest args)
(ignore-errors (apply #'parse-integer args)))
(defun get-next-version (basename)
(flet ((parse-version (pathname) (or (parse-integer-quietly (string-left-trim (file-namestring basename) (file-namestring pathname)) :start 1) 0))) (let* ((files (directory (format nil "~A,*" (namestring basename)))) (max (if files (reduce #'max files :key #'parse-version) 0))) (merge-pathnames (format nil "~a,~d" (file-namestring basename) (1+ max)) basename))))
(defun save-db (dbfile database)
(let ((file (probe-file dbfile))) (rename-file file (get-next-version file)) (with-open-file (out file :direction :output) (with-standard-io-syntax (let ((*print-case* :downcase)) (pprint database out))))))
(defun watch-save (dbfile)
(save-db dbfile *db*))
(defun load-db (dbfile)
(with-open-file (in dbfile) (with-standard-io-syntax (read in))))
(defun get-series (name database)
(cdr (assoc name database :test #'string-equal)))
(defun get-episode-list (series database)
(series-episodes (get-series series database)))
(defun print-series (title series)
(format t "~&~30a ~@[ (~{~a~^ ~})~]~%~@[ ~a~%~]" title (series-tags series) (series-description series)) (format t "~{~& ~a~%~}" (reverse (series-episodes series))))
(defun watch-series (title)
(let ((series (get-series title *db*))) (when series (print-series title series))))
(defun print-all-series (database)
(loop for (title . series) in (sort database #'(lambda (a b)(compare-by-date (car (series-episodes (cdr a))) (car (series-episodes (cdr b)))))) do (terpri) (print-series title series)))
(defun watch-all-series ()
(print-all-series *db*))
(defun watch-latest ()
(format t "~{~& ~a~%~}" (sort (get-latest *db*) #'compare-by-date)))
(defun timeline-all (database)
(let* ((all (get-all database)) (max (length all)) (count max) (all-series-names nil) (all-series (make-hash-table :test 'equal))) (loop for episode in (reverse (sort all #'compare-by-date)) do (unless (gethash (episode-series episode) all-series) (setf (gethash (episode-series episode) all-series) (make-array max :initial-element nil)) (setf all-series-names (cons (episode-series episode) all-series-names))) (setf (elt (gethash (episode-series episode) all-series) (decf count)) episode)) (values all-series all-series-names max)))
(defun watch-timeline ()
(multiple-value-bind (all-series all-series-names max) (timeline-all *db*) (loop for series in all-series-names do (format t "~30a (~{~:[ ~;•~]~})~%" series (coerce (subseq (gethash series all-series) (- max 60)) 'list)))))
(defun watch-timelinec ()
(multiple-value-bind (all-series all-series-names max) (timeline-all *db*) (let ((chart (make-array (list (length all-series-names) max) :initial-element nil)) (newcol 0) (oldrow -1)) (loop for oldcol upto (1- max) do (loop for series in all-series-names for row from 0 upto (length all-series-names) do (when (elt (gethash series all-series) oldcol) (when (<= row oldrow) (incf newcol)) (setf (aref chart row newcol) (elt (gethash series all-series) oldcol)) (setf oldrow row)))) (loop for series in all-series-names for i from 0 upto (length all-series-names) do (format t "~30a (" series) (loop for j from (- newcol 60) upto newcol do (format t "~:[ ~;•~]" (aref chart i j)) (if (= j newcol) (format t ")~%")))))))
(defun watch-timelinev ()
(multiple-value-bind (all-series all-series-names max) (timeline-all *db*) (loop for series in all-series-names counting series into count do (format t "~va ~30a~%" count " " series )) (loop for i from 0 upto (1- max) do (let ((episode nil)) (loop for series in all-series-names do (format t "~:[ ~;~:*~02a~]" (when (elt (gethash series all-series) i) (setf episode (elt (gethash series all-series) i)) (episode-episode episode)))) (format t " (~a)~%" (episode-series episode))))))
(defun watch-all ()
(format t "~{~& ~a~%~}" (sort (get-all *db*) #'compare-by-date)))
(defun watch-new-series (&key name description tags)
(cdar (push (cons name (make-series :description description :tags tags)) *db*)))
(defun get-or-add-series (name database)
(or (get-series name database) (if (y-or-n-p "Add new series? [y/n]: ") (watch-new-series :name name :description (prompt-read "Description" name) :tags (parse-tags (prompt-read "Tags" "active"))) nil)))
(defun watch-add ()
(let* ((series (loop thereis (get-or-add-series (prompt-read "Series") *db*))) (episode (prompt-for-episode (car (series-episodes series))))) (push episode (series-episodes series))))
(defun watch-series-names ()
(format T "~{~& ~a~%~}" (sort (mapcar #'car *db*) (lambda (series1 series2) (compare-by-date (car (series-episodes (get-value series1 *db*))) (car (series-episodes (get-value series2 *db*))))))))
(defun exact-match (term text)
(string-equal (format nil "~{~a~^ ~}" term) text))
(defun fuzzy-match (term text)
(loop for word in term when (search word text :test 'string-equal) collect it))
(defun match-tags (term tags)
(intersection (mapcar #'intern term) tags))
(defun search-title (term database)
(loop for episode in (get-all database) when (exact-match term (episode-title episode)) collect episode))
(defun search-tags (term database)
(sort (loop for episode in (get-all database) for matches = (match-tags term (episode-tags episode)) when matches collect (list (length matches) episode)) #'> :key #'car))
(defun search-title-fuzzy (term database)
(sort (loop for episode in (get-all database) for matches = (fuzzy-match term (episode-title episode)) when matches collect (list (length matches) episode)) #'> :key #'car))
(defun search-all (term database)
(let ((exact-results '()) (fuzzy-results '()) (tag-results '())) (dolist (episode (get-all database)) (cond ((exact-match term (episode-title episode)) (push episode exact-results)) ((fuzzy-match term (episode-title episode)) (push episode fuzzy-results)) ((match-tags term (episode-tags episode)) (push episode tag-results)))) (append (sort exact-results #'compare-by-date) (sort tag-results #'compare-by-date) (sort fuzzy-results #'compare-by-date))))
(defun watch-search (term)
(format t "~{~& ~a~%~}" (search-all term *db*)))
(defun list-all-tags (database)
(let ((tags (make-hash-table :test 'equal))) (dolist (tag (apply #'append (mapcar #'episode-tags (get-all database)))) (setf (gethash tag tags) (1+ (or (gethash tag tags) 0)))) tags))
(defun watch-tags ()
(maphash #'(lambda (tag count) (format t "~a (~d) " tag count)) (list-all-tags *db*)) (terpri))
(defun find-series-episode (term database)
(let ((series (get-series (format nil "~{~a~^ ~}" (butlast term)) database))) (if series (let* ((season-episode (car (last term))) (pos (position #\- season-episode)) (season-str (when pos (subseq season-episode 0 pos))) (season (or (parse-integer-quietly season-str) season-str)) (episode-str (if pos (subseq season-episode (1+ pos)) season-episode)) (episode-nr (or (parse-integer-quietly episode-str) episode-str))) (loop for episode in (series-episodes series) when (and (equal (episode-episode episode) episode-nr) (equal (episode-season episode) season)) collect episode)) (let ((series (get-series (format nil "~{~a~^ ~}" term) database))) (if series (list (car (series-episodes series))))))))
(defun find-episode (term database)
(or (find-series-episode term database) (search-title term database) (let* ((res (or (search-tags term database) (search-title-fuzzy term database))) (max (caar res))) (loop for (matches episode) in res when (equal matches max) collect episode))))
(defun edit-episode (episode database)
(format t "editing:~%~a~%" episode) (setf (episode-series episode) (prompt-read "Series Title" (episode-series episode))) (setf (episode-title episode) (prompt-read "Title" (episode-title episode))) (setf (episode-season episode) (parse-number (prompt-read "Season" (episode-season episode)))) (setf (episode-episode episode) (parse-number (prompt-read "Episode" (episode-episode episode)))) (setf (episode-part episode) (parse-number (prompt-read "Part" (episode-part episode)))) (setf (episode-date episode) (parse-date (prompt-read "Date watched" (format-ymd (episode-date episode))))) (setf (episode-tags episode) (parse-tags (prompt-read "Tags" (format nil "~{~a~^ ~}" (episode-tags episode))))))
(defun watch-edit (term)
(let ((episodes (find-episode term *db*))) (if (> (length episodes) 1) (format t "found more than one episode, please be more specific:~%~{~& ~a~%~}" episodes) (edit-episode (car episodes) *db*))))
(defun watch-load (dbfile)
(setf *db* (load-db dbfile)))
(defun argv ()
(or #+clisp (ext:argv) #+sbcl sb-ext:*posix-argv* #+clozure (ccl::command-line-arguments) #+gcl si:*command-args* #+ecl (loop for i from 0 below (si:argc) collect (si:argv i)) #+cmu extensions:*command-line-strings* #+allegro (sys:command-line-arguments) #+lispworks sys:*line-arguments-list* nil))
(defun main (argv)
(let ((dbfile (make-pathname :name "lwatch" :type nil :defaults *load-pathname*))) (watch-load dbfile) (format t "loaded db~%") (cond ((equal (cadr argv) "add") (watch-add) (watch-save dbfile)) ((equal (cadr argv) "latest") (watch-latest)) ((null (cadr argv)) (watch-latest)) ((equal (cadr argv) "series") (watch-series-names)) ((and (equal (cadr argv) "all") (equal (caddr argv) "series")) (watch-all-series)) ((equal (cadr argv) "all") (watch-all)) ((equal (cadr argv) "tags") (watch-tags)) ((equal (cadr argv) "search") (watch-search (cddr argv))) ((equal (cadr argv) "edit") (watch-edit (cddr argv)) (watch-save dbfile)) ((equal (cadr argv) "timeline") (watch-timeline)) ((equal (cadr argv) "timelinev") (watch-timelinev)) ((equal (cadr argv) "timelinec") (watch-timelinec)) (T (watch-series (format nil "~{~a~^ ~}" (cdr argv)))))))
(main (argv))</lang>
D
<lang d>import std.stdio, std.algorithm, std.string, std.conv, std.array,
std.file, std.csv, std.datetime;
private {
immutable filename = "simdb.csv";
struct Item { string name, date, category; }
void addItem(in string[] item) { if (item.length < 3) return printUsage(); auto db = load(); const date = text(cast(DateTime)Clock.currTime); const cat = (item.length == 4) ? item[3] : "none"; db ~= Item(item[2], date, cat); store(db); }
void printLatest(in string[] a) { auto db = load(); if (db.empty) return writeln("No entries in database."); db.sort!q{ a.date > b.date }; if (a.length == 3) { foreach (item; db) if (item.category == a[2]) writefln("%s, %s, %s", item.tupleof); } else { writefln("%s, %s, %s", db[0].tupleof); } }
void printAll() { auto db = load(); if (db.empty) return writeln("No entries in database."); db.sort!q{ a.date < b.date }; foreach (item; db) writefln("%s, %s, %s", item.tupleof); }
Item[] load() { Item[] db; if (filename.exists && filename.isFile) { try { const text = filename.readText; if (!text.empty) db = csvReader!Item(text).array; } catch (CSVException e) { writeln(e.msg); } } return db; }
void store(in Item[] db) { auto f = File(filename, "w+"); foreach (item; db) f.writefln("%s,%s,%s", item.tupleof); }
void printUsage() { writeln(
`Usage:
simdb cmd [categoryName]
add add item, followed by optional category latest print last added item(s), followed by optional category all print all
For instance: add "some item name" "some category name"`); }
}
void main(in string[] args) {
if (args.length < 2 || args.length > 4) return printUsage();
switch (args[1].toLower) { case "add": addItem(args); break; case "latest": printLatest(args); break; case "all": printAll(); break; default: printUsage(); break; }
}</lang>
- Output:
C:\>simdb add item1 cat1 C:\>simdb add item2 cat2 C:\>simdb add item3 cat3 C:\>simdb add item4 C:\>simdb add item5 C:\>simdb add item6 cat4 C:\>simdb add item7 cat4 C:\>simdb latest item7, 2013-Mar-03 15:01:36, cat4 C:\>simdb latest cat4 item7, 2013-Mar-03 15:01:36, cat4 item6, 2013-Mar-03 15:01:28, cat4 C:\>simdb all item1, 2013-Mar-03 15:01:00, cat1 item2, 2013-Mar-03 15:01:06, cat2 item3, 2013-Mar-03 15:01:12, cat3 item4, 2013-Mar-03 15:01:15, none item5, 2013-Mar-03 15:01:18, none item6, 2013-Mar-03 15:01:28, cat4 item7, 2013-Mar-03 15:01:36, cat4 C:\>simdb add Usage: simdb cmd [categoryname] add add item, followed by optional category latest print last added item(s), followed by optional category all print all For instance: add "some item name" "some category name"
File:
item1,2013-Mar-03 15:01:00,cat1 item2,2013-Mar-03 15:01:06,cat2 item3,2013-Mar-03 15:01:12,cat3 item4,2013-Mar-03 15:01:15,none item5,2013-Mar-03 15:01:18,none item6,2013-Mar-03 15:01:28,cat4 item7,2013-Mar-03 15:01:36,cat4
Erlang
<lang Erlang>
- ! /usr/bin/env escript
-compile({no_auto_import,[date/0]}).
main( ["add", Tag | Descriptions] ) -> add( date(), Tag, Descriptions ); main( ["add_date", Date, Tag | Descriptions] ) -> add( date_internal(string:tokens(Date, "-")), Tag, Descriptions ); main( ["print_latest"] ) -> print_latest( contents() ); main( ["print_latest_for_each"] ) -> print_latest_for_each( contents() ); main( ["print_all_date", Date] ) -> print_all_date( date_internal(string:tokens(Date, "-")), contents() ); main( _Error ) -> usage().
add( Date, Tag, Descriptions ) -> Contents = contents(), file:write_file( file(), io_lib:format("simple_database_v1.~n~p.~n", [[{Date, Tag, Descriptions} | Contents]]) ).
date() -> Template:Date, Time = calendar:local_time(), Date.
date_external( {Year, Month, Day} ) -> string:join( [erlang:integer_to_list(Year), erlang:integer_to_list(Month), erlang:integer_to_list(Day)], "-" ); date_external( _Error ) -> usage().
date_internal( [Year, Month, Day] ) -> {erlang:list_to_integer(Year), erlang:list_to_integer(Month), erlang:list_to_integer(Day)}; date_internal( _Error ) -> usage().
file() -> "simple_database_contents".
contents() -> contents( file:consult(file()) ).
contents( {ok, [simple_database_v1, Contents]} ) -> Contents; contents( {error, Error} ) when is_atom(Error) -> []; contents( {error, _Error} ) -> io:fwrite( "Error: ~p corrupt. Starting from scratch~n", [file()] ), [].
print_all_date( _Date, [] ) -> ok; print_all_date( Date, Contents ) -> [print_latest([{D, Tag, Descriptions}]) || {D, Tag, Descriptions} <- Contents, D =:= Date].
print_latest( [] ) -> ok; print_latest( [{Date, Tag, Descriptions} | _T] ) -> io:fwrite( "~s~n", [string:join( [date_external(Date), Tag | Descriptions], " ")] ).
print_latest_for_each( [] ) -> ok; print_latest_for_each( Contents ) -> Tags = lists:usort( [Tag || {_Date, Tag, _Descriptions} <- Contents] ), [print_latest([lists:keyfind(X, 2, Contents)]) || X <- Tags].
usage() -> io:fwrite( "Usage: ~p [add | add_date <date>] tag description ...~n", [escript:script_name()] ), io:fwrite( "Or: ~p [print_latest | print_latest_for_each | print_all_date <date>]~n", [escript:script_name()] ), io:fwrite( "Date format is YYYY-MM-DD~n" ), io:fwrite( "Data stored in ~p~n", [file()] ), init:stop(). </lang> Command line session started with these file contents as database:
simple_database_v1. [{{2013,9,16},"comic",["hobbe"]}, {{2013,9,16},"comic",["gustaf"]}].
Command line session:
./simple_database add num 1 2 3 ./simple_database add_date 1991-01-01 num 999 888 ./simple_database print_latest 1991-1-1 num 999 888 ./simple_database print_latest_for_each 2013-9-16 comic hobbe 1991-1-1 num 999 888 ./simple_database print_all_date 2013-09-16 2013-9-16 num 1 2 3 2013-9-16 comic hobbe 2013-9-16 comic gustaf
Forth
Simple in-memory database. Load/dump database from/to a file.
Example file 'test.sdb':
+: betty 1974.03.03;coworker;reading; +: geea 1980.01.01;friend;sketch writer; +: tom 1991.03.07;family member;reading; +: alice 1987.09.01;coworker;classical music; +: gammaQ3.14 3045.09.09;friend;watch movies, star walking;
Example usage:
$ gforth sdb.fs help [ -erased help message- ] load test.sdb ok .keys gammaQ3.14 alice tom geea betty ok betty .record betty 1974.03.03;coworker;reading; ok newline geea .record geea 1980.01.01;friend;sketch writer; ok +: theea 1979.04.05;coworker;astronomy; ok newline .bydate gammaQ3.14 3045.09.09;friend;watch movies, star walking; tom 1991.03.07;family member;reading; alice 1987.09.01;coworker;classical music; geea 1980.01.01;friend;sketch writer; theea 1979.04.05;coworker;astronomy; betty 1974.03.03;coworker;reading; dump t2.sdb ok bye $ $ cat t2.sdb +: betty 1974.03.03;coworker;reading; +: geea 1980.01.01;friend;sketch writer; +: tom 1991.03.07;family member;reading; +: alice 1987.09.01;coworker;classical music; +: gammaQ3.14 3045.09.09;friend;watch movies, star walking; +: theea 1979.04.05;coworker;astronomy; $
Code: <lang forth>\ sdb.fs Simple database. Gforth 0.7.0 specific ' noop is bootmessage
wordlist constant USER: \ USER functions wordlist constant SDB \ DB keys wordlist constant FIELDS \ DB fields
- -SDB?EXIT ( -- ; Continue if `SDB' non-empty )
SDB cell+ @ 0= IF rdrop exit THEN ;
\ MACROS
- |comp| ( MACRO: | code to be compiled | )
char parse evaluate ; immediate
- LIST ( node -- ) ]] BEGIN @ dup WHILE >R [[ ; immediate
- LOOP-LIST ( -- ) ]] R> REPEAT drop [[ ; immediate
- UNLIST ( -- ) POSTPONE rdrop ; immediate
\ Helper words
- .; [char] ; emit ;
- !+ dup cell+ -rot ! ;
- ($+) ( cas us cad ud -- cad us+ud ; append source string to destination )
swap >R 2dup + R> swap >R tuck + swap >R swap move R> R> ;
\ --- Working Record Fields current @ FIELDS current ! 2variable person 2variable birthday 2variable relationship 2variable hobby current ! FIELDS >order
\ --- Ring list
- new-node ( -- node ; root of circular list )
here dup , ;
- do-link ( node new-node -- ; do link after current node )
over @ over ! swap ! ;
- empty-ring? ( node -- f ) dup @ = ;
- RING ( node -- ) ]] dup BEGIN @ 2dup <> WHILE 2>R [[ ; immediate
- LOOP-RING ( -- ) ]] 2R> REPEAT 2drop [[ ; immediate
- UNRING ( -- ) postpone 2RDROP ; immediate
- noname
s" " person 2! s" 9999.01.01" birthday 2! s" " relationship 2! s" " hobby 2! ; new-node swap , CONSTANT OLD
\ --- Insertion into ring
- node>xt ( node -- xt ) cell+ @ ;
- bday@ ( node -- ca u ) node>xt execute birthday 2@ ;
- datecmp ( node node -- -1|0|1 ) bday@ rot bday@ compare ;
- search-by-date ( new-node node -- node' ; Linear search by birth date )
dup RING ( new-node node ) over I datecmp 0< IF nip UNRING exit ELSE drop I THEN LOOP-RING nip ;
- insert-ordered ( new-node -- )
OLD dup empty-ring? 0= IF over swap search-by-date THEN swap do-link ;
\ --- Field compiling
- fld: [char] ; parse postpone sliteral ; immediate
- _sdb-walk ( MACRO: | xt[--] -- | )
]] -SDB?EXIT here dup SDB cell+ LIST I name>int swap !+ LOOP-LIST 1 cells literal - DO I @ |comp| -1 cells literal +LOOP [[ ; immediate
2variable category
- msg_nocategory ( -- )
." No such category!" cr ;
\ --- USER definitions user: >order current @ USER: current !
- bye bye ;
- .s .s ;
- newline cr ;
- godb SDB >order ;
- .keys SDB wordlist-words ;
- .person person 2@ type ;
- .birthday birthday 2@ type ;
- .relationship relationship 2@ type ;
- .hobby hobby 2@ type ;
- .record .person 1 spaces .birthday .; .relationship .; .hobby .; ;
- .last SDB cell+ @ dup
IF name>int execute .record ELSE drop THEN ;
- .lastbycategory ( "field-name" -- )
here 0 category 2! parse-name FIELDS search-wordlist IF sdb cell+ LIST ( xt ) I name>int execute dup execute 2@ category 2@ 2swap search nip nip 0= IF .record cr dup execute 2@ category 2@ ($+) category 2! THEN LOOP-LIST drop ELSE ['] msg_nocategory stderr OUTFILE-EXECUTE THEN ;
- .bydate ( -- )
OLD RING I node>xt execute .record cr LOOP-RING ;
- .sdb _sdb-walk | execute .record cr | ;
- sdb-dump _sdb-walk | execute ." +: " .record cr | ;
- dump ( "db-file" -- )
parse-name w/o CREATE-FILE throw dup ['] sdb-dump swap OUTFILE-EXECUTE CLOSE-FILE throw ;
- load ( "db-file" -- ) include ;
- +: ( "person birth-day;relationship;hobby; -- )
new-node dup cell+ >R 0 , \ post `xt' current @ SDB current ! \ compile into `sdb' wordlist : latestxt dup R> ! \ fix-up `xt' ]] literal >name name>string person 2! fld: birthday 2! fld: relationship 2! fld: hobby 2! ; [[ current ! insert-ordered ;
- HELP cr
cr ." KEYWORDS: " USER: wordlist-words cr ." ---" cr ." Prefix `.' means print" cr ." To add a record, type: +: name yyyy.mm.dd;relationship;hobby;" cr cr ; current ! SEAL godb</lang>
Go
<lang go>package main
import (
"encoding/json" "fmt" "io" "os" "sort" "strings" "time" "unicode"
)
// Database record format. Time stamp and name are required. // Tags and notes are optional. type Item struct {
Stamp time.Time Name string Tags []string `json:",omitempty"` Notes string `json:",omitempty"`
}
// Item implements stringer interface func (i *Item) String() string {
s := i.Stamp.Format(time.ANSIC) + "\n Name: " + i.Name if len(i.Tags) > 0 { s = fmt.Sprintf("%s\n Tags: %v", s, i.Tags) } if i.Notes > "" { s += "\n Notes: " + i.Notes } return s
}
// collection of Items type db []*Item
// db implements sort.Interface func (d db) Len() int { return len(d) } func (d db) Swap(i, j int) { d[i], d[j] = d[j], d[i] } func (d db) Less(i, j int) bool { return d[i].Stamp.Before(d[j].Stamp) }
// hard coded database file name const fn = "sdb.json"
func main() {
if len(os.Args) == 1 { latest() return } switch os.Args[1] { case "add": add() case "latest": latest() case "tags": tags() case "all": all() case "help": help() default: usage("unrecognized command") }
}
func usage(err string) {
if err > "" { fmt.Println(err) } fmt.Println(`usage: sdb [command] [data] where command is one of add, latest, tags, all, or help.`)
}
func help() {
usage("") fmt.Println(`
Commands must be in lower case. If no command is specified, the default command is latest.
Latest prints the latest item. All prints all items in chronological order. Tags prints the lastest item for each tag. Help prints this message.
Add adds data as a new record. The format is,
name [tags] [notes]
Name is the name of the item and is required for the add command.
Tags are optional. A tag is a single word. A single tag can be specified without enclosing brackets. Multiple tags can be specified by enclosing them in square brackets.
Text remaining after tags is taken as notes. Notes do not have to be enclosed in quotes or brackets. The brackets above are only showing that notes are optional.
Quotes may be useful however--as recognized by your operating system shell or command line--to allow entry of arbitrary text. In particular, quotes or escape characters may be needed to prevent the shell from trying to interpret brackets or other special characters.
Examples: sdb add Bookends // no tags, no notes sdb add Bookends rock my favorite // tag: rock, notes: my favorite sdb add Bookends [rock folk] // two tags sdb add Bookends [] "Simon & Garfunkel" // notes, no tags sdb add "Simon&Garfunkel [artist]" // name: Simon&Garfunkel, tag: artist
As shown in the last example, if you use features of your shell to pass all data as a single string, the item name and tags will still be identified by separating whitespace.
The database is stored in JSON format in the file "sdb.json" `) }
// load data for read only purposes. func load() (db, bool) {
d, f, ok := open() if ok { f.Close() if len(d) == 0 { fmt.Println("no items") ok = false } } return d, ok
}
// open database, leave open func open() (d db, f *os.File, ok bool) {
var err error f, err = os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0666) if err != nil { fmt.Println("cant open??") fmt.Println(err) return } jd := json.NewDecoder(f) err = jd.Decode(&d) // EOF just means file was empty. That's okay with us. if err != nil && err != io.EOF { fmt.Println(err) f.Close() return } ok = true return
}
// handle latest command func latest() {
d, ok := load() if !ok { return } sort.Sort(d) fmt.Println(d[len(d)-1])
}
// handle all command func all() {
d, ok := load() if !ok { return } sort.Sort(d) for _, i := range d { fmt.Println("-----------------------------------") fmt.Println(i) } fmt.Println("-----------------------------------")
}
// handle tags command func tags() {
d, ok := load() if !ok { return } // we have to traverse the entire list to collect tags so there // is no point in sorting at this point. // collect set of unique tags associated with latest item for each latest := make(map[string]*Item) for _, item := range d { for _, tag := range item.Tags { li, ok := latest[tag] if !ok || item.Stamp.After(li.Stamp) { latest[tag] = item } } } // invert to set of unique items, associated with subset of tags // for which the item is the latest. type itemTags struct { item *Item tags []string } inv := make(map[*Item][]string) for tag, item := range latest { inv[item] = append(inv[item], tag) } // now we sort just the items we will output li := make(db, len(inv)) i := 0 for item := range inv { li[i] = item i++ } sort.Sort(li) // finally ready to print for _, item := range li { tags := inv[item] fmt.Println("-----------------------------------") fmt.Println("Latest item with tags", tags) fmt.Println(item) } fmt.Println("-----------------------------------")
}
// handle add command func add() {
if len(os.Args) < 3 { usage("add command requires data") return } else if len(os.Args) == 3 { add1() } else { add4() }
}
// add command with one data string. look for ws as separators. func add1() {
data := strings.TrimLeftFunc(os.Args[2], unicode.IsSpace) if data == "" { // data must have at least some non-whitespace usage("invalid name") return } sep := strings.IndexFunc(data, unicode.IsSpace) if sep < 0 { // data consists only of a name addItem(data, nil, "") return } name := data[:sep] data = strings.TrimLeftFunc(data[sep:], unicode.IsSpace) if data == "" { // nevermind trailing ws, it's still only a name addItem(name, nil, "") return } if data[0] == '[' { sep = strings.Index(data, "]") if sep < 0 { // close bracketed list for the user. no notes. addItem(name, strings.Fields(data[1:]), "") } else { // brackets make things easy addItem(name, strings.Fields(data[1:sep]), strings.TrimLeftFunc(data[sep+1:], unicode.IsSpace)) } return } sep = strings.IndexFunc(data, unicode.IsSpace) if sep < 0 { // remaining word is a tag addItem(name, []string{data}, "") } else { // there's a tag and some data addItem(name, []string{data[:sep]}, strings.TrimLeftFunc(data[sep+1:], unicode.IsSpace)) }
}
// add command with multiple strings remaining on command line func add4() {
name := os.Args[2] tag1 := os.Args[3] if tag1[0] != '[' { // no brackets makes things easy addItem(name, []string{tag1}, strings.Join(os.Args[4:], " ")) return } if tag1[len(tag1)-1] == ']' { // tags all in one os.Arg is easy too addItem(name, strings.Fields(tag1[1:len(tag1)-1]), strings.Join(os.Args[4:], " ")) return } // start a list for tags var tags []string if tag1 > "[" { tags = []string{tag1[1:]} } for x, tag := range os.Args[4:] { if tag[len(tag)-1] != ']' { tags = append(tags, tag) } else { // found end of tag list if tag > "]" { tags = append(tags, tag[:len(tag)-1]) } addItem(name, tags, strings.Join(os.Args[5+x:], " ")) return } } // close bracketed list for the user. no notes. addItem(name, tags, "")
}
// complete the add command func addItem(name string, tags []string, notes string) {
db, f, ok := open() if !ok { return } defer f.Close() // add the item and format JSON db = append(db, &Item{time.Now(), name, tags, notes}) sort.Sort(db) js, err := json.MarshalIndent(db, "", " ") if err != nil { fmt.Println(err) return } // time to overwrite the file if _, err = f.Seek(0, 0); err != nil { fmt.Println(err) return } f.Truncate(0) if _, err = f.Write(js); err != nil { fmt.Println(err) }
}</lang>
Haskell
The database is a list of type Item stored in a StateT monad transformer that allows for IO actions to be perfomed with it (StateT [Item] IO a). All IO actions occurring inside the StateT monad (handling command line arguments and printing items) must be lifted. The database is written to disk using "show" and read back using "read". Therefore, Item is declared as an instance of both Show and Read classes. An item in the database file looks like the following: Item {description = "La traviata", category = ["Classical"], date = Date 2012 10 19, optional = ["Giuseppe Verdi","1853"]}
<lang Haskell>import Control.Monad.State import Data.List (sortBy, nub) import System.Environment (getArgs, getProgName) import System.Directory (doesFileExist) import System.IO (openFile, hGetContents, hClose, IOMode(..),
Handle, hPutStrLn)
-- for storing dates data Date = Date Integer Int Int deriving (Show, Read, Eq, Ord)
-- for storing database items data Item = Item {description :: String
,category :: [String] ,date :: Date ,optional :: [String]} deriving (Show, Read)
-- a state monad transformer which wraps IO actions. -- the database (state) is passed implicitly between functions. type ItemList a = StateT [Item] IO a
-- add an item to the database addItem :: Item -> ItemList () addItem i = modify (++ [i])
-- get the newest of a list of items latest :: [Item] -> [Item] latest [] = [] latest [x]= [x] latest xs = take 1 $ sortBy newer xs
-- compare two items to see which one is newer newer :: Item -> Item -> Ordering newer a b = compare (date b) (date a)
-- list all different categories (no duplicates) categories :: ItemList [String] categories = liftM (nub . concatMap category) get
-- list only the items with the given category tag filterByCategory :: String -> ItemList [Item] filterByCategory c = liftM (filter (\i -> c `elem` category i)) get
-- get the newest of all items lastOfAll :: ItemList [Item] lastOfAll = liftM latest get
-- get the newest item in each category latestByCategory :: ItemList [Item] latestByCategory = do
cats <- categories filt <- mapM filterByCategory cats return $ concatMap latest filt
-- sort all items chronologically, newest first sortByDate :: ItemList [Item] sortByDate = liftM (sortBy newer) get
toScreen :: Item -> IO () toScreen (Item desc cats (Date y m d) opt) = putStrLn $
"Description:\t" ++ desc ++ "\nCategories:\t" ++ show cats ++ "\nDate:\t\t" ++ show y ++ "-" ++ show m ++ "-" ++ show d ++ "\nOther info:\t" ++ show opt
-- command line argument handling -- if the user called the program with the option "add", the -- new item is returned to main so that it can be saved to disk. -- the argument "opt" is a list. arguments :: ItemList [Item] arguments = do
args <- liftIO getArgs case args of ("add":desc:cat:year:month:day:opt) -> do let newItem = parseItem args addItem newItem return [newItem] ("latest":[]) -> do item <- lastOfAll lift $ mapM_ toScreen item return [] ("category":[]) -> do items <- latestByCategory lift $ mapM_ toScreen items return [] ("all":[]) -> do sorted <- sortByDate lift $ mapM_ toScreen sorted return [] _ -> do lift usage return []
parseItem :: [String] -> Item parseItem (_:desc:cat:year:month:day:opt) =
Item {description = desc, category = words cat, date = Date (read year) (read month) (read day), optional = opt}
usage :: IO () usage = do
progName <- getProgName putStrLn $ "Usage: " ++ progName ++ " add|all|category|latest \ \OPTIONS\n\nadd \"description\" \"category1 category2\"... \ \year month day [\"note1\" \"note2\"...]\n\tAdds a new record \ \to the database.\n\nall\n\tPrints all items in chronological \ \order.\n\ncategory\n\tPrints the latest item for each category.\ \\n\nlatest\n\tPrints the latest item."
-- the program creates, reads and writes to a file in the current directory main :: IO () main = do
progName <- getProgName let fileName = progName ++ ".db" e <- doesFileExist fileName if e then do hr <- openFile fileName ReadMode f <- hGetContents hr v <- evalStateT arguments (map read $ lines f) hClose hr -- must be called after working with contents! hw <- openFile fileName AppendMode mapM_ (hPutStrLn hw . show) v hClose hw else do v <- evalStateT arguments [] hw <- openFile fileName WriteMode mapM_ (hPutStrLn hw . show) v hClose hw
</lang>
J
J comes with a sql database, jdb. Jdb's columns are memory mapped files with header information. These won't meet the human readable data file requirement. Hence, this program:
<lang j>HELP=: 0 :0 Commands:
DBNAME add DATA DBNAME display the latest entry DBNAME display the latest entry where CATEGORY contains WORD DBNAME display all entries DBNAME display all entries order by CATEGORY
1) The first add with new DBNAME assign category names. 2) lower case arguments verbatim. 3) UPPER CASE: substitute your values.
Examples, having saved this program as a file named s : $ jconsole s simple.db display all entries $ jconsole s simple.db add "first field" "2nd field" )
Q=: ' NB. quote character time=: 6!:0@:('YYYY-MM-DD:hh:mm:ss.sss'"_)
embed=: >@:{.@:[ , ] , >@:{:@:[ assert '(x+y)' -: '()' embed 'x+y'
Duplicate=: 1 :'#~ m&= + 1 #~ #' assert 0 1 2 3 3 4 -: 3 Duplicate i.5
prepare=: LF ,~ [: }:@:; (Q;Q,';')&embed@:(Q Duplicate)&.>@:(;~ time) assert (-: }.@:".@:}:@:prepare) 'boxed';;'list';'of';strings
categorize=: dyad define i=. x i. y if. (1 (~: #) i) +. i (= #) x do.
smoutput 'choose 1 of' smoutput x exit 1
end. {. i NB. "index of" frame has rank 1. ) assert 0 -: 'abc' categorize 'a'
loadsdb=: (<'time') (<0 0)} ".;._2@:(1!:1)
Dispatch=: conjunction define help
commands=. y command=. {. commands x (u`help@.(command(i.~ ;:)n)) }.commands )
NB. the long fork in show groups (": #~ (1 1 embed (1j1 }:@:# (1 #~ #)))) show=: smoutput@:(": #~ 1 1 embed 1j1 }:@:# 1 #~ #)
in=: +./@:E. assert 'the' in'quick brown fox jumps over the lazy dog' assert 'the'-.@:in'QUICK BROWN FOX JUMPS OVER THE LAZY DOG'
where=: dyad define 'category contains word'=. 3 {. y if. 'contains' -.@:-: contains do.
help exit 1
end. i=. x ({.@:[ categorize <@:]) category j=. {: I. ; word&in&.> i {"1 x if. 0 (= #) j do.
smoutput 'no matches'
else.
x (show@:{~ 0&,) j
end. )
entry=: 4 : 0 if. a: = y do.
show@:({. ,: {:) x
else.
x `where Dispatch'where' y
end. )
latest=: `entry Dispatch'entry' the=: `latest Dispatch'latest'
by=: 4 : 0 i=. x (categorize~ {.)~ y show ({. , (/: i&{"1)@:}.) x )
order=: `by Dispatch'by'
entries=: 4 : 0 if. a: = y do.
show x
else.
x `order Dispatch'order' y
end. )
all=: `entries Dispatch'entries'
help=: smoutput@:(HELP"_) add=: 1!:3~ prepare NB. minimal---no error tests display=: (the`all Dispatch'the all'~ loadsdb)~ NB. load the simple db for some sort of display
({. add`display Dispatch'add display' }.)@:(2&}.)ARGV
exit 0 </lang> Assume the j code is stored in file s . These bash commands, stored in file input , create a database using add . <lang sh>D='jconsole s dataflow'
$D add name expression algebraic rank valence example explanation $D add insert 'f/ y' 'insert f within y' infinite dyad 'sum=: +/' 'continued_fraction=:+`%/' $D add fork '(f g h)y' 'g(f(y),h(y))' infinite monad 'average=: +/ % #' 'sum divided by tally' $D add hook '(f g)y' 'f(y,g(y))' infinite monad '(/: 2&{"1)table' 'sort by third column' $D add hook 'x(f g)y' 'f(x,g(y))' infinite dyad 'display verb in s' 'a reflexive dyadic hook' $D add fork 'x(f g h)y' 'g(f(x,y),h(x,y))' infinite dyad '2j1(+ * -)9 12' 'product of sum and difference' $D add reflexive 'f~ y' 'f(y,y)' infinite monad '^~y' 'y raised to the power of y' $D add passive 'x f~ y' 'f(y,x)' 'ranks of f' dyad '(%~ i.@:>:) 8x' '8 intervals from 0 to 1' $D add atop 'f@g y' 'f(g(y))' 'rank of g' monad '*:@(+/)' 'square the sum' $D add atop 'x f@g y' 'f(g(x,y))' 'rank of g' dyad '>@{.' '(lisp) open the car' $D add 'many more!' </lang> Now we look up data from the bash command line. <lang sh> $ . input # source the input $ echo $D jconsole s dataflow $ $D display the latest entry ┌───────────────────────┬──────────┬──────────┬─────────┬────┬───────┬───────┬───────────┐ │time │name │expression│algebraic│rank│valence│example│explanation│ │2012-02-07:20:36:54.749│many more!│ │ │ │ │ │ │ └───────────────────────┴──────────┴──────────┴─────────┴────┴───────┴───────┴───────────┘ $ $D display the latest entry where 'part of speech' contains verb choose 1 of ┌────┬────┬──────────┬─────────┬────┬───────┬───────┬───────────┐ │time│name│expression│algebraic│rank│valence│example│explanation│ └────┴────┴──────────┴─────────┴────┴───────┴───────┴───────────┘ $ $D display the latest entry where example contains average ┌───────────────────────┬────┬──────────┬────────────┬────────┬───────┬────────────────┬────────────────────┐ │time │name│expression│algebraic │rank │valence│example │explanation │ │2012-02-07:20:36:54.564│fork│(f g h)y │g(f(y),h(y))│infinite│monad │average=: +/ % #│sum divided by tally│ └───────────────────────┴────┴──────────┴────────────┴────────┴───────┴────────────────┴────────────────────┘ $ $D display all entries ordre by valence # oops! transposition typo. Commands:
DBNAME add DATA DBNAME display the latest entry DBNAME display the latest entry where CATEGORY contains WORD DBNAME display all entries DBNAME display all entries order by CATEGORY
1) The first add with new DBNAME assign category names. 2) lower case arguments verbatim. 3) UPPER CASE: substitute your values.
Examples, having saved this program as a file named s : $ jconsole s simple.db display all entries $ jconsole s simple.db add "first field" "2nd field"
$ $D display all entries order by valence ┌───────────────────────┬──────────┬──────────┬─────────────────┬──────────┬───────┬─────────────────┬─────────────────────────────┐ │time │name │expression│algebraic │rank │valence│example │explanation │ │2012-02-08:23:45:06.539│many more!│ │ │ │ │ │ │ │2012-02-08:23:45:06.329│insert │f/ y │insert f within y│infinite │dyad │sum=: +/ │continued_fraction=:+`%/ │ │2012-02-08:23:45:06.400│hook │x(f g)y │f(x,g(y)) │infinite │dyad │display verb in s│a reflexive dyadic hook │ │2012-02-08:23:45:06.426│fork │x(f g h)y │g(f(x,y),h(x,y)) │infinite │dyad │2j1(+ * -)9 12 │product of sum and difference│ │2012-02-08:23:45:06.471│passive │x f~ y │f(y,x) │ranks of f│dyad │(%~ i.@:>:) 8x │8 intervals from 0 to 1 │ │2012-02-08:23:45:06.515│atop │x f@g y │f(g(x,y)) │rank of g │dyad │>@{. │(lisp) open the car │ │2012-02-08:23:45:06.353│fork │(f g h)y │g(f(y),h(y)) │infinite │monad │average=: +/ % # │sum divided by tally │ │2012-02-08:23:45:06.376│hook │(f g)y │f(y,g(y)) │infinite │monad │(/: 2&{"1)table │sort by third column │ │2012-02-08:23:45:06.448│reflexive │f~ y │f(y,y) │infinite │monad │^~y │y raised to the power of y │ │2012-02-08:23:45:06.493│atop │f@g y │f(g(y)) │rank of g │monad │*:@(+/) │square the sum │ └───────────────────────┴──────────┴──────────┴─────────────────┴──────────┴───────┴─────────────────┴─────────────────────────────┘ $ $D display all entries ┌───────────────────────┬──────────┬──────────┬─────────────────┬──────────┬───────┬─────────────────┬─────────────────────────────┐ │time │name │expression│algebraic │rank │valence│example │explanation │ │2012-02-08:23:45:06.329│insert │f/ y │insert f within y│infinite │dyad │sum=: +/ │continued_fraction=:+`%/ │ │2012-02-08:23:45:06.353│fork │(f g h)y │g(f(y),h(y)) │infinite │monad │average=: +/ % # │sum divided by tally │ │2012-02-08:23:45:06.376│hook │(f g)y │f(y,g(y)) │infinite │monad │(/: 2&{"1)table │sort by third column │ │2012-02-08:23:45:06.400│hook │x(f g)y │f(x,g(y)) │infinite │dyad │display verb in s│a reflexive dyadic hook │ │2012-02-08:23:45:06.426│fork │x(f g h)y │g(f(x,y),h(x,y)) │infinite │dyad │2j1(+ * -)9 12 │product of sum and difference│ │2012-02-08:23:45:06.448│reflexive │f~ y │f(y,y) │infinite │monad │^~y │y raised to the power of y │ │2012-02-08:23:45:06.471│passive │x f~ y │f(y,x) │ranks of f│dyad │(%~ i.@:>:) 8x │8 intervals from 0 to 1 │ │2012-02-08:23:45:06.493│atop │f@g y │f(g(y)) │rank of g │monad │*:@(+/) │square the sum │ │2012-02-08:23:45:06.515│atop │x f@g y │f(g(x,y)) │rank of g │dyad │>@{. │(lisp) open the car │ │2012-02-08:23:45:06.539│many more!│ │ │ │ │ │ │ └───────────────────────┴──────────┴──────────┴─────────────────┴──────────┴───────┴─────────────────┴─────────────────────────────┘ $ cat dataflow '2012-02-08:23:45:06.304';'name';'expression';'algebraic';'rank';'valence';'example';'explanation' '2012-02-08:23:45:06.329';'insert';'f/ y';'insert f within y';'infinite';'dyad';'sum=: +/';'continued_fraction=:+`%/' '2012-02-08:23:45:06.353';'fork';'(f g h)y';'g(f(y),h(y))';'infinite';'monad';'average=: +/ % #';'sum divided by tally' '2012-02-08:23:45:06.376';'hook';'(f g)y';'f(y,g(y))';'infinite';'monad';'(/: 2&{"1)table';'sort by third column' '2012-02-08:23:45:06.400';'hook';'x(f g)y';'f(x,g(y))';'infinite';'dyad';'display verb in s';'a reflexive dyadic hook' '2012-02-08:23:45:06.426';'fork';'x(f g h)y';'g(f(x,y),h(x,y))';'infinite';'dyad';'2j1(+ * -)9 12';'product of sum and difference' '2012-02-08:23:45:06.448';'reflexive';'f~ y';'f(y,y)';'infinite';'monad';'^~y';'y raised to the power of y' '2012-02-08:23:45:06.471';'passive';'x f~ y';'f(y,x)';'ranks of f';'dyad';'(%~ i.@:>:) 8x';'8 intervals from 0 to 1' '2012-02-08:23:45:06.493';'atop';'f@g y';'f(g(y))';'rank of g';'monad';'*:@(+/)';'square the sum' '2012-02-08:23:45:06.515';'atop';'x f@g y';'f(g(x,y))';'rank of g';'dyad';'>@{.';'(lisp) open the car' '2012-02-08:23:45:06.539';'many more!' $ </lang>
PicoLisp
The 'rc' resource file handling function is used typically for such tasks. It also takes care of proper locking and protection. <lang PicoLisp>#!/usr/bin/pil
(de usage ()
(prinl "Usage:^J\ sdb <file> add <title> <cat> <date> ... Add a new entry^J\ sdb <file> get <title> Retrieve an entry^J\ sdb <file> latest Print the latest entry^J\ sdb <file> categories Print the latest for each cat^J\ sdb <file> Print all, sorted by date" ) )
(de printEntry (E)
(apply println (cdddr E) (car E) (cadr E) (datStr (caddr E))) )
(ifn (setq *File (opt))
(usage) (case (opt) (add (let (Ttl (opt) Cat (opt)) (if (strDat (opt)) (rc *File Ttl (cons Cat @ (argv))) (prinl "Bad date") ) ) ) (get (let Ttl (opt) (when (rc *File Ttl) (printEntry (cons Ttl @)) ) ) ) (latest (printEntry (maxi caddr (in *File (read)))) ) (categories (for Cat (by cadr group (in *File (read))) (printEntry (maxi caddr Cat)) ) ) (NIL (mapc printEntry (by caddr sort (in *File (read)))) ) (T (usage)) ) )
(bye)</lang> Test:
$ sdb CDs add "Title 1" "Category 1" 2011-11-13 $ sdb CDs add "Title 2" "Category 2" 2011-11-12 $ sdb CDs add "Title 3" "Category 1" 2011-11-14 foo bar $ sdb CDs add "Title 4" "Category 2" 2011-11-15 mumble $ sdb CDs get "Title 3" "Title 3" "Category 1" "2011-11-14" "foo" "bar" $ sdb CDs latest "Title 4" "Category 2" "2011-11-15" "mumble" $ sdb CDs categories "Title 4" "Category 2" "2011-11-15" "mumble" "Title 3" "Category 1" "2011-11-14" "foo" "bar" $ sdb CDs "Title 2" "Category 2" "2011-11-12" "Title 1" "Category 1" "2011-11-13" "Title 3" "Category 1" "2011-11-14" "foo" "bar" "Title 4" "Category 2" "2011-11-15" "mumble"
Pike
(simplified)
<lang Pike>mapping db = ([]);
mapping make_episode(string series, string title, string episode, array date) {
return ([ "series":series, "episode":episode, "title":title, "date":date ]);
}
void print_episode(mapping episode) {
write(" %-30s %10s %-30s (%{%d.%})\n", episode->series, episode->episode, episode->title, episode->date);
}
void print_series(mapping series) {
write("%-30s %-10s\n", series->series, series->status); map(series->episodes, print_episode);
}
void dump_db(mapping database) {
foreach(database; string name; mapping series) { print_series(series); }
}
array get_latest(mapping database) {
array latest = ({}); foreach(database; string name; mapping series) { latest += ({ series->episodes[0] }); } return latest;
}
int(0..1) compare_date(array a, array b) {
if (!arrayp(a) && !arrayp(b)) return false; if (!arrayp(a) || !sizeof(a)) return arrayp(b) && sizeof(b); if (!arrayp(b) || !sizeof(b)) return arrayp(a) && sizeof(a); if (a[0] == b[0]) return compare_date(a[1..], b[1..]); return a[0] < b[0];
}
int(0..1) compare_by_date(mapping a, mapping b) {
return compare_date(reverse(a->date), reverse(b->date));
}
void watch_list(mapping database) {
map(Array.sort_array(get_latest(database), compare_by_date), print_episode);
}
string prompt_read(string prompt) {
write("%s: ", prompt); return Stdio.stdin.gets();
}
array parse_date(string date) {
return (array(int))(date/".");
}
mapping prompt_for_episode() {
return make_episode(prompt_read("Series"), prompt_read("Title"), prompt_read("Episode"), parse_date(prompt_read("Date watched")));
}
// pike offers encode_value() and decode_value() as standard ways // to save and read data, but that is not a human readable format. // therefore we are instead printing the structure as debug-output // which is a readable form as long as it only contains integers, // strings, mappings, arrays and multisets this format can be read by pike. // to read it we are creating a class that contains the data as a value, // which is then compiled and instantiated to allow us to pull the data out. void save_db(string filename, mapping database) {
Stdio.write_file(filename, sprintf("%O", database));
}
void watch_save() {
save_db("pwatch", db);
}
mapping load_db(string filename) {
if (file_stat(filename)) return compile_string("mixed data = " + Stdio.read_file(filename) + ";")()->data; else return ([]);
}
mapping get_series(string name, mapping database) {
return database[name];
}
array get_episode_list(string series, mapping database) {
return database[series]->episodes;
}
void watch_new_series(string name, string status, mapping database) {
database[name] = (["series":name, "status":status, "episodes":({}) ]);
}
mapping get_or_add_series(string name, mapping database) {
if (!database[name]) { string answer = prompt_read("Add new series? [y/n]: "); if (answer == "y") watch_new_series(name, "active", database); } return database[name];
}
void watch_add(mapping database) {
mapping episode = prompt_for_episode(); string series_name = episode->series; mapping series = get_or_add_series(series_name, database); if (!series) watch_add(database); else series->episodes = Array.unshift(series->episodes, episode);
}
void watch_load() {
db = load_db("pwatch");
}
int main(int argc, array argv) {
watch_load(); if (argc>1 && argv[1] == "add") { watch_add(db); watch_save(); } else watch_list(db);
}</lang>
Python
<lang python>#!/usr/bin/python3
\ Simple database for: http://rosettacode.org/wiki/Simple_database
import argparse from argparse import Namespace import datetime import shlex
def parse_args():
'Set up, parse, and return arguments' parser = argparse.ArgumentParser(epilog=globals()['__doc__'])
parser.add_argument('command', choices='add pl plc pa'.split(), help=\
add: Add a new entry pl: Print the latest entry plc: Print the latest entry for each category/tag pa: Print all entries sorted by a date)
parser.add_argument('-d', '--description', help='A description of the item. (e.g., title, name)') parser.add_argument('-t', '--tag', help=(A category or tag (genre, topic, relationship such as “friend” or “family”))) parser.add_argument('-f', '--field', nargs=2, action='append', help='Other optional fields with value (can be repeated)')
return parser
def do_add(args, dbname):
'Add a new entry' if args.description is None: args.description = if args.tag is None: args.tag = del args.command print('Writing record to %s' % dbname) with open(dbname, 'a') as db: db.write('%r\n' % args)
def do_pl(args, dbname):
'Print the latest entry' print('Getting last record from %s' % dbname) with open(dbname, 'r') as db: for line in db: pass record = eval(line) del record._date print(str(record))
def do_plc(args, dbname):
'Print the latest entry for each category/tag' print('Getting latest record for each tag from %s' % dbname) with open(dbname, 'r') as db: records = [eval(line) for line in db] tags = set(record.tag for record in records) records.reverse() for record in records: if record.tag in tags: del record._date print(str(record)) tags.discard(record.tag) if not tags: break
def do_pa(args, dbname):
'Print all entries sorted by a date' print('Getting all records by date from %s' % dbname) with open(dbname, 'r') as db: records = [eval(line) for line in db] for record in records: del record._date print(str(record))
def test():
import time parser = parse_args() for cmdline in [ """-d Book -f title 'Windy places' -f type hardback --tag DISCOUNT add""", """-d Book -f title 'RC spammers' -f type paperback -t DISCOUNT add""", """-d Book -f title 'Splat it' -f type hardback -f special 'first edition' -t PREMIUM add""", """pl""", """plc""", ]: args = parser.parse_args(shlex.split(cmdline)) now = datetime.datetime.utcnow() args._date = now.isoformat() do_command[args.command](args, dbname) time.sleep(0.5)
do_command = dict(add=do_add, pl=do_pl, plc=do_plc, pa=do_pa) dbname = '_simple_db_db.py'
if __name__ == '__main__':
if 0: test() else: parser = parse_args() args = parser.parse_args() now = datetime.datetime.utcnow() args._date = now.isoformat() do_command[args.command](args, dbname)</lang>
- Sample session (Unix)
paddy3118:~$ ./simple_db.py -h usage: simple_db.py [-h] [-d DESCRIPTION] [-t TAG] [-f FIELD FIELD] {add,pl,plc,pa} positional arguments: {add,pl,plc,pa} add: Add a new entry pl: Print the latest entry plc: Print the latest entry for each category/tag pa: Print all entries sorted by a date optional arguments: -h, --help show this help message and exit -d DESCRIPTION, --description DESCRIPTION A description of the item. (e.g., title, name) -t TAG, --tag TAG A category or tag (genre, topic, relationship such as “friend” or “family”) -f FIELD FIELD, --field FIELD FIELD Other optional fields with value (can be repeated) Simple database for: http://rosettacode.org/wiki/Simple_database paddy3118:~$ ./simple_db.py -d Book -f title 'Windy places' -f type hardback --tag DISCOUNT add Writing record to _simple_db_db.py paddy3118:~$ ./simple_db.py -d Book -f title 'RC spammers' -f type paperback -t DISCOUNT add Writing record to _simple_db_db.py paddy3118:~$ ./simple_db.py -d Book -f title 'Splat it' -f type hardback -f special 'first edition' -t PREMIUM add Writing record to _simple_db_db.py paddy3118:~$ ./simple_db.py pl Getting last record from _simple_db_db.py Namespace(description='Book', field=[['title', 'Splat it'], ['type', 'hardback'], ['special', 'first edition']], tag='PREMIUM') paddy3118:~$ ./simple_db.py plc Getting latest record for each tag from _simple_db_db.py Namespace(description='Book', field=[['title', 'Splat it'], ['type', 'hardback'], ['special', 'first edition']], tag='PREMIUM') Namespace(description='Book', field=[['title', 'RC spammers'], ['type', 'paperback']], tag='DISCOUNT') paddy3118:~$ ./simple_db.py pa Getting all records by date from _simple_db_db.py Namespace(description='Book', field=[['title', 'Windy places'], ['type', 'hardback']], tag='DISCOUNT') Namespace(description='Book', field=[['title', 'RC spammers'], ['type', 'paperback']], tag='DISCOUNT') Namespace(description='Book', field=[['title', 'Splat it'], ['type', 'hardback'], ['special', 'first edition']], tag='PREMIUM') paddy3118:~$ paddy3118:~$ cat _simple_db_db.py Namespace(_date='2012-08-18T06:02:44.947091', description='Book', field=[['title', 'Windy places'], ['type', 'hardback']], tag='DISCOUNT') Namespace(_date='2012-08-18T06:03:11.477429', description='Book', field=[['title', 'RC spammers'], ['type', 'paperback']], tag='DISCOUNT') Namespace(_date='2012-08-18T06:03:34.319799', description='Book', field=[['title', 'Splat it'], ['type', 'hardback'], ['special', 'first edition']], tag='PREMIUM') paddy3118:~$
Racket
<lang racket>
- !/usr/bin/env racket
- lang racket
(define (*write file data) ; write data in human readable format (sexpr/line)
(with-output-to-file file #:exists 'replace (lambda () (for ([x data]) (printf "~s\n" x)))))
(define *read file->list) ; read our "human readable format"
(command-line
#:once-any [("-a") file title category date "Add an entry" (*write file `(,@(*read file) (,title ,category ,date)))] [("-d") file title "Delete an entry (all matching)" (*write file (filter-not (lambda (x) (equal? (car x) title)) (*read file)))] [("-p") file mode "Print entries, mode = latest, latest/cat, all, by-date" (define data (*read file)) (define (show item) (match item [(list title cat date) (printf "[~a] ~a; ~a\n" cat title date)])) (case (string->symbol mode) [(all) (for-each show data)] [(by-date) (for-each show (sort data string<? #:key cadr))] [(latest) (show (last data))] [(latest/cat) (define (last/cat c) (for/last ([x data] #:when (equal? c (cadr x))) x)) (for-each (compose1 show last/cat) (remove-duplicates (map cadr data)))] [else (error 'sdb "bad printout mode")])])
</lang>
Sample run:
$ ./sdb -h sdb [ <option> ... ] where <option> is one of / -a <file> <title> <category> <date> : Add an entry | -d <file> <title> : Delete an entry (all matching) \ -p <file> <mode> : Print entries, mode = latest, latest/cat, all --help, -h : Show this help -- : Do not treat any remaining argument as a switch (at this level) /|\ Brackets indicate mutually exclusive options. Multiple single-letter switches can be combined after one `-'; for example: `-h-' is the same as `-h --' $ ./sdb -a Stuffs A-Book books 2013-01-01 $ ./sdb -a Stuffs Another-Book books 2013-01-02 $ ./sdb -a Stuffs Some-CD cds 2013-01-03 $ ./sdb -a Stuffs A-Bad-CD cds 2013-01-04 $ ./sdb -p Stuffs all [books] A-Book; 2013-01-01 [books] Another-Book; 2013-01-02 [cds] Some-CD; 2013-01-03 [cds] A-Bad-CD; 2013-01-04 $ ./sdb -p Stuffs latest [cds] A-Bad-CD; 2013-01-04 $ ./sdb -p Stuffs latest/cat [books] Another-Book; 2013-01-02 [cds] A-Bad-CD; 2013-01-04 $ ./sdb -d Stuffs A-Bad-CD $ ./sdb -p Stuffs by-date [books] A-Book; 2013-01-01 [books] Another-Book; 2013-01-02 [cds] Some-CD; 2013-01-03
REBOL
<lang rebol>rebol [author: "Nick Antonaccio"] write/append %rdb "" db: load %rdb switch system/options/args/1 [
"new" [write/append %rdb rejoin [now " " mold/only next system/options/args newline]] "latest" [print copy/part tail sort/skip db 4 -4] "latestcat" [ foreach cat unique extract at db 3 4 [ t: copy [] foreach [a b c d] db [if c = cat [append t reduce [a b c d]]] print copy/part tail sort/skip t 4 -4 ] ] "sort" [probe sort/skip db 4]
] halt</lang>
Ruby
<lang ruby>require 'date' require 'json' require 'securerandom'
class SimpleDatabase
def initialize(dbname, *fields) @dbname = dbname @filename = @dbname + ".dat" @fields = fields @maxl = @fields.collect {|f| f.length}.max @data = { 'fields' => fields, 'items' => {}, 'history' => [], 'tags' => {}, } end attr_reader :dbname, :fields
def self.open(dbname) db = new(dbname) db.read db end
def read() if not File.exists?(@filename) raise ArgumentError, "Database #@dbname has not been created" end @data = JSON.parse(File.read(@filename)) @fields = @data['fields'] @maxl = @fields.collect {|f| f.length}.max end
def write() File.open(@filename, 'w') {|f| f.write(JSON.generate(@data))} end
def add(*values) id = SecureRandom.uuid @data['items'][id] = Hash[ @fields.zip(values) ] @data['history'] << [Time.now.to_f, id] id end
def tag(id, *tags) tags.each do |tag| if @data['tags'][tag].nil? @data['tags'][tag] = [id] else @data['tags'][tag] << id end end id end
def latest @data['history'].sort_by {|val| val[0]}.last.last end
def get_item(id) @data['items'][id] end
def tags() @data['tags'].keys.sort end
def ids_for_tag(tag) @data['tags'][tag] end
def tags_for_id(id) @data['tags'].keys.inject([]) do |tags, tag| tags << tag if @data['tags'][tag].include?(id) tags end end
def display(id) item = get_item(id) fmt = "%#{@maxl}s - %s\n" puts fmt % ['id', id] @fields.each {|f| print fmt % [f, item[f]]} puts fmt % ['tags', tags_for_id(id).join(',')] added = @data['history'].find {|x| x[1] == id}.first puts fmt % ['date added', Time.at(added).ctime] puts "" end
def each() @data['history'].each {|time, id| yield id} end
def each_item_with_tag(tag) @data['tags'][tag].each {|id| yield id} end
end def usage()
puts <<END
usage: #{$0} command args ...
commands:
help create dbname field ... fields dbname add dbname value ... tag dbname id tag ... tags dbname list dbname [tag ...] latest dbname latest_by_tag dbname
END end
def open_database(args)
dbname = args.shift begin SimpleDatabase.open(dbname) rescue ArgumentError => e STDERR.puts e.message exit 1 end
end
def process_command_line(command, *args)
case command when 'help' usage
when 'create' db = SimpleDatabase.new(*args) db.write puts "Database #{args[0]} created"
when 'fields' db = open_database(args) puts "Database #{db.dbname} fields:" puts db.fields.join(',')
when 'add' db = open_database(args) id = db.add(*args) db.write puts "Database #{db.dbname} added id #{id}"
when 'tag' db = open_database(args) id = args.shift db.tag(id, *args) db.write db.display(id) when 'tags' db = open_database(args) puts "Database #{db.dbname} tags:" puts db.tags.join(',')
when 'list' db = open_database(args) if args.empty? db.each {|id| db.display(id)} else args.each do |tag| puts "Items tagged #{tag}" db.each_item_with_tag(tag) {|id| db.display(id)} end end
when 'latest' db = open_database(args) db.display(db.latest)
when 'latest_by_tag' db = open_database(args) db.tags.each do |tag| puts tag db.display(db.ids_for_tag(tag).last) end
else puts "Error: unknown command '#{command}'" usage end
end
process_command_line *ARGV</lang>
Sample session
$ ruby simple_database.rb create appointments event_title start_time stop_time location event_description Database appointments created $ ruby simple_database.rb add appointments "Wife's Birthday" "2011-11-01" "2011-11-01" "" "happy 39th" Database appointments added id 6dd02195-1efe-40d1-b43e-c2efd852cd1d $ ruby simple_database.rb tag appointments 6dd02195-1efe-40d1-b43e-c2efd852cd1d birthday family id - 6dd02195-1efe-40d1-b43e-c2efd852cd1d event_title - Wife's Birthday start_time - 2011-11-01 stop_time - 2011-11-01 location - event_description - happy 39th tags - birthday,family date added - Thu Nov 3 15:52:31 2011 $ ruby simple_database.rb add appointments "Parent-Teacher Conference" "2011-11-03 19:30" "2011-11-03 20:00" "school" "desc" Database appointments added id 0190b835-401d-42da-9ed3-1d335d27b83c $ ruby simple_database.rb tag appointments 0190b835-401d-42da-9ed3-1d335d27b83c school family id - 0190b835-401d-42da-9ed3-1d335d27b83c event_title - Parent-Teacher Conference start_time - 2011-11-03 19:30 stop_time - 2011-11-03 20:00 location - school event_description - desc tags - family,school date added - Thu Nov 3 15:54:05 2011 $ ruby simple_database.rb add appointments "Buy gift for wife" "2011-10-31 16:00" "2011-10-31 16:30" "the mall" "hmm, maybe jewelery?" Database appointments added id 4023e6f1-bcc1-49e5-a59f-138157b413f4 $ ruby simple_database.rb tag appointments 4023e6f1-bcc1-49e5-a59f-138157b413f4 family last-minute id - 4023e6f1-bcc1-49e5-a59f-138157b413f4 event_title - Buy gift for wife start_time - 2011-10-31 16:00 stop_time - 2011-10-31 16:30 location - the mall event_description - hmm, maybe jewelery? tags - family,last-minute date added - Thu Nov 3 15:55:02 2011 $ ruby simple_database.rb fields appointments Database appointments fields: event_title,start_time,stop_time,location,event_description $ ruby simple_database.rb tags appointments Database appointments tags: birthday,family,last-minute,school $ ruby simple_database.rb list appointments id - 6dd02195-1efe-40d1-b43e-c2efd852cd1d event_title - Wife's Birthday start_time - 2011-11-01 stop_time - 2011-11-01 location - event_description - happy 39th tags - birthday,family date added - Thu Nov 3 15:52:31 2011 id - 0190b835-401d-42da-9ed3-1d335d27b83c event_title - Parent-Teacher Conference start_time - 2011-11-03 19:30 stop_time - 2011-11-03 20:00 location - school event_description - desc tags - family,school date added - Thu Nov 3 15:54:05 2011 id - 4023e6f1-bcc1-49e5-a59f-138157b413f4 event_title - Buy gift for wife start_time - 2011-10-31 16:00 stop_time - 2011-10-31 16:30 location - the mall event_description - hmm, maybe jewelery? tags - family,last-minute date added - Thu Nov 3 15:55:02 2011 $ cat appointments.dat {"fields":["event_title","start_time","stop_time","location","event_description"],"items":{"6dd02195-1efe-40d1-b43e-c2efd852cd1d":{"event_title":"Wife's Birthday","start_time":"2011-11-01","stop_time":"2011-11-01","location":"","event_description":"happy 39th"},"0190b835-401d-42da-9ed3-1d335d27b83c":{"event_title":"Parent-Teacher Conference","start_time":"2011-11-03 19:30","stop_time":"2011-11-03 20:00","location":"school","event_description":"desc"},"4023e6f1-bcc1-49e5-a59f-138157b413f4":{"event_title":"Buy gift for wife","start_time":"2011-10-31 16:00","stop_time":"2011-10-31 16:30","location":"the mall","event_description":"hmm, maybe jewelery?"}},"history":[[1320349951.000625,"6dd02195-1efe-40d1-b43e-c2efd852cd1d"],[1320350045.4736252,"0190b835-401d-42da-9ed3-1d335d27b83c"],[1320350102.9486248,"4023e6f1-bcc1-49e5-a59f-138157b413f4"]],"tags":{"birthday":["6dd02195-1efe-40d1-b43e-c2efd852cd1d"],"family":["6dd02195-1efe-40d1-b43e-c2efd852cd1d","0190b835-401d-42da-9ed3-1d335d27b83c","4023e6f1-bcc1-49e5-a59f-138157b413f4"],"school":["0190b835-401d-42da-9ed3-1d335d27b83c"],"last-minute":["4023e6f1-bcc1-49e5-a59f-138157b413f4"]}}
Run BASIC
<lang runbasic>sqliteconnect #sql, "f:\client.db" ' Connect to the DB
' ------------------------------- ' show user options ' ------------------------------- [sho] cls ' clear screen button #acd, "Add a new entry", [add] button #acd, "Print the latest entry", [last] button #acd, "Print the latest entry for each category", [lastCat] button #acd, "Print all entries sorted by a date", [date] button #ex, "Exit", [exit] wait
' ------------------------------------ ' add a new entry (user input screen) ' ------------------------------------ [add] cls ' clear the screen
html "
" html "" html "Client Maintenance | |
Client Num | "
textbox #clientNum,clientNum$,5html " |
Name | "
textbox #name,name$,30html " |
Client Date | "
textbox #clientDate,clientDate$,19html " |
Category | "
textbox #category,category$,10html " |
"
button #acd, "Add", [addIt] button #ex, "Exit", [sho]html " |
"
wait
' --------------------------------------------- ' Get data from the screen ' --------------------------------------------- [addIt] clientNum = #clientNum contents$() name$ = trim$(#name contents$()) clientDate$ = trim$(#clientDate contents$()) category$ = trim$(#category contents$()) dbVals$ = clientNum;",'";name$;"','";clientDate$;"','";category$;"'" sql$ = "INSERT into client VALUES ("; dbVals$ ; ")"
- sql execute(sql$)
goto [sho]
' ------------------------------------ ' Select last entry ' ------------------------------------ [last] sql$ = "SELECT *,client.rowid as rowid FROM client ORDER BY rowid desc LIMIT 1" what$ = "---- Last Entry ----" goto [shoQuery]
' ------------------------------------ ' Select by category (Last date only) ' ------------------------------------ [lastCat] sql$ = "SELECT * FROM client WHERE client.clientDate = (SELECT max(c.clientDate) FROM client as c WHERE c.category = client.category) ORDER BY category" what$ = "---- Last Category Sequence ----" goto [shoQuery]
' ------------------------------------ ' Select by date ' ------------------------------------ [date] sql$ = "SELECT * FROM client ORDER BY clientDate" what$ = "---- By Date ----"
[shoQuery] cls print what$
html "
" html "" ' heading- sql execute(sql$)
WHILE #sql hasanswer()
#row = #sql #nextrow() clientNum = #row clientNum() name$ = #row name$() clientDate$ = #row clientDate$() category$ = #row category$()html ""
WEND
html "Client Num | Name | Client Date | Category |
";clientNum;" | ";name$;" | ";clientDate$;" | ";category$;" |
"
button #c, "Continue", [sho] wait
' ------ the end ------- [exit] end</lang> Output:
User Input ----
Client Maintenance | |
Client Num | 5 |
Name | Dawnridge Winery |
Client Date | 2008-06-18 22:16 |
Category | wine |
[Add] [Exit] |
Last Entry ----
Client Num | Name | Client Date | Category |
5 | Dawnridge Winery | 2008-06-18 22:16 | wine |
Last category Sequence ----
Client Num | Name | Client Date | Category |
1 | Home Sales | 2012-01-01 10;20 | broker |
4 | Back 40 Equipment | 2009-09-18 20:18 | farm |
3 | Floral Designs | 2010-10-14 09:16 | flowers |
2 | Best Foods | 2011-02-02 12:33 | food |
5 | Dawnridge Winery | 2008-06-18 22:16 | wine |
Date Sequence ----
Client Num | Name | Client Date | Category |
5 | Dawnridge Winery | 2008-06-18 22;16 | wine |
4 | Back 40 Equipment | 2009-09-18 20:18 | farm |
3 | Floral Designs | 2010-10-14 09:16 | flowers |
2 | Best Foods | 2011-02-02 12:33 | food |
1 | Home Sales | 2012-01-01 10:20 | broker |
Tcl
The format used is that of a Tcl dictionary, where each entry uses the title as a key and the remaining information (category, date and miscellaneous metadata) is the value associated with it. The only variation from the standard internal format is that entries are separated by newlines instead of spaces; this is still a legal value, but is a non-canonical. <lang tcl>#!/usr/bin/env tclsh8.6 package require Tcl 8.6 namespace eval udb {
variable db {}
proc Load {filename} {
variable db if {[catch {set f [open $filename]}]} { set db {} return } set db [read $f] close $f
} proc Store {filename} {
variable db if {[catch {set f [open $filename w]}]} return dict for {nm inf} $db { puts $f [list $nm $inf] } close $f
}
proc add {title category {date "now"} args} {
variable db if {$date eq "now"} { set date [clock seconds] } else { set date [clock scan $date] } dict set db $title [list $category $date $args] return
} proc Rec {nm cat date xtra} {
dict create description $nm category $cat date [clock format $date] \ {*}$xtra _names [dict keys $xtra]
} proc latest Template:Category "" {
variable db if {$category eq ""} { set d [lsort -stride 2 -index {1 1} -integer -decreasing $db] dict for {nm inf} $d break return [list [Rec $nm {*}$inf]] } set latestbycat {} dict for {nm inf} [lsort -stride 2 -index {1 1} -integer $db] { dict set latestbycat [lindex $inf 0] [list $nm {*}$inf] } return [list [Rec {*}[dict get $latestbycat $category]]]
} proc latestpercategory {} {
variable db set latestbycat {} dict for {nm inf} [lsort -stride 2 -index {1 1} -integer $db] { dict set latestbycat [lindex $inf 0] [list $nm {*}$inf] } set result {} dict for {- inf} $latestbycat { lappend result [Rec {*}$inf] } return $result
} proc bydate {} {
variable db set result {} dict for {nm inf} [lsort -stride 2 -index {1 1} -integer $db] { lappend result [Rec $nm {*}$inf] } return $result
}
namespace export add latest latestpercategory bydate namespace ensemble create
}
if {$argc < 2} {
puts stderr "wrong # args: should be \"$argv0 dbfile subcommand ?args...?\"" exit 1
} udb::Load [lindex $argv 0] set separator "" if {[catch {udb {*}[lrange $argv 1 end]} msg]} {
puts stderr [regsub "\"udb " $msg "\"$argv0 dbfile "] exit 1
} foreach row $msg {
puts -nonewline $separator apply {row {
dict with row { puts "Title: $description" puts "Category: $category" puts "Date: $date" foreach v $_names { puts "${v}: [dict get $row $v]" } }
}} $row set separator [string repeat - 70]\n
}
udb::Store [lindex $argv 0]</lang> Sample session: <lang bash>bash$ udb.tcl db wrong # args: should be "udb.tcl dbfile subcommand ?args...?" bash$ udb.tcl db ? unknown or ambiguous subcommand "?": must be add, bydate, latest, or latestpercategory bash$ udb.tcl db add wrong # args: should be "udb.tcl dbfile add title category ?date? ?arg ...?" bash$ udb.tcl db add "Title 1" foo bash$ udb.tcl db add "Title 2" foo bash$ udb.tcl db add "Title 3" bar bash$ udb.tcl db bydate Title: Title 1 Category: foo Date: Tue Nov 15 18:11:58 GMT 2011
Title: Title 2 Category: foo Date: Tue Nov 15 18:12:01 GMT 2011
Title: Title 3 Category: bar Date: Tue Nov 15 18:12:07 GMT 2011 bash$ udb.tcl db latest Title: Title 3 Category: bar Date: Tue Nov 15 18:12:07 GMT 2011 bash$ udb.tcl db latest foo Title: Title 2 Category: foo Date: Tue Nov 15 18:12:01 GMT 2011 bash$ udb.tcl db latest bar Title: Title 3 Category: bar Date: Tue Nov 15 18:12:07 GMT 2011 bash$ udb.tcl db latestpercategory Title: Title 2 Category: foo Date: Tue Nov 15 18:12:01 GMT 2011
Title: Title 3 Category: bar Date: Tue Nov 15 18:12:07 GMT 2011 bash$ udb.tcl db add "Title 4" bar "12:00 Monday last week" bash$ udb.tcl db bydate Title: Title 4 Category: bar Date: Mon Nov 14 12:00:00 GMT 2011
Title: Title 1 Category: foo Date: Tue Nov 15 18:11:58 GMT 2011
Title: Title 2 Category: foo Date: Tue Nov 15 18:12:01 GMT 2011
Title: Title 3 Category: bar Date: Tue Nov 15 18:12:07 GMT 2011 bash$ cat db {Title 1} {foo 1321380718 {}} {Title 2} {foo 1321380721 {}} {Title 3} {bar 1321380727 {}} {Title 4} {bar 1321272000 {}} bash$ udb.tcl db add "Title 5" foo "12:00 Monday last week" Comment 'Wholly excellent!' bash$ cat db {Title 1} {foo 1321380718 {}} {Title 2} {foo 1321380721 {}} {Title 3} {bar 1321380727 {}} {Title 4} {bar 1321272000 {}} {Title 5} {foo 1321272000 {Comment {Wholly excellent!}}} bash$ udb.tcl db bydate Title: Title 4 Category: bar Date: Mon Nov 14 12:00:00 GMT 2011
Title: Title 5 Category: foo Date: Mon Nov 14 12:00:00 GMT 2011 Comment: Wholly excellent!
Title: Title 1 Category: foo Date: Tue Nov 15 18:11:58 GMT 2011
Title: Title 2 Category: foo Date: Tue Nov 15 18:12:01 GMT 2011
Title: Title 3 Category: bar Date: Tue Nov 15 18:12:07 GMT 2011</lang>
ToffeeScript
<lang coffeescript>#!/usr/local/bin/toffee
prog = require 'commander' fs = require 'fs-extra'
if not fs.exists! 'data.json'
fs.outputJson! 'data.json', {}
prog
.command('add <name> <category> [date]') .description('Add a new entry') .option('-n <text>', 'notes') .option('-t <tags>', 'tags') .action addentry
prog
.command('latest') .description('Print the latest entry') .action latest
prog
.command('catlatest') .description('Print the latest entry for each category') .action catlatestout
prog
.command('list') .description('Print all entries sorted by date') .action bydate
addentry = (name, category, dt, options) ->
if dt? then dat = new Date(dt) else dat = new Date() update = name: name category: category tags: options?.T notes: options?.N date: dat.getTime() e, data = fs.readJson! 'data.json' if not data[category]? data[category] = [] data[category].push update fs.outputJson 'data.json', data
byDateNew = (a, b) ->
if a.date<b.date then return 1 if b.date>a.date then return -1 return 0
catlatest = (cb) ->
e, data = fs.readJson! 'data.json' ret = [] for cat, list of data list.sort byDateNew ret.push list[0] cb ret
printFormatted = (entry) ->
tmp = entry.date entry.date = new Date(entry.date).toDateString() console.log entry entry.date = tmp
latest = ->
newpercat = catlatest! newpercat.sort byDateNew printFormatted newpercat[0]
catlatestout = ->
items = catlatest! for item in items printFormatted item
bydate = ->
e, data = fs.readJson! 'data.json' entries = [] for cat, list of data for item in list entries.push item entries.sort byDateNew for entry in entries printFormatted entry
prog.parse process.argv</lang>
UNIX Shell
This format is guaranteed to be human readable: if you can type it, you can read it. <lang bash>#!/bin/sh
db_create() { mkdir ./"$1" && mkdir "./$1/.tag" && echo "Create DB \`$1'" }
db_delete() { rm -r ./"$1" && echo "Delete DB \`$1'" }
db_show() { if [ -z "$2" ]; then show_help; fi for x in "./$1/$2/"*; do echo "$x:" | sed "s/.*\///" cat "$x" | sed "s/^/ /" echo done
printf "Tags: " ls "./$1/$2/.tag" }
db_tag() { local db="$1" item="$2" shift shift for tag in $@; do mkdir "./$db/.tag/$tag" ln -s "$PWD/$db/$item" "./$db/.tag/$tag/" touch "./$db/$item/.tag/$tag" done }
show_help() { echo "Usage: $0 command [args]" echo "Commands:" cat $0 | grep ") ##" | grep -v grep | sed 's/) ## /:\t/' exit }
if [ -z "$1" ]; then show_help; fi
action=$1 it=database shift case $action in create) ## db -- create $it db_create "$@" ;;
drop) ## db -- delete $it db_delete "$@" ;;
add) ## db item -- add new item to $it mkdir -p "./$1/$2/.tag" && touch "./$1/$2/Description" ;;
rem) ## db item -- delete item from $it rm -r "./$1/$2" rm "./$1/.tag/"*"/$2" ;;
show) ## db item -- show item db_show "$@" ;;
newtag) ## db new-tag-name -- create new tag name mkdir "./$1/.tag/$2" ;;
prop) ## db item property-name property-content -- add property to item echo "$4" > "./$1/$2/$3" ;;
tag) ## db item tag [more-tags...] -- mark item with tags db_tag "$@" ;;
last) ## db -- show latest item ls "$1" --sort=time | tail -n 1 ;;
list) ## db -- list all items ls "$1" -1 --sort=time ;;
last-all) ## db -- list items in each category for x in "$1/.tag/"*; do echo "$x" | sed 's/.*\//Tag: /' printf " " ls "$x" --sort=time | tail -n 1 echo done ;;
help) ## this message show_help ;;
*) echo Bad DB command: $1 show_help ;; esac</lang> Sample usage (assuming script is named "sdb"):<lang>$ sdb create CDs Create DB `CDs' $ sdb add CDs Bookends $ sdb prop CDs Bookends artists "Simon & Garfunkel" $ sdb add CDs "Ode to joy" $ sdb prop CDs "Ode to joy" artist "Beethoven" $ sdb tag CDs Bookends rock folk # I'm not sure about this $ sdb tag CDs "Ode to joy" classical $ sdb show CDs Bookends Description:
artists:
Simon & Garfunkel
Tags: folk rock $ sdb prop CDs "Ode to joy" Description "Sym. No. 9" $ sdb show CDs "Ode to joy" Description:
Sym. No. 9
artist:
Beethoven
Tags: classical $ sdb last-all CDs Tag: classical
Ode to joy
Tag: folk
Bookends
Tag: rock
Bookends
$ sdb drop CDs Delete DB `CDs' $</lang>