2
votes

Given a string of Key:Value pairs, I want to create a lookup hash but with lowercase values for the keys. I can do so with this code

my $a="KEY1|Value1|kEy2|Value2|KeY3|Value3";
my @a = split '\|', $a;
my %b = map { $a[$_] = (  !($_ % 2) ? lc($a[$_]) : $a[$_])  } 0 .. $#a ;

The resulting Hash would look like this Dumper output:

$VAR1 = {
          'key3' => 'Value3',
          'key2' => 'Value2',
          'key1' => 'Value1'
        };

Would it be possible to directly create hash %b without using temporary array @a or is there a more efficient way to achieve the same result?

Edit: I forgot to mention that I cannot use external modules for this. It needs to be basic Perl.

6

6 Answers

5
votes

You can use pairmap from List::Util to do this without an intermediate array at all.

use strict;
use warnings;
use List::Util 1.29 'pairmap';
my $str="KEY1|Value1|kEy2|Value2|KeY3|Value3";
my %hash = pairmap { lc($a) => $b } split /\|/, $str;

Note: you should never use $a or $b outside of sort (or List::Util pair function) blocks. They are special global variables for sort, and just declaring my $a in a scope can break all sorts (and List::Util pair functions) in that scope. An easy solution is to immediately replace them with $x and $y whenever you find yourself starting to use them as example variables.

2
votes

Since the key-value pair has to be around the | you can use a regex

my $v = "KEY1|Value1|kEy2|Value2|KeY3|Value3";

my %h = split /\|/, $v =~ s/([^|]+) \| ([^|]+)/lc($1).q(|).$2/xger;
1
votes
use strict;
use warnings;

use Data::Dumper;

my $i;
my %hash = map { $i++ % 2 ? $_ : lc } split(/\|/, 'KEY1|Value1|kEy2|Value2|KeY3|Value3');

print Dumper(\%hash);

Output:

$VAR1 = {
          'key1' => 'Value1',
          'key2' => 'Value2',
          'key3' => 'Value3'
        };
1
votes

For fun, here are two additional approaches.

A cheaper one than the original (since the elements are aliased rather than copied into @_):

my %hash = sub { map { $_ % 2 ? $_[$_] : lc($_[$_]) } 0..$#_ }->( ... );

A more expensive one than the original:

my %hash = ...;
@hash{ map lc, keys(%hash) } = delete( @hash{ keys(%hash) } );
1
votes

More possible solutions using regexes to do all the work, but not very pretty unless you really like regex:

use strict;
use warnings;
my $str="KEY1|Value1|kEy2|Value2|KeY3|Value3";
my %hash;
my $copy = $str;
$hash{lc $1} = $2 while $copy =~ s/^([^|]*)\|([^|]*)\|?//;

use strict;
use warnings;
my $str="KEY1|Value1|kEy2|Value2|KeY3|Value3";
my %hash;
$hash{lc $1} = $2 while $str =~ m/\G([^|]*)\|([^|]*)\|?/g;

use strict;
use warnings;
my $str="KEY1|Value1|kEy2|Value2|KeY3|Value3";
my %hash = map { my ($k, $v) = split /\|/, $_, 2; (lc($k) => $v) }
  $str =~ m/([^|]*\|[^|]*)\|?/g;
0
votes

Here's a solution that avoids mutating the input string, constructing a new string of the same length as the input string, or creating an intermediate array in memory.

The solution here changes the split into looping over a match statement.

#! /usr/bin/env perl

use strict;
use warnings;
use Data::Dumper;

my $a="KEY1|Value1|kEy2|Value2|KeY3|Value3";

sub normalize_alist_opt {
  my ($input) = @_;

  my %c;
  my $last_key;
  while ($input =~ m/([^|]*(\||\z)?)/g) {
    my $s = $1;
    next unless $s ne '';
    $s =~ s/\|\z//g;
    if (defined $last_key) {
      $c{ lc($last_key) } = $s;
      $last_key = undef;
    } else {
      $last_key = $s;
    }
  }

  return \%c;
}

print Dumper(normalize_alist_opt($a));

A potential solution that operates over the split directly. Perl might recognize and optimize the special case. Although based on discussions here and here, I'm not sure.

sub normalize_alist {
  my ($input) = @_;

  my %c;
  my $last_key;
  foreach my $s (split /\|/, $input) {
    if (defined $last_key) {
      $c{ lc($last_key) } = $s;
      $last_key = undef;
    } else {
      $last_key = $s;
    }
  }

  return \%c;
}