cl-strings.lisp
18.7 KB
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
;;;;;The MIT License (MIT)
;;;;;
;;;;;Copyright (c) 2016 Diogo Franco
;;;;;
;;;;;Permission is hereby granted, free of charge, to any person obtaining a copy
;;;;;of this software and associated documentation files (the "Software"), to deal
;;;;;in the Software without restriction, including without limitation the rights
;;;;;to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;;;;;copies of the Software, and to permit persons to whom the Software is
;;;;;furnished to do so, subject to the following conditions:
;;;;;
;;;;;The above copyright notice and this permission notice shall be included in all
;;;;;copies or substantial portions of the Software.
;;;;;
;;;;;THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;;;;;IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;;;;;FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;;;;;AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;;;;;LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;;;;;OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;;;;;SOFTWARE.
(in-package :cl-user)
(defpackage :cl-strings
(:use :common-lisp)
(:shadow)
(:export
#:starts-with
#:ends-with
#:shorten
#:repeat
#:join
#:replace-all
#:chars
#:split
#:chop
#:toggle-case
#:format-number
#:parse-number
#:clean-diacritics
#:clean
#:insert
#:camel-case
#:snake-case
#:kebab-case
#:title-case
#:make-template-parser))
(in-package :cl-strings)
(defvar *blank-chars* '(#\Space #\Newline #\Backspace #\Tab
#\Linefeed #\Page #\Return #\Rubout))
(defun starts-with (string target &key (ignore-case nil))
"Returns true if \"string\"'s first characters are equal to \"target\"."
(let ((target-len (length target))
(string-len (length string)))
(when (>= string-len target-len)
(funcall (if ignore-case #'string-equal #'string=)
string target :start1 0 :end1 target-len))))
(defun ends-with (string target &key (ignore-case nil))
"Returns true if \"string\"'s last characters are equal to \"target\"."
(let ((target-len (length target))
(string-len (length string)))
(when (>= string-len target-len)
(funcall (if ignore-case #'string-equal #'string=)
string target :start1 (- string-len target-len)))))
(defun shorten (string len &key (truncate-string "..."))
"If \"string\"'s length is bigger than \"length\", cut the last
characters out. Also replaces the last characters of the shortened
string for the omission string. It defaults to \"...\", but can be
nil or the empty string."
(let ((string-len (length string)))
(if (<= string-len len)
(return-from shorten string))
(concatenate 'string (subseq string 0 len)
truncate-string)))
(defun repeat (string count &key (separator ""))
"Repeats a given string \"count\" number of times"
(check-type count integer)
(if (> count 0)
(with-output-to-string (stream)
(write-string string stream)
(dotimes (i (1- count))
(write-string separator stream)
(write-string string stream)))
""))
(defun join (lst &key (separator ""))
"Joins a list of strings (or other objects) in a string,
delimited by \"separator\""
(check-type lst list)
(check-type separator string)
(if lst
(with-output-to-string (stream)
(princ (first lst) stream)
(dolist (el (rest lst))
(write-string separator stream)
(princ el stream)))
""))
(defun replace-all (string part replacement &key (ignore-case nil))
"Returns a new string in which all the occurences of \"part\" in \"string\"
are replaced with replacement."
(check-type string string)
(check-type part string)
(check-type replacement string)
(if (string= part "")
string
(with-output-to-string (out)
(loop with part-length = (length part)
for old-pos = 0 then (+ pos part-length)
for pos = (search part string
:start2 old-pos
:test (if ignore-case #'char-equal #'char=))
do (write-string string out
:start old-pos
:end (or pos (length string)))
when pos do (write-string replacement out)
while pos))))
(defun chars (string)
"Returns a list with the chars in \"string\""
(loop for c across string
collect c))
(defun split (string &optional (separator #\space) &key (ignore-case nil))
"Returns a list of substrings of string
divided by separator. Separator can be a string or
a character.
Note: Two consecutive separators will be seen as
if there were an empty string between them."
(labels ((%split-by-char (string separator)
(loop for i = 0 then (1+ j)
as j = (position separator string :start i :test (if ignore-case
#'char-equal
#'char=))
collect (subseq string i j)
while j))
(%split-by-str (string separator)
(loop for i = 0 then (+ j (length separator))
as j = (search separator string :start2 i :test (if ignore-case
#'string-equal
#'string=))
collect (subseq string i j)
while j)))
(check-type string string)
(cond ((typep separator 'character)
(%split-by-char string separator))
((string= separator "") (chars string))
((typep separator 'string)
(%split-by-str string separator))
(t (error 'type-error :datum separator :expected-type 'string)))))
(defun chop (string step)
"Returns a list with parts of \"string\", each with
length \"step\", except for the last one which might
have a length small than \"step\"."
(check-type string string)
(check-type step integer)
(if (> step 0)
(let ((string-len (length string)))
(loop for i = 0 then (+ i step)
for j = step then (+ j step)
collect (subseq string i (if (> j string-len)
string-len
j))
while (< j string-len)))
(list string)))
(defun toggle-case (string)
"Changes the case of each character in \"string\""
(check-type string string)
(with-output-to-string (stream)
(loop for c across string do
(if (upper-case-p c)
(write-char (char-downcase c) stream)
(write-char (char-upcase c) stream)))))
(defun format-number (number &key (precision 0) (decimal-separator ".") (order-separator ","))
"Converts a number to a string, with \"precision\" number of digits."
(check-type number number)
(if (< precision 0)
(error 'simple-type-error :format-control "Precision should be 0 or higher."))
(unless (and (or (stringp decimal-separator) (characterp decimal-separator))
(or (stringp order-separator) (characterp order-separator) (null order-separator)))
(error 'simple-type-error :format-control
"decimal-separator and order-separator should both be characters or strings."))
(let* ((float-formatted (format nil "~,vF" precision number))
(decimal-part (subseq float-formatted (- (length float-formatted) precision)))
(integer-formatted (parse-integer
(subseq float-formatted 0 (1- (- (length float-formatted) precision)))))
(integer-part (if (or (null order-separator) (string= order-separator ""))
(format nil "~a" integer-formatted)
(format nil "~0,'0,v,3:D"
(if (characterp order-separator)
order-separator
(char order-separator 0))
integer-formatted))))
(if (> precision 0)
(concatenate 'string integer-part (if (characterp decimal-separator)
(string decimal-separator)
decimal-separator)
decimal-part)
integer-part)))
(defun parse-number (number-str &key (decimal-separator #\.) (order-separator nil))
"Parses number-str without using the reader, returning the equivalent number"
(check-type number-str string)
(if (string= number-str "") (error 'parse-error))
(labels ((%clean-order-separators (number-str &optional order-separator)
(if (stringp order-separator)
(setf order-separator (char order-separator 0)))
(loop for c across number-str do
(when (not (digit-char-p c))
(if order-separator
(if (or (char= order-separator c))
(return-from %clean-order-separators
(%clean-order-separators (remove c number-str) c))
(error 'parse-error))
(error 'parse-error))))
number-str)
(%parse-int (number-str)
(if (string= number-str "")
0
(nth-value 0 (parse-integer (%clean-order-separators number-str order-separator)))))
(%parse-float (int-part decimal-part)
(let ((int-part-nr (%parse-int (%clean-order-separators int-part order-separator)))
(decimal-part-nr (%parse-int (%clean-order-separators decimal-part))))
(float (+ int-part-nr (/ decimal-part-nr (expt 10 (length decimal-part)))))))
(%parse-exp (coeff-str exponent-str coeff-separator)
(when (or (string= coeff-str "") (string= exponent-str ""))
(error 'parse-error))
(let ((exponent-part (if (char= (char exponent-str 0) #\-)
(expt 10 (* -1 (%parse-int (subseq exponent-str 1))))
(expt 10 (%parse-int exponent-str)))))
(if coeff-separator
(* (%parse-float (subseq coeff-str 0 coeff-separator)
(subseq coeff-str (1+ coeff-separator)))
exponent-part)
(* (%parse-int coeff-str) exponent-part))))
(%parse-div (numerator denominator)
(/ (%parse-int numerator) (%parse-int denominator)))
(%parse-positive (number-str decimal-separator)
(let* ((separator (if (stringp decimal-separator)
(char decimal-separator 0)
decimal-separator))
(separator-pos (position separator number-str))
(exponential-pos (position #\e number-str))
(divisor-pos (position #\/ number-str)))
(cond ((and separator-pos (not exponential-pos) (not divisor-pos))
(%parse-float (subseq number-str 0 separator-pos)
(subseq number-str (1+ separator-pos))))
((and exponential-pos (not divisor-pos))
(%parse-exp (subseq number-str 0 exponential-pos)
(subseq number-str (1+ exponential-pos))
(if (and separator-pos (< separator-pos exponential-pos))
separator-pos)))
((and divisor-pos (not exponential-pos) (not separator-pos))
(%parse-div (subseq number-str 0 divisor-pos)
(subseq number-str (1+ divisor-pos))))
((and (not separator-pos) (not exponential-pos) (not divisor-pos))
(%parse-int number-str))
(t (error 'parse-error))))))
(let ((first-char (char number-str 0)))
(cond ((char= first-char #\-)
(* -1 (%parse-positive (subseq number-str 1) decimal-separator)))
((char= first-char #\+)
(%parse-positive (subseq number-str 1) decimal-separator))
(t
(%parse-positive number-str decimal-separator))))))
(defun clean-diacritics (string)
"Returns a string with the diacritics replaced by their closest ASCII equivalents"
(let ((from "ąàáäâãåæăćčĉęèéëêĝĥìíïîĵłľńňòóöőôõðøśșşšŝťțţŭùúüűûñÿýçżźžĄÀÁÄÂÃÅÆĂĆČĈĘÈÉËÊĜĤÌÍÏÎĴŁĽŃŇÒÓÖŐÔÕÐØŚȘŞŠŜŤȚŢŬÙÚÜŰÛÑŸÝÇŻŹŽ")
(to "aaaaaaaaaccceeeeeghiiiijllnnoooooooossssstttuuuuuunyyczzzAAAAAAAAACCCEEEEEGHIIIIJLLNNOOOOOOOOSSSSSTTTUUUUUUNYYCZZZ"))
(map 'string #'(lambda (x)
(let ((pos (position x from)))
(if pos
(char to pos)
x)))
string)))
(defun clean (string &key (char #\space))
"Returns a trimmed string with multiple spaces replaced by one"
(check-type string string)
(check-type char character)
(let ((trimmed (string-trim (string char) string))
(char-found nil))
(with-output-to-string (stream)
(loop for c across trimmed do
(if (char= c char)
(when (not char-found)
(write-char c stream)
(setq char-found t))
(progn
(if char-found (setf char-found nil))
(write-char c stream)))))))
(defun insert (string original &key (position (length original)))
"Returns a string consisting of \"original\" with \"string\" inserted
at \"position\"."
(check-type original string)
(check-type string string)
(check-type position number)
(when (not (<= 0 position (length original)))
(error 'simple-type-error :format-control "Position out of bounds."))
(concatenate 'string (subseq original 0 position)
string
(subseq original position)))
(defun camel-case (string &key (delimiter #\space))
"Returns a string which concatenates every word separated by a space
(or a specified delimiter), and upcases every first letter
except for the first word of the string."
(check-type string string)
(unless (or (characterp delimiter) (stringp delimiter))
(error 'simple-type-error
:format-control "delimiter should be a character or a string."))
(let ((words (split (string-trim *blank-chars* string) delimiter)))
(with-output-to-string (stream)
(unless (= (length (first words)) 0)
(write-char (char (first words) 0) stream)
(write-string (string-downcase (subseq (first words) 1)) stream))
(loop for word in (rest words) do
(unless (= (length (first words)) 0)
(write-char (char-upcase (char word 0)) stream)
(write-string (string-downcase (subseq word 1)) stream))))))
(defun snake-case (string &key (delimiter #\space))
"Returns a string with every space (or a specified delimiter)
replaced by an underscore, and downcased, except for the first letter."
(check-type string string)
(unless (or (characterp delimiter) (stringp delimiter))
(error 'simple-type-error
:format-control "delimiter should be a character or a string."))
(let ((words (split (string-trim *blank-chars* string) delimiter)))
(with-output-to-string (stream)
(unless (= (length (first words)) 0)
(write-char (char (first words) 0) stream)
(write-string (string-downcase (subseq (first words) 1)) stream))
(loop for word in (rest words) do
(unless (= (length (first words)) 0)
(write-char #\_ stream)
(write-string (string-downcase word) stream))))))
(defun kebab-case (string &key (delimiter #\space))
"Returns a string with every space (or a specified char)
replaced by an hyphen, and every character lower cased."
(check-type string string)
(unless (or (characterp delimiter) (stringp delimiter))
(error 'simple-type-error
:format-control "delimiter should be a character or a string."))
(string-downcase
(replace-all string (if (stringp delimiter)
delimiter
(string delimiter))
"-")))
(defun title-case (string &key (remove-hyphens t))
"Returns a string with the first letter of every word
upcased, and the other ones downcased."
(check-type string string)
(let* ((clean (if remove-hyphens (replace-all string "-" " ") string))
(words (split (string-trim *blank-chars* clean) #\space)))
(with-output-to-string (stream)
(unless (= (length (first words)) 0)
(write-char (char-upcase (char (first words) 0)) stream)
(write-string (string-downcase (subseq (first words) 1)) stream))
(loop for word in (rest words) do
(unless (= (length (first words)) 0)
(write-char #\space stream)
(write-char (char-upcase (char word 0)) stream)
(write-string (string-downcase (subseq word 1)) stream))))))
(defun make-template-parser (start-delimiter end-delimiter &key (ignore-case nil))
"Returns a closure than can substitute variables
delimited by \"start-delimiter\" and \"end-delimiter\"
in a string, by the provided values."
(check-type start-delimiter string)
(check-type end-delimiter string)
(when (or (string= start-delimiter "")
(string= end-delimiter ""))
(error 'simple-type-error
:format-control "The empty string is not a valid delimiter."))
(let ((start-len (length start-delimiter))
(end-len (length end-delimiter))
(test (if ignore-case
#'string-equal
#'string=)))
(lambda (string values)
(check-type string string)
(unless (listp values)
(error 'simple-type-error
:format-control "values should be an association list"))
(with-output-to-string (stream)
(loop for prev = 0 then (+ j end-len)
for i = (search start-delimiter string)
then (search start-delimiter string :start2 j)
for j = (if i (search end-delimiter string :start2 i))
then (if i (search end-delimiter string :start2 i))
while (and i j)
do (write-string (subseq string prev i) stream)
(let ((instance (rest (assoc (subseq string (+ i start-len) j)
values
:test test))))
(if instance
(princ instance stream)
(write-string (subseq string i (+ j end-len)) stream)))
finally (write-string (subseq string prev) stream))))))