Simple database: Difference between revisions

Content added Content deleted
m (→‎{{header|J}}: improve accuracy of data in database.)
(Go solution)
Line 242: Line 242:
(main (argv))</lang>
(main (argv))</lang>


=={{header|Go}}==
<lang go>package main

import (
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strings"
"time"
"unicode"
)

// Database record format. Time stamp and name are required.
// Tags and notes are optional.
type Item struct {
Stamp time.Time
Name string
Tags []string `json:",omitempty"`
Notes string `json:",omitempty"`
}

// Item implements stringer interface
func (i *Item) String() string {
s := i.Stamp.Format(time.ANSIC) + "\n Name: " + i.Name
if len(i.Tags) > 0 {
s = fmt.Sprintf("%s\n Tags: %v", s, i.Tags)
}
if i.Notes > "" {
s += "\n Notes: " + i.Notes
}
return s
}

// collection of Items
type db []*Item

// db implements sort.Interface
func (d db) Len() int { return len(d) }
func (d db) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
func (d db) Less(i, j int) bool { return d[i].Stamp.Before(d[j].Stamp) }

// hard coded database file name
const fn = "sdb.json"

func main() {
if len(os.Args) == 1 {
latest()
return
}
switch os.Args[1] {
case "add":
add()
case "latest":
latest()
case "tags":
tags()
case "all":
all()
case "help":
help()
default:
usage("unrecognized command")
}
}

func usage(err string) {
if err > "" {
fmt.Println(err)
}
fmt.Println(`usage: sdb [command] [data]
where command is one of add, latest, tags, all, or help.`)
}

func help() {
usage("")
fmt.Println(`
Commands must be in lower case.
If no command is specified, the default command is latest.

Latest prints the latest item.
All prints all items in chronological order.
Tags prints the lastest item for each tag.
Help prints this message.

Add adds data as a new record. The format is,

name [tags] [notes]

Name is the name of the item and is required for the add command.

Tags are optional. A tag is a single word.
A single tag can be specified without enclosing brackets.
Multiple tags can be specified by enclosing them in square brackets.

Text remaining after tags is taken as notes. Notes do not have to be
enclosed in quotes or brackets. The brackets above are only showing
that notes are optional.

Quotes may be useful however--as recognized by your operating system shell
or command line--to allow entry of arbitrary text. In particular, quotes
or escape characters may be needed to prevent the shell from trying to
interpret brackets or other special characters.

Examples:
sdb add Bookends // no tags, no notes
sdb add Bookends rock my favorite // tag: rock, notes: my favorite
sdb add Bookends [rock folk] // two tags
sdb add Bookends [] "Simon & Garfunkel" // notes, no tags
sdb add "Simon&Garfunkel [artist]" // name: Simon&Garfunkel, tag: artist
As shown in the last example, if you use features of your shell to pass
all data as a single string, the item name and tags will still be identified
by separating whitespace.
The database is stored in JSON format in the file "sdb.json"
`)
}

// load data for read only purposes.
func load() (db, bool) {
d, f, ok := open()
if ok {
f.Close()
if len(d) == 0 {
fmt.Println("no items")
ok = false
}
}
return d, ok
}

// open database, leave open
func open() (d db, f *os.File, ok bool) {
var err error
f, err = os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("cant open??")
fmt.Println(err)
return
}
jd := json.NewDecoder(f)
err = jd.Decode(&d)
// EOF just means file was empty. That's okay with us.
if err != nil && err != io.EOF {
fmt.Println(err)
f.Close()
return
}
ok = true
return
}

// handle latest command
func latest() {
d, ok := load()
if !ok {
return
}
sort.Sort(d)
fmt.Println(d[len(d)-1])
}

// handle all command
func all() {
d, ok := load()
if !ok {
return
}
sort.Sort(d)
for _, i := range d {
fmt.Println("-----------------------------------")
fmt.Println(i)
}
fmt.Println("-----------------------------------")
}

