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