Browse Source

moved pending to temp file and simplified transaction process

bmallred 10 years ago
parent
commit
069bde778e
9 changed files with 319 additions and 231 deletions
  1. 20 228
      cash.go
  2. 24 0
      clear.go
  3. 111 0
      commit.go
  4. 33 0
      credit.go
  5. 33 0
      debit.go
  6. 0 3
      example.ledger
  7. 58 0
      helpers.go
  8. 14 0
      list.go
  9. 26 0
      status.go

+ 20 - 228
cash.go

@ -1,257 +1,49 @@
1 1
package main
2 2
3 3
import (
4
	"bytes"
5
	"errors"
6
	"fmt"
7 4
	"log"
8 5
	"os"
9
	"strings"
10
	"time"
6
	"path/filepath"
11 7
12 8
	"github.com/codegangsta/cli"
13 9
)
14 10
15 11
const (
16
	// Application name
17
	APP_NAME = "cash"
18
19
	// Application usage description
12
	APP_NAME  = "cash"
20 13
	APP_USAGE = "counting coins"
21
22
	// Application version
23
	APP_VER = "0.0.0"
14
	APP_VER   = "0.0.0"
24 15
)
25 16
26 17
var (
27
	// Ledger file name
28
	Ledger            = "example.ledger"
18
	Ledger            = "general.ledger"
29 19
	TransactionFormat = "%s\t%s\t%s"
30 20
	AccountFormat     = "\t%s\t%s"
21
	PendingFile       = filepath.Join(os.TempDir(), "pending.ledger")
31 22
)
32 23
33
// Application entry point
34
func main() {
35
	// Flags pertaining to a transaction action
36
	transactionFlags := []cli.Flag{
37
		cli.StringFlag{
38
			Name:  "date",
39
			Value: time.Now().UTC().Format("2006-01-02"),
40
			Usage: "",
41
		},
24
// Initialize the application
25
func init() {
26
	if _, err := os.Stat(PendingFile); os.IsNotExist(err) {
27
		_, err = os.Create(PendingFile)
28
		check(err)
29
		log.Println("Created pending file at", PendingFile)
42 30
	}
31
}
43 32
33
// Application entry point
34
func main() {
44 35
	app := cli.NewApp()
45 36
	app.Name = APP_NAME
46 37
	app.Usage = APP_USAGE
47 38
	app.Version = APP_VER
48 39
	app.Commands = []cli.Command{
49
		{
50
			Name:      "credit",
51
			ShortName: "cr",
52
			Usage:     "",
53
			Action:    actionCredit,
54
		},
55
		{
56
			Name:      "debit",
57
			ShortName: "dr",
58
			Usage:     "",
59
			Action:    actionDebit,
60
		},
61
		{
62
			Name:      "status",
63
			ShortName: "stat",
64
			Usage:     "",
65
			Action:    actionStatus,
66
		},
67
		{
68
			Name:      "commit",
69
			ShortName: "c",
70
			Usage:     "",
71
			Action:    actionCommit,
72
			Flags:     transactionFlags,
73
		},
74
		{
75
			Name:      "list",
76
			ShortName: "ls",
77
			Usage:     "",
78
			Action:    actionList,
79
		},
40
		commandCredit,
41
		commandDebit,
42
		commandStatus,
43
		commandCommit,
44
		commandList,
45
		commandClear,
80 46
	}
81 47
82 48
	app.Run(os.Args)
83 49
}
84
85
// Helper function to check for fatal errors
86
func check(e error) {
87
	if e != nil {
88
		log.Fatal(fmt.Sprintf("Error: %s", e))
89
	}
90
}
91
92
// Add a credit to the pending transaction
93
func actionCredit(c *cli.Context) {
94
	addPendingTransaction()
95
96
	// Format: /t{account}/t-{value}
97
}
98
99
// Add a debit to the pending transaction
100
func actionDebit(c *cli.Context) {
101
	addPendingTransaction()
102
103
	// Format: /t{account}/t+{value}
104
}
105
106
// Display the current status of the ledger
107
func actionStatus(c *cli.Context) {
108
	log.Println(hasPendingTransaction())
109
}
110
111
// Commit the pending transaction
112
func actionCommit(c *cli.Context) {
113
	date := parseDate(c.String("date"))
114
	if date == "" {
115
		log.Fatal("Invalid transaction date")
116
	}
117
118
	args := c.Args()
119
	project := parseProject(args)
120
	description := parseDescription(args, project)
121
122
	err := writeTransaction(date, project, description)
123
	if err != nil {
124
		log.Fatal(err)
125
	}
126
}
127
128
// List the ledger contents
129
func actionList(c *cli.Context) {
130
}
131
132
// Format the ledger so it is human readable
133
func formatLedger() {
134
}
135
136
// Determines if there is currently a pending transaction in the ledger
137
func hasPendingTransaction() bool {
138
	file, err := os.Open(Ledger)
139
	check(err)
140
	defer file.Close()
141
142
	info, err := file.Stat()
143
	check(err)
144
	size := info.Size()
145
	if size > 1024 {
146
		size = 1024
147
	}
148
149
	_, err = file.Seek(size*-1, 2)
150
	check(err)
151
152
	buffer := make([]byte, size)
153
	_, err = file.Read(buffer)
154
	check(err)
155
156
	return strings.Contains(string(buffer), "@pending")
157
}
158
159
// Adds a pending transaction if one is not already present
160
func addPendingTransaction() {
161
	if !hasPendingTransaction() {
162
		file, err := os.OpenFile(Ledger, os.O_APPEND|os.O_WRONLY, 0666)
163
		check(err)
164
		defer file.Close()
165
166
		_, err = file.WriteString("@pending")
167
		check(err)
168
	}
169
}
170
171
// Parse the given string to extract a proper date
172
func parseDate(in string) string {
173
	formats := []string{
174
		"2006-01-02",
175
		"2006/01/02",
176
		"2006-1-2",
177
		"2006/1/2",
178
		"01-02-2006",
179
		"01/02/2006",
180
		"1-2-2006",
181
		"1/2/2006",
182
		"Jan 2, 2006",
183
		"Jan 02, 2006",
184
		"2 Jan 2006",
185
		"02 Jan 2006",
186
	}
187
188
	for _, f := range formats {
189
		d, err := time.Parse(f, in)
190
		if err == nil {
191
			return d.Format(formats[0])
192
		}
193
	}
194
195
	return ""
196
}
197
198
// Parse a given string to extract a project name
199
func parseProject(fields []string) string {
200
	project := "@general"
201
202
	for i := 0; i < len(fields); i++ {
203
		if strings.HasPrefix(fields[i], "@") {
204
			project = fields[i]
205
			break
206
		}
207
	}
208
209
	return project
210
}
211
212
// Parse the description from the arguments
213
func parseDescription(fields []string, project string) string {
214
	for i := 0; i < len(fields); i++ {
215
		if fields[i] == project {
216
			fields[i] = ""
217
			break
218
		}
219
	}
220
221
	return strings.Replace(strings.Join(fields, " "), "  ", " ", -1)
222
}
223
224
// Write a transaction line where there is a pending transaction
225
func writeTransaction(date, project, description string) error {
226
	if !hasPendingTransaction() {
227
		return errors.New("No pending transaction to write")
228
	}
229
230
	file, err := os.OpenFile(Ledger, os.O_RDWR, 0666)
231
	check(err)
232
	defer file.Close()
233
234
	info, err := file.Stat()
235
	check(err)
236
	size := info.Size()
237
	if size > 1024 {
238
		size = 1024
239
	}
240
241
	_, err = file.Seek(size*-1, 2)
242
	check(err)
243
244
	buffer := make([]byte, size)
245
	_, err = file.Read(buffer)
246
	check(err)
247
248
	// Find the line containing @pending and replace it with our transaction
249
	line := fmt.Sprintf("%s\t%s\t%s", date, project, description)
250
	buffer = bytes.Replace(buffer, []byte("@pending"), []byte(line), -1)
251
252
	offset := info.Size() - size
253
	_, err = file.WriteAt(buffer, offset)
254
	check(err)
255
256
	return nil
257
}

+ 24 - 0
clear.go

@ -0,0 +1,24 @@
1
package main
2
3
import (
4
	"os"
5
6
	"github.com/codegangsta/cli"
7
)
8
9
var commandClear = cli.Command{
10
	Name:      "clear",
11
	ShortName: "",
12
	Usage:     "",
13
	Action:    actionClear,
14
}
15
16
// Clear current pending transaction
17
func actionClear(c *cli.Context) {
18
	file, err := os.OpenFile(PendingFile, os.O_TRUNC|os.O_WRONLY, 0666)
19
	check(err)
20
	defer file.Close()
21
22
	_, err = file.Write([]byte{})
23
	check(err)
24
}

+ 111 - 0
commit.go

@ -0,0 +1,111 @@
1
package main
2
3
import (
4
	"errors"
5
	"fmt"
6
	"io/ioutil"
7
	"os"
8
	"strings"
9
	"time"
10
11
	"github.com/codegangsta/cli"
12
)
13
14
var commandCommit = cli.Command{
15
	Name:      "commit",
16
	ShortName: "c",
17
	Usage:     "",
18
	Action:    actionCommit,
19
	Flags: []cli.Flag{
20
		cli.StringFlag{
21
			Name:  "date",
22
			Value: time.Now().UTC().Format("2006-01-02"),
23
			Usage: "",
24
		},
25
	},
26
}
27
28
// Commit the pending transaction
29
func actionCommit(c *cli.Context) {
30
	date, err := parseDate(c.String("date"))
31
	check(err)
32
33
	args := c.Args()
34
	project := parseProject(args)
35
	description := parseDescription(args, project)
36
37
	writeTransaction(date, project, description)
38
}
39
40
// Parse the given string to extract a proper date
41
func parseDate(in string) (string, error) {
42
	formats := []string{
43
		"2006-01-02",
44
		"2006/01/02",
45
		"2006-1-2",
46
		"2006/1/2",
47
		"01-02-2006",
48
		"01/02/2006",
49
		"1-2-2006",
50
		"1/2/2006",
51
		"Jan 2, 2006",
52
		"Jan 02, 2006",
53
		"2 Jan 2006",
54
		"02 Jan 2006",
55
	}
56
57
	for _, f := range formats {
58
		d, err := time.Parse(f, in)
59
		if err == nil {
60
			return d.Format(formats[0]), nil
61
		}
62
	}
63
64
	return "", errors.New("No valid date provided")
65
}
66
67
// Parse a given string to extract a project name
68
func parseProject(fields []string) string {
69
	project := "@general"
70
71
	for i := 0; i < len(fields); i++ {
72
		if strings.HasPrefix(fields[i], "@") {
73
			project = fields[i]
74
			break
75
		}
76
	}
77
78
	return project
79
}
80
81
// Parse the description from the arguments
82
func parseDescription(fields []string, project string) string {
83
	for i := 0; i < len(fields); i++ {
84
		if fields[i] == project {
85
			fields[i] = ""
86
			break
87
		}
88
	}
89
90
	return strings.Replace(strings.Join(fields, " "), "  ", " ", -1)
91
}
92
93
// Write a transaction line where there is a pending transaction
94
func writeTransaction(date, project, description string) {
95
	if !hasPendingTransaction() {
96
		check(errors.New("No pending transaction to write"))
97
	}
98
99
	pending, err := ioutil.ReadFile(PendingFile)
100
	check(err)
101
102
	file, err := os.OpenFile(Ledger, os.O_APPEND|os.O_WRONLY, 0666)
103
	check(err)
104
	defer file.Close()
105
106
	// Find the line containing @pending and replace it with our transaction
107
	transaction := fmt.Sprintf("%s\t%s\t%s\n%s\n", date, project, description, pending)
108
109
	_, err = file.WriteString(transaction)
110
	check(err)
111
}

+ 33 - 0
credit.go

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

+ 33 - 0
debit.go

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

+ 0 - 3
example.ledger

@ -5,6 +5,3 @@
5 5
    #cash       +10.00
6 6
    #liability  -5.00
7 7
    #a          -5.00
8
2015-01-03  @something
9
    #cash       +2.00
10
@pending

+ 58 - 0
helpers.go

@ -0,0 +1,58 @@
1
package main
2
3
import (
4
	"errors"
5
	"fmt"
6
	"log"
7
	"math/big"
8
	"os"
9
	"strings"
10
)
11
12
// Helper function to check for fatal errors
13
func check(e error) {
14
	if e != nil {
15
		log.Fatal(fmt.Sprintf("Error: %s", e))
16
	}
17
}
18
19
// Format the ledger so it is human readable
20
func formatLedger() {
21
}
22
23
// Determines if there is currently a pending transaction in the ledger
24
func hasPendingTransaction() bool {
25
	file, err := os.Open(PendingFile)
26
	check(err)
27
	defer file.Close()
28
29
	info, err := file.Stat()
30
	check(err)
31
32
	return info.Size() > 0
33
}
34
35
// Parse a given string to extract an account name
36
func parseAccount(fields []string) (string, error) {
37
	for i := 0; i < len(fields); i++ {
38
		if strings.HasPrefix(fields[i], "#") {
39
			return fields[i], nil
40
		}
41
	}
42
43
	return fields[0], nil
44
}
45
46
// Parse the value from the arguments
47
func parseValue(fields []string, account string) (*big.Rat, error) {
48
	r := new(big.Rat)
49
50
	for i := 0; i < len(fields); i++ {
51
		if fields[i] != account {
52
			r.SetString(fields[i])
53
			return r, nil
54
		}
55
	}
56
57
	return r, errors.New("No value found")
58
}

+ 14 - 0
list.go

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

+ 26 - 0
status.go

@ -0,0 +1,26 @@
1
package main
2
3
import (
4
	"fmt"
5
	"io/ioutil"
6
7
	"github.com/codegangsta/cli"
8
)
9
10
var commandStatus = cli.Command{
11
	Name:      "status",
12
	ShortName: "stat",
13
	Usage:     "",
14
	Action:    actionStatus,
15
}
16
17
// Display the current status of the ledger
18
func actionStatus(c *cli.Context) {
19
	if hasPendingTransaction() {
20
		pending, err := ioutil.ReadFile(PendingFile)
21
		check(err)
22
		fmt.Println(string(pending))
23
	} else {
24
		fmt.Println("No pending transactions")
25
	}
26
}