// handle tags command
func tags() {
d, ok := load()
if !ok {
return
}
// we have to traverse the entire list to collect tags so there
// is no point in sorting at this point.
// collect set of unique tags associated with latest item for each
latest := make(map[string]*Item)
for _, item := range d {
for _, tag := range item.Tags {
li, ok := latest[tag]
if !ok || item.Stamp.After(li.Stamp) {
latest[tag] = item
}
}
}
// invert to set of unique items, associated with subset of tags
// for which the item is the latest.
type itemTags struct {
item *Item
tags []string
}
inv := make(map[*Item][]string)
for tag, item := range latest {
inv[item] = append(inv[item], tag)
}
// now we sort just the items we will output
li := make(db, len(inv))
i := 0
for item := range inv {
li[i] = item
i++
}
sort.Sort(li)
// finally ready to print
for _, item := range li {
tags := inv[item]
fmt.Println("-----------------------------------")
fmt.Println("Latest item with tags", tags)
fmt.Println(item)
}
fmt.Println("-----------------------------------")
}
// handle add command
func add() {
if len(os.Args) < 3 {
usage("add command requires data")
return
} else if len(os.Args) == 3 {
add1()
} else {
add4()
}
}

// add command with one data string. look for ws as separators.
func add1() {
data := strings.TrimLeftFunc(os.Args[2], unicode.IsSpace)
if data == "" {
// data must have at least some non-whitespace
usage("invalid name")
return
}
sep := strings.IndexFunc(data, unicode.IsSpace)
if sep < 0 {
// data consists only of a name
addItem(data, nil, "")
return
}
name := data[:sep]
data = strings.TrimLeftFunc(data[sep:], unicode.IsSpace)
if data == "" {
// nevermind trailing ws, it's still only a name
addItem(name, nil, "")
return
}
if data[0] == '[' {
sep = strings.Index(data, "]")
if sep < 0 {
// close bracketed list for the user. no notes.
addItem(name, strings.Fields(data[1:]), "")
} else {
// brackets make things easy
addItem(name, strings.Fields(data[1:sep]),
strings.TrimLeftFunc(data[sep+1:], unicode.IsSpace))
}
return
}
sep = strings.IndexFunc(data, unicode.IsSpace)
if sep < 0 {
// remaining word is a tag
addItem(name, []string{data}, "")
} else {
// there's a tag and some data
addItem(name, []string{data[:sep]},
strings.TrimLeftFunc(data[sep+1:], unicode.IsSpace))
}
}

// add command with multiple strings remaining on command line
func add4() {
name := os.Args[2]
tag1 := os.Args[3]
if tag1[0] != '[' {
// no brackets makes things easy
addItem(name, []string{tag1}, strings.Join(os.Args[4:], " "))
return
}
if tag1[len(tag1)-1] == ']' {
// tags all in one os.Arg is easy too
addItem(name, strings.Fields(tag1[1:len(tag1)-1]),
strings.Join(os.Args[4:], " "))
return
}
// start a list for tags
var tags []string
if tag1 > "[" {
tags = []string{tag1[1:]}
}
for x, tag := range os.Args[4:] {
if tag[len(tag)-1] != ']' {
tags = append(tags, tag)
} else {
// found end of tag list
if tag > "]" {
tags = append(tags, tag[:len(tag)-1])
}
addItem(name, tags, strings.Join(os.Args[5+x:], " "))
return
}
}
// close bracketed list for the user. no notes.
addItem(name, tags, "")
}

// complete the add command
func addItem(name string, tags []string, notes string) {
db, f, ok := open()
if !ok {
return
}
defer f.Close()
// add the item and format JSON
db = append(db, &Item{time.Now(), name, tags, notes})
sort.Sort(db)
js, err := json.MarshalIndent(db, "", " ")
if err != nil {
fmt.Println(err)
return
}
// time to overwrite the file
if _, err = f.Seek(0, 0); err != nil {
fmt.Println(err)
return
}
f.Truncate(0)
if _, err = f.Write(js); err != nil {
fmt.Println(err)
}
}</lang>
=={{header|J}}==
=={{header|J}}==