package main import ( "errors" "fmt" "io/ioutil" "math/big" "os" "strings" "time" "github.com/codegangsta/cli" ) var commandCommit = cli.Command{ Name: "commit", ShortName: "c", Usage: "", Action: actionCommit, Flags: []cli.Flag{ cli.StringFlag{ Name: "date", Value: time.Now().UTC().Format("2006-01-02"), Usage: "", }, }, } // Commit the pending transaction func actionCommit(c *cli.Context) { date, err := parseDate(c.String("date")) check(err) args := c.Args() project := parseProject(args) description := parseDescription(args, project) writeTransaction(date.Format("2006-01-02"), project, description) } // Parse the given string to extract a proper date func parseDate(in string) (time.Time, error) { 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, nil } } return time.Now().UTC(), errors.New("No valid date provided") } // 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) } type Account struct { Name string Debit bool Amount *big.Rat } type Transaction struct { Date time.Time Project string Description string Accounts []Account } func (t *Transaction) FromString(text string) error { // Parse the lines of text lines := strings.Split(text, "\n") for i, line := range lines { fields := strings.Split(line, "\t") switch i { case 0: date, err := parseDate(fields[0]) check(err) project := fields[1] description := "" if len(fields) > 2 { description = strings.Join(fields[2:], " ") } t = &Transaction{ Date: date, Project: project, Description: description, Accounts: []Account{}, } break default: if len(fields) != 3 { break } account := fields[0] debit := true if strings.HasPrefix(fields[1], "-") { debit = false } value := new(big.Rat) value.SetString(fields[1][1:]) t.Accounts = append( t.Accounts, Account{ Name: account, Debit: debit, Amount: value, }) break } } // Check that they balance balance := new(big.Rat) for _, a := range t.Accounts { balance.Add(balance, a.Amount) } if balance.FloatString(2) != "0.00" { return errors.New("Transaction does not balance") } return nil } // Write a transaction line where there is a pending transaction func writeTransaction(date, project, description string) { if !hasPendingTransaction() { check(errors.New("No pending transaction to write")) } pending, err := ioutil.ReadFile(PendingFile) check(err) // Find the line containing @pending and replace it with our transaction s := fmt.Sprintf("%s\t%s\t%s\n%s\n", date, project, description, string(pending)) var t Transaction err = t.FromString(s) check(err) file, err := os.OpenFile(Ledger, os.O_APPEND|os.O_WRONLY, 0666) check(err) defer file.Close() _, err = file.WriteString(s) check(err) }