Next Previous Contents

Unix Review Column 35

Randal Schwartz

Декабрь 2000

[предполагаемый заголовок: В чем отличие?]

Часть общих задач программирования связана с изменяющимися вещами. И вещи на самом деле изменяются, и нам нужно знать как они изменяются.

Hапример, если у нас есть список данных:


  @one = qw(a b c d e f g);

и позднее, мы глянем на него снова, мы увидим отличный набор данных:


  @two = qw(b c e h i j);

Как мы можем определить что здесь новое, что старое и что удалено?

Мы могли бы сделать это применяя грубую силу:


  @one = qw(a b c d e f g);
  @two = qw(b c e h i j);
  foreach $one (@one) {
    if (grep $one eq $_, @two) {
      print "$one находится в обоих списках\n";
    } else {
      print "$one был удален\n";
    }
  }
  foreach $two (@two) {
    unless (grep $two eq $_, @one) {
       print "$two был добавлен\n";
    }
  }

И это действительно дает нам соответствующий ответ:


  a был удален
  b находится в обоих списках
  c находится в обоих списках
  d был удален
  e находится в обоих списках
  f был удален
  g был удален
  h был добавлен
  i был добавлен
  j был добавлен

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

Справочная страница perlfaq4 предлагает подход к данной задаче, давая примерно следующее решение:


  @union = @intersection = @difference = ();
  %count = ();
  foreach $element (@one, @two) { $count{$element}++ }
  foreach $element (keys %count) {
      push @union, $element;
      push @{ $count{$element} > 1 ? \@intersection : \@difference }, $element;
  }

с предупреждением, что мы предполагаем что каждый из элементов списка встречается только один раз внутри каждого из списков. Хотя это работает с нашими данными, но мы встретимся с проблемами при работе с более общими данными. Однако, немного изменив программу мы сможем обрабатывать даже дублирующиеся элементы в каждом из списков:


  @one = qw(a a a a b c d e f g);
  @two = qw(b c e h i i i i j);
  my %tracker = ();
  $tracker{$_} .= 1 for @one;
  $tracker{$_} .= 2 for @two;
  for (sort keys %tracker) {
    if ($tracker{$_} !~ /1/) {
      print "$_ has been added\n";
    } elsif ($tracker{$_} !~ /2/) {
      print "$_ has been deleted\n";
    } else {
      print "$_ is in both old and new\n";
    }
  }

Удача. Правильный вывод и достаточно эффективно. Если вы выполняете много подобных действий, то посмотрите на CPAN модули, чьи имена начинаются с Set::.

И мы приходим к проблеме о разнице между двумя последовательностями, в которых важен порядок следования. Великолепный модуль Algorithm::Diff имеющийся на CPAN вычисляет разумно короткий список отличий, аналогично команде diff из поставки Unix, которые описывает как преобразовать один список в другой. Существует несколько интерфейсов. Hаиболее интересным я нашел traverse_sequences, который последовательно дает мне все элементы обоих списков, но помечает их так, что я могу сказать к какому из списков (или к обоим) относится этот элемент.

Давайте взглянем на простой пример:


  use Algorithm::Diff qw(traverse_sequences);
  @one = qw(M N a b P Q c d e f V W g h);
  @two = qw(a b R S c d T U e f g h X Y);
  traverse_sequences(\@one, \@two, {
    MATCH => sub { show($one[$_[0]], $two[$_[1]]) },
    DISCARD_A => sub { show($one[$_[0]], "---") },
    DISCARD_B => sub { show("---", $two[$_[1]]) },
  });
  sub show {
    printf "%10s %10s\n", @_;
  }

Здесь имеется две заданные последовательности, хранящиеся в списках @one и @two. Используя функцию traverse_sequences, мы будем выдавать общие элементы последовательностей (используя правило MATCH), удаленные элементы (используя правило DISCARD_A), и новые элементы (используя правило DISCARD_B). Измененные данные отображаются как серии удалений, за которыми следуют серии вставок.

Правила (callbacks) определяются как ссылки на анонимный процедуры, более известные как ``coderefs''. Каждому из правил передается два параметра -- текущие индексы внутри массивов @one и @two. Поскольку это не значения, я должен взять индексы и заглянуть в соответствующий массив.

