Как удалить [sub] хеш на основе ключей / значений другого хеша? - PullRequest
4 голосов
/ 03 апреля 2010

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

, например

my %hash1 = ( 
        test1 => { inner1 => { more => "alpha", evenmore => "beta" } },
        test2 => { inner2 => { more => "charlie", somethingelse => "delta" } },
        test3 => { inner9999 => { ohlookmore => "golf", somethingelse => "foxtrot" } }
    );

my %hash2 = (
        major=> { test2 => "inner2",
              test3 => "inner3" }  );

Что я хотел бы сделать, так это удалить весь хэш в hash1, если он не существует в качестве ключа / значения в hash2 {major}, желательно без модулей. Информация, содержащаяся в «innerX», не имеет значения, ее просто нужно оставить в покое (если только не будет удален субхеш, он может исчезнуть).

В приведенном выше примере после этой операции hash1 будет выглядеть следующим образом:

my %hash1 = ( 
        test2 => { inner2 => { more => "charlie", somethingelse => "delta" } },
        );

Удаляет hash1 {test1} и hash1 {test3}, потому что они ничего не соответствуют в hash2.

Вот то, что я сейчас пробовал, но это не работает. И это, вероятно, не самый безопасный способ сделать, так как я перебираю хэш, пытаясь удалить его. Тем не менее, я удаляю на каждом, который должен быть в порядке?

Это была моя попытка сделать это, однако Perl жалуется на:

Невозможно использовать строку ("inner1") в качестве ссылки HASH, когда используются "строгие ссылки" на

while(my ($test, $inner) = each %hash1)
{
    if(exists $hash2{major}{$test}{$inner})
    {
        print "$test($inner) is in exists.\n";
    }
    else
    {
        print "Looks like $test($inner) does not exist, REMOVING.\n";
       #not to sure if $inner is needed to remove the whole entry
         delete ($hash1{$test}{$inner});
    } 
}

Ответы [ 4 ]

5 голосов
/ 03 апреля 2010

Вы были близки. Помните, что $hash2{major}{$test} - это скаляр, а не ссылка на хеш.

#! /usr/bin/perl

use strict;
use warnings;

my %hash1 = ( 
  test1 => { inner1 => { more => "alpha", evenmore => "beta" } },
  test2 => { inner2 => { more => "charlie", somethingelse => "delta" } },
  test3 => { inner9999 => { ohlookmore => "golf", somethingelse => "foxtrot" } }
);

my %hash2 = (
  major => { test2 => "inner2",
             test3 => "inner3" }
);

foreach my $k (keys %hash1) {
  my $delete = 1;
  foreach my $inner (keys %{ $hash1{$k} }) {
    $delete = 0, last if exists $hash2{major}{$k} &&
                                $hash2{major}{$k} eq $inner;
  }
  delete $hash1{$k} if $delete;
}

use Data::Dumper;
$Data::Dumper::Indent = 1;
print Dumper \%hash1;

Строка, начинающаяся с $delete = 0, ..., немного неприятна. Это эквивалентно $delete = 0; last; в другом условном выражении, но оно уже было вложено дважды. Не желая строить матрешку , я использовал модификатор оператора , но, как следует из названия, он модифицирует один оператор.

Вот где Оператор запятой Perl входит:

Binary , - оператор запятой. В скалярном контексте он оценивает свой левый аргумент, отбрасывает это значение, затем оценивает свой правый аргумент и возвращает это значение. Это так же, как оператор запятой C.

В этом случае левый аргумент - это выражение $delete = 0, а правый аргумент - last.

Условное выражение может показаться ненужным, но

... if $hash2{major}{$k} eq $inner;

выдает предупреждения с неопределенным значением при проверке тестов, не упомянутых в %hash2 (например, test1 / inner1). Использование

.. if $hash2{major}{$k} && $hash2{major}{$k} eq $inner;

неправильно удалит тест, упомянутый в %hash2, если его «внутреннее имя» будет ложным значением, таким как строка "0". Да, использование exists здесь может быть излишне суетливым, но, не зная ваших реальных хеш-ключей, я выбрал консервативный маршрут.

Выход:

$VAR1 = {
  'test2' => {
    'inner2' => {
      'somethingelse' => 'delta',
      'more' => 'charlie'
    }
  }
};

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

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

    while (($key, $value) = each %hash) {
      print $key, "\n";
      delete $hash{$key};   # This is safe
    }

Обновление: Поиск хэшей, как если бы они были массивами (поразите своих друзей-ботаников из CS, говоря «… линейно, а не логарифмически»), является красным флагом, и приведенный выше код делает именно это. Лучший подход, который оказывается похожим на ответ Пенфолда, это

%hash1 = map +($_ => $hash1{$_}),
         grep exists $hash2{major}{$_} &&
              exists $hash1{$_}{ $hash2{major}{$_} },
         keys %hash1;

В хорошем декларативном стиле он описывает желаемое содержимое %hash1, а именно

  1. ключи первого уровня %hash1 должны быть упомянуты в $hash2{major} и
  2. значение в $hash2{major}, соответствующее каждому ключу первого уровня, само должно быть подключом этого ключа обратно в %hash1
4 голосов
/ 03 апреля 2010

Вы можете сделать это как однострочник, все потому, что delete () будет принимать массив ключей. Это не так просто, как я думал сначала, но теперь я правильно прочитал проблему ...

delete @hash1{ 
        grep(
            !(
              exists($hash2{major}->{$_}) 
                && 
              exists( $hash1{$_}->{ $hash2{major}->{$_} } )
            ),
            keys %hash1
        )
    };
1 голос
/ 03 апреля 2010
# This is the actual hash we want to iterate over.
my $keepers = $hash2{major};

%hash1 = map { $_ => $hash1{$_} }  # existing key and hash contents in %hash1
             grep { exists $keepers->{$_} and               # key there?
                    exists $hash1{$_}->{ $keepers->{$_} } } # key in hash there?
             (keys %hash1);        # All the keys we might care about

Это работает, потому что мы по сути разрабатываем списки вещей, которые мы хотим / не хотим в три независимых этапа:

  1. Вызов ключей получает все ключи, находящиеся в хеш-1, за один шаг.
  2. grep генерирует (как один шаг) список ключей, соответствующих нашему критерию.
  3. Карта генерирует (как один шаг) набор ключей и значений, которые нам нужны.

Таким образом, мы никогда не изменяем основной хеш, пока не будем готовы это сделать. Если% hash1 содержит много ключей, мы собираемся использовать много памяти. Если вы беспокоитесь об этом, вы бы сделали что-то вроде этого:

# Initialization as before ...

use File::Temp qw(tempfile);

my ($fh, $file) = tempfile();
my $keepers = $hash2{major};

print $fh "$_\n" for (keys %hash1);
close $fh;
open $fh, "<", $file or die "can't reopen tempfile $file: $!\n";
while ( defined ($_ = <$fh>) ) {
  chomp;
  delete $hash1{$_} 
    unless exists $keepers->{$_} and 
           exists $hash1{$_}->{ $keepers->{$_} }; 
}

Этот работает, потому что мы перебираем не хеш, а сохраненную копию его ключей.

1 голос
/ 03 апреля 2010

Вот как я бы это сделал: (Третья попытка - прелесть)

foreach ( map { [ $_ => $hash2{major}{$_} ] } keys %hash1 ) { 
    my ( $key, $value ) = @$_;
    if ( defined $value and my $new_value = $hash1{$key}{$value} ) { 
        $hash1{$key} = $new_value;
    }
    else { 
        delete $hash1{$key};
    }
}
...