Browse Source

add list command

bmallred 10 years ago
parent
commit
94b04cacee
13 changed files with 346 additions and 151 deletions
  1. 0 22
      cash.go
  2. 13 2
      clear.go
  3. 19 32
      commit.go
  4. 47 0
      commandCredit.go
  5. 47 0
      commandDebit.go
  6. 98 0
      commandList.go
  7. 15 2
      status.go
  8. 0 33
      credit.go
  9. 0 33
      debit.go
  10. 38 2
      helpers.go
  11. 52 0
      ledger.go
  12. 0 14
      list.go
  13. 17 11
      transaction.go

+ 0 - 22
cash.go

1
package main
1
package main
2
2
3
import (
3
import (
4
	"log"
5
	"os"
4
	"os"
6
	"path/filepath"
7
5
8
	"github.com/codegangsta/cli"
6
	"github.com/codegangsta/cli"
9
)
7
)
14
	APP_VER   = "0.0.0"
12
	APP_VER   = "0.0.0"
15
)
13
)
16
14
17
var (
18
	LedgerFile  = "general.ledger"
19
	PendingFile = filepath.Join(os.TempDir(), "pending.ledger")
20
)
21
22
// Initialize the application
23
func init() {
24
	if _, err := os.Stat(LedgerFile); os.IsNotExist(err) {
25
		_, err = os.Create(LedgerFile)
26
		check(err)
27
		log.Println("Created ledger file at", LedgerFile)
28
	}
29
30
	if _, err := os.Stat(PendingFile); os.IsNotExist(err) {
31
		_, err = os.Create(PendingFile)
32
		check(err)
33
		log.Println("Created pending file at", PendingFile)
34
	}
35
}
36
37
// Application entry point
15
// Application entry point
38
func main() {
16
func main() {
39
	app := cli.NewApp()
17
	app := cli.NewApp()

+ 13 - 2
clear.go

2
2
3
import (
3
import (
4
	"os"
4
	"os"
5
	"path/filepath"
5
6
6
	"github.com/codegangsta/cli"
7
	"github.com/codegangsta/cli"
7
)
8
)
8
9
10
// Command line subcommand for "clear"
9
var commandClear = cli.Command{
11
var commandClear = cli.Command{
10
	Name:      "clear",
12
	Name:      "clear",
11
	ShortName: "",
13
	ShortName: "",
12
	Usage:     "",
14
	Usage:     "",
13
	Action:    actionClear,
15
	Action:    actionClear,
16
	Flags: []cli.Flag{
17
		cli.StringFlag{
18
			Name:  "file",
19
			Value: "general.ledger",
20
			Usage: "",
21
		},
22
	},
14
}
23
}
15
24
16
// Clear current pending transaction
25
// Clear current pending transaction
17
func actionClear(c *cli.Context) {
26
func actionClear(c *cli.Context) {
18
	file, err := os.OpenFile(PendingFile, os.O_TRUNC|os.O_WRONLY, 0666)
27
	pendingFile := filepath.Join(os.TempDir(), c.String("file"))
28
	ensureFileExists(pendingFile)
29
30
	file, err := os.OpenFile(pendingFile, os.O_TRUNC|os.O_WRONLY, 0666)
19
	check(err)
31
	check(err)
20
	defer file.Close()
32
	defer file.Close()
21
22
	_, err = file.Write([]byte{})
33
	_, err = file.Write([]byte{})
23
	check(err)
34
	check(err)
24
}
35
}

+ 19 - 32
commit.go

4
	"errors"
4
	"errors"
5
	"io/ioutil"
5
	"io/ioutil"
6
	"os"
6
	"os"
7
	"path/filepath"
7
	"strings"
8
	"strings"
8
	"time"
9
	"time"
9
10
10
	"github.com/codegangsta/cli"
11
	"github.com/codegangsta/cli"
11
)
12
)
12
13
14
// Command line subcommand for "commit"
13
var commandCommit = cli.Command{
15
var commandCommit = cli.Command{
14
	Name:      "commit",
16
	Name:      "commit",
15
	ShortName: "c",
17
	ShortName: "c",
21
			Value: time.Now().UTC().Format("2006-01-02"),
23
			Value: time.Now().UTC().Format("2006-01-02"),
22
			Usage: "",
24
			Usage: "",
23
		},
25
		},
26
		cli.StringFlag{
27
			Name:  "file",
28
			Value: "general.ledger",
29
			Usage: "",
30
		},
24
	},
31
	},
