diff --git a/README.md b/README.md index 5bc991b..f11947d 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,14 @@ The only dependency is `cl-ppcre`. - [Global parameters](#global-parameters) - [Functions](#functions) - [Tweak whitespace](#tweak-whitespace) - - [trim `(s)`](#trim-s) + - [trim `(s &key (char-bag *whitespaces*))`](#trim-s-key-char-bag-whitespaces) - [collapse-whitespaces `(s)`](#collapse-whitespaces-s) - [To longer strings](#to-longer-strings) - [join `(separator list-of-strings)`](#join-separator-list-of-strings) - [concat `(&rest strings)`](#concat-rest-strings) + - [ensure `(s &key wrapped-in prefix suffix)` NEW in March, 2023](#ensure-s-key-wrapped-in-prefix-suffix-new-in-march-2023) + - [ensure-prefix, ensure-suffix `(start/end s)` NEW in March, 2023](#ensure-prefix-ensure-suffix-startend-s-new-in-march-2023) + - [ensure-wrapped-in `(start/end s)`](#ensure-wrapped-in-startend-s) - [insert `(string/char index s)`](#insert-stringchar-index-s) - [repeat `(count s)`](#repeat-count-s) - [add-prefix, add-suffix `(items s)`](#add-prefix-add-suffix-items-s) @@ -58,16 +61,17 @@ The only dependency is `cl-ppcre`. - [from-file `(filename)`](#from-file-filename) - [to-file `(filename s)`](#to-file-filename-s) - [Predicates](#predicates) - - [emptyp `(s)`](#empty-emptyp-s) - - [blankp `(s)`](#blank-blankp-s) - - [starts-with-p `(start s &key ignore-case)`](#starts-with-starts-with-p-start-s-key-ignore-case) - - [ends-with-p `(end s &key ignore-case)`](#ends-with-ends-with-p-end-s-key-ignore-case) - - [containsp `(substring s &key (ignore-case nil))`](#contains-containsp-substring-s-key-ignore-case-nil) + - [emptyp `(s)`](#emptyp-s) + - [blankp `(s)`](#blankp-s) + - [starts-with-p `(start s &key ignore-case)`](#starts-with-p-start-s-key-ignore-case) + - [ends-with-p `(end s &key ignore-case)`](#ends-with-p-end-s-key-ignore-case) + - [containsp `(substring s &key (ignore-case nil))`](#containsp-substring-s-key-ignore-case-nil) - [s-member `(list s &key (ignore-case *ignore-case*) (test #'string=))`](#s-member-list-s-key-ignore-case-ignore-case-test-string) - - [prefixp and suffixp `(items s)`](#prefix-prefixp-and-suffix-suffixp-items-s) + - [prefixp and suffixp `(items s)`](#prefixp-and-suffixp-items-s) + - [wrapped-in-p (`start/end` `s`) NEW in March, 2023](#wrapped-in-p-startend-s-new-in-march-2023) - [Case](#case) - - [Functions to change case: camel-case, snake-case,... (new in 0.15, 2019/11)](#functions-to-change-case-camel-case-snake-case-new-in-015-201911) - - [downcase, upcase, capitalize `(s)` fixing a built-in suprise. (new in 0.11)](#downcase-upcase-capitalize-s-fixing-a-built-in-suprise-new-in-011) + - [Functions to change case: camel-case, snake-case,...](#functions-to-change-case-camel-case-snake-case) + - [downcase, upcase, capitalize `(s)` fixing a built-in suprise.](#downcase-upcase-capitalize-s-fixing-a-built-in-suprise) - [downcasep, upcasep `(s)`](#downcasep-upcasep-s) - [alphap, lettersp `(s)`](#alphap-lettersp-s) - [alphanump, lettersnump `(s)`](#alphanump-lettersnump-s) @@ -192,6 +196,60 @@ Simple call of the built-in [concatenate](https://lispcookbook.github.io/cl-cook We actually also have `uiop:strcat`. +#### ensure `(s &key wrapped-in prefix suffix)` NEW in March, 2023 + +The "ensure-" functions return a string that has the specified prefix or suffix, appended if necessary. + +This `str:ensure` function looks for the following key parameters, in order: + +- `:wrapped-in`: if non nil, call `str:ensure-wrapped-in`. This checks that `s` both starts and ends with the supplied string or character. +- `:prefix` and `:suffix`: if both are supplied and non-nil, call `str:ensure-suffix` followed by `str:ensure-prefix`. +- `:prefix`: call `str:ensure-prefix` +- `:suffix`: call `str:ensure-suffix`. + +Example: + +~~~lisp +(str:ensure "abc" :wrapped-in "/") ;; => "/abc/" +(str:ensure "/abc" :prefix "/") ;; => "/abc" => no change, still one "/" +(str:ensure "/abc" :suffix "/") ;; => "/abc/" => added a "/" suffix. +~~~ + +These fonctions accept strings and characters: + +~~~lisp +(str:ensure "/abc" :prefix #\/) +~~~ + + +#### ensure-prefix, ensure-suffix `(start/end s)` NEW in March, 2023 + +Ensure that `s` starts with `start/end` (or ends with `start/end`, respectively). + +Return a new string with its prefix (or suffix) added, if and only if necessary. + +Example: + +~~~lisp +(str:ensure-prefix "/" "abc/") => "/abc/" (a prefix was added) +;; and +(str:ensure-prefix "/" "/abc/") => "/abc/" (does nothing) +~~~ + +#### ensure-wrapped-in `(start/end s)` + +Ensure that `s` both starts and ends with `start/end`. + +Return a new string with the necessary added bits, if required. + +It simply calls `str:ensure-suffix` followed by `str:ensure-prefix`. + +See also `str:wrapped-in-p` and `uiop:string-enclosed-p prefix s suffix`. + +~~~lisp +(str:ensure-wrapped-in "/" "abc") ;; => "/abc/" (added both a prefix and a suffix) +(str:ensure-wrapped-in "/" "/abc/") ;; => "/abc/" (does nothing) +~~~ #### insert `(string/char index s)` @@ -572,9 +630,25 @@ See also `uiop:string-prefix-p prefix s`, which returns `t` if and `uiop:string-enclosed-p prefix s suffix`, which returns `t` if `s` begins with `prefix` and ends with `suffix`. +#### wrapped-in-p (`start/end` `s`) NEW in March, 2023 + +Does `s` start and end with `start/end'? + +If true, return `s`. Otherwise, return nil. + +Example: + +~~~lisp +(str:wrapped-in-p "/" "/foo/" ;; => "/foo/" +(str:wrapped-in-p "/" "/foo" ;; => nil +~~~ + +See also: `UIOP:STRING-ENCLOSED-P (prefix s suffix)`. + + ### Case -#### Functions to change case: camel-case, snake-case,... (new in 0.15, 2019/11) +#### Functions to change case: camel-case, snake-case,... We use [cl-change-case](https://github.com/rudolfochrist/cl-change-case/) (go @@ -600,7 +674,7 @@ The available functions are: More documentation and examples are there. -#### downcase, upcase, capitalize `(s)` fixing a built-in suprise. (new in 0.11) +#### downcase, upcase, capitalize `(s)` fixing a built-in suprise. The functions `str:downcase`, `str:upcase` and `str:capitalize` return a new string. They call the built-in `string-downcase`, @@ -804,6 +878,9 @@ Note that there is also http://quickdocs.org/string-case/. ## Changelog +* March, 2023: + * added `str:ensure`, `str:ensure-prefix`, `str:ensure-suffix`, `str:ensure-wrapped-in` and `str:wrapped-in-p`. + * small breaking change: fixed `prefix?` when used with a smaller prefix: "f" was not recognized as a prefix of "foobar" and "foobuz", only "foo" was. Now it is fixed. Same for `suffix?`. * January, 2023: added the `:char-barg` parameter to `trim`, `trim-left`, `trim-right`. - minor: `ends-with-p` now works with a character. * June, 2022: small breaking change: fixed `prefix?` when used with a smaller prefix: "f" was not recognized as a prefix of "foobar" and "foobuz", only "foo" was. Now it is fixed. Same for `suffix?`. diff --git a/str.lisp b/str.lisp index 0d919c7..3b4f667 100644 --- a/str.lisp +++ b/str.lisp @@ -68,6 +68,13 @@ #:suffixp #:add-prefix #:add-suffix + + #:ensure + #:ensure-prefix + #:ensure-suffix + #:ensure-wrapped-in + #:wrapped-in-p + #:pad #:pad-left #:pad-right @@ -84,6 +91,7 @@ #:s-assoc-value #:count-substring + ;; case-related functions: #:downcase #:upcase #:capitalize @@ -471,7 +479,7 @@ A simple call to the built-in `search` (which returns the position of the substr (defun suffixp (items suffix) "Return `suffix' if all items end with it. - Otherwise, retur nil" + Otherwise, return nil" (when (every (lambda (s) (str:ends-with-p suffix s)) items) @@ -485,6 +493,113 @@ A simple call to the built-in `search` (which returns the position of the substr "Append s to the end of eahc items." (mapcar #'(lambda (item) (concat item s)) items)) +(defun ensure-prefix (start s) + "Ensure that `s' starts with `start'. + Return a new string with its prefix added, if necessary. + +Example: + + (str:ensure-prefix \"/\" \"abc/\") ;; => \"/abc/\" + (str:ensure-prefix \"/\" \"/abc/\") ;; => \"/abc/\" (does nothing) + +See also `str:ensure-suffix' and `str:ensure-wrapped-in'." + (cond + ((null start) + s) + ((null s) + s) + (t + (let ((start-s (string start))) + (if (not (str:starts-with-p start-s s)) + (str:concat start-s s) + s))))) + +(defun ensure-suffix (end s) + "Ensure that `s' ends with `end'. +Return a new string with its suffix added, if necessary. + +Example: + + (str:ensure-suffix \"/\" \"/abc\") ;; => \"/abc/\" + (str:ensure-suffix \"/\" \"/abc/\") ;; => \"/abc/\" (does nothing) + +See also `str:ensure-prefix' and `str:ensure-wrapped-in'." + (cond + ((null end) + s) + ((null s) + s) + (t + (let ((end-s (string end))) + (if (not (str:ends-with-p end-s s)) + (str:concat s end-s) + s))))) + +(defun ensure-wrapped-in (start/end s) + "Ensure that S starts and ends with `START/END'. +Return a new string. + + Example: + + (str:ensure-wrapped-in \"/\" \"abc\") ;; => \"/abc/\" + (str:ensure-wrapped-in \"/\" \"/abc/\") ;; => \"/abc/\" (does nothing) + + See also: `str:enclosed-by-p'." + (str:ensure-prefix start/end (str:ensure-suffix start/end s))) + +(defun ensure (s &key wrapped-in prefix suffix) + "The ensure functions return a string that has the specified prefix or suffix, appended if necessary. + +This function looks for the following parameters, in order: +- :wrapped-in : if non nil, call STR:ENSURE-WRAPPED-IN. +- :prefix and :suffix : if both are supplied and non-nil, call STR:ENSURE-SUFFIX followed by STR:ENSURE-PREFIX. +- :prefix : call STR:ENSURE-PREFIX +- :suffix : call STR:ENSURE-SUFFIX. + +Example: + + (str:ensure \"abc\" :wrapped-in \"/\") ;; => \"/abc/\" + (str:ensure \"/abc\" :prefix \"/\") ;; => \"/abc\" (no change, still one \"/\") + (str:ensure \"/abc\" :suffix \"/\") ;; => \"/abc/\" (added a \"/\" suffix) + + These fonctions accept strings and characters: + + (str:ensure \"/abc\" :prefix #\\/) +" + (cond + (wrapped-in + (ensure-wrapped-in wrapped-in s)) + ((and prefix suffix) + (ensure-prefix prefix (ensure-suffix suffix s))) + (prefix + (ensure-prefix prefix s)) + (suffix + (ensure-suffix suffix s)) + (t + s))) + +(defun wrapped-in-p (start/end s) + "Does S start and end with `START/END'? +If true, return S. Otherwise, return nil. + +Example: + + (str:wrapped-in-p \"/\" \"/foo/\" ;; => \"/foo/\" + (str:wrapped-in-p \"/\" \"/foo\" ;; => nil + +See also: UIOP:STRING-ENCLOSED-P (prefix s suffix). +" + (cond + ((null start/end) + s) + ((null s) + s) + (t + ;; (starts-with-p nil "foo") returns NIL. + (when (and (str:starts-with-p start/end s) + (str:ends-with-p start/end s)) + s)))) + (defun pad (len s &key (pad-side *pad-side*) (pad-char *pad-char*)) "Fill `s' with characters until it is of the given length. By default, add spaces on the right. diff --git a/test/test-str.lisp b/test/test-str.lisp index c763487..2359fb4 100644 --- a/test/test-str.lisp +++ b/test/test-str.lisp @@ -222,10 +222,12 @@ 2 " (unlines '("1" "2" "")))) -(subtest "starts-with?" +(subtest "starts-with-p" (ok (starts-with? "foo" "foobar") "default case") (ok (starts-with? "" "foo") "with blank start") (ok (not (starts-with? "rs" "")) "with blank s") + (ok (not (starts-with? nil "")) "prefix is nil") + ;; (is (starts-with? "" nil) t) "s is nil: what do we want?") ;; XXX: fix? (ok (not (starts-with? "foobar" "foo")) "with shorter s") (ok (starts-with? "" "") "with everything blank") (ok (not (starts-with? "FOO" "foobar")) "don't ignore case") @@ -233,7 +235,7 @@ (ok (starts-with? "FOO" "foobar" :ignore-case t) "ignore case") (ok (let ((*ignore-case* t)) (starts-with? "FOO" "foobar")) "ignore case")) -(subtest "ends-with?" +(subtest "ends-with-p" (ok (ends-with? "bar" "foobar") "default case") (ok (ends-with-p "bar" "foobar") "ends-with-p alias") (ok (not (ends-with? "BAR" "foobar")) "don't ignore case") @@ -282,6 +284,55 @@ (is (add-suffix '("foo" nil) "bar") '("foobar" "bar") "with a nil") (is (add-suffix '() "foo") '() "with void list")) +(subtest "ensure-prefix" + (is (ensure-prefix "/" "/abc") "/abc" "default case: existing prefix.") + (is (ensure-prefix "/" "abc") "/abc" "default case: add prefix.") + (is (ensure-prefix "/" "") "/" "blank string: add prefix") + (is (ensure-prefix #\/ "") "/" "with a char") + (is (ensure-prefix "" "") "" "blank strings") + (is (ensure-prefix nil "") "" "prefix is nil, we want s") + (is (ensure-prefix nil nil) nil "prefix and s are nil") + (is (ensure-prefix "/" nil) nil "s is nil") + (is (ensure-prefix "/" "///abc") "///abc" "lots of slashes, but that's ok")) + +(subtest "ensure-suffix" + (is (ensure-suffix "/" "/abc/") "/abc/" "default case") + (is (ensure-suffix "/" "abc") "abc/" "default case 2") + (is (ensure-suffix "/" "") "/" "default case void string") + (is (ensure-suffix #\/ "") "/" "with a char") + (is (ensure-suffix "" "") "" "void strings") + (is (ensure-suffix nil "foo") "foo" "prefix is nil, we want s") + (is (ensure-suffix "/" "abc///") "abc///" "lots of slashes, but that's ok")) + +(subtest "ensure-wrapped-in" + (is (ensure-wrapped-in "/" "/abc/") "/abc/" "default case") + (is (ensure-wrapped-in "/" "abc") "/abc/" "default case 2") + (is (ensure-wrapped-in "/" "") "/" "default case void string") + (is (ensure-wrapped-in #\/ "") "/" "with a char") + (is (ensure-wrapped-in "" "") "" "blank strings") + (is (ensure-wrapped-in nil "") "" "prefix is nil, we want s") + ;; The following line is different that the original behaviour of starts-with-p: + (is (ensure-wrapped-in "" nil) nil "blank prefix, s is nil") + ;; (starts-with-p "" nil) ;; => T but below, we expect NIL. + (is (ensure-wrapped-in nil nil) nil "nils") + (is (ensure-wrapped-in "/" "abc///") "/abc///" "lots of slashes, but that's ok")) + +(subtest "wrapped-in-p" + (is (wrapped-in-p "/" "/foo/") "/foo/" "default case") + (is (wrapped-in-p "/" "/foo") nil "false case") + (is (wrapped-in-p "" "/foo/") "/foo/" "blank start/end") + (is (wrapped-in-p nil "/foo/") "/foo/" "blank start/end") + (is (wrapped-in-p nil nil) nil "nils") + (is (wrapped-in-p nil "") "" "nil and blank") + (is (wrapped-in-p "" nil) nil "blank and nil") + (is (wrapped-in-p #\/ "/foo") nil "with a char") + (is (wrapped-in-p "<3" "<3lisp<3") "<3lisp<3" "with a longer prefix.")) + +(subtest "ensure" + (is (ensure "foo" :prefix "-" :suffix "+") "-foo+" "ensure with prefix and suffix") + (is (ensure "foo") "foo" "ensure with no params") + (is (ensure "foo" :prefix nil :suffix nil :wrapped-in nil) "foo" "ensure with params to nil")) + (subtest "pad left, right, center" (is (pad 10 "foo") "foo " "pad adds spaces on the right by default.")