all tasks

This commit is contained in:
Ingy döt Net 2013-04-11 01:07:29 -07:00
parent b83f433714
commit 68f8f3e56b
14735 changed files with 178959 additions and 0 deletions

View file

@ -0,0 +1,23 @@
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 [[wp:Comparison_of_data_serialization_formats|others]].
See also [[Take notes on the command line]] for a related task.

View file

@ -0,0 +1,2 @@
---
note: Data Structures

View file

@ -0,0 +1,193 @@
#include <stdio.h>
#include <stdlib.h> /* malloc */
#include <string.h> /* strlen */
#define _XOPEN_SOURCE /* requred for time functions */
#define __USE_XOPEN
#include <time.h>
#define DB "database.csv" /* database name */
#define TRY(a) if (!(a)) {perror(#a);exit(1);}
#define TRY2(a) if((a)<0) {perror(#a);exit(1);}
#define FREE(a) if(a) {free(a);a=NULL;}
#define sort_by(foo) \
static int by_##foo (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);
}

View file

@ -0,0 +1,396 @@
(defvar *db* nil)
(defstruct series description tags episodes)
(defstruct (episode (:print-function print-episode-struct))
series title season episode part date tags)
(defun format-ymd (date)
(format nil "~{~a.~a.~a~}" date))
(defun print-episode-struct (ep stream level)
(let ((*print-pretty* nil))
(format stream (if *print-escape*
"#s(episode~@{~*~@[ :~1:*~a ~s~]~})"
"~32<~*~a~; ~*~@[~d-~]~*~d~> ~45<~*~@[~a ~]~*~@[(~a) ~]~;~*~@[(~a)~]~>~*~@[ (~{~a~^ ~})~]")
:series (episode-series ep)
:season (episode-season ep)
:episode (episode-episode ep)
:title (episode-title ep)
:part (episode-part ep)
:date (if *print-escape*
(episode-date ep)
(when (episode-date ep)
(format-ymd (episode-date ep))))
:tags (episode-tags ep))))
(defun get-value (key alist)
(cdr (assoc key alist)))
(defun get-latest (database)
(when database
(cons (car (series-episodes (cdar database))) (get-latest (cdr database)))))
(defun get-all (database)
(when database
(append (series-episodes (cdar database)) (get-all (cdr database)))))
(defun compare-date (a b)
(cond ((not a) t)
((not b) nil)
((= (first a) (first b))
(compare-date (rest a) (rest b)))
(t (< (first a) (first b)))))
(defun compare-by-date (a b)
(compare-date (episode-date a) (episode-date b)))
(defun prompt-read (prompt &optional default)
(format *query-io* "~a~@[ (~a)~]: " prompt default)
(force-output *query-io*)
(let ((answer (read-line *query-io*)))
(if (string= answer "")
default
answer)))
(defun split (seperator string)
(loop for i = 0 then (1+ j)
as j = (search seperator string :start2 i)
collect (subseq string i j)
while j))
(defun get-current-date ()
(multiple-value-bind
(second minute hour date month year day-of-week dst-p tz)
(get-decoded-time)
(declare (ignore second minute hour day-of-week dst-p tz))
(list date month year)))
(defun parse-date (date)
(reverse (mapcar #'parse-integer (split "." date))))
(defun parse-tags (tags)
(when (and tags (string-not-equal "" tags))
(mapcar #'intern (split " " (string-upcase tags)))))
(defun parse-number (number)
(if (stringp number)
(parse-integer number :junk-allowed t)
number))
(defun prompt-for-episode (&optional last)
(when (not last)
(setf last (make-episode)))
(let* ((series (prompt-read "Series Title" (episode-series last)))
(title (prompt-read "Title"))
(season (parse-number (prompt-read "Season" (episode-season last))))
(episode (parse-number (prompt-read "Episode"
(if (eq (episode-season last) season)
(1+ (episode-episode last))
1))))
(part (parse-number (prompt-read "Part"
(when (and (episode-part last)
(or (eq (episode-season last) season)
(eq (episode-part last) 1)))
(1+ (episode-part last))))))
(date (parse-date (prompt-read "Date watched" (format-ymd (get-current-date)))))
(tags (parse-tags (prompt-read "Tags"))))
(make-episode
:series series
:title title
:season season
:episode episode
:part part
:date date
:tags tags)))
(defun parse-integer-quietly (&rest args)
(ignore-errors (apply #'parse-integer args)))
(defun get-next-version (basename)
(flet ((parse-version (pathname)
(or (parse-integer-quietly
(string-left-trim (file-namestring basename)
(file-namestring pathname))
:start 1) 0)))
(let* ((files (directory (format nil "~A,*" (namestring basename))))
(max (if files
(reduce #'max files :key #'parse-version)
0)))
(merge-pathnames (format nil "~a,~d" (file-namestring basename) (1+ max))
basename))))
(defun save-db (dbfile database)
(let ((file (probe-file dbfile)))
(rename-file file (get-next-version file))
(with-open-file (out file :direction :output)
(with-standard-io-syntax
(let ((*print-case* :downcase))
(pprint database out))))))
(defun watch-save (dbfile)
(save-db dbfile *db*))
(defun load-db (dbfile)
(with-open-file (in dbfile)
(with-standard-io-syntax
(read in))))
(defun get-series (name database)
(cdr (assoc name database :test #'string-equal)))
(defun get-episode-list (series database)
(series-episodes (get-series series database)))
(defun print-series (title series)
(format t "~&~30a ~@[ (~{~a~^ ~})~]~%~@[ ~a~%~]" title (series-tags series)
(series-description series))
(format t "~{~& ~a~%~}" (reverse (series-episodes series))))
(defun watch-series (title)
(let ((series (get-series title *db*)))
(when series
(print-series title series))))
(defun print-all-series (database)
(loop for (title . series)
in (sort database #'(lambda (a b)(compare-by-date (car (series-episodes (cdr a)))
(car (series-episodes (cdr b))))))
do (terpri) (print-series title series)))
(defun watch-all-series ()
(print-all-series *db*))
(defun watch-latest ()
(format t "~{~& ~a~%~}" (sort (get-latest *db*) #'compare-by-date)))
(defun timeline-all (database)
(let* ((all (get-all database))
(max (length all))
(count max)
(all-series-names nil)
(all-series (make-hash-table :test 'equal)))
(loop for episode in (reverse (sort all #'compare-by-date))
do (unless (gethash (episode-series episode) all-series)
(setf (gethash (episode-series episode) all-series)
(make-array max :initial-element nil))
(setf all-series-names
(cons (episode-series episode) all-series-names)))
(setf (elt (gethash (episode-series episode) all-series) (decf count))
episode))
(values all-series all-series-names max)))
(defun watch-timeline ()
(multiple-value-bind (all-series all-series-names max) (timeline-all *db*)
(loop for series in all-series-names
do (format t "~30a (~{~:[ ~;•~]~})~%" series
(coerce (subseq (gethash series all-series) (- max 60)) 'list)))))
(defun watch-timelinec ()
(multiple-value-bind (all-series all-series-names max) (timeline-all *db*)
(let ((chart (make-array (list (length all-series-names) max) :initial-element nil))
(newcol 0)
(oldrow -1))
(loop for oldcol upto (1- max)
do (loop for series in all-series-names
for row from 0 upto (length all-series-names)
do (when (elt (gethash series all-series) oldcol)
(when (<= row oldrow)
(incf newcol))
(setf (aref chart row newcol)
(elt (gethash series all-series) oldcol))
(setf oldrow row))))
(loop for series in all-series-names
for i from 0 upto (length all-series-names)
do (format t "~30a (" series)
(loop for j from (- newcol 60) upto newcol
do (format t "~:[ ~;•~]" (aref chart i j))
(if (= j newcol)
(format t ")~%")))))))
(defun watch-timelinev ()
(multiple-value-bind (all-series all-series-names max) (timeline-all *db*)
(loop for series in all-series-names
counting series into count
do (format t "~va ~30a~%" count " " series ))
(loop for i from 0 upto (1- max)
do (let ((episode nil))
(loop for series in all-series-names
do (format t "~:[ ~;~:*~02a~]"
(when (elt (gethash series all-series) i)
(setf episode (elt (gethash series all-series) i))
(episode-episode episode))))
(format t " (~a)~%" (episode-series episode))))))
(defun watch-all ()
(format t "~{~& ~a~%~}" (sort (get-all *db*) #'compare-by-date)))
(defun watch-new-series (&key name description tags)
(cdar (push (cons name (make-series :description description :tags tags)) *db*)))
(defun get-or-add-series (name database)
(or (get-series name database)
(if (y-or-n-p "Add new series? [y/n]: ")
(watch-new-series
:name name
:description (prompt-read "Description" name)
:tags (parse-tags (prompt-read "Tags" "active")))
nil)))
(defun watch-add ()
(let* ((series (loop thereis (get-or-add-series (prompt-read "Series") *db*)))
(episode (prompt-for-episode (car (series-episodes series)))))
(push episode (series-episodes series))))
(defun watch-series-names ()
(format T "~{~& ~a~%~}"
(sort (mapcar #'car *db*)
(lambda (series1 series2)
(compare-by-date (car (series-episodes (get-value series1 *db*)))
(car (series-episodes (get-value series2 *db*))))))))
(defun exact-match (term text)
(string-equal (format nil "~{~a~^ ~}" term) text))
(defun fuzzy-match (term text)
(loop for word in term
when (search word text :test 'string-equal)
collect it))
(defun match-tags (term tags)
(intersection (mapcar #'intern term) tags))
(defun search-title (term database)
(loop for episode in (get-all database)
when (exact-match term (episode-title episode))
collect episode))
(defun search-tags (term database)
(sort (loop for episode in (get-all database)
for matches = (match-tags term (episode-tags episode))
when matches collect (list (length matches) episode))
#'> :key #'car))
(defun search-title-fuzzy (term database)
(sort (loop for episode in (get-all database)
for matches = (fuzzy-match term (episode-title episode))
when matches collect (list (length matches) episode))
#'> :key #'car))
(defun search-all (term database)
(let ((exact-results '())
(fuzzy-results '())
(tag-results '()))
(dolist (episode (get-all database))
(cond ((exact-match term (episode-title episode))
(push episode exact-results))
((fuzzy-match term (episode-title episode))
(push episode fuzzy-results))
((match-tags term (episode-tags episode))
(push episode tag-results))))
(append (sort exact-results #'compare-by-date)
(sort tag-results #'compare-by-date)
(sort fuzzy-results #'compare-by-date))))
(defun watch-search (term)
(format t "~{~& ~a~%~}" (search-all term *db*)))
(defun list-all-tags (database)
(let ((tags (make-hash-table :test 'equal)))
(dolist (tag (apply #'append (mapcar #'episode-tags (get-all database))))
(setf (gethash tag tags) (1+ (or (gethash tag tags) 0))))
tags))
(defun watch-tags ()
(maphash #'(lambda (tag count) (format t "~a (~d) " tag count))
(list-all-tags *db*))
(terpri))
(defun find-series-episode (term database)
(let ((series (get-series (format nil "~{~a~^ ~}" (butlast term)) database)))
(if series
(let* ((season-episode (car (last term)))
(pos (position #\- season-episode))
(season-str (when pos (subseq season-episode 0 pos)))
(season (or (parse-integer-quietly season-str) season-str))
(episode-str (if pos
(subseq season-episode (1+ pos))
season-episode))
(episode-nr (or (parse-integer-quietly episode-str) episode-str)))
(loop for episode in (series-episodes series)
when (and (equal (episode-episode episode) episode-nr)
(equal (episode-season episode) season))
collect episode))
(let ((series (get-series (format nil "~{~a~^ ~}" term) database)))
(if series
(list (car (series-episodes series))))))))
(defun find-episode (term database)
(or (find-series-episode term database)
(search-title term database)
(let* ((res (or (search-tags term database)
(search-title-fuzzy term database)))
(max (caar res)))
(loop for (matches episode) in res when (equal matches max) collect episode))))
(defun edit-episode (episode database)
(format t "editing:~%~a~%" episode)
(setf (episode-series episode)
(prompt-read "Series Title" (episode-series episode)))
(setf (episode-title episode)
(prompt-read "Title" (episode-title episode)))
(setf (episode-season episode)
(parse-number (prompt-read "Season" (episode-season episode))))
(setf (episode-episode episode)
(parse-number (prompt-read "Episode" (episode-episode episode))))
(setf (episode-part episode)
(parse-number (prompt-read "Part" (episode-part episode))))
(setf (episode-date episode)
(parse-date (prompt-read "Date watched" (format-ymd (episode-date episode)))))
(setf (episode-tags episode)
(parse-tags (prompt-read "Tags" (format nil "~{~a~^ ~}" (episode-tags episode))))))
(defun watch-edit (term)
(let ((episodes (find-episode term *db*)))
(if (> (length episodes) 1)
(format t "found more than one episode, please be more specific:~%~{~& ~a~%~}" episodes)
(edit-episode (car episodes) *db*))))
(defun watch-load (dbfile)
(setf *db* (load-db dbfile)))
(defun argv ()
(or
#+clisp (ext:argv)
#+sbcl sb-ext:*posix-argv*
#+clozure (ccl::command-line-arguments)
#+gcl si:*command-args*
#+ecl (loop for i from 0 below (si:argc) collect (si:argv i))
#+cmu extensions:*command-line-strings*
#+allegro (sys:command-line-arguments)
#+lispworks sys:*line-arguments-list*
nil))
(defun main (argv)
(let ((dbfile (make-pathname :name "lwatch" :type nil :defaults *load-pathname*)))
(watch-load dbfile)
(format t "loaded db~%")
(cond ((equal (cadr argv) "add") (watch-add) (watch-save dbfile))
((equal (cadr argv) "latest") (watch-latest))
((null (cadr argv)) (watch-latest))
((equal (cadr argv) "series") (watch-series-names))
((and (equal (cadr argv) "all") (equal (caddr argv) "series")) (watch-all-series))
((equal (cadr argv) "all") (watch-all))
((equal (cadr argv) "tags") (watch-tags))
((equal (cadr argv) "search") (watch-search (cddr argv)))
((equal (cadr argv) "edit") (watch-edit (cddr argv)) (watch-save dbfile))
((equal (cadr argv) "timeline") (watch-timeline))
((equal (cadr argv) "timelinev") (watch-timelinev))
((equal (cadr argv) "timelinec") (watch-timelinec))
(T (watch-series (format nil "~{~a~^ ~}" (cdr argv)))))))
(main (argv))

View file

@ -0,0 +1,87 @@
import std.stdio, std.algorithm, std.string, std.conv, std.array,
std.file, std.csv, std.datetime;
private {
immutable filename = "simdb.csv";
struct Item {
string name, date, category;
}
void addItem(in string[] item) {
if (item.length < 3)
return printUsage();
auto db = load();
const date = text(cast(DateTime)Clock.currTime);
const cat = (item.length == 4) ? item[3] : "none";
db ~= Item(item[2], date, cat);
store(db);
}
void printLatest(in string[] a) {
auto db = load();
if (db.empty)
return writeln("No entries in database.");
db.sort!q{ a.date > b.date };
if (a.length == 3) {
foreach (item; db)
if (item.category == a[2])
writefln("%s, %s, %s", item.tupleof);
} else {
writefln("%s, %s, %s", db[0].tupleof);
}
}
void printAll() {
auto db = load();
if (db.empty)
return writeln("No entries in database.");
db.sort!q{ a.date < b.date };
foreach (item; db)
writefln("%s, %s, %s", item.tupleof);
}
Item[] load() {
Item[] db;
if (filename.exists && filename.isFile) {
try {
const text = filename.readText;
if (!text.empty)
db = csvReader!Item(text).array;
} catch (CSVException e) {
writeln(e.msg);
}
}
return db;
}
void store(in Item[] db) {
auto f = File(filename, "w+");
foreach (item; db)
f.writefln("%s,%s,%s", item.tupleof);
}
void printUsage() {
writeln(
`Usage:
simdb cmd [categoryName]
add add item, followed by optional category
latest print last added item(s), followed by optional category
all print all
For instance: add "some item name" "some category name"`);
}
}
void main(in string[] args) {
if (args.length < 2 || args.length > 4)
return printUsage();
switch (args[1].toLower) {
case "add": addItem(args); break;
case "latest": printLatest(args); break;
case "all": printAll(); break;
default: printUsage(); break;
}
}

View file

@ -0,0 +1,129 @@
\ sdb.fs Simple database. Gforth 0.7.0 specific
' noop is bootmessage
wordlist constant USER: \ USER functions
wordlist constant SDB \ DB keys
wordlist constant FIELDS \ DB fields
: -SDB?EXIT ( -- ; Continue if `SDB' non-empty )
SDB cell+ @ 0= IF rdrop exit THEN ;
\ MACROS
: |comp| ( MACRO: | code to be compiled | )
char parse evaluate ; immediate
: LIST ( node -- ) ]] BEGIN @ dup WHILE >R [[ ; immediate
: LOOP-LIST ( -- ) ]] R> REPEAT drop [[ ; immediate
: UNLIST ( -- ) POSTPONE rdrop ; immediate
\ Helper words
: .; [char] ; emit ;
: !+ dup cell+ -rot ! ;
: ($+) ( cas us cad ud -- cad us+ud ; append source string to destination )
swap >R 2dup + R> swap >R tuck + swap >R swap move R> R> ;
\ --- Working Record Fields
current @ FIELDS current !
2variable person
2variable birthday
2variable relationship
2variable hobby
current ! FIELDS >order
\ --- Ring list
: new-node ( -- node ; root of circular list )
here dup , ;
: do-link ( node new-node -- ; do link after current node )
over @ over ! swap ! ;
: empty-ring? ( node -- f ) dup @ = ;
: RING ( node -- ) ]] dup BEGIN @ 2dup <> WHILE 2>R [[ ; immediate
: LOOP-RING ( -- ) ]] 2R> REPEAT 2drop [[ ; immediate
: UNRING ( -- ) postpone 2RDROP ; immediate
:noname
s" " person 2! s" 9999.01.01" birthday 2! s" " relationship 2! s" " hobby 2! ;
new-node swap , CONSTANT OLD
\ --- Insertion into ring
: node>xt ( node -- xt ) cell+ @ ;
: bday@ ( node -- ca u ) node>xt execute birthday 2@ ;
: datecmp ( node node -- -1|0|1 ) bday@ rot bday@ compare ;
: search-by-date ( new-node node -- node' ; Linear search by birth date )
dup
RING ( new-node node )
over I datecmp 0<
IF nip UNRING exit
ELSE drop I THEN
LOOP-RING nip ;
: insert-ordered ( new-node -- )
OLD dup empty-ring? 0=
IF over swap search-by-date THEN swap do-link ;
\ --- Field compiling
: fld: [char] ; parse postpone sliteral ; immediate
: _sdb-walk ( MACRO: | xt[--] -- | )
]] -SDB?EXIT here dup SDB cell+
LIST I name>int swap !+ LOOP-LIST [[ 1 cells ]] literal -
DO I @ |comp| [[ -1 cells ]] literal +LOOP [[ ; immediate
2variable category
: msg_nocategory ( -- )
." No such category!" cr ;
\ --- USER definitions
user: >order current @ USER: current !
: bye bye ;
: .s .s ;
: newline cr ;
: godb SDB >order ;
: .keys SDB wordlist-words ;
: .person person 2@ type ;
: .birthday birthday 2@ type ;
: .relationship relationship 2@ type ;
: .hobby hobby 2@ type ;
: .record .person 1 spaces .birthday .; .relationship .; .hobby .; ;
: .last SDB cell+ @ dup
IF name>int execute .record ELSE drop THEN ;
: .lastbycategory ( "field-name" -- )
here 0 category 2! parse-name FIELDS search-wordlist
IF sdb cell+
LIST ( xt )
I name>int execute dup execute 2@ category 2@ 2swap search nip nip 0=
IF .record cr dup execute 2@ category 2@ ($+) category 2! THEN
LOOP-LIST drop
ELSE ['] msg_nocategory stderr OUTFILE-EXECUTE THEN ;
: .bydate ( -- )
OLD RING I node>xt execute .record cr LOOP-RING ;
: .sdb _sdb-walk | execute .record cr | ;
: sdb-dump _sdb-walk | execute ." +: " .record cr | ;
: dump ( "db-file" -- )
parse-name w/o
CREATE-FILE throw dup
['] sdb-dump swap OUTFILE-EXECUTE
CLOSE-FILE throw ;
: load ( "db-file" -- ) include ;
: +: ( "person birth-day;relationship;hobby; -- )
new-node dup cell+ >R 0 , \ post `xt'
current @ SDB current ! \ compile into `sdb' wordlist
: latestxt dup R> ! \ fix-up `xt'
]] literal >name name>string person 2! fld: birthday 2! fld: relationship 2! fld: hobby 2! ; [[
current !
insert-ordered ;
: HELP cr
cr ." KEYWORDS: " USER: wordlist-words cr ." ---"
cr ." Prefix `.' means print"
cr ." To add a record, type: +: name yyyy.mm.dd;relationship;hobby;"
cr cr ;
current !
SEAL godb

View file

@ -0,0 +1,340 @@
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)
}
}

View file

@ -0,0 +1,128 @@
import Control.Monad.State
import Data.List (sortBy, nub)
import System.Environment (getArgs, getProgName)
import System.Directory (doesFileExist)
import System.IO (openFile, hGetContents, hClose, IOMode(..),
Handle, hPutStrLn)
-- for storing dates
data Date = Date Integer Int Int deriving (Show, Read, Eq, Ord)
-- for storing database items
data Item = Item {description :: String
,category :: [String]
,date :: Date
,optional :: [String]}
deriving (Show, Read)
-- a state monad transformer which wraps IO actions.
-- the database (state) is passed implicitly between functions.
type ItemList a = StateT [Item] IO a
-- add an item to the database
addItem :: Item -> ItemList ()
addItem i = modify (++ [i])
-- get the newest of a list of items
latest :: [Item] -> [Item]
latest [] = []
latest [x]= [x]
latest xs = take 1 $ sortBy newer xs
-- compare two items to see which one is newer
newer :: Item -> Item -> Ordering
newer a b = compare (date b) (date a)
-- list all different categories (no duplicates)
categories :: ItemList [String]
categories = liftM (nub . concatMap category) get
-- list only the items with the given category tag
filterByCategory :: String -> ItemList [Item]
filterByCategory c = liftM (filter (\i -> c `elem` category i)) get
-- get the newest of all items
lastOfAll :: ItemList [Item]
lastOfAll = liftM latest get
-- get the newest item in each category
latestByCategory :: ItemList [Item]
latestByCategory = do
cats <- categories
filt <- mapM filterByCategory cats
return $ concatMap latest filt
-- sort all items chronologically, newest first
sortByDate :: ItemList [Item]
sortByDate = liftM (sortBy newer) get
toScreen :: Item -> IO ()
toScreen (Item desc cats (Date y m d) opt) = putStrLn $
"Description:\t" ++ desc ++ "\nCategories:\t" ++ show cats ++
"\nDate:\t\t" ++ show y ++ "-" ++ show m ++ "-" ++ show d ++
"\nOther info:\t" ++ show opt
-- command line argument handling
-- if the user called the program with the option "add", the
-- new item is returned to main so that it can be saved to disk.
-- the argument "opt" is a list.
arguments :: ItemList [Item]
arguments = do
args <- liftIO getArgs
case args of
("add":desc:cat:year:month:day:opt) -> do
let newItem = parseItem args
addItem newItem
return [newItem]
("latest":[]) -> do
item <- lastOfAll
lift $ mapM_ toScreen item
return []
("category":[]) -> do
items <- latestByCategory
lift $ mapM_ toScreen items
return []
("all":[]) -> do
sorted <- sortByDate
lift $ mapM_ toScreen sorted
return []
_ -> do
lift usage
return []
parseItem :: [String] -> Item
parseItem (_:desc:cat:year:month:day:opt) =
Item {description = desc, category = words cat,
date = Date (read year) (read month) (read day),
optional = opt}
usage :: IO ()
usage = do
progName <- getProgName
putStrLn $ "Usage: " ++ progName ++ " add|all|category|latest \
\OPTIONS\n\nadd \"description\" \"category1 category2\"... \
\year month day [\"note1\" \"note2\"...]\n\tAdds a new record \
\to the database.\n\nall\n\tPrints all items in chronological \
\order.\n\ncategory\n\tPrints the latest item for each category.\
\\n\nlatest\n\tPrints the latest item."
-- the program creates, reads and writes to a file in the current directory
main :: IO ()
main = do
progName <- getProgName
let fileName = progName ++ ".db"
e <- doesFileExist fileName
if e
then do
hr <- openFile fileName ReadMode
f <- hGetContents hr
v <- evalStateT arguments (map read $ lines f)
hClose hr -- must be called after working with contents!
hw <- openFile fileName AppendMode
mapM_ (hPutStrLn hw . show) v
hClose hw
else do
v <- evalStateT arguments []
hw <- openFile fileName WriteMode
mapM_ (hPutStrLn hw . show) v
hClose hw

View file

@ -0,0 +1,108 @@
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';'';'li''st';'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

View file

@ -0,0 +1,13 @@
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!'

View file

@ -0,0 +1,76 @@
$ . 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!'
$

View file

@ -0,0 +1,36 @@
#!/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)

View file

@ -0,0 +1,154 @@
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);
}

View file

@ -0,0 +1,108 @@
#!/usr/bin/python3
'''\
Simple database for: http://rosettacode.org/wiki/Simple_database
'''
import argparse
from argparse import Namespace
import datetime
import shlex
def parse_args():
'Set up, parse, and return arguments'
parser = argparse.ArgumentParser(epilog=globals()['__doc__'])
parser.add_argument('command', choices='add pl plc pa'.split(),
help='''\
add: Add a new entry
pl: Print the latest entry
plc: Print the latest entry for each category/tag
pa: Print all entries sorted by a date''')
parser.add_argument('-d', '--description',
help='A description of the item. (e.g., title, name)')
parser.add_argument('-t', '--tag',
help=('''A category or tag (genre, topic, relationship '''
'''such as “friend” or “family”)'''))
parser.add_argument('-f', '--field', nargs=2, action='append',
help='Other optional fields with value (can be repeated)')
return parser
def do_add(args, dbname):
'Add a new entry'
if args.description is None:
args.description = ''
if args.tag is None:
args.tag = ''
del args.command
print('Writing record to %s' % dbname)
with open(dbname, 'a') as db:
db.write('%r\n' % args)
def do_pl(args, dbname):
'Print the latest entry'
print('Getting last record from %s' % dbname)
with open(dbname, 'r') as db:
for line in db: pass
record = eval(line)
del record._date
print(str(record))
def do_plc(args, dbname):
'Print the latest entry for each category/tag'
print('Getting latest record for each tag from %s' % dbname)
with open(dbname, 'r') as db:
records = [eval(line) for line in db]
tags = set(record.tag for record in records)
records.reverse()
for record in records:
if record.tag in tags:
del record._date
print(str(record))
tags.discard(record.tag)
if not tags: break
def do_pa(args, dbname):
'Print all entries sorted by a date'
print('Getting all records by date from %s' % dbname)
with open(dbname, 'r') as db:
records = [eval(line) for line in db]
for record in records:
del record._date
print(str(record))
def test():
import time
parser = parse_args()
for cmdline in [
"""-d Book -f title 'Windy places' -f type hardback --tag DISCOUNT add""",
"""-d Book -f title 'RC spammers' -f type paperback -t DISCOUNT add""",
"""-d Book -f title 'Splat it' -f type hardback -f special 'first edition' -t PREMIUM add""",
"""pl""",
"""plc""",
]:
args = parser.parse_args(shlex.split(cmdline))
now = datetime.datetime.utcnow()
args._date = now.isoformat()
do_command[args.command](args, dbname)
time.sleep(0.5)
do_command = dict(add=do_add, pl=do_pl, plc=do_plc, pa=do_pa)
dbname = '_simple_db_db.py'
if __name__ == '__main__':
if 0:
test()
else:
parser = parse_args()
args = parser.parse_args()
now = datetime.datetime.utcnow()
args._date = now.isoformat()
do_command[args.command](args, dbname)

View file

@ -0,0 +1,187 @@
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

View file

@ -0,0 +1,100 @@
sqliteconnect #sql, "f:\client.db" ' Connect to the DB
' -------------------------------
' show user options
' -------------------------------
[sho]
cls ' clear screen
button #acd, "Add a new entry", [add]
button #acd, "Print the latest entry", [last]
button #acd, "Print the latest entry for each category", [lastCat]
button #acd, "Print all entries sorted by a date", [date]
button #ex, "Exit", [exit]
wait
' ------------------------------------
' add a new entry (user input screen)
' ------------------------------------
[add]
cls ' clear the screen
html "<TABLE BORDER=1 CELLPADDING=0 CELLSPACING=0 bgcolor=wheat>"
html "<TR align=center BGCOLOR=tan><TD colspan=2>Client Maintenance</TD></TR><TR>"
html "<TD bgcolor=tan align=right>Client Num</TD><TD>"
textbox #clientNum,clientNum$,5
html "</TD></TR><TR><TD bgcolor=tan align=right>Name</TD><TD>"
textbox #name,name$,30
html "</TD></TR><TR><TD bgcolor=tan align=right>Client Date</TD><TD>"
textbox #clientDate,clientDate$,19
html "</TD></TR><TR><TD bgcolor=tan align=right>Category</TD><TD>"
textbox #category,category$,10
html "</TD></TR><TR><TR bgcolor=tan><TD colspan=2 ALIGN=CENTER>"
button #acd, "Add", [addIt]
button #ex, "Exit", [sho]
html "</TD></TR></TABLE>"
wait
' ---------------------------------------------
' Get data from the screen
' ---------------------------------------------
[addIt]
clientNum = #clientNum contents$()
name$ = trim$(#name contents$())
clientDate$ = trim$(#clientDate contents$())
category$ = trim$(#category contents$())
dbVals$ = clientNum;",'";name$;"','";clientDate$;"','";category$;"'"
sql$ = "INSERT into client VALUES ("; dbVals$ ; ")"
#sql execute(sql$)
goto [sho]
' ------------------------------------
' Select last entry
' ------------------------------------
[last]
sql$ = "SELECT *,client.rowid as rowid FROM client ORDER BY rowid desc LIMIT 1"
what$ = "---- Last Entry ----"
goto [shoQuery]
' ------------------------------------
' Select by category (Last date only)
' ------------------------------------
[lastCat]
sql$ = "SELECT * FROM client
WHERE client.clientDate = (SELECT max(c.clientDate)
FROM client as c WHERE c.category = client.category)
ORDER BY category"
what$ = "---- Last Category Sequence ----"
goto [shoQuery]
' ------------------------------------
' Select by date
' ------------------------------------
[date]
sql$ = "SELECT * FROM client ORDER BY clientDate"
what$ = "---- By Date ----"
[shoQuery]
cls
print what$
html "<TABLE BORDER=1 CELLPADDING=0 CELLSPACING=0>"
html "<TR align=center bgcolor=wheat><TD>Client<br>Num</TD><TD>Name</TD><TD>Client<br>Date</TD><TD>Category</TD></TR>" ' heading
#sql execute(sql$)
WHILE #sql hasanswer()
#row = #sql #nextrow()
clientNum = #row clientNum()
name$ = #row name$()
clientDate$ = #row clientDate$()
category$ = #row category$()
html "<TR><TD align=right>";clientNum;"</TD><TD>";name$;"</TD><TD>";clientDate$;"</TD><TD>";category$;"</TD></TR>"
WEND
html "</TABLE>"
button #c, "Continue", [sho]
wait
' ------ the end -------
[exit]
end

View file

@ -0,0 +1,101 @@
#!/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 {{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]

View file

@ -0,0 +1,91 @@
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

View file

@ -0,0 +1,96 @@
#!/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

View file

@ -0,0 +1,37 @@
$ 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'
$