Personal finances application for Emacs
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

637 lines
22 KiB

  1. ;;; elbank-report.el --- Elbank report functionality -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2017-2018 Nicolas Petton
  3. ;; Author: Nicolas Petton <nicolas@petton.fr>
  4. ;; This program is free software; you can redistribute it and/or modify
  5. ;; it under the terms of the GNU General Public License as published by
  6. ;; the Free Software Foundation, either version 3 of the License, or
  7. ;; (at your option) any later version.
  8. ;; This program is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;; GNU General Public License for more details.
  12. ;; You should have received a copy of the GNU General Public License
  13. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. ;;; Commentary:
  15. ;;
  16. ;;; Code:
  17. (require 'seq)
  18. (require 'map)
  19. (require 'subr-x)
  20. (require 'cl-lib)
  21. (require 'button)
  22. (require 'elbank-common)
  23. (require 'elbank-transaction)
  24. (require 'elbank-compat)
  25. ;;;###autoload
  26. (defgroup elbank-report nil
  27. "Elbank report settings"
  28. :prefix "elbank-report-"
  29. :group 'elbank)
  30. ;;;###autoload
  31. (defcustom elbank-report-columns '(date label category amount)
  32. "List of transaction columns to print in reports."
  33. :type '(repeat (symbol :tag "Key")))
  34. ;;;###autoload
  35. (defcustom elbank-saved-monthly-reports nil
  36. "Saved report filters for monthly reports.
  37. \"Group by\" can be either one of the available columns or nil.
  38. \"Sort by\" can be either one of the available columns or nil.
  39. When nil, transactions are sorted using the first column of the
  40. report.
  41. \"Category\" can be any string (or empty for no category filter).
  42. Available columns:
  43. - `date'
  44. - `rdate' (real date)
  45. - `label'
  46. - `raw' (raw transaction text)
  47. - `category'
  48. - `account'
  49. - `amount'."
  50. :type `(repeat (list (string :tag "Name")
  51. (string :tag "Category")
  52. (symbol :tag "Group by")
  53. (symbol :tag "Sort by" :value date)
  54. (repeat :tag "Columns"
  55. :value ,elbank-report-columns
  56. (symbol :tag "Column"))
  57. (boolean :tag "Reverse sort"))))
  58. ;;;###autoload
  59. (defcustom elbank-saved-yearly-reports nil
  60. "Saved report filters for yearly reports.
  61. \"Group by\" can be either one of the available columns or nil.
  62. \"Sort by\" can be either one of the available columns or nil.
  63. When nil, transactions are sorted using the first column of the
  64. report.
  65. \"Category\" can be any string (or empty for no category filter).
  66. Available columns:
  67. - `date'
  68. - `rdate' (real date)
  69. - `label'
  70. - `raw' (raw transaction text)
  71. - `category'
  72. - `amount'."
  73. :type `(repeat (list (string :tag "Name")
  74. (string :tag "Category")
  75. (symbol :tag "Group by")
  76. (symbol :tag "Sort by" :value date)
  77. (repeat :tag "Columns"
  78. :value ,elbank-report-columns
  79. (symbol :tag "Column"))
  80. (boolean :tag "Reverse sort"))))
  81. (defvar elbank-report-mode-map
  82. (let ((map (copy-keymap elbank-base-report-mode-map)))
  83. (define-key map (kbd "f c") #'elbank-report-filter-category)
  84. (define-key map (kbd "f a") #'elbank-report-filter-account)
  85. (define-key map (kbd "f p") #'elbank-report-filter-period)
  86. (define-key map (kbd "G") #'elbank-report-group-by)
  87. (define-key map (kbd "S") #'elbank-report-sort-by)
  88. (define-key map (kbd "s") #'elbank-report-sort-reverse)
  89. (define-key map (kbd "c") #'elbank-report-set-category)
  90. (define-key map (kbd "l") #'elbank-report-set-custom-label)
  91. (define-key map (kbd "+") #'elbank-report-split-transaction)
  92. (define-key map (kbd "-") #'elbank-report-unsplit-transaction)
  93. map)
  94. "Keymap for `elbank-report-mode'.")
  95. (define-derived-mode elbank-report-mode elbank-base-report-mode "Elbank Report"
  96. "Major mode for viewing a report.
  97. \\{elbank-report-mode-map}"
  98. (setq-local revert-buffer-function #'elbank-report-refresh)
  99. (setq-local truncate-lines t)
  100. (add-hook 'elbank-base-report-refresh-hook 'elbank-report-refresh nil t))
  101. (defvar elbank-report-amount-columns '(amount)
  102. "List of columns for which values are numbers.")
  103. (make-variable-buffer-local 'elbank-report-amount-columns)
  104. (defvar elbank-report-max-column-width 40
  105. "Maximum width a report column can take.")
  106. (defvar elbank-report-group-by nil
  107. "Column by which transactions are grouped.")
  108. (make-variable-buffer-local 'elbank-report-group-by)
  109. (defvar elbank-report-sort-by nil
  110. "Column uses for sorting transactions.")
  111. (make-variable-buffer-local 'elbank-report-sort-by)
  112. (defvar elbank-report-sort-reversed nil
  113. "Reverse the sorting order when non-nil.")
  114. (make-variable-buffer-local 'elbank-report-sort-reversed)
  115. (defvar elbank-report-column-widths nil
  116. "List of column widths required to correctly display a report.")
  117. (make-variable-buffer-local 'elbank-report-widths)
  118. (defvar elbank-report-account-id nil
  119. "Account filter used in a report buffer.")
  120. (make-variable-buffer-local 'elbank-report-account-id)
  121. (defvar elbank-report-category nil
  122. "Category filter used in a report buffer.")
  123. (make-variable-buffer-local 'elbank-report-category)
  124. (defvar elbank-report-inhibit-update nil
  125. "When non-nil, do not perform a report update after setting a filter.")
  126. ;;;###autoload
  127. (cl-defun elbank-report (&key account-id period category group-by sort-by reverse-sort columns)
  128. "Build a report for transactions matching ACCOUNT-ID PERIOD and CATEGORY.
  129. When called interactively, prompt for ACCOUNT-ID, PERIOD and CATEGORY.
  130. Build the report for COLUMNS when non-nil,
  131. `elbank-report-columns' otherwise.
  132. Transactions are grouped by the GROUP-BY column when non-nil.
  133. Transactions are sorted by the SORT-BY column, or by the first
  134. column if nil.
  135. When a PERIOD is provided, append a sum row to the report.
  136. Return the report buffer."
  137. (interactive)
  138. (let ((buf (generate-new-buffer "*elbank report*")))
  139. (pop-to-buffer buf)
  140. (elbank-report-mode)
  141. (setq elbank-report-category category)
  142. (setq elbank-report-period period)
  143. (setq elbank-report-account-id account-id)
  144. (setq elbank-report-group-by group-by)
  145. (setq elbank-report-sort-by sort-by)
  146. (setq elbank-report-sort-reversed reverse-sort)
  147. (when columns
  148. (setq-local elbank-report-columns columns))
  149. (when (called-interactively-p 'interactive)
  150. (let ((elbank-report-inhibit-update t))
  151. (elbank-report-filter-account)
  152. (elbank-report-filter-period)
  153. (elbank-report-filter-category)))
  154. (elbank-report-refresh)
  155. buf))
  156. (defun elbank-report--all-saved-reports ()
  157. "Return a list of all saved reports, monthly and yearly."
  158. (append elbank-saved-monthly-reports elbank-saved-yearly-reports))
  159. (defun elbank-report--open-saved-report (report period-type)
  160. "Open a buffer presenting REPORT.
  161. PERIOD-TYPE is either `month' or `year'."
  162. (let ((time (if (eq period-type 'month)
  163. (car (last (elbank-transaction-months)))
  164. (car (last (elbank-transaction-years))))))
  165. (elbank-report :category (seq-elt report 1)
  166. :group-by (seq-elt report 2)
  167. :sort-by (seq-elt report 3)
  168. :columns (seq-elt report 4)
  169. :reverse-sort (seq-elt report 5)
  170. :period (list period-type time))))
  171. ;;;###autoload
  172. (defun elbank-report-open-by-name (name)
  173. "Open report named NAME."
  174. (interactive (list (completing-read
  175. "Report: "
  176. (seq-map (lambda (report) (seq-elt report 0))
  177. (elbank-report--all-saved-reports)))))
  178. (when-let* ((report (seq-find
  179. (lambda (report) (string= (seq-elt report 0) name))
  180. (elbank-report--all-saved-reports)))
  181. (type (if (seq-contains elbank-saved-monthly-reports report)
  182. 'month
  183. 'year)))
  184. (elbank-report--open-saved-report report type)))
  185. (defun elbank-report-filter-category ()
  186. "Prompt for a category and update the report buffer."
  187. (interactive)
  188. (setq elbank-report-category
  189. (completing-read "Category: " (map-keys elbank-categories)
  190. nil
  191. nil
  192. (or elbank-report-category "")))
  193. (elbank-report-refresh))
  194. (defun elbank-report-filter-account ()
  195. "Prompt for an account and update the report buffer."
  196. (interactive)
  197. (let* ((accounts (map-elt elbank-data 'accounts))
  198. (labels (seq-map (lambda (account)
  199. (map-elt account 'label))
  200. accounts))
  201. (label (completing-read "Select account: " labels)))
  202. (setq elbank-report-account-id
  203. (when-let ((position (seq-position labels label)))
  204. (map-elt (seq-elt accounts position) 'id))))
  205. (elbank-report-refresh))
  206. (defun elbank-report-filter-period ()
  207. "Prompt for a period to select for the current report."
  208. (interactive)
  209. (let ((type (completing-read "Period type: " '(month year))))
  210. (pcase type
  211. ("year" (elbank-report-filter-year))
  212. ("month" (elbank-report-filter-month))
  213. (_ (setq elbank-report-period nil)
  214. (elbank-report-refresh)))))
  215. (defun elbank-report-filter-year ()
  216. "Prompt for a year to select for the current report."
  217. (interactive)
  218. (let* ((years (seq-reverse (elbank-transaction-years)))
  219. (labels (seq-map (lambda (year)
  220. (format-time-string "%Y" year))
  221. years))
  222. (label (completing-read "Select year: " labels)))
  223. (setq elbank-report-period
  224. (when-let ((position (seq-position labels label)))
  225. `(year ,(seq-elt years position))))
  226. (elbank-report-refresh)))
  227. (defun elbank-report-filter-month ()
  228. "Prompt for a month to select for the current report."
  229. (interactive)
  230. (let* ((months (seq-reverse (elbank-transaction-months)))
  231. (labels (seq-map (lambda (month)
  232. (format-time-string "%B %Y" month))
  233. months))
  234. (label (completing-read "Select month: " labels)))
  235. (setq elbank-report-period
  236. (when-let ((position (seq-position labels label)))
  237. (setq elbank-report-period `(month ,(seq-elt months position)))))
  238. (elbank-report-refresh)))
  239. (defun elbank-report-group-by (column-name)
  240. "Prompt for a COLUMN-NAME to group transactions."
  241. (interactive (list (completing-read "Group by: "
  242. elbank-report-available-columns)))
  243. (setq-local elbank-report-group-by
  244. (if (string-empty-p column-name)
  245. nil
  246. (intern column-name)))
  247. (elbank-report-refresh))
  248. (defun elbank-report-sort-by (column-name)
  249. "Prompt for a COLUMN-NAME to sort the current report."
  250. (interactive (list (completing-read "Sort by: "
  251. elbank-report-available-columns)))
  252. (setq-local elbank-report-sort-by
  253. (if (string-empty-p column-name)
  254. nil
  255. (intern column-name)))
  256. (elbank-report-refresh))
  257. (defun elbank-report-sort-reverse ()
  258. "Reverse the sort order of the current report."
  259. (interactive)
  260. (setq-local elbank-report-sort-reversed
  261. (not elbank-report-sort-reversed))
  262. (elbank-report-refresh))
  263. (defun elbank-report-refresh (&rest _)
  264. "Update the report in the current buffer.
  265. When `elbank-report-inhibit-update' is non-nil, do not update."
  266. (unless elbank-report-inhibit-update
  267. (let ((inhibit-read-only t)
  268. (transactions (elbank-filter-transactions
  269. (elbank-all-transactions)
  270. :account-id elbank-report-account-id
  271. :period elbank-report-period
  272. :category elbank-report-category))
  273. (inhibit-read-only t))
  274. (let ((pos (point)))
  275. (erase-buffer)
  276. (elbank-report--update-column-widths transactions)
  277. (elbank-report--insert-preambule)
  278. (elbank-report--insert-column-titles)
  279. (elbank-report--insert-separator "═")
  280. (if elbank-report-group-by
  281. (elbank-report--insert-groups transactions)
  282. (elbank-report--insert-transactions transactions))
  283. (elbank-report--insert-separator "═")
  284. (elbank-report--insert-sum transactions)
  285. (goto-char (min (point-max) pos))))))
  286. (defun elbank-report-set-category (category &optional transaction)
  287. "Update the CATEGORY of TRANSACTION.
  288. When called interactively, prompt for the category.
  289. If TRANSACTION is nil, set the category of the transaction at
  290. point."
  291. (interactive (list (completing-read "Category: "
  292. (map-keys elbank-categories))))
  293. (unless transaction
  294. (setq transaction (elbank-report--transaction-at-point)))
  295. (setf (elbank-transaction-elt transaction 'category)
  296. category)
  297. (elbank-write-data)
  298. (elbank-report-refresh))
  299. (defun elbank-report-set-custom-label (label &optional transaction)
  300. "Set a custom LABEL for TRANSACTION.
  301. When called interactively, prompt for the label.
  302. If LABEL is an empty string, set nil as the custom label.
  303. If TRANSACTION is nil, set the custom label of the transaction at
  304. point."
  305. (interactive (list (read-from-minibuffer "Custom label: ")))
  306. (unless transaction
  307. (setq transaction (elbank-report--transaction-at-point)))
  308. (setf (elbank-transaction-elt transaction 'custom-label)
  309. (if (string-empty-p label)
  310. nil
  311. label))
  312. (elbank-write-data)
  313. (elbank-report-refresh))
  314. (defun elbank-report--split-amount (amount)
  315. "Split amount into a list of (CATEGORY-NAME . AMOUNT)."
  316. (let ((amount-left amount)
  317. (categories (list)))
  318. (while (not (zerop amount-left))
  319. (let ((sub-label (completing-read "Category: "
  320. (map-keys elbank-categories)))
  321. (sub-amount (read-from-minibuffer
  322. (format "Amount (%s left): " amount-left)
  323. (number-to-string amount-left))))
  324. (push (cons sub-label sub-amount) categories)
  325. (setq amount-left (/ (round (* 100 (- amount-left
  326. (string-to-number sub-amount))))
  327. 100.0))))
  328. categories))
  329. (defun elbank-report-split-transaction (&optional transaction)
  330. "Split TRANSACTION, transaction at point if nil.
  331. Splitting is done by assigning multiple categories to
  332. transaction, each one with an amount."
  333. (interactive)
  334. (let* ((trans (or transaction (elbank-report--transaction-at-point)))
  335. (amount (elbank-transaction-elt trans 'amount)))
  336. (if (elbank-sub-transaction-p trans)
  337. (let* ((main-transaction (elbank-transaction-elt trans 'split-from))
  338. (sub-category (map-elt trans 'category))
  339. (all-categories (map-elt main-transaction 'category))
  340. (other-categories (cl-remove (cons sub-category amount) all-categories :count 1 :test #'equal))
  341. (new-sub-categories (elbank-report--split-amount (string-to-number amount))))
  342. (elbank-report-set-category (append other-categories new-sub-categories) main-transaction))
  343. (elbank-report-set-category (elbank-report--split-amount (string-to-number amount)) trans))))
  344. (defun elbank-report-unsplit-transaction ()
  345. "Unsplit the parent of the sub transaction at point.
  346. Combining the parent is done by setting its category to nil."
  347. (interactive)
  348. (let ((trans (elbank-report--transaction-at-point)))
  349. (unless (elbank-sub-transaction-p trans)
  350. (user-error "Cannot combine transaction"))
  351. (elbank-report-set-category nil (elbank-transaction-elt trans 'split-from))))
  352. (defun elbank-report--transaction-at-point ()
  353. "Return the transaction at point.
  354. Signal an error if there is no transaction at point."
  355. (let ((tr (get-text-property (point) 'transaction)))
  356. (unless tr (user-error "No transaction at point"))
  357. tr))
  358. (defun elbank-report--insert-preambule ()
  359. "Display the report filters in the current buffer."
  360. (if (or elbank-report-account-id
  361. elbank-report-period
  362. elbank-report-category)
  363. (progn
  364. (seq-do (lambda (filter)
  365. (when (cdr filter)
  366. (insert (car filter))
  367. (put-text-property (point-at-bol) (point)
  368. 'face 'elbank-subheader-face)
  369. (insert " ")
  370. (insert (format "%s" (cdr filter)))
  371. (insert "\n")))
  372. `(("Account:" . ,(and elbank-report-account-id
  373. (map-elt (elbank-account-with-id
  374. elbank-report-account-id)
  375. 'label)))
  376. ("Period:" . ,(and elbank-report-period
  377. (elbank-format-period elbank-report-period)))
  378. ("Category:" . ,elbank-report-category))))
  379. (progn
  380. (insert "All transactions")
  381. (put-text-property (point-at-bol) (point)
  382. 'face 'elbank-subheader-face)
  383. (insert "\n")))
  384. (insert "\n"))
  385. (defun elbank-report--update-column-widths (transactions)
  386. "Locally set report columns widths needed to print TRANSACTIONS."
  387. (setq elbank-report-column-widths
  388. (elbank-seq-map-indexed
  389. (lambda (col index)
  390. (let ((row-max-width
  391. (seq-reduce (lambda (acc trans)
  392. (max acc
  393. (seq-length
  394. (elbank-report--cell trans col))))
  395. transactions
  396. 0)))
  397. (min (+ 2 (max row-max-width
  398. (seq-length (symbol-name
  399. (seq-elt elbank-report-columns
  400. index)))))
  401. elbank-report-max-column-width)))
  402. elbank-report-columns)))
  403. (cl-defgeneric elbank-report--cell (transaction column)
  404. "Return the text for the cell for TRANSACTION at COLUMN."
  405. (let ((str (elbank-transaction-elt transaction column "")))
  406. (elbank-report--truncate str)))
  407. (cl-defmethod elbank-report--cell (transaction (_column (eql label)))
  408. "Return a button text with the label of TRANSACTION.
  409. When clicking the button, jump to the transaction."
  410. (with-temp-buffer
  411. (insert (or (elbank-transaction-elt transaction 'custom-label)
  412. (elbank-transaction-elt transaction 'label)
  413. (elbank-transaction-elt transaction 'raw)
  414. ""))
  415. (make-text-button (point-at-bol) (point)
  416. 'follow-link t
  417. 'action
  418. (lambda (&rest _)
  419. (elbank-show-transaction transaction)))
  420. (elbank-report--truncate (buffer-string))))
  421. (cl-defmethod elbank-report--cell (transaction (_column (eql account)))
  422. "Return the label of the account associated with TRANSACTION."
  423. (elbank-report--truncate (elbank-account-name (elbank-transaction-elt transaction 'account))))
  424. (defun elbank-report--truncate (str)
  425. "Truncate STR to `elbank-report-max-column-width'.
  426. If STR overflows, add an ellipsis."
  427. (if (> (seq-length str) elbank-report-max-column-width)
  428. (format "%s…" (seq-take str (- elbank-report-max-column-width 1)))
  429. str))
  430. (defun elbank-report--insert-column-titles ()
  431. "Insert the report headers into the current buffer."
  432. (elbank-report--insert-title-row
  433. (seq-map (lambda (col) (capitalize (symbol-name col)))
  434. elbank-report-columns)))
  435. (defun elbank-report--insert-transactions (transactions)
  436. "Insert TRANSACTIONS rows the current buffer."
  437. (seq-do (lambda (trans)
  438. (let ((beg (point)))
  439. (elbank-report--insert-row
  440. (seq-map (lambda (col)
  441. (format "%s"
  442. (elbank-report--cell trans col)))
  443. elbank-report-columns)
  444. t)
  445. (put-text-property beg (point) 'transaction trans)))
  446. (elbank-report--sort-transactions transactions)))
  447. (defun elbank-report--insert-groups (transactions)
  448. "Insert TRANSACTIONS grouped by a property.
  449. The grouping property is defined by `elbank-report-group-by'."
  450. (seq-do (lambda (group)
  451. (elbank-report--insert-separator " ")
  452. (elbank-report--insert-title-row (list (or (car group) "None")))
  453. (elbank-report--insert-separator)
  454. (elbank-report--insert-transactions (cdr group))
  455. (elbank-report--insert-separator)
  456. (elbank-report--insert-sum (cdr group)))
  457. (elbank-report--sort-groups
  458. (seq-group-by (lambda (trans)
  459. (elbank-transaction-elt trans elbank-report-group-by ""))
  460. transactions))))
  461. (defun elbank-report--insert-sum (transactions)
  462. "Insert the sum row for TRANSACTIONS the current buffer."
  463. (elbank-report--insert-row
  464. (seq-map (lambda (col)
  465. (if (seq-contains elbank-report-amount-columns col)
  466. (elbank--propertize-amount (elbank-sum-transactions transactions))
  467. ""))
  468. elbank-report-columns)))
  469. (defun elbank-report--insert-row (row &optional propertize-amounts spacer)
  470. "Insert each element of ROW in the current buffer.
  471. When PROPERTIZE-AMOUNTS is non-nil, insert amounts using
  472. `elbank--propertize-amount'. SPACER is used for padding if
  473. non-nil."
  474. (let ((spacer (or spacer " ")))
  475. (elbank-seq-map-indexed
  476. (lambda (col index)
  477. (let* ((amount (seq-contains elbank-report-amount-columns col))
  478. (raw-item (or (seq-elt row index) ""))
  479. (item (if (and amount propertize-amounts)
  480. (elbank--propertize-amount raw-item)
  481. raw-item))
  482. (width (seq-elt elbank-report-column-widths index))
  483. (padding (- width (seq-length item))))
  484. (unless amount
  485. (insert (format "%s%s%s" spacer item spacer)))
  486. (dotimes (_ padding)
  487. (insert spacer))
  488. (when amount
  489. (insert (format "%s%s%s" spacer item spacer)))))
  490. elbank-report-columns))
  491. (insert "\n"))
  492. (defun elbank-report--insert-title-row (row)
  493. "Insert ROW as a title row.
  494. Unlike `elbank-report--insert-row', elements of ROW are displayed
  495. in bold."
  496. (let ((beg (point)))
  497. (elbank-report--insert-row row)
  498. (add-text-properties beg (point)
  499. '(face bold))))
  500. (defun elbank-report--insert-separator (&optional separator)
  501. "Insert a separator line in the current buffer.
  502. Use SEPARATOR if non-nil,\"─\" otherwise."
  503. (elbank-report--insert-row (seq-map (lambda (_) "") elbank-report-columns)
  504. nil
  505. (or separator "─")))
  506. (defun elbank-report--sort-transactions (transactions)
  507. "Sort TRANSACTIONS.
  508. Transactions are sorted by `elbank-report-sort-by' if
  509. non-nil, or by the first column if nil."
  510. (let ((sort-column (or elbank-report-sort-by
  511. (car elbank-report-columns))))
  512. (elbank-report--sort transactions
  513. (lambda (trans)
  514. (elbank-transaction-elt trans sort-column ""))
  515. (seq-contains elbank-report-amount-columns sort-column))))
  516. (defun elbank-report--sort-groups (groups)
  517. "Sort GROUPS.
  518. If the sorting column is an amount, GROUPS are sorted by summing
  519. their transactions."
  520. (let* ((sort-column (or elbank-report-sort-by
  521. (car elbank-report-columns)))
  522. (amounts (seq-contains elbank-report-amount-columns sort-column)))
  523. (elbank-report--sort
  524. groups
  525. (lambda (group)
  526. (if amounts
  527. (elbank-sum-transactions (cdr group))
  528. (car group)))
  529. amounts)))
  530. (defun elbank-report--sort (collection accessor &optional amounts)
  531. "Sort COLLECTION by ACCESSOR.
  532. If AMOUNTS is non-nil, the sort is done by comparing numeric
  533. values, converting items of collection to numbers if needed."
  534. (let ((sort-fn (if amounts
  535. (lambda (a b)
  536. (< (if (numberp a) a (string-to-number a))
  537. (if (numberp b) b (string-to-number b))))
  538. #'string-lessp)))
  539. (seq-sort (lambda (a b)
  540. (let ((sort (funcall sort-fn
  541. (funcall accessor a)
  542. (funcall accessor b))))
  543. (if elbank-report-sort-reversed
  544. (not sort)
  545. sort)))
  546. collection)))
  547. (provide 'elbank-report)
  548. ;;; elbank-report.el ends here