Навчальний курс. 18. Робота з рядками і символами, Різне, Програмування, статті

Введення


Обробка тексту – одна з найпоширеніших завдань програмування. Якщо потрібно обробити будь текстові дані, то без знань того матеріалу, що буде викладено нижче, просто не обійтися. Особливо, якщо дані сформовані не особисто вами, а будь-якої сторонньої програмою або іншою людиною.


Символи


Символ – це одна одиниця тексту. Це буква, цифра, якої-небудь знак. Кодова таблиця символів складається з 256 позицій, тобто кожен символ має свій унікальний код від 0 до 255. Символ з деяким кодом N записують так: # N. Прямо так символи і вказуються в коді програми. Так як код символу являє собою число не більше 255, то очевидно, що в пам’яті символ займає 1 байт. Як відомо, менш байта розмірності немає. Точніше, вона є – це біт, але працювати з битами в програмі ми не можемо: байт – мінімальна одиниця. Дивитись таблицю символів і їх коди можна за допомогою стандартної утиліти “Таблиця символів”, що входить в Windows (ярлик розташований в меню Пуск – Програми – Стандартні – Службові). Але зовсім скоро ми й самі напишемо щось подібне.


Рядки


Рядок, вона ж текст – Це набір символів, будь-яка їх послідовність. Відповідно, один символ – це теж рядок, теж текст. Текстовий рядок має певну довжину. Фраза – це кількість символів, які вона містить. Якщо один символ займає 1 байт, то рядок з N символів займає відповідно N байт.
Є й інші кодові таблиці, в яких 1 символ представлений не одним байтом, а двома. Це Юнікод (Unicode). У таблиці Юнікоду є символи всіх мов світу. На жаль, робота з Юнікодом досить ускладнена і його підтримка поки що носить лише локальний характер. Delphi не надає можливостей для роботи з Юнікодом. Програмна частина є, але от візуальні елементи – форми, кнопки і т.д. не вміють відображати текст у форматі Юнікод. Будемо сподіватися, в найближчому майбутньому така підтримка з’явиться. 2 байта також називають словом (word). Звідси і назва відповідного числового типу даних – Word (число, що займає в пам’яті 2 байта, значення від 0 до 65535). Кількість “осередків” в таблиці Юнікоду складає 65536 і цього цілком достатньо для зберігання всіх мов світу. Якщо ви вирішили, що “1 байт – 256 значень, значить 2 байта – 2 * 256 = 512 значень”, раджу згадати двійкову систему і принцип зберігання даних в комп’ютері.


Типи даних


Перейдемо безпосередньо до програмування. Для роботи з символами і рядками існують відповідні типи даних:


Char – один символ (тобто 1 байт);
String – рядок символів, текст (N байт).


Офіційно рядка вміщують лише 255 символів, однак в Delphi в рядок можна записати набагато більше. Для зберігання великих текстів і текстів із спеціальними символами існують спеціальні типи даних AnsiString і WideString (Останній, до речі, багатобайтових, тобто для Юнікоду).


Для завдання текстових значень в Pascal використовуються одинарні лапки (Не подвійні!). Тобто коли ви хочете привласнити строкової змінної якесь значення, слід зробити це так:

s:=”text”;

Символи вказуються аналогічно, тільки в лапках присутній один-єдиний символ.


Якщо ви хочете жорстко обмежити довжину тексту, що зберігається в строкової змінної, можна зробити це таким чином:

var s: string[10];

В дужках вказується максимальна довжина рядка.


Операції з рядками


Основною операцією з рядками є складання. Подібно числах, рядки можна складати. І якщо в числах стільці з апельсинами складати не можна, то в рядках – можна. Додавання рядків – це просто їх об’єднання. Приклад:

var s: string;

s:=”123″+”456″;
//s = “123456”

Оскільки кожен рядок – це послідовність символів, кожен символ має свій порядковий номер. В Pascal нумерація символів у рядках починається з 1. Тобто в рядку “ABC” символ “A” – перший, ” B “- другий і т.д.
Порядковий номер символу в рядку придуманий не випадково, адже саме за цими номерами, індексам, здійснюються дії над рядками. Отримати будь-який символ з рядка можна зазначенням його номера в квадратних дужках поруч з ім’ям змінної. Наприклад:

var s: string; c: char;

s:=”Hello!”;
c:=s[2];
//c = “e”

Трохи пізніше, коли ми будемо вивчати масиви, стане зрозуміло, що рядок – це масив символів. Звідси випливає і форма звернення до окремих символів.


Обробка рядків


Перейдемо до функцій і процедур обробки рядків.


Довжина рядка


Довжину рядка можна дізнатися за допомогою функції Length(). Функція приймає єдиний параметр – рядок, а повертає її довжину. Приклад:

var Str: String; L: Integer;
{ … }
Str:=”Hello!”;
L:=Length(Str);
//L = 6

Знаходження підрядка в рядку


Невід’ємною завданням є знаходження підрядка в рядку. Тобто завдання формулюється так: є рядок S1. Визначити, починаючи з якої позиції в неї входить рядок S2. Без виконання цієї операції ні одну обробку уявити неможливо.
Отже, для такого перебування існує функція Pos(). Функція приймає два параметри: перший – підрядок, Яку потрібно знайти, другий – рядок, В якій потрібно виконати пошук. Пошук здійснюється з урахуванням регістру символів. Якщо функція знайшла входження підрядка в рядок, повертається номер позиції її першого входження. Якщо входження не знайдено, функція дає результат 0. Приклад:

var Str1, Str2: String; P: Integer;
{ … }
Str1:=”Hi! How do you do?”;
Str2:=”do”;
P:=Pos(Str2, Str1);
//P = 9

Видалення частини рядка


Видалити частину рядка можна процедурою Delete(). Слід звернути увагу, що це саме процедура, а не функція – вона виробляє дії безпосередньо над тією змінною, яка їй передана. Отже, перший параметр – мінлива строкового типу, з якої видаляється фрагмент (саме мінлива! конкретне значення не задається, тому що процедура не повертає результат), другий параметр – номер символу, починаючи з якого потрібно видалити фрагмент, третій параметр – кількість символів для видалення. Приклад:

var Str1: String;
{ … }
Str1:=”Hello, world!”;
Delete(Str1, 6, 7);
// Str1 = “Hello!”

Слід зазначити, що якщо довжина удаляемого фрагмента виявиться більше кількості символів в рядку, починаючи з зазначеної позиції (тобто “виліземо за край”), функція нормально відпрацює. Тому, якщо потрібно видалити фрагмент з рядка з якогось символу до кінця, не потрібно обчислювати кількість цих символів. Кращим способом буде задати довжину самої цієї рядка.


Ось приклад. Припустимо, потрібно знайти в рядку першу літеру “a” і видалити наступну за нею частину рядка. Зробимо так: позицію букви в рядку знайдемо функцією Pos (), а фрагмент видалимо функцією Delete() .

var Str: String;
{ … }
Str:=”This is a test.”;
Delete(Str,Pos(“a”,Str),Length(Str));

Спробуємо підставити значення і подивитися, що передається функції Delete. Перша літера “a” в рядку стоїть на позиції 9. Довжина всього рядка – 15 символів. Значить виклик функції відбувається такий: Delete (Str, 9,15). Видно, що від букви “a” до кінця рядка всього 7 символів … Але функція зробить свою справу, не дивлячись на цю різницю. Результатом, звичайно, буде рядок “This is”. Даний приклад одночасно показав і комбінування декількох функцій.


Копіювання (витяг) частини рядка


Ще одним важливим завданням є копіювання частини рядка. Наприклад, вилучення з тексту окремих слів. Виділити фрагмент рядка можна видаленням зайвих частин, але цей спосіб незручний. Функція Copy() дозволяє скопіювати з рядка зазначену частину. Функція приймає 3 параметри: текст (рядок), звідки копіювати, номер символу, починаючи з якого скопіювати і кількість символів для копіювання. Результатом роботи функції і буде фрагмент рядка.


Приклад: нехай потрібно виділити з пропозиції перше слово (слова розділені пропуском). На формі розмістимо Edit1 (TEdit), в який буде введено пропозицію. Операцію буде виконувати після натискання на кнопку. Маємо:

procedure TForm1.Button1Click(Sender: TObject);
var s,word: string;
begin
s:=Edit1.Text;
word:=Copy(s,1,Pos(” “,s)-1); ShowMessage (“Перше слово:” + word);
end;

В даному випадку з рядка копіюється фрагмент від початку до першого пробілу. Число символів береться на одиницю менше, тому що в іншому випадку пробіл також буде скопійовано.


Приклад “серйозніше”


Приклади, наведені вище, лише демонструють принцип роботи з рядками за допомогою функцій Length (), Pos (), Delete () і Copy (). Тепер вирішимо завдання складніше, яка зажадає комбінованого застосування цих функцій.


Завдання: текст, введений в поле Memo, розбити на слова і вивести їх в ListBox по одному на рядок. Слова відокремлюються один від одного пробілами, крапками, комами, оклику і питальними знаками. Крім цього вивести загальну кількість слів у тексті і найдовше з цих слів.


Ось вже так … Завдання зовсім не проста. По-перше, ви відразу повинні здогадатися, що потрібно використовувати цикли. Без них ніяк, адже ми не знаємо, який текст буде переданий програмі для обробки. По-друге, слова відокремлюються різними символами – це створює додаткові труднощі. Що ж, підемо по порядку.


Інтерфейс: Memo1 (TMemo), Button1 (TButton), ListBox1 (TListBox), Label1, Label2 (TLabel).


Спочатку перенесемо введений текст в змінну. Для того, щоб разом взяти весь текст з Memo, звернемося до властивості Lines.Text:

procedure TForm1.Button1Click(Sender: TObject);
var Text: string;
begin
Text:=Memo1.Lines.Text;
end;

Тепер перейдемо до обробки. Перше, що потрібно зробити – розібратися з символами-роздільниками. Справа в тому, що такі символи можуть запросто йти поспіль, адже після ком, крапок та інших знаків ставиться прогалину. Обійти ці труднощі можна таким простим способом: всі розділяють символи замінимо на якийсь один, наприклад на кому. Для цього пройдемо всі символи і зробимо необхідні заміни. Щоб визначити, чи є символ роздільником, запишемо все роздільники в окрему строкову змінну (константу), а потім будемо шукати в цьому рядку кожен символ функцією Pos (). Всі ці заміни будуть проводитися у змінній, щоб оригінальний текст в Memo (тобто на екрані) не було порушено. Тим не менш, для перевірки проміжних результатів роботи має сенс виводити оброблений текст будь-куди. Наприклад, в інше поле Memo. Щоб пройти всі символи, скористаємося циклом FOR, де мінлива пройде порядкові номери всіх символів, тобто від 1 до довжини рядка тексту:

procedure TForm1.Button1Click(Sender: TObject);
const DelSym = ” .,!?”;
var Text: string; i: integer;
begin
Text:=Memo1.Lines.Text;
for i := 1 to Length(Text) do
if Pos(Text[i],DelSym) > 0 then
Text[i]:=”,”;
 
Memo2.Text:=Text;
end;

Тепер потрібно усунути перешкоди. По-перше, перший символ не повинен бути роздільником, тобто якщо перший символ – кома, його потрібно видалити. Далі, якщо поспіль йдуть кілька ком, їх потрібно замінити на одну. І нарешті, щоб коректно обробити весь текст, останнім символом повинна бути кома.

if Text[1] = “,” then
Delete(Text,1,1);
while Pos(“,,”,Text) > 0 do
Delete(Text,Pos(“,,”,Text),1);
if Text[Length(Text)] <> “,” then
Text:=Text+”,”;

Тут заміна зроблена таким чином: організований цикл, в якому одна з ком віддаляється, але відбувається це до тих пір, поки в тексті є дві що йдуть підряд коми.


Ну ось, тепер в тексті не залишилося нічого зайвого – тільки слова, розділені комами. Спочатку доб’ємося того, щоб програма отримала з тексту перше слово. Для цього знайдений перший кому, скопіюємо слово від початку тексту до цієї коми, після чого видалимо це слово з тексту разом з комою. Видалення робиться для того, щоб далі можна було, виконавши ту ж саму операцію, вирізати наступне слово.

