Попросили меня выполнить code-review скрипта, сгенерированного AI. Скрипт написан на PL/SQL, и выполняет следующие действия:
- во входной строке ищет и заменяет различные написания слова "строение", например "с.", "ст.", "стр-е" итп и заменяет на стандартизированное написание "стр.",
- затем разбивает полученную строку на части по набору разделителей (в данном примере это "," и ";")
- полученный набор строк дополнительно разбивается на подстроки по длине в 10 символов
Вот исходный код:
DECLARE
original_text VARCHAR2(4000) := 'Это строение, которое находится рядом со ст. 5; также есть стр-е в центре города. Это очень важное стро.';
modified_text VARCHAR2(4000);
chunk_size INTEGER := 10;
start_pos INTEGER := 1;
delimiter VARCHAR2(10) := ',;'; -- Разделители
current_chunk VARCHAR2(4000);
BEGIN
-- Заменяем слова
modified_text := REPLACE(original_text, 'строение', 'стр.');
modified_text := REPLACE(modified_text, 'стр-е', 'стр.');
modified_text := REPLACE(modified_text, 'стро.', 'стр.');
modified_text := REPLACE(modified_text, 'ст.', 'стр.');
DBMS_OUTPUT.PUT_LINE('Измененный текст: ' || modified_text);
-- Если строка больше 10 символов, делим ее на части
IF LENGTH(modified_text) > chunk_size THEN
-- Разделяем строку по разделителям
FOR chunk IN (
SELECT REGEXP_SUBSTR(modified_text, '[^'||delimiter||']+', 1, LEVEL) AS part
FROM dual
CONNECT BY REGEXP_SUBSTR(modified_text, '[^'||delimiter||']+', 1, LEVEL) IS NOT NULL
) LOOP
current_chunk := TRIM(chunk.part);
SELECT REGEXP_SUBSTR(modified_text, '[^'||delimiter||']+', 1, LEVEL) AS part
FROM dual
CONNECT BY REGEXP_SUBSTR(modified_text, '[^'||delimiter||']+', 1, LEVEL) IS NOT NULL
) LOOP
current_chunk := TRIM(chunk.part);
start_pos := 1;
-- Делим текущую часть на подстроки по 10 символов
WHILE start_pos <= LENGTH(current_chunk) LOOP
DBMS_OUTPUT.PUT_LINE(SUBSTR(current_chunk, start_pos, chunk_size));
start_pos := start_pos + chunk_size;
END LOOP;
END LOOP;
ELSE
DBMS_OUTPUT.PUT_LINE(modified_text);
END IF;
END;
/
Казалось бы, ну чего тут такого... ну код как код, написан без особых изысков, можно сказать, что "в лоб". Но работает, ничего особо плохого сказать не могу. По ощущениям, написан программистом среднего уровня, ближе к начинающему.
А вот как можно изменить этот код, чтобы сразу было понятно, что его писал не AI и не начинающий разработчик? Вот я и задумался, а как бы написал такой код я. Вот что у меня в итоге получилось путем нескольких итераций:
declare
original_text varchar2(4000) := 'Это строение, которое находится рядом со ст. 5; также есть стр-е в центре города. Это очень важное стро.';
modified_text varchar2(4000);
chunk_size INTEGER := 10;
delimiter varchar2(10) := ',;'; -- разделители
begin
dbms_output.put_line('Исходный текст : ' || original_text);
-- заменяем слова
modified_text := regexp_replace(srcstr => original_text,
pattern => '(\W|^)(строение|стр-е|стро.|ст.)(\W|$)',
replacestr => '\1стр.\3',
position => 1,
occurrence => 0,
modifier => 'i');
dbms_output.put_line('Измененный текст: ' || modified_text);
-- если строка больше 10 символов, делим ее на части
for chunk in (
with q_chunks as (
select rownum rn, regexp_replace(regexp_substr(modified_text, '[^'||delimiter||']+', 1, level), '(^\s+|\s+$)', null) t
from dual
connect by level <= regexp_count(modified_text, '[^'||delimiter||']+'))
select distinct chunk.rn, level, regexp_substr(chunk.t, '.{1,'||chunk_size||'}', 1, level) t
from q_chunks chunk
connect by regexp_substr(chunk.t, '.{1,'||chunk_size||'}', 1, level) is not null
order by rn, level
) loop
dbms_output.put_line(chunk.t);
end loop;
end;
/
Во первых, очевидно, что заменять различные написания на нечто стандартное - это вотчина регулярных выражений, непонятно, почему AI решил не пользоваться ими. Тем более, что очевидно, что требуется менять только отдельные слова, но не части слов, что нельзя сделать обычными сериями REPLACE. Т.е. исходный код уже логически неверен. Ну ладно, идем дальше.
Часть кода, которая делит текст по разделителям, в принципе работает корректно, причем тут как раз и использованы регулярные выражения. Я фактически оставил ее без изменений, только добавил вызов regexp_replace для удаления пробельных символов спереди и сзади порезанных частей строк. Непонятно, почему в исходном коде это действие вынесено в отдельный вызов TRIM, который удаляет только пробелы.
Далее начинается самое интересное, собственно тема поста. Я задумался - можно ли используя SQL разбить текст на строки по длине. Оказалось, что да - можно это сделать используя известный прием с select ... from dual connect by ...
Таким образом можно совместить шаг разбиения строки по разделителям с шагом разбиения подстрок по длине. Тут хотелось бы заметить, что у меня, как всегда, возникли некоторые трудности с увязыванием обоих иерархических запросов, поэтому не взыщите за использование rownum, distinct и order by, предлагаю списать это на небрежность написания примера.
В целом результат моего code review - оптимизация предложенного кода, устранение логических ошибок, минимизация переключения контекстов выполнения, а также использование иерархического SQL для разбиения строки на подстроки по длине. Для более ясного понимания прикладываю простейший пример по теме с нарезкой строки по 5 символов с помощью SQL:
with q_data_to_split as (select '0123456789abcdefghijklnmopqrstuvwxyzABCDEFGHIJKLNMOPQRSTUVWXYZ' t, '.{1,5}' p from dual)
select regexp_substr(t, p, 1, level) t
from q_data_to_split
connect by regexp_substr(t, p, 1, level) is not null
На мой взгляд - это проще чем разбиение строки циклами в PL/SQL с использованием переменных. Вот PL/SQL аналог вышеприведенного запроса:
declare
t varchar2(4000) := '0123456789abcdefghijklnmopqrstuvwxyzABCDEFGHIJKLNMOPQRSTUVWXYZ';
chunk_size integer := 5;
start_pos integer := 1;
begin
WHILE start_pos <= LENGTH(t) LOOP
DBMS_OUTPUT.PUT_LINE(SUBSTR(t, start_pos, chunk_size));
start_pos := start_pos + chunk_size;
END LOOP;
end;