Число в строку в 1С: неразрывный пробел в ключах и как его вычистить
Иван Недомолков · Chop etilgan:
Загрузка отрабатывает, в журнале пусто, а сопоставленных записей ноль или заметно меньше, чем должно быть. Если ключ сопоставления собирается из числа через Строка(), первым делом смотрите на символ с кодом 160. Это неразрывный пробел, и 1С вставляет его сама, когда показывает число человеку: Строка(123456) возвращает 123 456, семь символов вместо шести.
Ниже справочник по тому, как платформа превращает число в строку, запросы и код для поиска испорченных значений, правильная замена Строка() с одной неочевидной ловушкой и порядок правки. Все фрагменты встроенного языка прогнаны 27.09.2026 на платформе 8.3.27.1606.
Что делает платформа с числом
Прогон на пяти значениях. В колонке кодов видно, где появляется символ 160:
| Число | Строка(Ч) | Длина | Коды символов | Формат(Ч, "ЧГ=0") | Формат(Ч, "ЧН=0; ЧГ=0") |
|---|---|---|---|---|---|
| 0 | 0 | 1 | 48 | пустая строка | 0 |
| 999 | 999 | 3 | 57 57 57 | 999 | 999 |
| 1000 | 1 000 | 5 | 49 160 48 48 48 | 1000 | 1000 |
| 123456 | 123 456 | 7 | 49 50 51 160 52 53 54 | 123456 | 123456 |
| 1234567 | 1 234 567 | 9 | 49 160 50 51 52 160 53 54 55 | 1234567 | 1234567 |
Из таблицы читаются три вещи, на которых дальше строится всё остальное.
Разделитель появляется с четырёх знаков. Число меньше тысячи выходит из Строка() чистым, поэтому в одной и той же таблице ключей часть значений целая, а часть испорчена.
Прирост длины зависит от разрядности. У шестизначного числа разделитель один, у семизначного два. Проверка “длина больше ожидаемой на единицу” найдёт одни и пропустит другие.
Формат с одним ЧГ=0 превращает ноль в пустую строку. Это вторая ловушка, и она поджидает ровно тех, кто взялся чинить первую. Если в поле может оказаться 0, нужен ЧН=0 в той же форматной строке, иначе ключ “0” молча станет пустым и совпадёт со всеми незаполненными значениями сразу.
Для строк Формат безопасен: Формат("АБ 12", "ЧГ=0") вернул АБ 12 без изменений. Поэтому замена работает и на смешанных полях, где рядом лежат числовые коды и буквенные артикулы.
Почему символ не находится обычным поиском
Отладчик показывает пробел. Редактор показывает пробел. А условие с обычным пробелом ничего не находит:
Строка1000 = Строка(1000);
СтрНайти(Строка1000, " "); // 0, обычного пробела (код 32) там нет
СтрНайти(Строка1000, Символы.НПП); // 2, неразрывный на второй позиции
Строка(123456) = "123 456"; // Ложь, в литерале набран обычный пробел
Символы.НПП возвращает символ с кодом 160, этим им удобно пользоваться в коде и в параметрах запроса. СокрЛП() неразрывный пробел по краям строки убирает, мы проверили: из строки “НПП + 12 + НПП” остались два символа. Но разделитель разрядов стоит в середине, и туда СокрЛП() не дотягивается.
Как найти испорченные ключи у себя
Шаг 1. Распределение длин
Начинать стоит с данных. Запрос раскладывает значения по длине и сразу считает, сколько в каждой группе содержат символ 160:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ДЛИНАСТРОКИ(Т.Артикул) КАК Длина,
| КОЛИЧЕСТВО(*) КАК Всего,
| СУММА(ВЫБОР
| КОГДА Т.Артикул ПОДОБНО &Шаблон
| ТОГДА 1
| ИНАЧЕ 0
| КОНЕЦ) КАК СНеразрывнымПробелом
|ИЗ
| Справочник.Номенклатура КАК Т
|
|СГРУППИРОВАТЬ ПО
| ДЛИНАСТРОКИ(Т.Артикул)
|
|УПОРЯДОЧИТЬ ПО
| Длина";
Запрос.УстановитьПараметр("Шаблон", "%" + Символы.НПП + "%");
Результат = Запрос.Выполнить().Выгрузить();
Справочник и реквизит здесь для примера, подставьте свою таблицу сопоставления. Ожидаемая длина видна сразу, рядом окажется группа длиннее, и в ней ненулевой счётчик в последней колонке.
Шаг 2. Проверка по кодам символов
Для точечной проверки значения, которое должно состоять только из цифр:
Функция ТолькоЦифры(Значение)
Если СтрДлина(Значение) = 0 Тогда
Возврат Ложь;
КонецЕсли;
Для Позиция = 1 По СтрДлина(Значение) Цикл
Код = КодСимвола(Значение, Позиция);
Если Код < 48 Или Код > 57 Тогда
Возврат Ложь;
КонецЕсли;
КонецЦикла;
Возврат Истина;
КонецФункции
На прогоне функция вернула Ложь для Строка(123456) и для AB 12, Истина для Формат(123456, "ЧН=0; ЧГ=0"). Проверка по кодам дороже поиска подстроки, зато не зависит ни от шрифта, ни от редактора, и не путает символ 160 с обычным пробелом.
Отбор “содержит пробел” врёт в обе стороны: не находит символ 160 там, где он есть, и находит законный пробел внутри буквенного артикула. Поэтому проверяем по кодам, а поиск пробела оставляем для глаз.
Шаг 3. Места в коде
Глобальный поиск по конфигурации: Строка( рядом со словами ключ, код, артикул, идентификатор. Интересуют все места, где из числа собирается значение для сравнения, имени файла, параметра запроса к внешней системе. Обычно их несколько: загрузка, выгрузка, отчёт сверки, ручное заполнение. Если ключи собирают внешние обработки загрузки, их код можно разобрать анализатором кода внешних обработок, он работает без отправки кода наружу.
Шаги 1 и 2 отвечают на вопрос “есть ли у меня такое” без отладчика и без чтения модулей. Шаг 3 нужен, когда ответ “да”.
Чем заменить Строка()
Для любого значения, которое уйдёт в ключ, сравнение, имя файла или внешнюю систему:
// Было: форматирование для человека, с разделителем разрядов
Ключ = Строка(Код);
// Стало: без разделителя, ноль остаётся нулём
Ключ = Формат(Код, "ЧН=0; ЧГ=0");
Для дробных чисел тот же вопрос встаёт с десятичным разделителем: Строка(1234.5) на прогоне дала 1 234,5, с запятой. Если число с дробной частью уходит во внешнюю систему, разделитель дробной части тоже задаётся в форматной строке явно, под то, что ждёт приёмник.
Две правки, которые напрашиваются и вредят:
- Вычистить из значений все пробелы. В артикулах поставщиков пробел бывает значимым. Массовая зачистка даст новые несовпадения, редкие и потому незаметные. К тому же ключ из числа можно пересобрать заново, а стёртый пробел внутри чужого артикула восстановить неоткуда. Если чистить накопленные данные, то одним символом:
СтрЗаменить(Значение, Символы.НПП, ""), и только в полях, которые по смыслу числовые. - Проверять результат по длине. У семизначных разделителей два, проверка “на единицу длиннее” пропустит их и отчитается, что всё исправлено.
Почему это годами не всплывает
Самое неприятное свойство дефекта: он может жить с первого дня внедрения и не давать никаких последствий. Если обе стороны сопоставления собирают ключ одним и тем же кодом, слева получается 123 456 и справа 123 456. Строки равны, связь находится, платформа ведёт себя совершенно корректно. На прогоне Строка(123456) = Строка(123456) вернуло Истина, а Строка(123456) = Формат(123456, "ЧГ=0") вернуло Ложь.
Совпадение ломается в тот день, когда одна сторона начинает формироваться иначе. Подключили новый источник, загрузили данные из файла, кто-то переписал один из двух модулей и сделал ключ правильно. Для дежурного это выглядит как “сломалось после новой интеграции”, хотя новая интеграция единственная, кто собрал ключ как надо.
Отсюда практическое следствие для проверок: сверка “наши ключи против их ключей” такую порчу не видит по построению, обе половины испорчены одинаково. Ловит её только проверка каждого значения против ожидаемого формата, то есть функция из шага 2 или запрос из шага 1.
Порядок правки, чтобы не порвать работающее
Из симметричной порчи вытекает неприятное: там, где обе стороны сейчас испорчены одинаково, сопоставление работает. Починив одну сторону, вы эти связи разорвёте.
- Посчитать обе стороны. Сколько значений с символом 160 в источнике, сколько в таблице сопоставления.
- Посчитать пересечение. Сколько связей сейчас держится на совпадении двух испорченных значений. Ровно это количество сломается при односторонней правке.
- Найти все места сборки ключа по шагу 3. Наполовину исправленная система хуже исходной: часть связей рвётся, и объяснение неочевидно.
- Править код и приводить накопленные данные одним заходом, обе стороны сразу, с заменой
Строка()наФормат(..., "ЧН=0; ЧГ=0")и разовой заменойСимволы.НППв числовых полях. - Повторить запрос из шага 1. В группе ожидаемой длины должны оказаться все числовые значения, счётчик символа 160 должен стать нулём.
Пересечение из пункта 2 считайте до выкатки: узнавать его размер от пользователей дороже.
Как это выглядело в живой таблице
В разборе, с которого началась эта статья, в таблице сопоставления лежало 1 449 ключей. Испорченными оказались 1 370, это 94,55 процента: ключ ожидался шестью цифрами подряд, а в базе лежали семь символов с разделителем между третьей и четвёртой цифрой. Сопоставление при этом отрабатывало без ошибок и возвращало пустоту.
Уцелели 79 ключей, 5,45 процента. По механизму это должны быть числа меньше тысячи или буквенно-цифровые значения, которые Строка() не трогает. Запросом по длинам это в том разборе не проверяли, так что считайте это предсказанием: найдётся среди уцелевших четырёхзначное число, и механизм описан неполно. Замера “после правки” там тоже нет, риск разрыва связей выведен из механизма. Замера под ним нет.
Короткая версия этой истории, с акцентом на то, почему порча жила годами, вышла на Инфостарте.
Этот же символ при переезде на PostgreSQL
У символа 160 есть и обратная сторона. Здесь он делает одинаковые на вид строки разными. При переносе базы на PostgreSQL он делает разные строки одинаковыми: сравнение через библиотеку ICU считает неразрывный пробел равным обычному, и уникальный индекс перестаёт строиться. Этот случай целиком разобран в статье о миграции 1С на PostgreSQL, а значения, которые схлопнутся при переезде, заранее показывает проверка базы перед миграцией на PostgreSQL. Сколько подобного мусора накопилось в справочниках вообще, отвечает чек-ап чистоты базы.
Если сопоставление в загрузке или обмене теряет записи и непонятно почему, разберём ключи и правила сопоставления в рамках настройки обмена данными 1С: найдём все места сборки, посчитаем, что сломается при правке, и выкатим исправление обеими сторонами сразу.