Строки и знаки
Строки в CL являются специальным типом вектора, элементы которого относятся к знаковому (characters) типу (или его подтипу), и, кроме того, строки относятся к перечисляемым типам. Это значит, что все функции для перечисляемых типов можно применить и к строкам. Существуют также и функции, предназначенные для работы только со строками. Наконец, некоторую функциональность, не определенную в стандарте, окупают библиотеки.
Взгляните на словарь функций для перечислимых типов. А также на список всех функций, работающих со строками и список функций для работы со знаками.
Кодировки и знаки
Каждый знак в строке имеет свой целочисленный номер (код) и название, которое определяется используемой кодировкой. Это могут быть как 1-2 байтные кодировки, так и 4-8 байтный юникод, например UTF-8 (как говорится в PCL - не проблема использовать и 16 байтные кодировки).
character, то есть знак, записывается в виде #\название-символа.
Функция code-char переводит код символа в его название, а функция char-code - наоборот. Например:
; получаем название символа:
(code-char 5555)
#\CANADIAN_SYLLABICS_BLACKFOOT_A
; и переводим обратно в код:
(char-code #\CANADIAN_SYLLABICS_BLACKFOOT_A)
5555
Знаки строки
С помощью функции ''char'' можно как читать, так и писать знаки строки. Обычному синтаксису str[n] в CL соответствует:
(char *str* n)
; n-ый символ строки
(setf (char *str* n) #\char)
; изменяем n-ый символ строки
Существуют также следующие подобные функции - ''schar'', которая действует быстрее, чем ''char''. А также ''aref'' и ''elt'', которые, напротив, более общие (относятся к функциям для перечислимых типов, т.е. являются аналогом CHAR для всех перечислимых типов), но и более медленные.
Заметим, что как и в остальных языках, в CL индексы векторов, строк и списков начинаются с нуля. Первый по счету элемент - нулевой по индексу.
Доступ к частям строки, или срезы
Это можно сделать с помощью функции ''subseq'', которая применима ко всем перечисляемым типам. Например для чтения подстрок без побочного эффекта:
(subseq *str* n)
; Получаем подстроку начиная с n-ого символа и до конца строки
; Выход индекса за границы строки приведёт к ошибке
(subseq *str* n1 n2)
; Получаем подстроку начиная с n1-ого символа и до n2-ого.
; Опять же, "неправильные" индексы приводят к ошибке
Или для записи в подстроку:
(setf (subseq *str* n1 n2) *some-str*)
; Теперь строка *str* изменена!
Если записываемая строка больше отведенного места, то она обрезается.
В общем функция ''subseq'' позволяет делать срезы строк. В таких языках как Python это обеспечивается синтаксисом вроде str[n1 : n2]. Тем не менее, поведение срезов строк в CL отличается - отрицательные индексы, и индексы "вне строки" функцией subseq не обрабатываются, для этого можно написать небольшое расширение для функции срезов:
Напишем функцию [:], объединяющую функции char и subseq, которая будет следить за валидностью индексов:
(defun |[:]| (seq &optional n1 n2)
(flet ((mod-index(n len)
(cond ((> n len) len)
((< n (- len)) 0)
((< n 0) (+ n len))
(t n)))
(let ((len (length seq)))
(cond ((eql n1 NIL) seq) ; случай seq[] = seq
((and (eql n1 'F) (eql n2 'F)) seq) ; случай seq[:] = seq
((eql n1 'F) (subseq seq 0 (mod-index n2 len))) ; случай seq[ : i]
((eql n2 'F) (subseq seq (mod-index n1 len) len)) ; случай seq[i : ]
((eql n2 NIL) (char seq (mod-index n1 len))) ; случай seq[i]
(t (subseq seq (mod-index n1 len) (mod-index n2 len))) ; случай seq[i : k]
)))
Ну вот :) Теперь мы получаем "элегантное" поведение срезов, которым так хвастают программисты Python и Ruby:
(|[:]| "12345" -100 100)
; за границами с обеих сторон - строка осталась
(|[:]| "12345" 'f -3)
; убираем 3 символа в конце
(|[:]| "12345" -3 'f)
; берём последние 3 символа
(|[:]| "12345" 3 -3)
; убираем по три символа в начале и в конце
(|[:]| "12345" -1)
; последний символ
И даже лучше - отрицательные индексы тоже контролируются на "out of range". Так как функция возвращает subseq и char - возможна и запись:
; пример - функция раскавычивания:
(defun de-quote (str) (|[:]| str 1 -1))
(setf str "'12345'")
(setf str (de-quote str))
"12345"
Создание строк
Самый простой вариант – само-вычисляющийся объект "знаки". Вариант с использованием функции string:
(defparameter *my-string* (string "йцукен"))
Определение как вектора знаков:
(defparameter *my-string* (make-array 0
:element-type 'character
:fill-pointer 0
:adjustable t))
Создание строки из 1000 точек:
(make-sequence 'string 1000 :initial-element #\.)
Конкатенация, или сложение строк
Чтобы сложить произвольное количество строк используйте concatenate:
(concatenate 'string str1 str2 str3)
; конкатенирует произвольное количество строк.
Ещё существует такое понятие как интерполяция переменных в строке - в CL её осуществляет функция ''format''. У этой функции есть свой подъязык, о котором можно прочитать в спецификации. Вот несколько примеров:
(format nil "Вставит вместо А список: ~A." '(1 2 3))
"Вставит вместо А список: (1 2 3)."
(format nil "Вставит элементы списка:~{ ~A~}." '(a b c))
"Вставит элементы списка: a b c."
(format nil "Вставит элементы списка через запятую: ~{ ~A~^,~}." (map 'list #'(lambda(i)(* i i)) '(1 2 3 4 5)))
"Вставит элементы списка через запятую: 1, 4, 9, 16, 25."
Также, для конкатенации можно использовать макрос with-output-to-string:
(with-output-to-string (stream)
;; создает строку stream, в которую можно писать с помощью
;; print, princ, и т.д. в пределах формы
;; форма возвращает строку stream
)
Итераторы по строке
Универсальный итератор по перечислимым типам map можно использовать для строк:
(map 'string #'(lambda (c) (print c)) *str*)
Или с помощью макроса loop:
(loop for char across *str*
collect char)
Список знаков строки
(concatenate 'list str)
; возвращает список знаков строки,
; чтобы собрать обратно, можно написать:
(defparameter *my-string* (make-array 0 :element-type 'character
:fill-pointer 0
:adjustable t))
(dolist (char '(...))
(vector-push-extend char *my-string*))
Обращение строк
Существуют функции reverse и nreverse для обращения знаков в строке. Вторая отличается тем, что обладает побочным эффектом.
(reverse *my-string*)
Для обращения строк по словам можно сначала разбить строку на слова, а затем обратить список с помощью той же функции reverse:
(defun split-by-one-space (string)
"Возвращает список слов, разделённых одним
пробелом в строке string."
(loop for i = 0 then (1+ j)
as j = (position #\Space string :start i)
collect (subseq string i j)
while j))
(defun join-string-list (string-list)
"Конкатенирует список строк
проставляя пробелы."
(format nil "~{~A~^ ~}" string-list))
(defun reverse-string(string)
"Обращает строку по словам,
разделённым одним пробелом."
(join-string-list
(reverse
(split-by-one-space string)))
Контроль регистра
string-upcase - переводит все символы в верхний регистр string-downcase - переводит все символы в нижний регистр string-capitalize - первый символ переводится в верхний регистр, а остальные - в нижний
(string-upcase "To Be") => "TO BE"
(string-downcase "Or not to BE?") => "or not to be?"
(string-capitalize "to beet or not to beet") => "To Beet Or Not To Beet"
(string-capitalize " hello.hello ") => " Hello.Hello "
есть такие же функции с приставкой n - они имеют побочный эффект.
Сравнение строк
Для сравнения строк нужно использовать другие функции, нежели чем для сравнения чисел:
(string= "foo" "foo") => true
(string= "foo" "Foo") => false
(string/= "foo" "bar") => true
(string= "together" "frog" :start1 1 :end1 3 :start2 2) => true
(string-equal "foo" "Foo") => true
(string= "abcd" "01234abcd9012" :start2 5 :end2 9) => true
(string< "aaaa" "aaab") => 3
(string>= "aaaaa" "aaaa") => 4
(string-not-greaterp "Abcde" "abcdE") => 5
(string-lessp "012AAAA789" "01aaab6" :start1 3 :end1 7
:start2 2 :end2 6) => 6
(string-not-equal "AAAA" "aaaA") => false
Общие функции equal и equalp также сравнивают строки, но работают медленней.
Сравнение знаков
Как и в случае строк, сравнивать знаки лучше с помощью специальных, а не общих, функций:
(char= #\a #\a)
T
(char/= #\a #\c)
T
(char< #\z #\a)
NIL
(char> #\x #\a)
T
(char<= #\z #\z)
T
(char> #\x #\a)
T
Перевод чисел в строку
Функция write-to-string занимается анализом и переводом чисел в строки:
(write-to-string 10)
"10"
; для целого
(write-to-string 3.14159)
"3.14159"
; для дробного
(write-to-string (/ 1 2))
"1/2"
; для рационального
(write-to-string F :base 16)
"16"
; даже для числа в произвольной системе счисления
Перевод строк в числа
Для первода целого в строку используется функция parse-integer:
(parse-integer "10")
10
(parse-integer " 10 ")
10
эта функция имеет несколько ключей - :start и :end для указания индексов начала и конца парсинга, :radix - для указания системы счисления и :junk-allowed - для поиска числа в строке с текстом:
(parse-integer "42" :start 1)
2
2
(parse-integer "42" :end 1)
4
1
(parse-integer "42" :radix 8)
34
2
(parse-integer " 42 is forty-two" :junk-allowed t)
42
3
Для более гибкого перевода подойдёт read-from-string:
(read-from-string "#X23")
35
(read-from-string "3.14")
3.14
(read-from-string "2/4")
1/2
(read-from-string "#C(6/8 1)")
#C(3/4 1)
(read-from-string "1.2e2")
120.00001
(read-from-string "symbol")
SYMBOL
Другие модификаторы
(fill str chr :start 1 :end 3)
; заполнение строки знаками (можно символами)
(remove char str)
; удаляет все знаки char из строки str
(remove char str :start n1 :endl n2)
; аналогично, на начиная со :start и до :endl
(remove-if #'upper-case-p str)
; небольшой итератор по строке, принимающий условие удаления
(substitute char1 char2 str)
; Меняет в строке str символы char2 на char1
(substitute-if char #'upper-case-p str)
; Заменяет в строке str на char только те символы, которые удовлетворяют предикату
А вот пример функции, реализующей замену всех вхождений:
(defun replace-all (string part replacement &key (test #'char=))
"Returns a new string in which all the occurences of the part
is replaced with replacement."
(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 test)
do (write-string string out
:start old-pos
:end (or pos (length string)))
when pos do (write-string replacement out)
while pos)))
Функция replace-all не входит в стандарт. Кроме того, существуют более быстрые методы работы со строками - например, регулярные выражения.
Обрезка строк
Функции string-trim, string-left-trim и string-right-trim занимаются тем, что удаляют определённые символы в начале и в конце строки (left и right - только слева, и только справа, соответственно). Например:
(string-trim "-" "-string-")
"string"
(string-trim " et" " trim me ")
"rim m"
(string-left-trim " et" " trim me ")
"rim me "
(string-right-trim " et" " trim me ")
" trim m"
(string-right-trim '(#\Space #\e #\t) " trim me ")
" trim m"
(string-right-trim '(#\Space #\e #\t #\m) " trim me ")
Поиск знаков в строке
Поиск знаков и знаков, удовлетворяющих условию осуществляют функции find и find-if:
* (find #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equal)
#\t
* (find #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
#\T
* (find #\z "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
NIL
* (find-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks.")
#\1
* (find-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks." :from-end t)
#\0
аналогично работают функции position и position-if, но возвращают не булевое значение, а индекс первого найденного знака (NIL при
отсутствии):
(position #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equal)
17
(position #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
0
(position-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks.")
37
(position-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks." :from-end t)
43
функции count и count-if считают количество вхождений:
(count #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equal)
2
(count #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
3
(count-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks.")
6
(count-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks." :start 38)
5
(count #\a "how many A's are there in here?")
2
(count-if-not #'oddp '((1) (2) (3) (4)) :key #'car)
2
(count-if #'upper-case-p "The Crying of Lot 49" :start 4)
2
Поиск подстрок
С помощью функции search:
(search "we" "If we can't be free we can at least be cheap")
3
(search "we" "If we can't be free we can at least be cheap" :from-end t)
20
(search "we" "If we can't be free we can at least be cheap" :start2 4)
20
(search "we" "If we can't be free we can at least be cheap" :end2 5 :from-end t)
3
(search "FREE" "If we can't be free we can at least be cheap")
NIL
(search "FREE" "If we can't be free we can at least be cheap" :test #'char-equal)
15
Конвертация знаков и строк
Converting between Characters and Strings
Функция coerce (дословно "принудить") с параметром 'character преобразует строку длинной в один символ в соответствующий знак. coerce с параметром 'list - ещё один способ получить из строки список её знаков. Наконец используя параметр 'string можно сделать обратное преобразование списка знаков к строке:
(coerce "a" 'character)
#\a
(coerce "abc" 'list)
(#\a #\b #\c)
(coerce '(#\a #\b #\c) 'string)
"abc"
(coerce (coerce "abc" 'list) 'string)
"abc"
Универсальная функция образования строк string тоже может тут помочь:
(string #\a)
"a"
Конвертация символов и строк
Функция intern конвертирует строку в символ. Фактически она проверяет, не содержит ли текущий пакет (смотрите главу о пакетах) символ - если нет, она вводит его и возвращает вновь введённый символ и NIL, иначе возвращает уже существующий символ и название пакета.
(in-package "COMMON-LISP-USER")
#<The COMMON-LISP-USER package, 35/44 internal, 0/9 external>
(intern "MY-SYMBOL")
MY-SYMBOL
NIL
(intern "MY-SYMBOL")
MY-SYMBOL
:INTERNAL
(export 'MY-SYMBOL)
T
(intern "MY-SYMBOL")
MY-SYMBOL
:EXTERNAL
(intern "My-Symbol")
|My-Symbol|
NIL
(intern "MY-SYMBOL" "KEYWORD")
:MY-SYMBOL
NIL
(intern "MY-SYMBOL" "KEYWORD")
:MY-SYMBOL
:EXTERNAL
Чтобы осуществить обратную операцию, можно воспользоваться функциями symbol-name или string
(symbol-name 'MY-SYMBOL)
"MY-SYMBOL"
(symbol-name 'my-symbol)
"MY-SYMBOL"
(symbol-name '|my-symbol|)
"my-symbol"
(string 'abc)
"ABC"