В качестве примера можно рассмотреть функцию IsDigital() или аналогичные функции на этой же странице в "См. также". Лучше скопировать пример в IDE, чтобы видеть оба описание и пример, о котором рассказывается.
Функция может не изменять передаваемую в неё строку или изменять. В первом случае можно передать указатель на строку IsDigital(@"123"), а принимать этот указатель в функцию в виде *c.Character, чтобы не копировать данные. Во втором случае нужно передать строку и тогда анализатор может изменять в ней данные, так как это не затронет оригинал.
Строки в PureBasic имеют формат Unicode, поэтому тип данных используется .Character (можно .Unicode). Если же используется функция Ascii() и передаётся указатель на буфер в формате Ascii, то входящий тип данных является .Ascii, то есть переменная указатель будет выглядеть как *a.Ascii
После того как объявлены локальные переменные нужно сделать проверку данных. Часто если сторонняя функция не может вернуть указатель, то возвращает 0, поэтому важно перед началом обработки проверить, что указатель не равен нулю, а также если указатель валидный, но текст не содержит данных, то у него код первого символа равен 0, то есть нультерминированная пустая строка, соответственно парсить нечего.
Почему применён Repeat, а не While? While проверяет ненулевое значение символа при входе в цикл, но так как ненулевое значение проверено предыдущим условием, то можно использовать Repeat, а валидность символа проверить в конце цикла с помощью Until. Хотя лишнее инвертирование Not толкает к применению While.
Дальше внутри цикла проверяется условие, если символ меньше 0 или больше 9, то выпрыгиваем из цикла с результатом #False. То есть в строке встретился символ не принадлежащий диапазону 0-9.
Repeat If*c\c<'0'Or*c\c>'9' flag=#False Break EndIf *c+SizeOf(Character) Until Not*c\c
Что такое *c\c и почему это сравнивается с числом '0' обрамлённым апострофами? Итак Character и Ascii указанные как тип по факту являются структурами объявленными нативно PureBasic`ом имеют одно поле \c для Character и \a для Ascii. Аналогично есть структуры для всех типов Integer, String с полями \i и \s соответственно, но это уже другая история и используются в других целях. Символ в апострофах возвращает код числа, для '0' это 48, проверка: Debug'0'. Чтобы проверить верно ли утверждение нужно открыть charmap.exe в Windows, он же программа "Таблица символов". Аналогичное есть в Linux. Далее кликаем на символ 0 и встроке состояния видим "U+0030" это шестнадцатеричный код символа, если использовать отладчик "Debug$30" то он вернёт 48. В данном случае *c\c это тоже код числа, то есть просмотр данных выполняется в числовом/бинарном виде без преобразования в строку.
Условие может быть записано в разном виде:
If*c\c<'0'Or*c\c>'9' If*c\c<48Or*c\c>57 If Not(*c\c>='0'And*c\c<='9') If Not(*c\c>=48And*c\c<=57)
Используем вариант более читабельный и с меньшим вычислением. Символ в апострофах преобразуется на этапе компиляции, поэтому выгоднее видеть литерально, чем код символа. А оператор Not принуждает вычислять скобки, а потом инвертировать, в то время как первый вариант может вычислить только первое сравнение и при #True не проверять/вычислять последующее сравнение после Or
Каждый шаг цикла сопровождается сдвигом указателя на размер символа в памяти. Функция SizeOf(Character) выполняется компилятором и возвращает 2, но в старой версии "PureBasic 5.42" в режиме ASCII возвратит 1, поэтому при использовании этой функции используется более понятное назначение этого числа, не просто 2, как часто можно встретить на форуме, а величина размера типа. Если не планируется использовать исходник для старых версий можно хотя бы придумать константу, например #SizeCharacter, что для программиста читающего код будет более понятней, чем магическое число.
*c+SizeOf(Character)
Итак цикл проходит по всем символам до конца строки пока не встретит бинарный ноль, что является концом строки, именно поэтому каждый шаг цикла всегда сопровождается проверкой, является ли код символа нулём.
В данной простой задаче проверяется каждый символ строки и если он не соответствует условию (проверка, что символ принадлежит диапазону 0-9), то выпрыгивает и возвращает #False, а если дойдя до конца строки не встретился символ противоречащий условию, то возвращается #True. То есть по факту цикл имеет два условия проверка диапазона и проверка что код символа не равен 0.
Задача 2
Теперь немного усложним задачу, в качестве примера возьмём функцию SplitListByWords() - разделение строки по словам. Здесь функция портит строку, поэтому если нужен оригинал строки то необходимо скопировать её. В чём заключается простота функции? Для простоты понимания функционала в качестве разделителя слов примем термин "пробел" (на самом деле это любой символ не слова), а в качестве символа слова используем термин "буква" (диапазон алфавита: а-я или a-z). Нужно запомнить в указатель первую букву слова, где меняется пробел на букву, а в конце слова, где буква меняется на пробел вставить бинарный ноль взамен пробела и стандартной функцией PeekS() прочитать это слово и дальше двигаться к следующей границе смены пробела на букву. Вводится новый указатель *S. Изначально указатели приравнены. Указатель *c всегда двигается вперёд на каждом шаге цикла, а вот указатель *S двигается только если символ не является буквой. Если указатель *c является пробелом, то он заменяется на 0 (нультерминированная строка)
*c\c=0
Если указатель *S отстаёт от его собрата *c, так как не выполнялось условие, что символ является буквой. То есть указатель *S стоит на первой букве слова.
Если указатель *c снова становится пробелом, то при разнице указателей *c и *S выполняется чтение с указателя *S, а так как код символа указателя *c приравнен к нулю (создание нультерминированной строки), то читается одно слово и далее оба указателя вновь приравнены и двигаются к новому слову.
Опускаем за скобки, что функция добавляет слово в список, так легче сделать возврат данных добавляя в список переданный по указателю.
Задача 3
В качестве примера возьмём функцию LTrimChar(). Есть нативная функция LTrim() удаляющая некий символ слева. Но что если необходимо удалить несколько переносов строк CRLF? Тогда надо циклически вызывать LTrim() то с одним то с другим символом, пока длина строки перестанет укорачиваться. Выглядит это как костыль при выполнении задачи. Логическое описание задачи выглядит так: слева проверяется каждый символ на принадлежность символов второй строки, в данном случае CRLF, и как только символ перестанет принадлежать любому символу второй строки, то это позиция-указатель откуда нужно прочитать строку стандартной функцией PeekS(). Так из первой задачи стал понятен смысл посимвольного перемещения по строке, то в этой задаче добавляется вложенный цикл перемещения по второй строке и возврат в начало при следующем шаге внешнего цикла. Во внешнем цикле проверяется *c\c, что это не конец строки, а значит его можно сравнивать со второй строкой, поэтому выполняется вход во внутренний цикл перечисления второй строки, где очередной символ первой строки сравнивается с каждым символом второй строки *jc\c. Если найдено равенство, то нет смысла просматривать дальше, символ уже забракован и ему задаётся значение 0.
Ниже по циклу идёт проверка, является ли *c\c не равным нулю. Выше его всегда обнуляли, но если символ перестанет быть равным хоть одному из символов второй строки, то он не обнулится и сработает это условие чтение с текущего указателя.
Похожая функция RTrimChar() отличается тем, что при старте указатель ставится на последний символ строки и движется назад в начало строки.
Здесь длина в байтах ставит указатель на нультерминированный конец строки, поэтому минусуется один символ назад, чтобы указатель встал на последний символ строки.
Для перечисления используется цикл For, так как он не позволит идти назад за пределы начала строки, если вдруг окажется что вся строка состоит из бракованных символов. У строки ведь нет нультерминации для начала строки. Поэтому валидность символа определяется, когда цикл пройдёт от последнего символа до первого и на этом закончится.
Внутренний цикл одинаков, так как он проверяет каждый символ на принадлежность символов второй строки. А вот последующее условие немного отличается, как только *c\c перестал обнуляться происходит выпрыг из цикла и переменная уже будет читаться до последнего обнулённого символа. Можно было бы перечитать строку, но в этом особого смысла нет. Скорее всего обрежется несколько байт и памяти это не сэкономит. Первое же приравнивание/переопределение переменной выполнит эту работу.
Ещё нюанс, в начале цикла указатель второй строки всегда возвращается в стартовую позицию.
*jc=*jc0
Задача 4
Рассмотрим функцию EscapeRegularExpression(). Задача перед каждым символом указанным в перечислении поставить знак экранирования "\". Так как неизвестно сколько символов требуют экранирования, то за максимальное необходимо принять все символы, то есть выделить память в 2 раза больше обрабатываемой строки. Писать в ту же строку невозможно, так как вставка символа подразумевает увеличение строки. Для экономии памяти можно использовать список. Каждый экранируемый символ требует создание одного элемента, а символы между экранированными символам становятся одним элементом списка. В конце концов элементы списка объединяются в одну строку и это будет результатом работы функции. Принцип работы функции имеет уже знакомый алгоритм: если *c\c является символом, который требуется экранировать, то он обнуляется, но ничего страшного, если он равен *jc\c, то это в исполняемом блоке команд является как кэш, как временная ячейка хранения. Алгоритм повторяет вторую задачу с указателем *S, он останавливается, когда символ не принадлежит символу второй строки, а является буквой. А как только дошли до символа требующего экранирования, то вписываем вместо него 0 и получаем текст стандартной функцией PeekS(), а символ требующий экранирования добавляется в следующий элемент списка из кеша *jc\c.
Когда список готов, то он передаётся функции ListToString() для объединения. Конкатенация строк работает медленно, поэтому вместо обычного объединения (с перевыделением памяти) сначала создаётся буфер памяти методом вычисления размера всех элементов списка. И далее элементы просто пишутся в этот буфер. Здесь используется структура String для создание переменной *Result.String с полем \s. Это позволяет передавать параметр как указатель, не копируя строку, не создавая ещё один дубликат в памяти. По факту есть строка исходник, такого же размера список и строка приёмник, то есть тройной объём строки. Можно признать, что выделяя двойной объём памяти для результата и имея оригинал строки получаем также тройной объём, и в таком случае можно было писать данные в выделенный буфер.
Задача 5
Рассмотрим функцию RemoveNonWordChars(). Это несложная задача, требует удалить из строки символы не принадлежащие буквам. Принцип прост: создать два указателя, которые движутся по одной и той же строке, но один будет отставать по мере появления пробелов. Один указатель всегда сдвигается вперёд, другой сдвигается только если символ является буквой. Если в начале строки одни буквы, то указатели просто увеличивают позицию. Как только попался пробел, то второй указатель не изменил своего положения, но ввиду сдвига на предыдущем шаге он стоит в позиции пробела. Как только первый указатель опять попадает на букву, то начинается запись первого указателя в позицию второго указателя и так будет продолжаться до конца строки, потому что первый указатель уже обогнал второго. Когда первый указатель дошёл до конца строки, то второй указатель записывает 0 для нультерминированной строки.
Задача 6
Можно ли анализировать код? Легко. Конечно же в качестве примера используем анализатор языка PureBasic. Для примера сделаем удаление комментариев из кода. Известно, что символ ";" в любом месте, кроме строки в кавычках/апострофах является началом комментария до конца строки. Также если строка начинается с "!" то пропускаем эту строку. Так как в PureBasic используются только однострочные комментарии, то достаточно анализировать файл построчно, то есть использовать функцию ReadString().
Вместо If используем Select
Пока игнорируем вариант строк с тильдой "~", так как он требует более сложного анализа (поиск кавычки без нечётного префикса "\"). Итак если встречаем любой символ то просто движемся дальше по тексту. Как только встретили кавычку, то запускаем вложенный цикл поиска следующей кавычки (также проверяя что символ не равен концу строки 0), так как внутри строки обрамлённой кавычкой нет смысла проверять символ комментария. Как только встречается символ комментария ";", то заменить символ на 0 (нултерминированная строка) и перечитываем строку стандартной функцией PeekS(). Каждый раз добавляем строку в новый элемент списка, а потом объединить в одну строку и сохранить в файл. Можно сразу писать строки в новый файл.
На счёт "!", он не обязательно первым символом в строке, поэтому важно пропустить пробелы/табы в начале строки. Для этого нужно ввести флаг, который имеет одно значение в начале строки, а при первом непробельнои символе (символ начала данных) меняет значение на противоположное и далее последующие пробелы не берутся во внимание.