var Word: string;
{…}
Word:=Copy(Text,1,Pos(“,”,Text)-1);
Delete(Text,1,Length(Word)+1);

Тепер у змінній Word у нас слово з тексту, а у змінній Text вся інша частина тексту. Вирізане слово тепер додаємо в ListBox, викликаючи ListBox.Items.Add (строка_для_добавленія).


Тепер нам потрібно організувати такий цикл, який дозволив би вирізати з тексту всі слова, а не тільки перше. В даному випадку підійде скоріше REPEAT, ніж WHILE. В якості умови слід вказати Length (Text) = 0, тобто завершити цикл тоді, коли текст стане порожнім, тобто коли ми виріжемо з нього все слова.

repeat
Word:=Copy(Text,1,Pos(“,”,Text)-1);
Delete(Text,1,Length(Word)+1);
ListBox1.Items.Add(Word);
until Length(Text) = 0;

Отже, на даний момент маємо:

procedure TForm1.Button1Click(Sender: TObject);
const DelSym = ” .,!?”;
var Text,Word: string; i: integer;
begin
Text:=Memo1.Lines.Text;
for i := 1 to Length(Text) do
if Pos(Text[i],DelSym) > 0 then
Text[i]:=”,”;
 
if Text[1] = “,” then
Delete(Text,1,1);
while Pos(“,,”,Text) > 0 do
Delete(Text,Pos(“,,”,Text),1);

repeat
Word:=Copy(Text,1,Pos(“,”,Text)-1);
Delete(Text,1,Length(Word)+1);
ListBox1.Items.Add(Word);
until Length(Text) = 0;
end;


Якщо ви зараз запустите програму, то побачите, що все чудово працює. За винятком одного моменту – в ListBox в кінці з’явилися якісь порожні рядки … Виникає питання: звідки ж вони взялися? Про це ви дізнаєтесь у наступному розділі уроку, а поки давайте реалізуємо потрібне до кінця.


Кількість слів в тексті визначити дуже просто – не потрібно заново нічого писати. Т.к. слова у нас занесені в ListBox, досить просто дізнатися, скільки там рядків – ListBox.Items.Count.

Label1.Caption: = “Кількість слів в тексті:” + IntToStr (ListBox1.Items.Count);

Тепер потрібно знайти найдовше з усіх слів. Алгоритм знаходження максимального числа такий: приймаємо в якості максимального перше з чисел. Потім перевіряємо всі інші числа таким чином: якщо число більше того, яке зараз записано як максимальне, робимо максимальним це число. У нашому випадку потрібно шукати максимальну довжину слова. Для цього можна додати код в цикл вирізання слів з тексту або провести пошук після додавання всіх слів в ListBox. Зробимо другим способом: організовуємо цикл по рядках ListBox. Слід зазначити, що рядки нумеруються з нуля, а не з одиниці! В окремій змінній будемо зберігати найдовше слово. Здавалося б, треба ж ще зберігати максимальну довжину слова, щоб було з чим порівнювати … Але не потрібно заводити для цього окрему змінну, адже ми завжди можемо дізнатися довжину слова функцією Length (). Отже, припустимо, що перше слово найдовше …

var LongestWord: string;
{…}
LongestWord:=ListBox1.Items[0];
for i := 1 to ListBox1.Items.Count-1 do
if Length(ListBox1.Items[i]) > Length(LongestWord) then
LongestWord:=ListBox1.Items[i];
  Label2.Caption: = “Найдовше слово:” + LongestWord + “(” + IntToStr (Length (LongestWord)) + “букв)”;

Чому цикл до ListBox.Items.Count-1, а не просто до Count, розберіться самостійно 🙂


Ось тепер все готово!

procedure TForm1.Button1Click(Sender: TObject);
const DelSym = ” .,!?”;
var Text,Word,LongestWord: string; i: integer;
begin
Text:=Memo1.Lines.Text;
for i := 1 to Length(Text) do
if Pos(Text[i],DelSym) > 0 then
Text[i]:=”,”;
 
if Text[1] = “,” then
Delete(Text,1,1);
while Pos(“,,”,Text) > 0 do
Delete(Text,Pos(“,,”,Text),1);
 
