Регистрация | Войти
Lisp — программируемый язык программирования

Строки и знаки

Строки в 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 индексы векторов, строк и списков начинаются с нуля. Первый по счету элемент - нулевой по индексу.

Длинна строки

Это просто:

(length "abc")
3

Доступ к частям строки, или срезы

Это можно сделать с помощью функции ''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"

Библиотеки для работы со строками

@2009-2013 lisper.ru