3

Consider the following HoH:

$h = {
    a => {
           1 => x
    },
    b => {
           2 => y
    },
    ...
}

Is there a way to check whether a hash key exists on the second nested level without calling keys(%$h)? For example, I want to say something like:

if ( exists($h->{*}->{1}) ) { ...

(I realize you can't use * as a hash key wildcard, but you get the idea...)

I'm trying to avoid using keys() because it will reset the hash iterator and I am iterating over $h in a loop using:

while ( (my ($key, $value) = each %$h) ) {
    ...
}

The closest language construct I could find is the smart match operator (~~) mentioned here (and no mention in the perlref perldoc), but even if ~~ was available in the version of Perl I'm constrained to using (5.8.4), from what I can tell it wouldn't work in this case.

If it can't be done I suppose I'll copy the keys into an array or hash before entering my while loop (which is how I started), but I was hoping to avoid the overhead.

Community
  • 1
  • 1
MisterEd
  • 1,725
  • 1
  • 14
  • 15
  • If you're going to copy the list of keys, you no longer need to use the (unfortunately dangerous) `each %$h` and can do a foreach loop instead. How big is this hash? – Schwern Nov 09 '11 at 01:19
  • @Schwern: about 24,000 keys (sum of all nested hashes). It contains all privilege assignments for a Sybase database. I realize I can just avoid `each`, or copy the keys, or find some other implementation. Just thought I'd pose the question to see if it was possible. – MisterEd Nov 09 '11 at 01:30
  • 1
    I think it's possible to do it efficiently, but would require changing the interface as well as XS code to retain the hash iterator after each iteration in case it gets reset. perl5i's `each()` method has such an interface, but it does not defend against your case. It would like to. https://github.com/schwern/perl5i/issues/210 – Schwern Nov 12 '11 at 02:09

5 Answers5

3

Not really. If you need to do that, I think I'd create a merged hash listing all the second level keys (before starting your main loop):

my $h = {
    a => {
           1 => 'x'
    },
    b => {
           2 => 'y'
    },
};

my %all = map { %$_ } values %$h;

Then your exists($h->{*}->{1}) becomes exists($all{1}). Of course, this won't work if you're modifying the second-level hashes inside the loop (unless you update %all appropriately). The code also assumes that all values in $h are hashrefs, but that would be easy to fix if necessary.

cjm
  • 61,471
  • 9
  • 126
  • 175
  • Thanks, that's how I started, but I removed the extra hash because I already had the data in a hash and it seemed wasteful; not to mention the problem you mentioned of having another structure to update if things change. – MisterEd Nov 09 '11 at 00:03
2

No. each uses the hash's iterator, and you cannot iterate over a hash without using its iterator, not even in the C API. (That means smart match wouldn't help anyway.)

Since each hash has its own iterator, you must be calling keys on the same hash that you are already iterating over using each to run into this problem. Since you have no problem calling keys on that hash, could you just simply use keys instead of each? Or maybe call keys once, store the result, then iterate over the stored keys?

ikegami
  • 367,544
  • 15
  • 269
  • 518
  • Thanks, this is the definitive yes-or-no answer I was looking for. I'll just go back to putting the keys in a hash like you and @cjm suggested (and like I was doing in the first place). – MisterEd Nov 09 '11 at 15:53
1

You will almost certainly find that the 'overhead' of aggregating the second-level hashes is less than that of any other solution. A simple hash lookup is far faster than iterating over the entire data structure every time you want to make the check.

Borodin
  • 126,100
  • 9
  • 70
  • 144
0

are you trying to do this without any while loop? You can test for existence in a hash just by referencing it, without generating an error

while (  my ($key, $value) = each %{$h} ) {
    if ($value->{1}) { .. } 

}
ryansstack
  • 1,396
  • 1
  • 15
  • 33
  • He's not trying to determine whether the _current_ hashref has that key. He's trying to determine whether _any_ of the hashrefs in `$h` has that key. – cjm Nov 08 '11 at 23:59
  • don't you love perl? my $yes; foreach (sort { defined $h->{$b}->{1} ? 1:0 } keys %$h) { $yes = 1 if (defined $h->{$_}->{1}; last; } – ryansstack Nov 09 '11 at 00:15
  • but that use of keys resets the iterator – ysth Nov 09 '11 at 03:52
-1

Why not do this in Sybase itself instead of Perl?

You are trying to do a set operation which is what Sybase is built to do in the first place.

Assuming you retrieved the data from table with columns "key1", "key2", "valye" as "select *", simply do:

-- Make sure mytable has index on key1
SELECT key1
FRIN mytable t1
WHERE NOT EXISTS (
    SELECT 1 FROM mytable t2
    WHERE t1.key1=t2.key1
    AND t2.key2 = 1
)

-----------
-- OR
-----------

SELECT DISTINCT key1
INTO   #t
FROM   mytable

CREATE INDEX idx1_t on #t (key1)

DELETE #t
FROM   mytable
WHERE #t.key1=mytable.key1
AND   mytable.key2 = 1

SELECT key1 from #t    

Either query returns a list of 1st level keys that don't have key2 of 1

DVK
  • 126,886
  • 32
  • 213
  • 327
  • 1
    Because the question is about Perl. Sybase is totally irrelevant. – dolmen Nov 09 '11 at 13:54
  • @dolmen - google "XY problem". There are usually no "Perl problems". There are "Business problem to be solved by the best tools". Sybase seems to be one of the available tools based on OP's comments. – DVK Nov 09 '11 at 16:09