Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions ledger-sort.el
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,41 @@
(beginning-of-line)
(insert "\n; Ledger-mode: End sort\n\n"))

(defconst ledger-sort--year-directive-regex

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably go in ledger-regex.el for consistency.

"^\\(?:Y\\|year\\)\\s-+\\([0-9]+\\)\\s-*$"
"Regex matching a `year NNNN' or `Y NNNN' directive at the start of a line.
The year number is captured in group 1.")

(defun ledger-sort--preceding-year ()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This algorithm is very quadratic. On my personal ledger file with 13k transactions (3 MB file), if I convert it to use year directives instead of YYYY-MM-DD, ledger-sort-buffer takes 6-7 seconds just gathering sort keys.

Perhaps a better approach would be to, before sorting, scan once from the beginning of the buffer until the end of the sort region for year directives, and look up the year for a transaction based on its position in the buffer relative to the year directives found (which ought to be a pretty small number).

"Return the year from the most recent `year NNNN' or `Y NNNN' directive.
Searches backward from the start of the current line, ignoring any
restriction so that directives above a narrowed sort region are still
consulted. Returns a number, or nil if no such directive exists."
(save-excursion
(save-restriction
(widen)
(beginning-of-line)
(when (re-search-backward ledger-sort--year-directive-regex nil t)
(string-to-number (match-string 1))))))

(defun ledger-sort-startkey ()
"Return a numeric sort key based on the date of the xact beginning at point."
"Return a numeric sort key based on the date of the xact beginning at point.
Dates with a full four-digit year are parsed directly. Short dates of the
form M/D or MM/DD are interpreted relative to the most recent `year NNNN'
directive preceding the current transaction, falling back to the current
calendar year if no such directive exists."
;; Can use `time-convert' to return an integer instead of a floating-point
;; number, starting in Emacs 27.
(float-time
(ledger-parse-iso-date
(buffer-substring-no-properties (point) (+ 10 (point))))))
(cond
((looking-at ledger-iso-date-regexp)
(ledger-parse-iso-date (match-string 0)))
((looking-at "\\([0-9]\\{1,2\\}\\)[-/]\\([0-9]\\{1,2\\}\\)\\(?:[^-/0-9]\\|$\\)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more-or-less equivalent to ledger-incomplete-date-regexp.

(let ((month (string-to-number (match-string 1)))
(day (string-to-number (match-string 2)))
(year (or (ledger-sort--preceding-year)
(nth 5 (decode-time)))))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems very strange for this function to depend on the actual current wall time. Is that compatible with the actual semantics of ledger here? (admittedly I do not personally use year directives)

(encode-time 0 0 0 day month year))))))

(defun ledger-sort-region (beg end)
"Sort the region from BEG to END in chronological order."
Expand Down
140 changes: 140 additions & 0 deletions test/sort-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,146 @@ http://bugs.ledger-cli.org/show_bug.cgi?id=260"
(should (< first-pos mid-pos)))))


(ert-deftest ledger-sort/test-1068-year-directive ()
"Regress test for Bug 1068
https://github.com/ledger/ledger/issues/1068

Dates lacking a year component should be interpreted relative to the
preceding `year NNNN' directive when sorting."
:tags '(sort regress)

(ledger-tests-with-temp-file
"year 2040

07/17 Payee B
Expenses:B $20.00
Assets:Cash

2040/07/15 Payee A
Expenses:A $10.00
Assets:Cash
"
(ledger-sort-buffer)
(should (equal (buffer-string)
"year 2040

2040/07/15 Payee A
Expenses:A $10.00
Assets:Cash

07/17 Payee B
Expenses:B $20.00
Assets:Cash
"))))


(ert-deftest ledger-sort/test-1068-Y-directive ()
"Regress test for Bug 1068
https://github.com/ledger/ledger/issues/1068

The `Y NNNN' form of the year directive should also be honored."
:tags '(sort regress)

(ledger-tests-with-temp-file
"Y 2040

07/17 Payee B
Expenses:B $20.00
Assets:Cash

2040/07/15 Payee A
Expenses:A $10.00
Assets:Cash
"
(ledger-sort-buffer)
(should (equal (buffer-string)
"Y 2040

2040/07/15 Payee A
Expenses:A $10.00
Assets:Cash

07/17 Payee B
Expenses:B $20.00
Assets:Cash
"))))


(ert-deftest ledger-sort/test-1068-unpadded-dates ()
"Regress test for Bug 1068
https://github.com/ledger/ledger/issues/1068

Dates written without leading zeros (e.g. 2015/5/7) should sort
chronologically, not lexicographically."
:tags '(sort regress)

(ledger-tests-with-temp-file
"2015/5/10 Payee A
Expenses:A $10.00
Assets:Cash

2015/5/7 Payee B
Expenses:B $20.00
Assets:Cash
"
(ledger-sort-buffer)
(should (equal (buffer-string)
"2015/5/7 Payee B
Expenses:B $20.00
Assets:Cash

2015/5/10 Payee A
Expenses:A $10.00
Assets:Cash
"))))


(ert-deftest ledger-sort/test-1068-year-directive-above-region ()
"Regress test for Bug 1068
https://github.com/ledger/ledger/issues/1068

A `year NNNN' directive preceding the sort region should still apply
when only a sub-region is sorted (so narrowing does not hide it)."
:tags '(sort regress)

(ledger-tests-with-temp-file
"year 2040

2040/01/01 Prelude
Expenses:Foo $1.00
Assets:Cash

07/17 Payee B
Expenses:B $20.00
Assets:Cash

03/05 Payee A
Expenses:A $10.00
Assets:Cash
"
;; Sort only the last two transactions (below the year directive).
(goto-char (point-min))
(search-forward "07/17")
(beginning-of-line)
(let ((region-start (point)))
(ledger-sort-region region-start (point-max)))
(should (equal (buffer-string)
"year 2040

2040/01/01 Prelude
Expenses:Foo $1.00
Assets:Cash

03/05 Payee A
Expenses:A $10.00
Assets:Cash

07/17 Payee B
Expenses:B $20.00
Assets:Cash
"))))


(provide 'sort-test)

;;; sort-test.el ends here
Loading