Text:=AnsiReplaceText(Text,Chr(13),””);
Text:=AnsiReplaceText(Text,Chr(10),””);
 
repeat
Word:=Copy(Text,1,Pos(“,”,Text)-1);
Delete(Text,1,Length(Word)+1);
ListBox1.Items.Add(Word);
until Length(Text) = 0;
  Label1.Caption: = “Кількість слів в тексті:” + IntToStr (ListBox1.Items.Count);
 
LongestWord:=ListBox1.Items[0];
for i := 1 to ListBox1.Items.Count-1 do
if Length(ListBox1.Items[i]) > Length(LongestWord) then
LongestWord:=ListBox1.Items[i];
  Label2.Caption: = “Найдовше слово:” + LongestWord + “(” + IntToStr (Length (LongestWord)) + “букв)”;
end;

Робота з символами


Власне, робота з символами зводиться до використання двох основних функцій – Ord() і Chr(). З ними ми вже зустрічалися. Функція Ord() повертає код зазначеного символу, а функція Chr() – Навпаки, повертає символ з вказаним кодом.


Пам’ятаєте “Таблицю символів”? Давайте зробимо її самі!


Висновок здійснимо в TStringGrid. Цей компонент є таблицею, де в кожному осередку записано текстове значення. Компонент розташований на вкладці Additional (за замовчуванням слід прямо за Standard). Насамперед налаштуємо нашу табличку. Нам потрібні лише дві колонки: в одній будемо відображати код символу, а в іншій – сам символ. Кількість колонок задається у властивості з логічним назвою ColCount. Встановлюємо його рівним 2. За замовчуванням у StringGrid заданий один фіксований стовпець і одна фіксована рядок (вони відображаються сірим кольором). Стовпець нам не потрібен, а от рядок дуже до речі, тому ставимо FixedCols = 0, а FixedRows залишаємо = 1.


Заповнення здійснимо прямо при запуску програми, тобто не будемо ставити ніяких кнопок. Отже, створюємо обробник події OnCreate () форми.


Кількість символів в кодовій таблиці 256, плюс заголовок – разом 257. Задамо число рядків програмно (хоча можна задати і в Інспекторові Об’єкту):

procedure TForm1.FormCreate(Sender: TObject);
begin
StringGrid1.RowCount:=257;
end;

Висновок робиться вкрай просто – за допомогою циклу. Просто проходимо числа від 0 до 255 і виводимо відповідний символ. Також виводимо написи в заголовок. Доступ до осередків StringGrid здійснюється за допомогою властивості Cells: Cells [номер_стовпчика, номер_рядка]. У квадратних дужках вказуються номера стовпця і рядка (починаються з нуля). Значення текстові.

procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
StringGrid1.RowCount:=257; StringGrid1.Cells [0,0]: = “Код”; StringGrid1.Cells [1,0]: = “Символ”;
for i := 0 to 255 do
begin
StringGrid1.Cells[0,i+1]:=IntToStr(i);
StringGrid1.Cells[1,i+1]:=Chr(i);
end;
end;

Запускаємо, дивимося.


Спеціальні символи


Якщо ви уважно подивитеся на нашу таблицю, то побачите, що багато символи відображаються у вигляді квадратиків. Ні, це не значки. Так відображаються символи, які не мають візуального відображення. Тобто символ, наприклад, з кодом 13 існує, але він невидимий. Ці символи використовуються в додаткових цілях. Наприклад, символ # 0 (тобто символ з кодом 0) часто застосовується для вказівки відсутності символу. Існують також рядки, які називаються null-terminated – це рядки, що закінчуються символом # 0. Такі рядки використовуються в мові Сі.
За кодами можна впізнавати натискання клавіш. Наприклад, клавіша Enter має код 13, Escape – 27, пробіл – 32, Tab – 9 і т.д.
Давайте додамо в нашу програму можливість дізнатися код будь-якої клавіші. Для цього обробимо подія форми OnKeyPress (). Щоб цей механізм працював, необхідно встановити у форми KeyPreview = True.

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin ShowMessage (“Код натиснутою клавіші:” + IntToStr (Ord (Key)));
end;

