Simple database: Difference between revisions

From Rosetta Code
Content added Content deleted
Line 1,518: Line 1,518:
' ---------------------------------------------
' ---------------------------------------------
[addIt]
[addIt]
clientNum = val(clientNum$)
clientNum = #clientNum contents$()
name$ = trim$(#name contents$())
name$ = trim$(#name contents$())
clientDate$ = trim$(#clientDate contents$())
clientDate$ = trim$(#clientDate contents$())

Revision as of 07:01, 16 March 2012

Task
Simple database
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>

  1. include <stdlib.h> /* malloc */
  2. include <string.h> /* strlen */
  3. define _XOPEN_SOURCE /* requred for time functions */
  4. define __USE_XOPEN
  5. include <time.h>
  6. define DB "database.csv" /* database name */
  7. define TRY(a) if (!(a)) {perror(#a);exit(1);}
  8. define TRY2(a) if((a)<0) {perror(#a);exit(1);}
  9. define FREE(a) if(a) {free(a);a=NULL;}
  10. define sort_by(foo) \

static int by_##foo (pdb_t *p1, pdb_t *p2) { \

   return strcmp ((*p1)->foo, (*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)(pdb_t*, pdb_t*); typedef int (*compfn)(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 sort by_date; /* main */ int main (int argc, char **argv) {

   char buf[100], *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,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,(compfn)sortby);
       pdb[i-1]->next=NULL;
       for (;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>

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~]~})" 
                    "~&~34<  ~*~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)))
 (make-episode
   :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" (1+ (or (episode-episode last) 0))))
   :part (parse-number (prompt-read "Part" (when (episode-part last)
                                             (1+ (episode-part last)))))
   :date (parse-date (prompt-read "Date watched" (format-ymd (get-current-date))))
   :tags (parse-tags (prompt-read "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 #'equal)))

(defun get-episode-list (series database)

 (series-episodes (get-series series database)))

(defun print-series (series)

 (format t "~30a ~a ~@[ (~{~a~^ ~})~]~%" 
         (car series) (series-description (cdr series)) (series-tags (cdr series)))
 (format t "~{~a~%~}" (series-episodes series)))

(defun watch-series (title)

 (let ((series (get-series title *db*)))
   (format t "~30a ~@[ (~{~a~^ ~})~]~%~@[    ~a~%~]" title (series-tags series) 
           (series-description series))
   (format t "~{~a~%~}" (reverse (series-episodes series)))))

(defun dump-db (database)

 (dolist (series database)
   (print-series (cdr series))))

(defun watch-latest ()

 (format t "~{~a~%~}" (sort (get-latest *db*) #'compare-by-date)))

(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 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)
   (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))
         ((equal (cadr argv) "all") (watch-all))
         (T (watch-series (format nil "~{~a~^ ~}" (cdr argv)))))))

(main (argv))</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>

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

Translation of: Common Lisp

<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>

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 ' ------------------------------------ [add] cls ' clear the screen

html "

" html "" html "
Client Maintenance
Client Num"
      textbox #clientNum,clientNum$,5
html "
Name"
    textbox #name,name$,30
html "
Client Date"
    textbox #clientDate,clientDate$,19
html "
Category"
    textbox #category,category$,10
html "
"
    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 ("; dbFields$; ") VALUES ("; dbVals$ ; ")"

  1. 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 ' ------------------------------------ [lastCat] sql$ = "SELECT *,client.rowid FROM client WHERE client.rowid = (SELECT max(c.rowid) 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 "" html "" html "" html "" html "" html ""
  1. sql execute(sql$)
WHILE #sql hasanswer() #row = #sql #nextrow() clientNum = #row clientNum() name$ = #row name$() clientDate$ = #row clientDate$() category$ = #row category$() html "" html "" html "" html "" html "" html "" WEND html "
Client
Num
NameClient
Date
Category
";clientNum;"";name$;"";clientDate$;"";category$;"

"

button #c, "Continue", [sho] wait

' ------ the end ------- [exit] end</lang> Output:


User Input ----

Client Maintenance
Client Num5
NameDawnridge Winery
Client Date<2008-06-18 22;16
Categorywine>
[Add] [Exit]

Last Entry ----

Client
Num
NameClient
Date
Category
5Dawnridge Winery2008-06-18 22;16wine

Last category Sequence ----

Client
Num
NameClient
Date
Category
1Home Sales2012-01-01 10;20broker
4Back 40 Equipment2009-09-18 20:18farm
3Floral Designs2010-10-14 09:16flowers
2Best Foods2011-02-02 12;33food
5Dawnridge Winery2008-06-18 22;16wine

Date Sequence ----

Client
Num
NameClient
Date
Category
5Dawnridge Winery2008-06-18 22;16wine
4Back 40 Equipment2009-09-18 20:18farm
3Floral Designs2010-10-14 09:16flowers
2Best Foods2011-02-02 12;33food
1Home Sales2012-01-01 10;20broker

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>

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>