summaryrefslogtreecommitdiff
path: root/ledger.el
blob: a987a018ec927653a589c344477df38a8488b515 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
(defun ledger-add-entry (entry)
  (interactive
   (list (read-string "Entry: "
		      (format-time-string "%m.%d " (current-time)))))
  (let ((args (mapcar 'shell-quote-argument (split-string entry))))
    (shell-command
     (concat "ledger entry "
	     (mapconcat 'identity args " ")) t)
    (delete-char 5)
    (exchange-point-and-mark)))

(defun ledger-clear-current ()
  (interactive)
  (save-excursion
    (when (re-search-backward "^[0-9]" nil t)
      (skip-chars-forward "0-9./")
      (insert " *"))))

(define-derived-mode ledger-mode text-mode "Ledger"
  "A mode for editing ledger data files."
  (setq comment-start ";" comment-end nil)
  (let ((map (current-local-map)))
    (define-key map [(control ?c) (control ?n)] 'ledger-add-entry)
    (define-key map [(control ?c) (control ?c)]
  'ledger-clear-current)))

(defun ledger-parse-entries (account)
  (let* ((now (current-time))
	 (current-year (nth 5 (decode-time now)))
	 (then now)
	 entries)
    ;; `then' is 45 days ago
    (setq then (time-subtract then (seconds-to-time (* 45 24 60 60))))
    (while (not (eobp))
      (when (looking-at
	     (concat "\\(Y\\s-+\\([0-9]+\\)\\|\\([0-9]{4}+\\)?[./]?"
		     "\\([0-9]+\\)[./]\\([0-9]+\\)\\s-+"
		     "\\(\\*\\s-+\\)?\\(.+\\)\\)"))
	(let ((found (match-string 2))
	      when)
	  (if found
	      (setq current-year (string-to-number found))
	    (let ((start (match-beginning 0))
		  (year (match-string 3))
		  (month (string-to-number (match-string 4)))
		  (day (string-to-number (match-string 5)))
		  (mark (match-string 6))
		  (desc (match-string 7)))
	      (if (and year (> (length year) 0))
		  (setq year (string-to-number year)))
	      (setq when (encode-time 0 0 0 day month
				      (or year current-year)))
	      (when (or (not mark) (time-less-p then when))
		(forward-line)
		(while (looking-at
			(concat "\\s-+\\([A-Za-z_].+?\\)  \\s-*"
				"\\([^0-9]+\\)\\s-*\\([0-9.]+\\)"))
		  (let ((acct (match-string 1))
			(amt (match-string 3)))
		    (if (string= account acct)
			(setq entries
			      (cons (list (copy-marker start)
					  mark when desc amt)
				    entries))))
		  (forward-line)))))))
      (forward-line))
    (nreverse entries)))

(define-derived-mode ledger-reconcile-mode text-mode "Reconcile"
  "A mode for reconciling ledger entries."
  (let ((map (make-sparse-keymap)))
    (define-key map [space] 'ledger-reconcile-toggle)
    (use-local-map map)))

(defvar ledger-buf nil)
(make-variable-buffer-local 'ledger-buf)

(defun ledger-reconcile-toggle ()
  (interactive)
  (let ((where (get-text-property (point) 'where)))
    (with-current-buffer ledger-buf
      (goto-char where)
      (ledger-clear-current))))

(defun ledger-reconcile (account)
  (interactive "sAccount to reconcile: ")
  (let ((items (save-excursion
		 (goto-char (point-min))
		 (ledger-parse-entries account)))
	(buf (current-buffer)))
    (pop-to-buffer (generate-new-buffer "*Reconcile*"))
    (ledger-reconcile-mode)
    (setq ledger-buf buf)
    (dolist (item items)
      (let ((beg (point)))
	(insert (format "%s %-30s %8.2f\n"
			(format-time-string "%Y.%m.%d" (nth 2 item))
			(nth 3 item)
			(string-to-number (nth 4 item))))
	(if (nth 1 item)
	    (set-text-properties beg (1- (point))
				 (list 'face 'bold
				       'where (nth 0 item))))))))