February 10, 2023

Unfold и выделение цифр из целого числа

Как выделить цифры из целого числа?

Все знают простой способ: преобразовать целое в строку и затем каждый символ строки назад преобразовать в цифру. Вот решение в виде функции:

function Digits0(a: integer): sequence of integer;
begin
  Result := a.ToString.Select(c -> c.ToDigit);
end;

begin
  var n := 234635681;
  Digits0(n).Print
end.

Но такой способ крайне непроизводителен - преобразование числа в строку и назад - крайне ресурсоёмкая операция.

С другой стороны, известен простой способ - выделение цифр с конца:

procedure Digits1(n: integer);
begin
  while n<>0 do
  begin
    Print(n mod 10);
    n := n div 10;
  end;
end;

begin
  var n := 234635681;
  Digits1(n)
end.

Увы - это процедура, и она просто выводит цифры.

Превратим ее в функцию, сохраняя цифры в списке:

function Digits2(n: integer): sequence of integer;
begin
  var L := new List<integer>;
  while n<>0 do
  begin
    L.Add(n mod 10);
    n := n div 10;
  end;
  Result := L;
end;

Такой код существенно лучше, но тратится память под список.

Обойдёмся последовательностями. Здесь нам на помощь приходят функции-генераторы последовательностей. Ключевое здесь - использование оператора yield, выбрасывающего следующий член последовательности:

function Digits3(n: integer): sequence of integer;
begin
  while n<>0 do
  begin
    yield n mod 10;
    n := n div 10;
  end;
end;

Можно даже написать рекурсивную версию данной функции:

function Digits4(n: integer): sequence of integer;
begin
  if n = 0 then exit;
  yield sequence Digits4(n div 10);
  yield n mod 10;
end;

Здесь используется оператор yield sequence, генерирующий подпоследовательность в рамках функции-генератора последовательности. Логика данной функции предельно проста: вначале генерируем подпоследовательность из всех цифр кроме последней, а затем добавляем к ней последнюю цифру. Кстати, этот способ позволяет сразу инвертировать последовательность цифр, записав их в порядке слева направо.

Посмотрим еще раз на код

  while n<>0 do
  begin
    Print(n mod 10);
    n := n div 10;
  end;

Здесь есть три основных элемента: условие продолжения цикла n<>0, генерируемый элемент последовательности n mod 10 и переход к следующему числу без последней цифры n div 10.

Всё это - элементы функции Unfold, преобразующей скаляр (в данном случае целое число) в последовательность. Приведём её реализацию:

function Unfold<T,TRes> (Self: T; generator: T -> (TRes,T)): sequence of TRes; extensionmethod;
begin
  var next := Self;
  while True do
  begin
    var res := generator(next);
    if res = nil then
      break;
    yield res[0];
    next := res[1];
  end;
end;

Посмотрим ее в действии:

n.Unfold(n -> n = 0 ? nil : (n mod 10, n div 10)).Println;

Это самый короткий универсальный способ преобразования скаляра в последовательность. Здесь n = 0 возвращает nil - признак конца последовательности, а иначе возвращается кортеж из двух элементов: член генерируемой последовательности n mod 10 и число n div 10 без последней цифры.

Если написать еще одну элементарную функцию-негенератор

function Iterate<T>(first: T; next: T->T): sequence of T;
begin
  yield first;
  while True do
  begin
    first := next(first);
    yield first;
  end;
end;

которая генерирует бесконечную рекуррентную последовательность с функцией next перехода к следующему элементу, то можно представить алгоритм выделения цифр из числа цепочкой методов - как мы любим:

var digits := Iterate(n,n -> n div 10)
  .TakeWhile(n -> n <> 0)
  .Select(n -> n mod 10);

Здесь налицо и переход к следующему числу n div 10 и условие продолжения n<>0 и выделение последней цифры n mod 10

Заключение

Существует много сложных способов выделения цифр из целого числа :)

А если серьёзно - Unfold и Iterate - базовые примитивы для последовательностей, которые можно использовать не только здесь, но и в других задачах.

Мыслите функционально!