Как вычислить факториал числа в 1С без использования явных операторов цикла?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Мы столкнулись с интересной и нетривиальной задачей: как рассчитать факториал числа в среде 1С:Предприятие, полностью избегая при этом явных операторов цикла, таких как Для или Пока. Хотя циклы являются фундаментальной частью любого языка программирования, поиск альтернативных подходов может быть полезен для демонстрации гибкости платформы, решения специфических задач или просто для изучения ее возможностей. Разберем несколько методов, которые позволяют достичь этой цели, а также обсудим их особенности и ограничения.

1. Использование запросов 1С для генерации последовательности и динамическое выполнение строки

Один из наиболее изобретательных способов обойти явные циклы в коде — это делегировать процесс генерации числовой последовательности механизму запросов 1С, а затем использовать встроенную функцию Выполнить для произведения вычислений. Этот метод позволяет создать необходимую последовательность чисел (от 1 до N) с помощью запроса, а затем "собрать" их в строку с операторами умножения, которую платформа затем интерпретирует и выполняет как программный код.

Рассмотрим шаги реализации:

  1. Генерация числовой последовательности с помощью запроса. Нам нужно получить набор чисел от 1 до size. Для этого можно использовать "порождающий запрос", который последовательно генерирует числа. Изначально предлагалось использовать сложную конвертацию массива в XML и обратно в таблицу значений (как в сообщении 1), но более элегантный и производительный способ — это прямой запрос, который строит числовую последовательность, объединяя результаты нескольких самосоединений.

    Разберем пример такого запроса, способного генерировать последовательности чисел, как показано в сообщении 5. Он постепенно строит числа, используя комбинации ранее сгенерированных:

    
    &НаСервереБезКонтекста
    Функция ФакториалБезЦикла(size=100) Экспорт
        Перем f;
        Если size = 0 Тогда
            Возврат 1;
        КонецЕсли;
        
        Запрос1 = Новый Запрос(
            " |ВЫБРАТЬ"
            " | 0 КАК Х"
            " |ПОМЕСТИТЬ Регистр1"
            " |"
            " |ОБЪЕДИНИТЬ"
            " |"
            " |ВЫБРАТЬ"
            " | 1"
            " |;
            " |
            " |////////////////////////////////////////////////////////////­////////////////////"
            " |ВЫБРАТЬ"
            " | Младшие.Х + 2 * Старшие.Х КАК Х"
            " |ПОМЕСТИТЬ Регистр2"
            " |ИЗ"
            " | Регистр1 КАК Младшие,"
            " | Регистр1 КАК Старшие"
            " |;
            " |
            " |////////////////////////////////////////////////////////////­////////////////////"
            " |ВЫБРАТЬ"
            " | Младшие.Х + 4 * Старшие.Х КАК Х"
            " |ПОМЕСТИТЬ Регистр4"
            " |ИЗ"
            " | Регистр2 КАК Младшие,"
            " | Регистр2 КАК Старшие"
            " |;
            " |
            " |////////////////////////////////////////////////////////////­////////////////////"
            " |ВЫБРАТЬ ПЕРВЫЕ " + size + "
            " | Младшие.Х + 16 * Старшие.Х КАК Х"
            " |ИЗ"
            " | Регистр4 КАК Младшие,"
            " | Регистр4 КАК Старшие"
            " |ГДЕ"
            " | Младшие.Х + 16 * Старшие.Х > 0"
            " |
            " |УПОРЯДОЧИТЬ ПО"
            " | Х");
        arr = Запрос1.Выполнить().Выгрузить().ВыгрузитьКолонку("Х");
        // ... далее код для Выполнить
    

    Этот запрос создает временные таблицы Регистр1, Регистр2, Регистр4, каждая из которых содержит все больше чисел. Наконец, последний блок ВЫБРАТЬ объединяет их, генерируя последовательность до size. После выполнения запроса мы получаем массив arr, содержащий числа от 1 до size.

  2. Формирование строки для выполнения. Полученный массив чисел (например, [1, 2, 3] для size = 3) нам необходимо превратить в строку вида "1*2*3". Для этого в 1С предусмотрена функция СтрСоединить.

    
    Выполнить("f=" + СтрСоединить(arr, "*"));
    Возврат f;
    

    Важно отметить, что иногда функция СтрСоединить может добавлять неразрывные пробелы (Символ(160)), которые могут помешать корректному выполнению строки. Поэтому рекомендуется использовать дополнительную замену, как предложено в сообщении 9:

    Выполнить("f=" + СтрЗаменить(СтрСоединить(arr, "*"), Символ(160), ""));

    Эта строка эффективно вычисляет произведение всех чисел в массиве, присваивая результат переменной f.

Преимущества метода:

Ограничения и недостатки:

2. Рекурсивное решение

Хотя автор вопроса просил рассмотреть варианты "кроме рекурсии", это классический и элегантный способ вычисления факториала без явного использования циклов. Рекурсия подразумевает, что функция вызывает саму себя с измененным входным параметром до достижения базового условия.

Рассмотрим пример рекурсивной функции:


Функция ФакториалРекурсия(i)
    Если i <= 1 Тогда
        Возврат 1;
    Иначе
        Возврат i * ФакториалРекурсия(i-1);
    КонецЕсли;
