1

I have below hash structure.

$VAR1 = {
    'USA' => {
        'Alabama' => {
            'ISO3'    => 'ISO3:3166-2:US',
            'ISO2'    => 'ISO2:4166-23:US',
            'UNI'     => 'UNIABR-A',
            'UNDP'    => 'UNDP-ZXC-1',
            'FAOSTAT' => 'STAT20.98',
            'GAUL'    => 'UL-SD-20/40'
        },
        'Washington' => {
            'ISO3'    => 'ISO3:40-166-2:US',
            'ISO2'    => 'ISO2:30-23:US',
            'UNI'     => 'UNIISO-B',
            'UNDP'    => 'UNDP-YXC-2',
            'FAOSTAT' => 'STAT30.98.78',
            'GAUL'    => 'UL-SD-30/60'
        }
    }
};

What i would like to achieve is to iterate through the above hash and get the statename and country name for value inside hash "ISO2:4166-23:US". what i have tried to do is:

I can get the required output with the below code.

my $find = "ISO2:4166-23:US";
my $statename;

while ( my ($country, $states) = each (%$VAR1) ) {
    while (my ($states, $otherkeys) = each (%$states) ) {
        while (my ($otherkeys, $value) = each %$otherkeys) {
            $statename = $states if ($value eq $find);
        }
    }
}

print "State name for value [$find] is :: $statename \n"; ### Output : Alabama

Is there any way I can get -

Get the top-level key "USA" and second-level key "Alabama" if $value is equal to ISO2:4166-23:US. What i know is the value inside hash for which I need to get the output, key corresponding to my search value doesn't matter.

with one-liner grep command from above hash?

Any pointers in the right direction would be useful. Thanks.

2
  • Why are you asking this question? It feels sort of like an XY-problem. There is nothing wrong with using a loop. Commented Oct 7, 2020 at 15:19
  • This would require a chain of maps and greps that would probably be less legible than using while loops. And you're not gaining anything, since grep and map are also loops. Commented Oct 7, 2020 at 15:26

2 Answers 2

1

Your variables are poorly named. Reusing $states for the state name? ouch.

my $find = "ISO2:4166-23:US";

my $found_state_name;
while ( my ($country_name, $states) = each(%$VAR1) ) {
    while (my ($state_name, $state) = each(%$states) ) {
        while ( my ($key, $value) = each(%$state) ) {
            if ($value eq $find) {
                $found_state_name = $state_name;
            }
        }
    }
}

Now, it would be nice to stop searching as soon as you a result is found. We can't do that while still using each(cause it'll screw up later keys/values/each on those hashes).

my $find = "ISO2:4166-23:US";

my $found_state_name;
FIND:
for my $country_name (keys(%$VAR1)) {
    my $country = $VAR1->{$country_name};
    for my $state_name (keys(%$country)) {
        my $state = $country->{$state_name};
        for my $key (keys(%$state)) {
            if ($state->{$key} eq $find) {
                $found_state_name = $state_name;
                last FIND;
            }
        }
    }
}

We never use $country_name or $key except to get the value.

my $find = "ISO2:4166-23:US";

my $found_state_name;
FIND:
for my $states (values(%$VAR1)) {
    for my $state_name (keys(%$states)) {
        my $state = $country->{$state_name};
        for my $value (values(%$state)) {
            if ($value eq $find) {
                $found_state_name = $state_name;
                last FIND;
            }
        }
    }
}

If you know you're looking for an ISO2 value, this simplifies to the following:

my $find = "ISO2:4166-23:US";

my $found_state_name;
FIND:
for my $states (values(%$VAR1)) {
    for my $state_name (keys(%$states)) {
        my $state = $states->{$state_name};
        if ($state->{ISO2} eq $find) {
            $found_state_name = $state_name;
            last FIND;
        }
    }
}

You want to use grep, eh? Since you want a state name as a result, we need to grep a list of state names.

my @state_names = ...;

my ($found_state_name) =
   grep { ... }
      @state_names;

We can obtain the list of state name using

