package main import ( "bytes" "errors" "fmt" "log" "os" "strings" "time" "github.com/codegangsta/cli" ) const ( // Application name APP_NAME = "cash" // Application usage description APP_USAGE = "counting coins" // Application version APP_VER = "0.0.0" ) var ( // Ledger file name Ledger = "example.ledger" TransactionFormat = "%s\t%s\t%s" AccountFormat = "\t%s\t%s" ) // Application entry point func main() { // Flags pertaining to a transaction action transactionFlags := []cli.Flag{ cli.StringFlag{ Name: "date", Value: time.Now().UTC().Format("2006-01-02"), Usage: "", }, } app := cli.NewApp() app.Name = APP_NAME app.Usage = APP_USAGE app.Version = APP_VER app.Commands = []cli.Command{ { Name: "credit", ShortName: "cr", Usage: "", Action: actionCredit, }, { Name: "debit", ShortName: "dr", Usage: "", Action: actionDebit, }, { Name: "status", ShortName: "stat", Usage: "", Action: actionStatus, }, { Name: "commit", ShortName: "c", Usage: "", Action: actionCommit, Flags: transactionFlags, }, { Name: "list", ShortName: "ls", Usage: "", Action: actionList, }, } app.Run(os.Args) } // Helper function to check for fatal errors func check(e error) { if e != nil { log.Fatal(fmt.Sprintf("Error: %s", e)) } } // Add a credit to the pending transaction func actionCredit(c *cli.Context) { addPendingTransaction() // Format: /t{account}/t-{value} } // Add a debit to the pending transaction func actionDebit(c *cli.Context) { addPendingTransaction() // Format: /t{account}/t+{value} } // Display the current status of the ledger func actionStatus(c *cli.Context) { log.Println(hasPendingTransaction()) } // Commit the pending transaction func actionCommit(c *cli.Context) { date := parseDate(c.String("date")) if date == "" { log.Fatal("Invalid transaction date") } args := c.Args() project := parseProject(args) description := parseDescription(args, project) err := writeTransaction(date, project, description) if err != nil { log.Fatal(err) } } // List the ledger contents func actionList(c *cli.Context) { } // Format the ledger so it is human readable func formatLedger() { } // Determines if there is currently a pending transaction in the ledger func hasPendingTransaction() bool { file, err := os.Open(Ledger) check(err) defer file.Close() info, err := file.Stat() check(err) size := info.Size() if size > 1024 { size = 1024 } _, err = file.Seek(size*-1, 2) check(err) buffer := make([]byte, size) _, err = file.Read(buffer) check(err) return strings.Contains(string(buffer), "@pending") } // Adds a pending transaction if one is not already present func addPendingTransaction() { if !hasPendingTransaction() { file, err := os.OpenFile(Ledger, os.O_APPEND|os.O_WRONLY, 0666) check(err) defer file.Close() _, err = file.WriteString("@pending") check(err) } } // Parse the given string to extract a proper date func parseDate(in string) string { formats := []string{ "2006-01-02", "2006/01/02", "2006-1-2", "2006/1/2", "01-02-2006", "01/02/2006", "1-2-2006", "1/2/2006", "Jan 2, 2006", "Jan 02, 2006", "2 Jan 2006", "02 Jan 2006", } for _, f := range formats { d, err := time.Parse(f, in) if err == nil { return d.Format(formats[0]) } } return "" } // Parse a given string to extract a project name func parseProject(fields []string) string { project := "@general" for i := 0; i < len(fields); i++ { if strings.HasPrefix(fields[i], "@") { project = fields[i] break } } return project } // Parse the description from the arguments func parseDescription(fields []string, project string) string { for i := 0; i < len(fields); i++ { if fields[i] == project { fields[i] = "" break } } return strings.Replace(strings.Join(fields, " "), " ", " ", -1) } // Write a transaction line where there is a pending transaction func writeTransaction(date, project, description string) error { if !hasPendingTransaction() { return errors.New("No pending transaction to write") } file, err := os.OpenFile(Ledger, os.O_RDWR, 0666) check(err) defer file.Close() info, err := file.Stat() check(err) size := info.Size() if size > 1024 { size = 1024 } _, err = file.Seek(size*-1, 2) check(err) buffer := make([]byte, size) _, err = file.Read(buffer) check(err) // Find the line containing @pending and replace it with our transaction line := fmt.Sprintf("%s\t%s\t%s", date, project, description) buffer = bytes.Replace(buffer, []byte("@pending"), []byte(line), -1) offset := info.Size() - size _, err = file.WriteAt(buffer, offset) check(err) return nil }