-
Notifications
You must be signed in to change notification settings - Fork 3
/
commify.el
487 lines (416 loc) · 17.7 KB
/
commify.el
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
;;; commify.el --- Toggle grouping commas in numbers
;;
;; Copyright (C) 2020 Daniel E. Doherty
;;
;; This program is free software: you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by the Free
;; Software Foundation, either version 3 of the License, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
;; more details.
;;
;; You should have received a copy of the GNU General Public License along
;; with this program. If not, see <http://www.gnu.org/licenses/>.
;;
;; Author: Daniel E. Doherty <[email protected]>
;; Version: 1.3.6
;; Package-Requires: ((s "1.9.0"))
;; Keywords: convenience, editing, numbers, grouping, commas
;; URL: https://github.com/ddoherty03/commify
;;
;;; Commentary:
;;
;; This package provides a simple command to toggle a number under the cursor
;; between having grouped digits and not. For example, if the buffer is as
;; shown with the cursor at the '*':
;;
;; Travel expense is 4654254654*
;;
;; invoking commify-toggle will change the buffer to:
;;
;; Travel expense is 4,654,254,654*
;;
;; Calling commify-toggle again removes the commas. The cursor can also be
;; anywhere in the number or immediately before or after the number.
;; commify-toggle works on floating or scientific numbers as well, but it only
;; ever affects the digits before the decimal point. Afterwards, the cursor
;; will be placed immediately after the affected number.
;;
;; Commify now optionally works with hexadecimal, octal, and binary numbers,
;; with variables for independently setting the group char and group size for
;; those bases. They are recognized by prefixes "0x", "0o", and "0b",
;; respectively, but these can also be set. See the README at the github page
;; for details.
;;
;; You can configure these variables:
;; - commify-group-char (default ",") to the char used for grouping
;; - commify-group-size (default 3) to number of digits per group
;; - commify-decimal-char (default ".") to the char used as a decimal point.
;;
;; Bind the main function to a convenient key in you init.el file:
;;
;; (key-chord-define-global ",," 'commify-toggle)
;;
;;; Code:
(require 's)
;;;; Customize options.
(defvar commify-version "1.3.6")
(defgroup commify nil
"Toggle insertion of commas in numbers in buffer."
:group 'convenience)
(defcustom commify-currency-chars "$€₠¥£"
"Currency characters that might be prefixed to a number."
:type 'string
:local t)
(defcustom commify-open-delims "({<'\"\["
"Opening delimiters that might be prefixed to a number."
:type 'string
:local t)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;; Decimal numbers customization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgroup commify-decimal nil
"Customization of commify for decimal numbers."
:group 'commify)
(defcustom commify-group-char ","
"Character to use for separating groups of digits in decimal numbers."
:type 'string
:local t)
(defcustom commify-decimal-char "."
"Character recognized as the decimal point for decimal numbers."
:type 'string
:local t)
(defcustom commify-group-size 3
"Number of digits in each group for decimal numbers."
:type 'integer
:local t)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;; Hexadecimal numbers customization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgroup commify-hex nil
"Customization of commify for hexadecimal numbers."
:group 'commify)
(defcustom commify-hex-enable t
"Enable commify for hexadecimal numbers.
You can enable commify to commify hexadecimal numbers. If
enabled, hexadecimal numbers are identified by defining appropriate
regular expressions for `commify-hex-prefix-re' and
`commify-hex-suffix-re' and a character range for
`commify-hex-digits' to recognize hexadecimal digits. If you do so,
commify will separate hexadecimal digits into groups of
`commify-hex-group-size' using the `commify-hex-group-char'."
:type 'boolean
:local t)
(defcustom commify-hex-group-char "_"
"Character to use for separating groups of hexadecimal digits."
:type 'string
:local t)
(defcustom commify-hex-prefix-re "0[xX]"
"Regular expression prefix required before a number in a non-decimal base."
:type 'regexp
:local t)
(defcustom commify-hex-digits "0-9A-Fa-f"
"Character class of valid digits in a number in a non-decimal base."
:type 'regexp
:local t)
(defcustom commify-hex-suffix-re ""
"Regular expression suffux required after a number in a non-decimal base."
:type 'regexp
:local t)
(defcustom commify-hex-group-size 4
"Number of digits in each group for non-decimal base numbers."
:type 'integer
:local t)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;; Octal numbers customization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgroup commify-octal nil
"Customization of commify for octal numbers."
:group 'commify)
(defcustom commify-oct-enable t
"Enable commify for octal numbers.
You can enable commify to commify octal numbers. If
enabled, octal numbers are identified by defining appropriate
regular expressions for `commify-oct-prefix-re' and
`commify-oct-suffix-re' and a character range for
`commify-oct-digits' to recognize octal digits. If you do so,
commify will separate octal digits into groups of
`commify-oct-group-size' using the `commify-oct-group-char'."
:type 'boolean
:local t)
(defcustom commify-oct-group-char "_"
"Character to use for separating groups of octal digits."
:type 'string
:local t)
(defcustom commify-oct-prefix-re "0[oO]"
"Regular expression prefix required before an octal number."
:type 'regexp
:local t)
(defcustom commify-oct-digits "0-7"
"Character class of valid digits in an octal number."
:type 'regexp
:local t)
(defcustom commify-oct-suffix-re ""
"Regular expression suffux required after an octal number."
:type 'regexp
:local t)
(defcustom commify-oct-group-size 2
"Number of digits in each group for octal numbers."
:type 'integer
:local t)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;; Binary numbers customization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgroup commify-binary nil
"Customization of commify for binary numbers."
:group 'commify)
(defcustom commify-bin-enable t
"Enable commify for binary numbers.
You can enable commify to commify binary numbers. If
enabled, binary numbers are identified by defining appropriate
regular expressions for `commify-bin-prefix-re' and
`commify-bin-suffix-re' and a character range for
`commify-bin-digits' to recognize binary digits. If you do so,
commify will separate binary digits into groups of
`commify-bin-group-size' using the `commify-bin-group-char'."
:type 'boolean
:local t)
(defcustom commify-bin-group-char "_"
"Character to use for separating groups of binary digits."
:type 'string
:local t)
(defcustom commify-bin-prefix-re "0[bB]"
"Regular expression prefix required before a binary number."
:type 'regexp
:local t)
(defcustom commify-bin-digits "0-1"
"Character class of valid digits in a binary number."
:type 'regexp
:local t)
(defcustom commify-bin-suffix-re ""
"Regular expression suffux required after a binary number."
:type 'regexp
:local t)
(defcustom commify-bin-group-size 4
"Number of digits in each group for binary numbers."
:type 'integer
:local t)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Regex constructors
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun commify--decimal-re ()
"Regular expression of a valid decimal number string.
A valid decimal number has a mandatory whole number part, which
it captures as the second group. The number may contain the
`commify-group-char' in the whole number part and uses
`commify-decimal-char' as the separator between the whole and
fractional part of the number. A leading sign, `+' or `-' is
optional, as is a trailing exponent introduced by `e' or `E'.
The matched sub-parts are:
1. the optional sign,
2. the whole number part,
3. the optional fractional part, including the decimal point, and
4. the optional exponent part."
(let ((sign "\\([-+]\\)?")
(whole (concat "\\([0-9" (regexp-quote commify-group-char) "]+\\)"))
(frac (concat "\\(" (regexp-quote commify-decimal-char) "[0-9]+\\)?"))
(exp "\\([eE][-+0-9]+\\)?"))
(concat sign whole frac exp)))
(defun commify--hex-number-re ()
"Regular expression of a valid number string in a non-decimal base.
A valid hexadecimal number has an optional sign, a mandatory
prefix, a mandatory whole number part composed of the valid
digits and the grouping character, which it captures as the third
group, and a mandatory suffix, which may be empty.
The matched sub-parts are:
1. the optional sign
2. the pre-fix,
3. the whole number part, and
4. the suffix."
(let ((sign (concat "\\([-+]\\)?"))
(pre (concat "\\(" commify-hex-prefix-re "\\)"))
(whole (concat "\\([" (regexp-quote commify-hex-group-char)
(regexp-quote commify-hex-digits) "]+\\)"))
(suffix (concat "\\(" (regexp-quote commify-hex-suffix-re) "\\)")))
(concat sign pre whole suffix)))
(defun commify--oct-number-re ()
"Regular expression of a valid number string in a non-decimal base.
A valid octal number has an optional sign, a mandatory prefix, a
mandatory whole number part composed of the valid digits and the
grouping character, which it captures as the third group, and a
mandatory suffix, which may be empty.
The matched sub-parts are:
1. the optional sign
2. the pre-fix,
3. the whole number part, and
4. the suffix."
(let ((sign (concat "\\([-+]\\)?"))
(pre (concat "\\(" commify-oct-prefix-re "\\)"))
(whole (concat "\\([" (regexp-quote commify-oct-group-char)
(regexp-quote commify-oct-digits) "]+\\)"))
(suffix (concat "\\(" (regexp-quote commify-oct-suffix-re) "\\)")))
(concat sign pre whole suffix)))
(defun commify--bin-number-re ()
"Regular expression of a valid binary number string.
A valid binary number has an optional sign, a mandatory prefix, a
mandatory whole number part composed of the valid digits and the
grouping character, which it captures as the third group, and a
mandatory suffix, which may be empty.
The matched sub-parts are:
1. the optional sign
2. the pre-fix,
3. the whole number part, and
4. the suffix."
(let ((sign (concat "\\([-+]\\)?"))
(pre (concat "\\(" commify-bin-prefix-re "\\)"))
(whole (concat "\\([" (regexp-quote commify-bin-group-char)
(regexp-quote commify-bin-digits) "]+\\)"))
(suffix (concat "\\(" (regexp-quote commify-bin-suffix-re) "\\)")))
(concat sign pre whole suffix)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Exception predicates
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun commify--exception-p (str)
"Should the STR be excluded from commify?"
(or (commify--date-p str)
(commify--identifier-p str)
(commify--zero-filled-p str)))
(defun commify--date-p (str)
"Is STR part of a date?"
(save-match-data
(or (string-match-p "\\(?:19\\|20\\)[[:digit:]]\\{2\\}[-/]" str)
(string-match-p "[-/]\\(?:19\\|20\\)[[:digit:]]\\{2\\}" str))))
(defun commify--identifier-p (str)
"Is STR part of an identifier?"
(save-match-data
(string-match-p "^[A-Za-z]\\s_" str)))
(defun commify--zero-filled-p (str)
"Is STR a zero-padded number?"
(save-match-data
(string-match-p "^0[0-9]" str)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Buffer query and movement
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun commify--current-nonblank ()
"Return the string from the buffer of all non-blank characters around the cursor."
(save-excursion
(skip-chars-backward (concat "^[:blank:]" commify-currency-chars commify-open-delims)
(max (point-min) (line-beginning-position)))
(let ((beg (point)))
(skip-chars-forward "^[:blank:]$" (min (point-max) (line-end-position)))
(buffer-substring beg (point)))))
(defun commify--move-to-next-nonblank ()
"Move the cursor to the beginning of the next run of non-blank characters after the cursor."
(if (< (point) (point-max))
(progn
(skip-chars-forward
(concat "^\n[:blank:]" commify-currency-chars commify-open-delims) (point-max))
(skip-chars-forward
(concat "\n[:blank:]" commify-currency-chars commify-open-delims) (point-max)))
0))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Adding and removing grouping characters
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun commify--commas (n &optional group-char group-size valid-digits)
"For an integer string N, insert GROUP-CHAR between groups of GROUP-SIZE VALID-DIGITS."
(unless group-char (setq group-char commify-group-char))
(unless group-size (setq group-size commify-group-size))
(unless valid-digits (setq valid-digits "0-9"))
(if (< group-size 1)
n
(let ((num nil)
(grp-re nil)
(rpl-str nil))
;; reverse the string so we can insert the commas left-to-right
(setq num (s-reverse n))
;; form the re to look for groups of group-size digits, e.g. "[0-9]\{3\}"
(setq grp-re (concat "[" valid-digits "]" "\\{" (format "%s" group-size) "\\}"))
;; form the replacement, e.g., "\&,"
(setq rpl-str (concat "\\&" group-char))
;; do the replacement in the reversed string
(setq num (replace-regexp-in-string grp-re rpl-str num))
;; now chop off any trailing group-char and re-reverse the string.
(s-reverse (s-chop-suffix group-char num)))))
(defun commify--uncommas (n &optional group-char)
"For an integer string N, remove all instances of GROUP-CHAR."
(unless group-char (setq group-char commify-group-char))
(s-replace-all `((,group-char . "")) n))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Commands
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;###autoload
(defun commify-toggle-at-point ()
"Toggle insertion or deletion of grouping characters in the number around point."
(interactive)
(unless (commify--exception-p (commify--current-nonblank))
(save-excursion
;; find the beginning of the non-blank run of text the cursor is in or
;; after, limited to the beginning of the line or the beginning of buffer.
(skip-chars-backward (concat "^[:blank:]" commify-currency-chars commify-open-delims)
(max (point-min) (line-beginning-position)))
(cond
;; a hexadecimal number
((and commify-hex-enable (looking-at (commify--hex-number-re)))
(let ((num (match-string 3)))
(if (s-contains? commify-hex-group-char num)
(replace-match (commify--uncommas num commify-hex-group-char)
t t nil 3)
(replace-match (commify--commas
num commify-hex-group-char commify-hex-group-size
commify-hex-digits)
t t nil 3))))
;; an octal number
((and commify-oct-enable (looking-at (commify--oct-number-re)))
(let ((num (match-string 3)))
(if (s-contains? commify-oct-group-char num)
(replace-match (commify--uncommas num commify-oct-group-char)
t t nil 3)
(replace-match (commify--commas
num commify-oct-group-char commify-oct-group-size
commify-oct-digits)
t t nil 3))))
;; a binary number
((and commify-bin-enable (looking-at (commify--bin-number-re)))
(let ((num (match-string 3)))
(if (s-contains? commify-bin-group-char num)
(replace-match (commify--uncommas num commify-bin-group-char)
t t nil 3)
(replace-match (commify--commas
num commify-bin-group-char commify-bin-group-size
commify-bin-digits)
t t nil 3))))
;; a decimal number, always enabled
((looking-at (commify--decimal-re))
(let ((num (match-string 2)))
(if (s-contains? commify-group-char num)
(replace-match (commify--uncommas num commify-group-char)
t t nil 2)
(replace-match (commify--commas
num commify-group-char commify-group-size)
t t nil 2))))))))
;;;###autoload
(defun commify-toggle-on-region (beg end)
"Toggle insertion or deletion of numeric grouping characters.
Do so for all numbers in the region between BEG and END."
(interactive "r")
(save-excursion
(let ((deactivate-mark)
(end-mark (copy-marker end)))
(goto-char beg)
(commify-toggle-at-point)
(while (and (> (commify--move-to-next-nonblank) 0)
(<= (point) end-mark))
(commify-toggle-at-point)))))
;;;###autoload
(defun commify-toggle ()
"Toggle commas at point or on the region from BEG to END."
(interactive)
(save-excursion
(if (use-region-p)
(commify-toggle-on-region (region-beginning) (region-end))
(commify-toggle-at-point))))
(provide 'commify)
;;; commify.el ends here