Quellcode durchsuchen

add list command

bmallred vor 10 Jahren
Ursprung
Commit
94b04cacee
13 geänderte Dateien mit 346 neuen und 151 gelöschten Zeilen
  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,9 +1,7 @@
1 1
package main
2 2
3 3
import (
4
	"log"
5 4
	"os"
6
	"path/filepath"
7 5
8 6
	"github.com/codegangsta/cli"
9 7
)
@ -14,26 +12,6 @@ const (
14 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 15
// Application entry point
38 16
func main() {
39 17
	app := cli.NewApp()

+ 13 - 2
clear.go

@ -2,23 +2,34 @@ package main
2 2
3 3
import (
4 4
	"os"
5
	"path/filepath"
5 6
6 7
	"github.com/codegangsta/cli"
7 8
)
8 9
10
// Command line subcommand for "clear"
9 11
var commandClear = cli.Command{
10 12
	Name:      "clear",
11 13
	ShortName: "",
12 14
	Usage:     "",
13 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 25
// Clear current pending transaction
17 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 31
	check(err)
20 32
	defer file.Close()
21
22 33
	_, err = file.Write([]byte{})
23 34
	check(err)
24 35
}

+ 19 - 32
commit.go

@ -4,12 +4,14 @@ import (
4 4
	"errors"
5 5
	"io/ioutil"
6 6
	"os"
7
	"path/filepath"
7 8
	"strings"
8 9
	"time"
9 10
10 11
	"github.com/codegangsta/cli"
11 12
)
12 13
14
// Command line subcommand for "commit"
13 15
var commandCommit = cli.Command{
14 16
	Name:      "commit",
15 17
	ShortName: "c",
@ -21,11 +23,22 @@ var commandCommit = cli.Command{
21 23
			Value: time.Now().UTC().Format("2006-01-02"),
22 24
			Usage: "",
23 25
		},
26
		cli.StringFlag{
27
			Name:  "file",
28
			Value: "general.ledger",
29
			Usage: "",
30
		},
24 31
	},
25 32
}
26 33
27 34
// Commit the pending transaction
28 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 42
	date, err := parseDate(c.String("date"))
30 43
	check(err)
31 44
@ -33,34 +46,8 @@ func actionCommit(c *cli.Context) {
33 46
	project := parseProject(args)
34 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 53
// Parse a given string to extract a project name
@ -90,12 +77,12 @@ func parseDescription(fields []string, project string) string {
90 77
}
91 78
92 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 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 86
	check(err)
100 87
101 88
	t := Transaction{
@ -117,7 +104,7 @@ func writeTransaction(date time.Time, project, description string) {
117 104
	err = t.CheckBalance()
118 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 108
	check(err)
122 109
	defer file.Close()
123 110
	_, err = file.WriteString(t.ToString())

+ 47 - 0
commandCredit.go

@ -0,0 +1,47 @@
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

@ -0,0 +1,47 @@
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

@ -0,0 +1,98 @@
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,21 +3,34 @@ package main
3 3
import (
4 4
	"fmt"
5 5
	"io/ioutil"
6
	"os"
7
	"path/filepath"
6 8
7 9
	"github.com/codegangsta/cli"
8 10
)
9 11
12
// Command line subcommand for "status"
10 13
var commandStatus = cli.Command{
11 14
	Name:      "status",
12 15
	ShortName: "stat",
13 16
	Usage:     "",
14 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 27
// Display the current status of the ledger
18 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 34
		check(err)
22 35
		fmt.Println(string(pending))
23 36
	} else {

+ 0 - 33
credit.go

@ -1,33 +0,0 @@
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,33 +0,0 @@
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,6 +7,7 @@ import (
7 7
	"math/big"
8 8
	"os"
9 9
	"strings"
10
	"time"
10 11
)
11 12
12 13
// Helper function to check for fatal errors
@ -16,13 +17,20 @@ func check(e error) {
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 27
// Format the ledger so it is human readable
20 28
func formatLedger() {
21 29
}
22 30
23 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 34
	check(err)
27 35
	defer file.Close()
28 36
@ -43,7 +51,35 @@ func parseAccount(fields []string) (string, error) {
43 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 81
// Parse the value from the arguments
82
47 83
func parseValue(fields []string, account string) (*big.Rat, error) {
48 84
	r := new(big.Rat)
49 85

+ 52 - 0
ledger.go

@ -0,0 +1,52 @@
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,14 +0,0 @@
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,6 +20,10 @@ func (t *Transaction) FromString(text string) {
20 20
	// Parse the lines of text
21 21
	lines := strings.Split(text, "\n")
22 22
	for i, line := range lines {
23
		if len(line) == 0 {
24
			continue
25
		}
26
23 27
		switch i {
24 28
		case 0:
25 29
			fields := strings.Split(line, "\t")
@ -34,20 +38,17 @@ func (t *Transaction) FromString(text string) {
34 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 45
			break
44 46
45 47
		default:
46
			var account Account
47
			err := account.FromString(line)
48
			var a Account
49
			err := a.FromString(line)
48 50
			check(err)
49
50
			t.Accounts = append(t.Accounts, account)
51
			t.Accounts = append(t.Accounts, a)
51 52
			break
52 53
		}
53 54
	}
@ -84,5 +85,10 @@ func (t *Transaction) ToString() string {
84 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
}