Simple database

From Rosetta Code
Revision as of 05:46, 5 November 2011 by rosettacode>EMBee (→‎{{header|Pike}}: fix line length)
Simple database is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

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. (eg title, name)
  • A category or tag (genre, topic, relationship (friend, 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 (making all entries in that category subitems)

The fileformat 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)

(defun make-episode (series title episode date-watched)

  `((series . ,series) (episode . ,episode) (title . ,title) (date . ,date-watched)))

(defun print-episode (episode)

 (format t "  ~30a ~10a ~30a (~{~a~^.~})~%" 
         (cdr (assoc 'series episode)) (cdr (assoc 'episode episode)) 
         (cdr (assoc 'title episode)) (cdr (assoc 'date episode))))

(defun print-series (series)

(format t "~30a ~10a~%" (cdr (assoc 'series series)) (cdr (assoc 'status series)))
 (mapcar #'print-episode (cdr (assoc 'episodes series))))

(defun dump-db (database)

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


(defun get-latest (database)

 (cond ((endp database) nil)
       (T (cons (cadr (assoc 'episodes (cdar database))) (get-latest (cdr database))))))

(defun compare-date (a b)

 (cond ((not (and (listp a) (listp b))) nil)
       ((null a) (not (null b)))
       ((null 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 (reverse (cdr (assoc 'date a))) (reverse (cdr (assoc 'date b)))))

(defun watch-list ()

(mapcar #'print-episode (sort (get-latest db) #'compare-by-date)))

(defun prompt-read (prompt)

 (format *query-io* "~a: " prompt)
 (force-output *query-io*)
 (read-line *query-io*))

(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 parse-date (date)

 (mapcar #'parse-integer (split "." date)))

(defun prompt-for-episode ()

 (make-episode
  (prompt-read "Series")
  (prompt-read "Title")
  (prompt-read "Episode")
  (parse-date (prompt-read "Date watched"))))

(defun save-db (filename database)

 (with-open-file (out filename
                  :direction :output
                  :if-exists :supersede)
   (with-standard-io-syntax
     (pprint database out))))

(defun watch-save ()

(save-db "lwatch" db))

(defun load-db (filename)

 (with-open-file (in filename)
   (with-standard-io-syntax
     (read in))))

(defun get-series (name list)

 (cdr (assoc name list :test #'equal)))

(defun get-episode-list (series list)

 (cdr (assoc 'episodes (get-series series list))))

(defun watch-new-series (name status)

 (cdar (push `(,name (series . ,name) (status . ,status) (episodes)) 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 'active) nil)))

(defun watch-add ()

 (let* ((episode (prompt-for-episode))
        (series-name (cdr (assoc 'series episode)))
        (series (get-or-add-series series-name db)))
   (if (endp series) (watch-add)
     (rplacd (assoc 'episodes series) 
           (cons episode (get-episode-list series-name db))))))

(defun watch-load ()

 (setf db (load-db "lwatch")))


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

 (watch-load)
 (cond ((equal (cadr argv) "add") (watch-add) (watch-save))
       (T (watch-list))))

(main (argv))</lang>

Pike

an almost direct translation from common lisp: <lang Pike>mapping db = ([]);

mapping make_episode(string series, string title, string episode, array date) {

   return ([ "series":series, "episode":episode, "title":title, "date":date ]);

}

void print_episode(mapping episode) {

   write("  %-30s %10s %-30s (%{%d.%})\n", 
         episode->series, episode->episode, episode->title, episode->date);

}

void print_series(mapping series) {

   write("%-30s %-10s\n", series->series, series->status);
   map(series->episodes, print_episode);

}

void dump_db(mapping database) {

   foreach(database; string name; mapping series)
   {
       print_series(series);
   }

}

array get_latest(mapping database) {

   array latest = ({});
   foreach(database; string name; mapping series)
   {
      latest += ({ series->episodes[0] });
   }
   return latest;

}

int(0..1) compare_date(array a, array b) {

   if (!arrayp(a) && !arrayp(b)) return false;
   if (!arrayp(a) || !sizeof(a)) return arrayp(b) && sizeof(b);
   if (!arrayp(b) || !sizeof(b)) return arrayp(a) && sizeof(a);
   if (a[0] == b[0]) return compare_date(a[1..], b[1..]);
   return a[0] < b[0];

}

int(0..1) compare_by_date(mapping a, mapping b) {

   return compare_date(reverse(a->date), reverse(b->date));

}

void watch_list(mapping database) {

   map(Array.sort_array(get_latest(database), compare_by_date), 
       print_episode);

}

string prompt_read(string prompt) {

   write("%s: ", prompt);
   return Stdio.stdin.gets();

}

array parse_date(string date) {

   return (array(int))(date/".");

}

mapping prompt_for_episode() {

   return make_episode(prompt_read("Series"), 
                       prompt_read("Title"), 
                       prompt_read("Episode"), 
                       parse_date(prompt_read("Date watched")));

}

// pike offers encode_value() and decode_value() as standard ways // to save and read data, but that is not a human readable format. // therefore we are instead printing the structure as debug-output // which is a readable form as long as it only contains integers, // strings, mappings, arrays and multisets this format can be read by pike. // to read it we are creating a class that contains the data as a value, // which is then compiled and instantiated to allow us to pull the data out. void save_db(string filename, mapping database) {

   Stdio.write_file(filename, sprintf("%O", database));

}

void watch_save() {

   save_db("pwatch", db);

}

mapping load_db(string filename) {

   if (file_stat(filename))
       return compile_string("mixed data = " + 
                             Stdio.read_file(filename) + ";")()->data;
   else return ([]);

}

mapping get_series(string name, mapping database) {

   return database[name];

}

array get_episode_list(string series, mapping database) {

   return database[series]->episodes;

}

void watch_new_series(string name, string status, mapping database) {

   database[name] = (["series":name, "status":status, "episodes":({}) ]);

}

mapping get_or_add_series(string name, mapping database) {

   if (!database[name])
   {
       string answer = prompt_read("Add new series? [y/n]: ");
       if (answer == "y")
           watch_new_series(name, "active", database);
   }
   return database[name];

}

void watch_add(mapping database) {

   mapping episode = prompt_for_episode();
   string series_name = episode->series;
   mapping series = get_or_add_series(series_name, database);
   if (!series)
       watch_add(database);
   else
       series->episodes = Array.unshift(series->episodes, episode);

}

void watch_load() {

   db = load_db("pwatch");

}

int main(int argc, array argv) {

   watch_load();
   if (argc>1 && argv[1] == "add")
   {
       watch_add(db);
       watch_save();
   }
   else
       watch_list(db);

}</lang>

Ruby

<lang ruby>require 'date' require 'json' require 'securerandom'

class SimpleDatabase

 def initialize(dbname, *fields)
   @dbname = dbname
   @filename = @dbname + ".dat"
   @fields = fields
   @maxl = @fields.collect {|f| f.length}.max
   @data = {
     'fields' => fields,
     'items' => {},
     'history' => [],
     'tags' => {},
   }
 end
 attr_reader :dbname, :fields
 def self.open(dbname)
   db = new(dbname)
   db.read
   db
 end
 def read()
   if not File.exists?(@filename)
     raise ArgumentError, "Database #@dbname has not been created"
   end
   @data = JSON.parse(File.read(@filename))
   @fields = @data['fields']
   @maxl = @fields.collect {|f| f.length}.max
 end
 def write()
   File.open(@filename, 'w') {|f| f.write(JSON.generate(@data))}
 end
 def add(*values)
   id = SecureRandom.uuid
   @data['items'][id] = Hash[ @fields.zip(values) ]
   @data['history'] << [Time.now.to_f, id]
   id
 end
 def tag(id, *tags)
   tags.each do |tag|
     if @data['tags'][tag].nil?
       @data['tags'][tag] = [id]
     else
       @data['tags'][tag] << id
     end
   end
   id
 end
 def latest
   @data['history'].sort_by {|val| val[0]}.last.last
 end
 def get_item(id)
   @data['items'][id]
 end
 def tags()
   @data['tags'].keys.sort
 end
 def ids_for_tag(tag)
   @data['tags'][tag]
 end
 def tags_for_id(id)
   @data['tags'].keys.inject([]) do |tags, tag| 
     tags << tag if @data['tags'][tag].include?(id)
     tags
   end
 end
 def display(id)
   item = get_item(id)
   fmt = "%#{@maxl}s - %s\n"
   puts fmt % ['id', id]
   @fields.each {|f| print fmt % [f, item[f]]}
   puts fmt % ['tags', tags_for_id(id).join(',')]
   added = @data['history'].find {|x| x[1] == id}.first
   puts fmt % ['date added', Time.at(added).ctime]
   puts ""
 end
 def each()
   @data['history'].each {|time, id| yield id}
 end
 def each_item_with_tag(tag)
   @data['tags'][tag].each {|id| yield id}
 end

end def usage()

 puts <<END

usage: #{$0} command args ...

commands:

 help 
 create dbname field ...
 fields dbname
 add dbname value ...
 tag dbname id tag ...
 tags dbname
 list dbname [tag ...]
 latest dbname
 latest_by_tag dbname

END end

def open_database(args)

 dbname = args.shift
 begin
   SimpleDatabase.open(dbname)
 rescue ArgumentError => e
   STDERR.puts e.message
   exit 1
 end

end

def process_command_line(command, *args)

 case command
 when 'help'
   usage
 when 'create'
   db = SimpleDatabase.new(*args)
   db.write
   puts "Database #{args[0]} created"
 when 'fields'
   db = open_database(args)
   puts "Database #{db.dbname} fields:"
   puts db.fields.join(',')
 when 'add'
   db = open_database(args)
   id = db.add(*args)
   db.write
   puts "Database #{db.dbname} added id #{id}"
 when 'tag'
   db = open_database(args)
   id = args.shift
   db.tag(id, *args)
   db.write
   db.display(id)
   
 when 'tags'
   db = open_database(args)
   puts "Database #{db.dbname} tags:"
   puts db.tags.join(',')
 when 'list'
   db = open_database(args)
   if args.empty?
     db.each {|id| db.display(id)}
   else
     args.each do |tag| 
       puts "Items tagged #{tag}"
       db.each_item_with_tag(tag) {|id| db.display(id)}
     end
   end
 when 'latest'
   db = open_database(args)
   db.display(db.latest)
 when 'latest_by_tag'
   db = open_database(args)
   db.tags.each do |tag|
     puts tag
     db.display(db.ids_for_tag(tag).last)
   end
 else
   puts "Error: unknown command '#{command}'"
   usage
 end

end

process_command_line *ARGV</lang>

Sample session

$ ruby simple_database.rb create appointments event_title start_time stop_time location event_description
Database appointments created
$ ruby simple_database.rb add appointments "Wife's Birthday" "2011-11-01" "2011-11-01" "" "happy 39th"
Database appointments added id 6dd02195-1efe-40d1-b43e-c2efd852cd1d
$ ruby simple_database.rb tag appointments 6dd02195-1efe-40d1-b43e-c2efd852cd1d birthday family
               id - 6dd02195-1efe-40d1-b43e-c2efd852cd1d
      event_title - Wife's Birthday
       start_time - 2011-11-01
        stop_time - 2011-11-01
         location -
event_description - happy 39th
             tags - birthday,family
       date added - Thu Nov  3 15:52:31 2011

$ ruby simple_database.rb add appointments "Parent-Teacher Conference" "2011-11-03 19:30" "2011-11-03 20:00" "school" "desc"
Database appointments added id 0190b835-401d-42da-9ed3-1d335d27b83c
$ ruby simple_database.rb tag appointments 0190b835-401d-42da-9ed3-1d335d27b83c school family
               id - 0190b835-401d-42da-9ed3-1d335d27b83c
      event_title - Parent-Teacher Conference
       start_time - 2011-11-03 19:30
        stop_time - 2011-11-03 20:00
         location - school
event_description - desc
             tags - family,school
       date added - Thu Nov  3 15:54:05 2011

$ ruby simple_database.rb add appointments "Buy gift for wife" "2011-10-31 16:00" "2011-10-31 16:30" "the mall" "hmm, maybe jewelery?"
Database appointments added id 4023e6f1-bcc1-49e5-a59f-138157b413f4
$ ruby simple_database.rb tag appointments 4023e6f1-bcc1-49e5-a59f-138157b413f4 family last-minute
               id - 4023e6f1-bcc1-49e5-a59f-138157b413f4
      event_title - Buy gift for wife
       start_time - 2011-10-31 16:00
        stop_time - 2011-10-31 16:30
         location - the mall
event_description - hmm, maybe jewelery?
             tags - family,last-minute
       date added - Thu Nov  3 15:55:02 2011

$ ruby simple_database.rb fields appointments
Database appointments fields:
event_title,start_time,stop_time,location,event_description
$ ruby simple_database.rb tags appointments
Database appointments tags:
birthday,family,last-minute,school
$ ruby simple_database.rb list appointments
               id - 6dd02195-1efe-40d1-b43e-c2efd852cd1d
      event_title - Wife's Birthday
       start_time - 2011-11-01
        stop_time - 2011-11-01
         location -
event_description - happy 39th
             tags - birthday,family
       date added - Thu Nov  3 15:52:31 2011

               id - 0190b835-401d-42da-9ed3-1d335d27b83c
      event_title - Parent-Teacher Conference
       start_time - 2011-11-03 19:30
        stop_time - 2011-11-03 20:00
         location - school
event_description - desc
             tags - family,school
       date added - Thu Nov  3 15:54:05 2011

               id - 4023e6f1-bcc1-49e5-a59f-138157b413f4
      event_title - Buy gift for wife
       start_time - 2011-10-31 16:00
        stop_time - 2011-10-31 16:30
         location - the mall
event_description - hmm, maybe jewelery?
             tags - family,last-minute
       date added - Thu Nov  3 15:55:02 2011
$ cat appointments.dat
{"fields":["event_title","start_time","stop_time","location","event_description"],"items":{"6dd02195-1efe-40d1-b43e-c2efd852cd1d":{"event_title":"Wife's Birthday","start_time":"2011-11-01","stop_time":"2011-11-01","location":"","event_description":"happy 39th"},"0190b835-401d-42da-9ed3-1d335d27b83c":{"event_title":"Parent-Teacher Conference","start_time":"2011-11-03 19:30","stop_time":"2011-11-03 20:00","location":"school","event_description":"desc"},"4023e6f1-bcc1-49e5-a59f-138157b413f4":{"event_title":"Buy gift for wife","start_time":"2011-10-31 16:00","stop_time":"2011-10-31 16:30","location":"the mall","event_description":"hmm, maybe jewelery?"}},"history":[[1320349951.000625,"6dd02195-1efe-40d1-b43e-c2efd852cd1d"],[1320350045.4736252,"0190b835-401d-42da-9ed3-1d335d27b83c"],[1320350102.9486248,"4023e6f1-bcc1-49e5-a59f-138157b413f4"]],"tags":{"birthday":["6dd02195-1efe-40d1-b43e-c2efd852cd1d"],"family":["6dd02195-1efe-40d1-b43e-c2efd852cd1d","0190b835-401d-42da-9ed3-1d335d27b83c","4023e6f1-bcc1-49e5-a59f-138157b413f4"],"school":["0190b835-401d-42da-9ed3-1d335d27b83c"],"last-minute":["4023e6f1-bcc1-49e5-a59f-138157b413f4"]}}

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>