my @state_names =
   map { keys(%$_) }
      values(%$VAR1);

But that's not quite enough to perform the check. (For now, I'm going to assume that only the ISO2 property needs to be checked.)

my @state_names =
   map { keys(%$_) }
      values(%$VAR1);

my ($found_state_name) =
   grep { $VAR1->{???}{$_}{ISO2} eq $find }
      @state_names;

There are two solutions. You can work with country-state pairs.

my @country_state_name_pairs =;
   map {
      my $country_name = $_;
      map { [ $country_name, $_ ] }
         keys(%{ $VAR1->{$country_name} )
   }
      keys(%$VAR1);

my ($found_state_name) =
   map { $_->[1] }
      grep {
         my ($country_name, $state_name) = @$_;
         $VAR1->{$country_name}{$state_name}{ISO2} eq $find
      }
         @country_state_name_pairs;

Or you can create a flat lists of states and search that.

my @states_with_name = 
   map { [ $_, $VAR1->{$_} ] }
      values(%$VAR1);

my ($found_state_name) =
   map { $_->[0] }
      grep { $_->[1]{ISO2} eq $find }
         @states_with_name;

Noting stops us from merging the two statements.

my ($found_state_name) =
   map { $_->[0] }                      # Get the state name.
      grep { $_->[1]{ISO2} eq $find }   # Filter out undesireable states.
         map { [ $_, $VAR1->{$_} ] }    # $state_name => [ $state_name, $state ]
            values(%$VAR1);             # Get the countries.

This last one isn't too bad!


Finally, there are two ways to modify each of the above to search all the fields instead of just ISO2). (I'm going to only the modifications to the latter of the above two solutions.)

my ($found_state_name) =
   map { $_->[0] }                      # Get the state name.
      grep {                            # Filter out undesireable states.
         grep { $_ eq $find }           # Filter out undesireable properties of the state.
            values(%{ $_->[1] })        # Get the state's property values.
      }
         map { [ $_, $VAR1->{$_} ] }    # $state_name => [ $state_name, $state ]
            values(%$VAR1);             # Get the countries.

or

my ($found_state_name) =
   map { $_->[0] }                        # Get the state name.
      grep { $_->[1] eq $find }           # Filter out undesireable states.
         map {                            # $state_name => multiple [ $state_name, $value ]
            my $state_name = $_;
            map { [ $state_name, $_ ] }   # value => [ $state_name, $value ]
               values(%{ $VAR1->{$_} )    # Get the state's property values.
         }
            values(%$VAR1);               # Get the countries.

These aren't readable. They are best avoided.


Finally, if you are going to perform many searches based on ISO2, it would be best if you organized your data in terms of ISO2.

my %by_iso2 = (
   'ISO2:4166-23:US' => {
      country_name => 'USA',
      state_name   => 'Alabama',
      ISO2         => 'ISO2:4166-23:US',
      ISO3         => 'ISO3:3166-2:US',
      ...
   },
   'ISO2:4166-23:US' => {
      country_name => 'USA',
      state_name   => 'Washington',
      ISO2         => 'ISO2:30-23:US',
      ISO3         => 'ISO3:40-166-2:US',
      ...
   },
   ...
);
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for your time and providing the explanation with suggested approaches and correcting the mistakes in approach taken by me.
1

No. Hashes are one-directional, so you have to loop over all the values to find one you are searching for.

It sounds like what you really want is some sort of database and that a hash is the wrong tool for your job.

You can build your hash the other way around, using the ISO2 as the key, e.g.

$VAR1 = { 
    "ISO2:4166-23:US" => { Country => 'USA', State => 'Alabama' },
    "ISO2:4166-23:US" => { Country => 'USA', State => 'Washington' }
}

If you are planning to do these lookups a lot, it might be worth the while. Doing that can be automated too. Using a similar loop to build a new hash to use as lookup.

Speed-wise, there is nothing wrong with looping over all the hash keys. The difference between direct lookup and looping will be negligible, unless you have a truly huge hash.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.