КонецФункции // ФакториалРекурсия
// Можно записать и в одну строку, как было предложено:
// Возврат (?(i<=1, 1, i * ФакториалРекурсия(i-1)));

В этом примере:

  1. Базовый случай: Если i равно 0 или 1, функция возвращает 1 (факториал 0! и 1! равен 1).
  2. Рекурсивный случай: Если i больше 1, функция возвращает произведение i на факториал (i-1), вызывая саму себя.

Ограничения рекурсии:

Важнейшим аспектом при использовании рекурсии является ограничение на глубину стека вызовов. Каждое рекурсивное вызов функции занимает место в стеке памяти. В 1С:Предприятии существует предел на количество вложенных вызовов, превышение которого приводит к аварийному завершению программы ("Аварийное завершение").

В обсуждении на форуме (сообщения 18-23) пользователи экспериментально выяснили, что в файловой базе глубина стека может быть около 600-700 вызовов, а в клиент-серверном варианте — до 1700+ вызовов. Это означает, что для вычисления факториала числа, значительно превышающего эти значения (например, ФакториалРекурсия(2000)), данное решение будет неприменимо.

3. Мемоизация с использованием регистра сведений

Для сценариев, когда факториалы одних и тех же чисел могут требоваться многократно, самым эффективным подходом является мемоизация — хранение результатов ранее выполненных вычислений. В 1С это удобно реализовать с помощью Регистра сведений.

Принцип работы:

  1. Создаем Регистр сведений, например, "ФакториалыЧисел".
  2. Измерение: "Число" (тип Число).
  3. Ресурс: "ЗначениеФакториала" (тип Число с необходимой точностью).
  4. При первом запросе к функции факториала, проверяем, есть ли уже запись для данного числа в регистре.
  5. Если запись найдена, просто возвращаем сохраненное значение.
  6. Если запись отсутствует, вычисляем факториал (используя цикл, рекурсию для небольших значений, или запрос с Выполнить) и записываем результат в регистр для последующего использования.

В сообщении 25 предлагается этот подход:


// Пример концепции
Функция ПолучитьФакториал(Число)
    // Попытка получить из регистра сведений
    Запрос = Новый Запрос;
    Запрос.Текст = "
        |ВЫБРАТЬ
        | ФакториалыЧиселСрезПоследних.ЗначениеФакториала КАК Значение
        |ИЗ
        | РегистрСведений.ФакториалыЧисел.СрезПоследних(&Дата, Число = &Число) КАК ФакториалыЧиселСрезПоследних";
    Запрос.УстановитьПараметр("Дата", ТекущаяДата());
    Запрос.УстановитьПараметр("Число", Число);
    
    Результат = Запрос.Выполнить().Выбрать();
    
    Если Результат.Следующий() Тогда
        Возврат Результат.Значение;
    Иначе
        // Если не найдено, вычисляем
        // Здесь можно использовать любой метод: рекурсию, цикл, запрос с Выполнить.
        // Для примера используем рекурсию, но для больших чисел лучше итерационный метод
        // или метод с запросом и Выполнить, если Число небольшое.
        Если Число = 0 Тогда
            ВычисленныйФакториал = 1;
        Иначе
            ВычисленныйФакториал = Число * ПолучитьФакториал(Число - 1);
        КонецЕсли;
        
        // Записываем в регистр сведений
        НаборЗаписей = РегистрыСведений.ФакториалыЧисел.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.Число.Установить(Число);
        НоваяЗапись = НаборЗаписей.Добавить();
        НоваяЗапись.Число = Число;
        НоваяЗапись.ЗначениеФакториала = ВычисленныйФакториал;
        НаборЗаписей.Записать();
        
        Возврат ВычисленныйФакториал;
    КонецЕсли;
КонецФункции

Этот подход особенно ценен, когда значения факториалов нужны для различных отчетов или расчетов, которые выполняются регулярно.

Преимущества:

Ограничения:

Особенности работы с большими числами в 1С

Важный аспект, который обсуждался на форуме (сообщение 16), — это способность платформы 1С:Предприятие работать с числами произвольной точности. Это означает, что если факториал числа 100 (которое является числом со 158 знаками) будет вычислен корректно любым из описанных методов (например, через Выполнить или итеративно), то результат будет точным, без потери данных из-за ограничений стандартных типов данных.

Это значительно упрощает работу с факториалами больших чисел по сравнению с другими языками программирования, где часто приходится использовать специализированные библиотеки для "больших чисел" (BigInteger).

Кроме того, стоит упомянуть Формулу Стирлинга (сообщение 10), которая позволяет получить приближенное значение факториала для очень больших чисел. Хотя она не дает точного результата, для некоторых аналитических задач, где достаточно приближения, это может быть единственным практичным способом.

Заключение

Мы рассмотрели несколько подходов к вычислению факториала числа в 1С без использования явных операторов цикла. Каждый из них имеет свои преимущества и недостатки:

Выбор метода зависит от конкретной задачи, требуемой точности (для приближенных значений) и частоты использования. В любом случае, платформа 1С предоставляет достаточные инструменты для решения подобных задач, даже при наличии специфических ограничений.

← На главную