Тут ми виводимо віконце з текстом. У події є змінна Key, в якій зберігається символ, відповідний натиснутій клавіші. За допомогою функції Ord () дізнаємося код цього символу, а потім функцією IntToStr () перетворимо це число в рядок.


Приклад “серйозніше” – продовження


Повернемося до нашого прикладу. Настав час з’ясувати, звідки в ListBox беруться порожні рядки. Справа в тому, що вони не зовсім порожні. Так, візуально вони порожні, але насправді в кожної з них по 2 спеціальних символу. Це символи з кодами 13 та 10 (тобто рядок # 13 # 10). У Windows така послідовність цих двох невізуальних символів означає кінець поточного рядка і початок нового рядка. Тобто в будь-якому файлі і взагалі де завгодно переноси рядків – це два символи. А весь текст, відповідно, залишається безперервної послідовністю символів. Ці символи можна (і навіть треба) використовувати у випадках, коли потрібно вставити перенос рядка. Детальніше про це можна прочитати в статті Що таке # 13 # 10?. Знань, отриманих у всіх попередніх уроках, і в цьому в тому числі, цілком достатньо для розуміння цієї статті – вона зовсім невелика.


Доведемо нашу програму з пошуку слів до логічного кінця. Отже, щоб позбутися від порожніх рядків, нам потрібно видалити з тексту символи # 13 і # 10. Зробити це можна за допомогою циклу, за аналогією з тим, як ми робили заміну двох ком на одну:

while Pos(Chr(13),Text) > 0 do
Delete(Text,Pos(Chr(13),Text),1);
 
while Pos(Chr(10),Text) > 0 do
Delete(Text,Pos(Chr(10),Text),1);

Ну от – тепер програма повністю працездатна!


Додаткові функції для роботи з рядками – модуль StrUtils


Додатковий модуль StrUtils.pas містить додаткові функції для роботи з рядками. Серед цих функцій безліч корисних. Більш докладно деякі з функцій розглянуті у статті Робота з рядковими типами даних. А ось короткий опис часто використовуваних функцій:


PosEx(Підрядок, рядок, відступ) – функція, аналогічна функції Pos (), але виконує пошук з зазначеної позиції (тобто з відступом від початку рядка). Наприклад, якщо ви хочете знайти в рядку другої пробіл, а не перший, без цієї функції вам не обійтися. Щоб зробити пошук другого пробілу вручну, потрібно попередньо вирізати частину з вихідної рядка.


AnsiReplaceStr, AnsiReplaceText (Рядок, текст_1, текст_2) – функції виконують заміну в рядку рядок рядки текст_1 на текст_2. Функції відрізняються тільки тим, що перша веде заміну з урахуванням регістру символів, а друга – без нього.
У нашій програмі можна використовувати ці функції для вирізання з рядка символів # 13 і # 10 – для цього в якості тексту для заміни слід вказати порожній рядок. Ось рішення в один рядок коду:

Text:=AnsiReplaceText(AnsiReplaceText(Text,Chr(13),””),Chr(10),””);

DupeString(Рядок, чісло_повтореній) – формує рядок, що складається з рядка рядок шляхом повторення її задану кількість разів.


ReverseString(Рядок) – інвертує рядок (“123” -> “321”).


Також слід згадати у функціях перетворення регістра.


UpperCase(Рядок) – перетворює рядок у верхній регістр; LowerCase(Рядок) – перетворює рядок у нижній регістр.


Для перетворення окремих символів слід використовувати ці ж функції.


Детальну інформацію про кожної функції можна отримати, ввівши її назву в будь-якому місці редактора коду, встановивши курсор на цю назву (або виділивши його) і натиснувши F1.


Скріншоти програм, описаних у статті








Таблиця символів

Висновок


Довгий вийшов урок … І як раз афоризм опинився в тему. Що дивно, афоризм був підібраний мною на самому початку, а сам урок написаний пізніше 🙂


Отже, сьогодні ми познайомилися з рядками і символами і навчилися з ними працювати. Вивчені прийоми використовуються практично повсюдно. Не бійтеся експериментувати – самостійно підвищуйте свій рівень навичок програмування!

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*