Simple database
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.
You are encouraged to solve this task according to the task description, using any language you may know.
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.
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>
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' 'sort table 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 monad '+ * -' '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-07:20:36:54.749│many more!│ │ │ │ │ │ │ │2012-02-07:20:36:54.541│insert │f/ y │insert f within y│infinite │dyad │sum=: +/ │continued_fraction=:+`%/ │ │2012-02-07:20:36:54.612│hook │x(f g)y │f(x,g(y)) │infinite │dyad │display verb in s│a reflexive dyadic hook │ │2012-02-07:20:36:54.682│passive │x f~ y │f(y,x) │ranks of f│dyad │(%~ i.@:>:) 8x │8 intervals from 0 to 1 │ │2012-02-07:20:36:54.727│atop │x f@g y │f(g(x,y)) │rank of g │dyad │>@{. │(lisp) open the car │ │2012-02-07:20:36:54.564│fork │(f g h)y │g(f(y),h(y)) │infinite │monad │average=: +/ % # │sum divided by tally │ │2012-02-07:20:36:54.589│hook │(f g)y │f(y,g(y)) │infinite │monad │/: 2&{"1 │sort table by third column │ │2012-02-07:20:36:54.635│fork │x(f g h)y │g(f(x,y),h(x,y)) │infinite │monad │+ * - │product of sum and difference│ │2012-02-07:20:36:54.660│reflexive │f~ y │f(y,y) │infinite │monad │^~y │y raised to the power of y │ │2012-02-07:20:36:54.705│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-07:20:36:54.541│insert │f/ y │insert f within y│infinite │dyad │sum=: +/ │continued_fraction=:+`%/ │ │2012-02-07:20:36:54.564│fork │(f g h)y │g(f(y),h(y)) │infinite │monad │average=: +/ % # │sum divided by tally │ │2012-02-07:20:36:54.589│hook │(f g)y │f(y,g(y)) │infinite │monad │/: 2&{"1 │sort table by third column │ │2012-02-07:20:36:54.612│hook │x(f g)y │f(x,g(y)) │infinite │dyad │display verb in s│a reflexive dyadic hook │ │2012-02-07:20:36:54.635│fork │x(f g h)y │g(f(x,y),h(x,y)) │infinite │monad │+ * - │product of sum and difference│ │2012-02-07:20:36:54.660│reflexive │f~ y │f(y,y) │infinite │monad │^~y │y raised to the power of y │ │2012-02-07:20:36:54.682│passive │x f~ y │f(y,x) │ranks of f│dyad │(%~ i.@:>:) 8x │8 intervals from 0 to 1 │ │2012-02-07:20:36:54.705│atop │f@g y │f(g(y)) │rank of g │monad │*:@(+/) │square the sum │ │2012-02-07:20:36:54.727│atop │x f@g y │f(g(x,y)) │rank of g │dyad │>@{. │(lisp) open the car │ │2012-02-07:20:36:54.749│many more!│ │ │ │ │ │ │ └───────────────────────┴──────────┴──────────┴─────────────────┴──────────┴───────┴─────────────────┴─────────────────────────────┘ $ cat dataflow '2012-02-07:20:36:54.512';'name';'expression';'algebraic';'rank';'valence';'example';'explanation' '2012-02-07:20:36:54.541';'insert';'f/ y';'insert f within y';'infinite';'dyad';'sum=: +/';'continued_fraction=:+`%/' '2012-02-07:20:36:54.564';'fork';'(f g h)y';'g(f(y),h(y))';'infinite';'monad';'average=: +/ % #';'sum divided by tally' '2012-02-07:20:36:54.589';'hook';'(f g)y';'f(y,g(y))';'infinite';'monad';'/: 2&{"1';'sort table by third column' '2012-02-07:20:36:54.612';'hook';'x(f g)y';'f(x,g(y))';'infinite';'dyad';'display verb in s';'a reflexive dyadic hook' '2012-02-07:20:36:54.635';'fork';'x(f g h)y';'g(f(x,y),h(x,y))';'infinite';'monad';'+ * -';'product of sum and difference' '2012-02-07:20:36:54.660';'reflexive';'f~ y';'f(y,y)';'infinite';'monad';'^~y';'y raised to the power of y' '2012-02-07:20:36:54.682';'passive';'x f~ y';'f(y,x)';'ranks of f';'dyad';'(%~ i.@:>:) 8x';'8 intervals from 0 to 1' '2012-02-07:20:36:54.705';'atop';'f@g y';'f(g(y))';'rank of g';'monad';'*:@(+/)';'square the sum' '2012-02-07:20:36:54.727';'atop';'x f@g y';'f(g(x,y))';'rank of g';'dyad';'>@{.';'(lisp) open the car' '2012-02-07:20:36:54.749';'many more!' '2012-02-08:22:48:34.361';'name';'expression';'algebraic';'rank';'valence';'example';'explanation' '2012-02-08:22:48:34.386';'insert';'f/ y';'insert f within y';'infinite';'dyad';'sum=: +/';'continued_fraction=:+`%/' '2012-02-08:22:48:34.410';'fork';'(f g h)y';'g(f(y),h(y))';'infinite';'monad';'average=: +/ % #';'sum divided by tally' '2012-02-08:22:48:34.433';'hook';'(f g)y';'f(y,g(y))';'infinite';'monad';'/: 2&{"1';'sort table by third column' '2012-02-08:22:48:34.459';'hook';'x(f g)y';'f(x,g(y))';'infinite';'dyad';'display verb in s';'a reflexive dyadic hook' '2012-02-08:22:48:34.482';'fork';'x(f g h)y';'g(f(x,y),h(x,y))';'infinite';'monad';'+ * -';'product of sum and difference' '2012-02-08:22:48:34.504';'reflexive';'f~ y';'f(y,y)';'infinite';'monad';'^~y';'y raised to the power of y' '2012-02-08:22:48:34.527';'passive';'x f~ y';'f(y,x)';'ranks of f';'dyad';'(%~ i.@:>:) 8x';'8 intervals from 0 to 1' '2012-02-08:22:48:34.550';'atop';'f@g y';'f(g(y))';'rank of g';'monad';'*:@(+/)';'square the sum' '2012-02-08:22:48:34.573';'atop';'x f@g y';'f(g(x,y))';'rank of g';'dyad';'>@{.';'(lisp) open the car' '2012-02-08:22:48:34.595';'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
<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"]}}
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>