Результат выполнения выглядит следующим образом:


         M        ---
         N        ---
         a          a
         b          b
         P        ---
         Q        ---
       ---          R
       ---          S
         c          c
         d          d
       ---          T
       ---          U
         e          e
         f          f
         V        ---
         W        ---
         g          g
         h          h
       ---          X
       ---          Y

Заметьте общие последовательности элементов. Оператор printf красиво форматирует колонки.

Текстовый поколоночный вывод это хорошо, но мы можем получить более красивое оформление, если мы будем делать вывод в формате HTML. Давайте раскрасим все удаления красным, а все вставки зеленым.

Hа первый взгляд, этот алгоритм генерирует слишком много тагов font:


  use Algorithm::Diff qw(traverse_sequences);
  @one = qw(M N a b P Q c d e f V W g h);
  @two = qw(a b R S c d T U e f g h X Y);
  traverse_sequences(\@one, \@two, {
    MATCH => sub { colorshow("", $one[$_[0]]) },
    DISCARD_A => sub { colorshow("red", $one[$_[0]]) },
    DISCARD_B => sub { colorshow("green", $two[$_[1]]) },
  });
  sub colorshow {
    my $color = shift;
    my $string = shift;
    if (length $color) {
      print "<font color=$color>$string</font>\n";
    } else {
      print "$string\n";
    }
  }

Этот код генерирует корректный результат, но при этом вывод становится чрезвычайно избыточным: <font color=red>M</font> <font color=red>N</font> a b <font color=red>P</font> <font color=red>Q</font> <font color=green>R</font> <font color=green>/font c d <font color=green>T</font> <font color=green>U</font> e f <font color=red>V</font> <font color=red>W</font> g h <font color=green>X</font> <font color=green>Y</font>

Все что нам надо -- это отслеживание информации о состоянии для отслеживания того, в режиме какого цвета мы находимся:


  use Algorithm::Diff qw(traverse_sequences);
  @one = qw(M N a b P Q c d e f V W g h);
  @two = qw(a b R S c d T U e f g h X Y);
  traverse_sequences(\@one, \@two, {
    MATCH => sub { colorshow("", $one[$_[0]]) },
    DISCARD_A => sub { colorshow("red", $one[$_[0]]) },
    DISCARD_B => sub { colorshow("green", $two[$_[1]]) },
  });
  colorshow(""); # reset back to 
  BEGIN {
    my $currentcolor = "";

    sub colorshow {
      my $color = shift;
      my $string = shift;
      if ($color ne $currentcolor) {
        print "</font>\n" if length $currentcolor;
        print "<font color=$color>\n" if length $color;
        $currentcolor = $color;
      }
      if (defined $string and length $string) {
        print "$string\n";
      }
    }
  }

Здеся я отслеживаю состояние текущего цвета HTML с помощью статической переменной $currentcolor. При ее изменении я посылаю соответствующие таги окончания и начала тага font. Единственным неудобством является то, что нам необходимо выполнить заключительный вызов colorshow с бесцветным тагом для закрытия существующего начального тага. Этот вызов является безвредным если мы находимся снаружи раскрашенного региона.

И это намного лучше, давая следующий результат:


    <font color=red>
    M
    N
    </font>
    a
    b
    <font color=red>
    P
    Q
    </font>
    <font color=green>
    R
    S
    </font>
    c
    d
    <font color=green>
    T
    U
    </font>
    e
    f
    <font color=red>
    V
    W
    </font>
    g
    h
    <font color=green>
    X
    Y
    </font>

Хотя мои друзья-знатоки web могут предпочитать видеть это в таком виде:


    <span style="background: red; color: black">
    M
    N
    </span>
    a
    b
    <span style="background: red; color: black">
    P
    Q
    </span>
    <span style="background: green; color: black">
    R
    S
    </span>
    c
    d
    <span style="background: green; color: black">
    T
    U
    </span>
    e
    f
    <span style="background: red; color: black">
    V
    W
    </span>
    g
    h
    <span style="background: green; color: black">
    X
    Y
    </span>

Это совсем маленькое изменение, но я оставляю его для вам. Hемного маловато пробельных символов, до того стиля, который я люблю, но по крайней мере работа выполняется с небольшими трудозатратами.

Так, что теперь, когда кто-нибудь спросит вас ``в чем отличие?'', вы можете продемонстрировать различные способы ответа на этот вопрос. До следующей встречи, наслаждайтесь!


Next Previous Contents

Наш баннер
Вы можете установить наш баннер на своем сайте или блоге, скопировав этот код:
RSS новости