При разработке алгоритмов часто возникает задача парсинга пользовательского ввода. Представим ситуацию: пользователь вводит ряд чисел через пробел в одно текстовое поле, а нашей программе необходимо проанализировать эту строку и найти наибольшее из введенных чисел. На первый взгляд, задача кажется тривиальной, однако в ней скрывается множество технических нюансов: от локализации дробей до уязвимостей безопасности.
Давайте подробно разберем все способы решения этой задачи, проанализируем возможные ошибки и напишем надежный и безопасный код на встроенном языке 1С.
Наиболее надежный, предсказуемый и безопасный алгоритм — это разбор строки на подстроки с последующим конвертированием в числа. Рассмотрим по шагам, как это реализовать.
СтрРазделить.Число.Макс.Здесь важно учесть две критические ловушки, в которые часто попадают разработчики.
Ловушка инициализации максимума: Интуитивно хочется объявить переменную МаксЧисло = 0 до начала цикла. Но проанализируем ситуацию, когда пользователь введет строку, состоящую только из отрицательных чисел (например, "-10 -25 -5"). Наш алгоритм вернет ноль, хотя такого числа даже не было в строке! Поэтому стартовое значение должно инициализироваться первым обработанным элементом.
Ловушка региональных настроек: Пользователь может вводить десятичные дроби как через запятую, так и через точку. Платформа 1С строго привязана к локали. Если разделителем дробной части в базе считается запятая, функция Число выдаст ошибку преобразования на строке "45.5". Обязательно нормализуем строку до конвертации.
Посмотрим на пример правильного кода:
Процедура НайтиМаксимумВСтроке(СтрокаПользователя)
МассивПодстрок = СтрРазделить(СтрокаПользователя, " ", Ложь);
МаксимальноеЗначение = Неопределено;
Для Каждого СтроковоеЗначение Из МассивПодстрок Цикл
// Заменяем возможные точки на запятые для корректного преобразования в дробь
НормализованнаяСтрока = СтрЗаменить(СтроковоеЗначение, ".", ",");
Попытка
ТекущееЧисло = Число(НормализованнаяСтрока);
Исключение
Продолжить; // Игнорируем текст и случайный мусор
КонецПопытки;
Если МаксимальноеЗначение = Неопределено Тогда
МаксимальноеЗначение = ТекущееЧисло; // Инициализация первым реальным числом
Иначе
МаксимальноеЗначение = Макс(МаксимальноеЗначение, ТекущееЧисло);
КонецЕсли;
КонецЦикла;
Сообщить("Максимальное число: " + МаксимальноеЗначение);
КонецПроцедуры
Рассмотрим альтернативный подход, который избавляет нас от необходимости писать логику сравнения вручную. Мы можем преобразовать элементы строки в числа, поместить их в объект СписокЗначений и отсортировать его по убыванию. Первое значение в таком списке автоматически станет максимальным.
Процедура МаксимумЧерезСписокЗначений(СтрокаПользователя)
СписокЧисел = Новый СписокЗначений;
МассивПодстрок = СтрРазделить(СтрокаПользователя, " ", Ложь);
Для Каждого Элемент Из МассивПодстрок Цикл
Попытка
СписокЧисел.Добавить(Число(СтрЗаменить(Элемент, ".", ",")));
Исключение
// Игнорируем ошибки приведения типов
КонецПопытки;
КонецЦикла;
Если СписокЧисел.Количество() > 0 Тогда
СписокЧисел.СортироватьПоЗначению(НаправлениеСортировки.Убыв);
МаксимальноеЗначение = СписокЧисел[0].Значение;
Сообщить("Наибольшее число: " + МаксимальноеЗначение);
КонецЕсли;
КонецПроцедуры
Лайфхак для ускорения: Если ваша инфраструктура работает в среде ОС Windows, вместо объекта 1С СписокЗначений целесообразно использовать COM-объект System.Collections.ArrayList. Он гораздо более легковесный и выполняет метод .Sort() значительно быстрее на больших массивах данных, так как не тянет за собой визуальный интерфейс, свойственный объектам платформы.
На форумах часто предлагают "изящное" решение в одну строку с использованием метода Вычислить. Логика такова: мы заменяем пробелы в строке на запятые, оборачиваем результат в вызов функции Макс() и просим платформу исполнить эту строку как программный код.
Например, из строки "25 45 13" мы формируем выражение Макс(25, 45, 13).
Многие разработчики совершают ошибку, пытаясь передать строку целиком: МаксимальноеЗначение = Макс("25, 45, 13"). Синтакс-помощник гласит, что функция Макс принимает перечень значений. Передав единую строку, вы получите ее же обратно. Именно поэтому используется метод вычисления кода в рантайме:
СтрокаДляПоиска = СтрЗаменить(СтрокаПользователя," ", ",");
МаксимальноеЗначение = Вычислить("Макс(" + СтрокаДляПоиска + ")");
Почему мы настоятельно не рекомендуем этот метод? Выясним причину.
Вычислить компилирует и исполняет любой переданный ей текст. Если злоумышленник (или невнимательный пользователь) введет в поле не числа, а вредоносный код, вызывающий удаление данных или запуск внешних обработок, система послушно попытается это выполнить.Вычислить просто запрещено.Использовать этот метод можно только в сугубо личных утилитах с полным контролем входящих данных.
Давайте проанализируем сложную ситуацию. Что, если строка, которую ввел пользователь, содержит не только числа и пробелы, но и буквы, символы табуляции, переносы строк, единицы измерения (например: "Вес товара 25 кг, брутто 45.5 кг, тара -2 кг")? Стандартная функция СтрРазделить в такой ситуации потребует сложной многоступенчатой очистки текста.
Для надежного решения воспользуемся регулярными выражениями через COM-объект VBScript.RegExp. Напишем паттерн, который будет находить все числа в строке (включая отрицательные и дроби) и игнорировать весь остальной текст.
Процедура НайтиМаксимумВГрязнойСтроке(СтрокаПользователя)
RegExp = Новый COMОбъект("VBScript.RegExp");
RegExp.Global = Истина; // Искать все совпадения, а не только первое
RegExp.IgnoreCase = Истина;
// Паттерн означает: опциональный минус, затем одна или более цифр,
// затем опциональная дробная часть с точкой или запятой
RegExp.Pattern = "-?\d+([.,]\d+)?";
Matches = RegExp.Execute(СтрокаПользователя);
МаксимальноеЗначение = Неопределено;
Для Сч = 0 По Matches.Count - 1 Цикл
НайденнаяПодстрока = Matches.Item(Сч).Value;
ТекущееЧисло = Число(СтрЗаменить(НайденнаяПодстрока, ".", ","));
Если МаксимальноеЗначение = Неопределено Тогда
МаксимальноеЗначение = ТекущееЧисло;
Иначе
МаксимальноеЗначение = Макс(МаксимальноеЗначение, ТекущееЧисло);
КонецЕсли;
КонецЦикла;
Если МаксимальноеЗначение <> Неопределено Тогда
Сообщить("Найдено наибольшее число: " + МаксимальноеЗначение);
Иначе
Сообщить("В строке не найдено ни одного числа.");
КонецЕсли;
КонецПроцедуры
Этот подход гарантирует, что программа не сломается от случайных двойных пробелов, неразрывных пробелов (символ с кодом 160, часто копируемый из интернета) или букв, оказавшихся внутри числового ряда.
Подводя итоги, мы рекомендуем использовать первый способ с циклическим обходом массива как самый универсальный и безопасный для чистых строк. Если же вы не уверены в качестве входящих данных и вам необходимо вычленить числа из хаотичного текста, смело применяйте регулярные выражения. Избегайте использования динамической компиляции и всегда помните о правильной инициализации переменных перед началом поиска.