25
}
32
}
26
33
27
// Commit the pending transaction
34
// Commit the pending transaction
28
func actionCommit(c *cli.Context) {
35
func actionCommit(c *cli.Context) {
36
	ledgerFile := c.String("file")
37
	ensureFileExists(ledgerFile)
38
39
	pendingFile := filepath.Join(os.TempDir(), c.String("file"))
40
	ensureFileExists(pendingFile)
41
29
	date, err := parseDate(c.String("date"))
42
	date, err := parseDate(c.String("date"))
30
	check(err)
43
	check(err)
31
44
33
	project := parseProject(args)
46
	project := parseProject(args)
34
	description := parseDescription(args, project)
47
	description := parseDescription(args, project)
35
48
36
	writeTransaction(date, project, description)
37
}
38
39
// Parse the given string to extract a proper date
40
func parseDate(in string) (time.Time, error) {
41
	formats := []string{
42
		"2006-01-02",
43
		"2006/01/02",
44
		"2006-1-2",
45
		"2006/1/2",
46
		"01-02-2006",
47
		"01/02/2006",
48
		"1-2-2006",
49
		"1/2/2006",
50
		"Jan 2, 2006",
51
		"Jan 02, 2006",
52
		"2 Jan 2006",
53
		"02 Jan 2006",
54
	}
55
56
	for _, f := range formats {
57
		d, err := time.Parse(f, in)
58
		if err == nil {
59
			return d, nil
60
		}
61
	}
62
63
	return time.Now().UTC(), errors.New("No valid date provided")
49
	writeTransaction(ledgerFile, pendingFile, project, description, date)
50
	actionClear(c)
64
}
51
}
65
52
66
// Parse a given string to extract a project name
53
// Parse a given string to extract a project name
90
}
77
}
91
78
92
// Write a transaction line where there is a pending transaction
79
// Write a transaction line where there is a pending transaction
93
func writeTransaction(date time.Time, project, description string) {
94
	if !hasPendingTransaction() {
80
func writeTransaction(ledgerFile, pendingFile, project, description string, date time.Time) {
81
	if !hasPendingTransaction(pendingFile) {
95
		check(errors.New("No pending transaction to write"))
82
		check(errors.New("No pending transaction to write"))
96
	}
83
	}
97
84
98
	pending, err := ioutil.ReadFile(PendingFile)
85
	pending, err := ioutil.ReadFile(pendingFile)
99
	check(err)
86
	check(err)
100
87
101
	t := Transaction{
88
	t := Transaction{
117
	err = t.CheckBalance()
104
	err = t.CheckBalance()
118
	check(err)
105
	check(err)
119
106
120
	file, err := os.OpenFile(LedgerFile, os.O_APPEND|os.O_WRONLY, 0666)
107
	file, err := os.OpenFile(ledgerFile, os.O_APPEND|os.O_WRONLY, 0666)
121
	check(err)
108
	check(err)
122
	defer file.Close()
109
	defer file.Close()
123
	_, err = file.WriteString(t.ToString())
110
	_, err = file.WriteString(t.ToString())

+ 47 - 0
commandCredit.go

1
package main
2
3
import (
4
	"os"
5
	"path/filepath"
6
7
	"github.com/codegangsta/cli"
8
)
9
10
// Command line subcommand for "credit"
11
var commandCredit = cli.Command{
12
	Name:      "credit",
13
	ShortName: "cr",
14
	Usage:     "",
15
	Action:    actionCredit,
16
	Flags: []cli.Flag{
17
		cli.StringFlag{
18
			Name:  "file",
19
			Value: "general.ledger",
20
			Usage: "",
21
		},
22
	},
23
}
24
25
// Add a credit to the pending transaction
26
func actionCredit(c *cli.Context) {
27
	pendingFile := filepath.Join(os.TempDir(), c.String("file"))
28
	ensureFileExists(pendingFile)
29
30
	args := c.Args()
31
	name, err := parseAccount(args)
32
	check(err)
33
	amount, err := parseValue(args, name)
34
	check(err)
35
36
	a := Account{
37
		Name:   name,
38
		Debit:  false,
39
		Amount: amount,
40
	}
41
42
	f, err := os.OpenFile(pendingFile, os.O_APPEND|os.O_WRONLY, 0666)
43
	check(err)
44
	defer f.Close()
45
	_, err = f.WriteString(a.ToString())
46
	check(err)
47
}

+ 47 - 0
commandDebit.go

1
package main
2
3
import (
4
	"os"
5
	"path/filepath"
6
7
	"github.com/codegangsta/cli"
8
)
9
10
// Command line subcommand for "debit"
11
var commandDebit = cli.Command{
12
	Name:      "debit",
13
	ShortName: "dr",
14
	Usage:     "",
15
	Action:    actionDebit,
16
	Flags: []cli.Flag{
17
		cli.StringFlag{
18
			Name:  "file",
19
			Value: "general.ledger",
20
			Usage: "",
21
		},
22
	},
23
}
24
25
// Add a debit to the pending transaction
26
func actionDebit(c *cli.Context) {
27
	pendingFile := filepath.Join(os.TempDir(), c.String("file"))
28
	ensureFileExists(pendingFile)
29
30
	args := c.Args()
31
	name, err := parseAccount(args)
32
	check(err)
33
	amount, err := parseValue(args, name)
34
	check(err)
35
36
	a := Account{
37
		Name:   name,
38
		Debit:  true,
39
		Amount: amount,
40
	}
41
42
	f, err := os.OpenFile(pendingFile, os.O_APPEND|os.O_WRONLY, 0666)
43
	check(err)
44
	defer f.Close()
45
	_, err = f.WriteString(a.ToString())
46
	check(err)
47
}

+ 98 - 0
commandList.go

1
package main
2
3
import (
4
	"bufio"
5
	"bytes"
6
	"fmt"
7
	"os"
8
9
	"github.com/codegangsta/cli"
10
)
11
12
// Command line subcommand for "list"
13
var commandList = cli.Command{
14
	Name:      "list",
15
	ShortName: "ls",
16
	Usage:     "",
17
	Action:    actionList,
18
	Flags: []cli.Flag{
19
		cli.StringFlag{
20
			Name:  "file",
21
			Value: "general.ledger",
22
			Usage: "",
23
		},
24
		cli.StringFlag{
25
			Name:  "project",
26
			Value: "",
27
			Usage: "",
28
		},
29
		cli.StringFlag{
30
			Name:  "sort",
31
			Value: "account",
32
			Usage: "",
33
		},
34
		cli.BoolFlag{
35
			Name:  "asc",
36
			Usage: "",
37
		},
38
	},
39
}
40
41
// List the ledger contents
42
func actionList(c *cli.Context) {
43
	ledgerFile := c.String("file")
44
	ensureFileExists(ledgerFile)
45
46
	f, err := os.Open(ledgerFile)
47
	check(err)
48
	defer f.Close()
49
50
	l := Ledger{}
51
	scanner := bufio.NewScanner(f)
52
	scanner.Split(ScanTransactions)
53
	for scanner.Scan() {
54
		text := scanner.Text()
55
56
		t := Transaction{}
57
		t.FromString(text)
58
		l.Transactions = append(l.Transactions, t)
59
	}
60
61
	fmt.Print(l.ToString())
62
}
63
64
// ScanTransactions is a split function for a Scanner that returns each line of
65
// text, stripped of any trailing end-of-line marker. The returned line may be
66
// empty. The end-of-line marker is one optional carriage return followed
67
// by one mandatory newline. In regular expression notation, it is `\r\n`.
68
// The last non-empty line of input will be returned even if it has no newline.
69
//
70
// source: https://golang.org/src/bufio/scan.go
71
func ScanTransactions(data []byte, atEOF bool) (advance int, token []byte, err error) {
72
	if atEOF && len(data) == 0 {
73
		return 0, nil, nil
74
	}
75
76
	if i := bytes.Index(data, []byte("\n\n")); i >= 0 {
77
		// We have a double newline terminated line
78
		return i + 2, dropCR(data[0:i]), nil
79
	}
80
81
	// If we're at EOF, we have a final, non-terminated line. Return it
82
	if atEOF {
83
		return len(data), dropCR(data), nil
84
	}
85
86
	// Request more data
87
	return 0, nil, nil
88
}
89
90
// dropCR drops a terminal \r from the data
91
// source: https://golang.org/src/bufio/scan.go
92
func dropCR(data []byte) []byte {
93
	if len(data) > 0 && data[len(data)-1] == '\r' {
94
		return data[0 : len(data)-1]
95
	}
96
97
	return data
98
}

+ 15 - 2
status.go

3
import (
3
import (
4
	"fmt"
4
	"fmt"
5
	"io/ioutil"
5
	"io/ioutil"
6
	"os"
7
	"path/filepath"
6
8
7
	"github.com/codegangsta/cli"
9
	"github.com/codegangsta/cli"
8
)
10
)
9
11
12
// Command line subcommand for "status"
10
var commandStatus = cli.Command{
13
var commandStatus = cli.Command{
11
	Name:      "status",
14
	Name:      "status",
12
	ShortName: "stat",
15
	ShortName: "stat",
13
	Usage:     "",
16
	Usage:     "",
14
	Action:    actionStatus,
17
	Action:    actionStatus,
18
	Flags: []cli.Flag{
19
		cli.StringFlag{
20
			Name:  "file",
21
			Value: "general.ledger",
22
			Usage: "",
23
		},
24
	},
15
}
25
}
16
26
17
// Display the current status of the ledger
27
// Display the current status of the ledger
18
func actionStatus(c *cli.Context) {
28
func actionStatus(c *cli.Context) {
19
	if hasPendingTransaction() {
20
		pending, err := ioutil.ReadFile(PendingFile)
29
	pendingFile := filepath.Join(os.TempDir(), c.String("file"))
30
	ensureFileExists(pendingFile)
31
32
	if hasPendingTransaction(pendingFile) {
33
		pending, err := ioutil.ReadFile(pendingFile)
21
		check(err)
34
		check(err)
22
		fmt.Println(string(pending))
35
		fmt.Println(string(pending))
23
	} else {
36
	} else {

+ 0 - 33
credit.go

1
package main
2
3
import (
4
	"fmt"
5
	"os"
6
7
	"github.com/codegangsta/cli"
8
)
9
10
var commandCredit = cli.Command{
11
	Name:      "credit",
12
	ShortName: "cr",
13
	Usage:     "",
14
	Action:    actionCredit,
15
}
16
17
// Add a credit to the pending transaction
18
func actionCredit(c *cli.Context) {
19
	args := c.Args()
20
21
	account, err := parseAccount(args)
22
	check(err)
23
24
	value, err := parseValue(args, account)
25
	check(err)
26
27
	f, err := os.OpenFile(PendingFile, os.O_APPEND|os.O_WRONLY, 0666)
28
	check(err)
29
	defer f.Close()
30
31
	_, err = f.WriteString(fmt.Sprintf("\t%s\t-%s\n", account, value.FloatString(2)))
32
	check(err)
33
}

+ 0 - 33
debit.go

1
package main
2
3
import (
4
	"fmt"
5
	"os"
6
7
	"github.com/codegangsta/cli"
8
)
9
10
var commandDebit = cli.Command{
11
	Name:      "debit",
12
	ShortName: "dr",
13
	Usage:     "",
14
	Action:    actionDebit,
15
}
16
17
// Add a debit to the pending transaction
18
func actionDebit(c *cli.Context) {
19
	args := c.Args()
20
21
	account, err := parseAccount(args)
22
	check(err)
23
24
	value, err := parseValue(args, account)
25
	check(err)
26
27
	f, err := os.OpenFile(PendingFile, os.O_APPEND|os.O_WRONLY, 0666)
28
	check(err)
29
	defer f.Close()
30
31
	_, err = f.WriteString(fmt.Sprintf("\t%s\t+%s\n", account, value.FloatString(2)))
32
	check(err)
33
}

+ 38 - 2
helpers.go

7
	"math/big"
7
	"math/big"
8
	"os"
8
	"os"
9
	"strings"
9
	"strings"
10
	"time"
10
)
11
)
11
12
12
// Helper function to check for fatal errors
13
// Helper function to check for fatal errors
16
	}
17
	}
17
}
18
}
18
19
20
func ensureFileExists(fileName string) {
21
	if _, err := os.Stat(fileName); os.IsNotExist(err) {
22
		_, err = os.Create(fileName)
23
		check(err)
24
	}
25
}
26
19
// Format the ledger so it is human readable
27
// Format the ledger so it is human readable
20
func formatLedger() {
28
func formatLedger() {
21
}
29
}
22
30
23
// Determines if there is currently a pending transaction in the ledger
31
// Determines if there is currently a pending transaction in the ledger
24
func hasPendingTransaction() bool {
25
	file, err := os.Open(PendingFile)
32
func hasPendingTransaction(pendingFile string) bool {
33
	file, err := os.Open(pendingFile)
26
	check(err)
34
	check(err)
27
	defer file.Close()
35
	defer file.Close()
28
36
43
	return fields[0], nil
51
	return fields[0], nil
44
}
52
}
45
53
54
// Parse the given string to extract a proper date
55
func parseDate(in string) (time.Time, error) {
56
	formats := []string{
57
		"2006-01-02",
58
		"2006/01/02",
59
		"2006-1-2",
60
		"2006/1/2",
61
		"01-02-2006",
62
		"01/02/2006",
63
		"1-2-2006",
64
		"1/2/2006",
65
		"Jan 2, 2006",
66
		"Jan 02, 2006",
67
		"2 Jan 2006",
68
		"02 Jan 2006",
69
	}
70
71
	for _, f := range formats {
72
		d, err := time.Parse(f, in)
73
		if err == nil {
74
			return d, nil
75
		}
76
	}
77
78
	return time.Now().UTC(), errors.New("No valid date provided")
79
}
80
46
// Parse the value from the arguments
81
// Parse the value from the arguments
82
47
func parseValue(fields []string, account string) (*big.Rat, error) {
83
func parseValue(fields []string, account string) (*big.Rat, error) {
48
	r := new(big.Rat)
84
	r := new(big.Rat)
49
85

+ 52 - 0
ledger.go

1
package main
2
3
import (
4
	"fmt"
5
	"math/big"
6
	"sort"
7
)
8
9
type Ledger struct {
10
	Transactions []Transaction
11
}
12
13
func (l *Ledger) ToString() string {
14
	balance := make(map[string]*big.Rat)
15
16
	for _, t := range l.Transactions {
17
		err := t.CheckBalance()
18
		check(err)
19
20
		for _, a := range t.Accounts {
21
			if b, ok := balance[a.Name]; ok {
22
				if a.Debit {
23
					b.Add(b, a.Amount)
24
				} else {
25
					b.Sub(b, a.Amount)
26
				}
27
			} else {
28
				if a.Debit {
29
					balance[a.Name] = a.Amount
30
				} else {
31
					neg := new(big.Rat)
32
					neg.SetInt64(-1)
33
					balance[a.Name] = a.Amount.Mul(a.Amount, neg)
34
				}
35
			}
36
		}
37
	}
38
39
	keys := make([]string, len(balance))
40
	i := 0
41
	for k := range balance {
42
		keys[i] = k
43
		i++
44
	}
45
	sort.Strings(keys)
46
47
	boom := ""
48
	for _, key := range keys {
49
		boom += fmt.Sprintf("%s\t\t%s\n", key, balance[key].FloatString(2))
50
	}
51
	return boom
52
}

+ 0 - 14
list.go

1
package main
2
3
import "github.com/codegangsta/cli"
4
5
var commandList = cli.Command{
6
	Name:      "list",
7
	ShortName: "ls",
8
	Usage:     "",
9
	Action:    actionList,
10
}
11
12
// List the ledger contents
13
func actionList(c *cli.Context) {
14
}

+ 17 - 11
transaction.go

20
	// Parse the lines of text
20
	// Parse the lines of text
21
	lines := strings.Split(text, "\n")
21
	lines := strings.Split(text, "\n")
22
	for i, line := range lines {
22
	for i, line := range lines {
23
		if len(line) == 0 {
24
			continue
25
		}
26
23
		switch i {
27
		switch i {
24
		case 0:
28
		case 0:
25
			fields := strings.Split(line, "\t")
29
			fields := strings.Split(line, "\t")
34
				description = strings.Join(fields[2:], " ")
38
				description = strings.Join(fields[2:], " ")
35
			}
39
			}
36
40
37
			t = &Transaction{
38
				Date:        date,
39
				Project:     project,
40
				Description: description,
41
				Accounts:    []Account{},
42
			}
41
			t.Date = date
42
			t.Project = project
43
			t.Description = description
44
			t.Accounts = []Account{}
43
			break
45
			break
44
46
45
		default:
47
		default:
46
			var account Account
47
			err := account.FromString(line)
48
			var a Account
49
			err := a.FromString(line)
48
			check(err)
50
			check(err)
49
50
			t.Accounts = append(t.Accounts, account)
51
			t.Accounts = append(t.Accounts, a)
51
			break
52
			break
52
		}
53
		}
53
	}
54
	}
84
		accounts += account.ToString()
85
		accounts += account.ToString()
85
	}
86
	}
86
87
87
	return fmt.Sprintf("%s\t%s\t%s\n%s\n", t.Date.Format("2006-01-02"), t.Project, t.Description, string(accounts))
88
	err := t.CheckBalance()
89
	if err != nil {
90
		accounts += fmt.Sprintf("Error: %s\n", err)
91
	}
92
93
	return fmt.Sprintf("%s\t%s\t%s\n%s\n", t.Date.Format("2006-01-02"), t.Project, t.Description, accounts)
88
}
94
}