How to skip splitting for some part of the line - perl

Say I have a line lead=george wife=jane "his boy"=elroy. I want to split with space but that does not include the "his boy" part. I should be considered as one.
With normal split it is also splitting "his boy" like taking "his" as one and "boy" as second part. How to escape this
Following this i tried
split " ", $_
Just came to know that this will work
use strict; use warnings;
my $string = q(hi my name is 'john doe');
my #parts = $string =~ /'.*?'|\S+/g;
print map { "$_\n" } #parts;
But it does not looks good. Any other simple thing with split itself?

You could use Text::ParseWords for this
use Text::ParseWords;
$list = "lead=george wife=jane \"his boy\"=elroy";
#words = quotewords('\s+', 0, $list);
$i = 0;
foreach (#words) {
print "$i: <$_>\n";
$i++;
}
ouput:
0: <lead=george>
1: <wife=jane>
2: <his boy=elroy>

sub split_space {
my ( $text ) = #_;
while (
$text =~ m/
( # group ($1)
\"([^\"]+)\" # first try find something in quotes ($2)
|
(\S+?) # else minimal non-whitespace run ($3)
)
=
(\S+) # then maximum non-whitespace run ($4)
/xg
) {
my $key = defined($2) ? $2 : $3;
my $value = $4;
print( "key=$key; value=$value\n" );
}
}
split_space( 'lead=george wife=jane "his boy"=elroy' );
Outputs:
key=lead; value=george
key=wife; value=jane
key=his boy; value=elroy

PP posted a good solution. But just to make it sure, that there is a cool other way to do it, comes my solution:
my $string = q~lead=george wife=jane "his boy"=elroy~;
my #split = split / (?=")/,$string;
my #split2;
foreach my $sp (#split) {
if ($sp !~ /"/) {
push #split2, $_ foreach split / /, $sp;
} else {
push #split2,$sp;
}
}
use Data::Dumper;
print Dumper #split2;
Output:
$VAR1 = 'lead=george';
$VAR2 = 'wife=jane';
$VAR3 = '"his boy"=elroy';
I use a Lookahead here for splitting at first the parts which keys are inside quotes " ". After that, i loop through the complete array and split all other parts, which are normal key=values.

You can get the required result using a single regexp, which extract the keys and the values and put the result inside a hash table.
(\w+|"[\w ]+") will match both a single and multiple word in the key side.
The regexp captures only the key and the value, so the result of the match operation will be a list with the following content: key #1, value #1, key #2, value#2, etc.
The hash is automatically initiated with the appropriate keys and values, when the match result is assigned to it.
here is the code
my $str = 'lead=george wife=jane "hello boy"=bye hello=world';
my %hash = ($str =~ m/(?:(\w+|"[\w ]+")=(\w+)(?:\s|$))/g);
## outputs the hash content
foreach $key (keys %hash) {
print "$key => $hash{$key}\n";
}
and here is the output of this script
lead => george
wife => jane
hello => world
"hello boy" => bye

Related

Handling Nested Delimiters in perl

use strict;
use warnings;
my %result_hash = ();
my %final_hash = ();
Compare_results();
foreach my $key (sort keys %result_hash ){
print "$key \n";
print "$result_hash{$key} \n";
}
sub Compare_results
{
while ( <DATA> )
{
my($instance,$values) = split /\:/, $_;
$result_hash{$instance} = $values;
}
}
__DATA__
1:7802315095\d\d,7802315098\d\d;7802025001\d\d,7802025002\d\d,7802025003\d\ d,7802025004\d\d,7802025005\d\d,7802025006\d\d,7802025007\d\d
2:7802315095\d\d,7802025002\d\d,7802025003\d\d,7802025004\d\d,7802025005\d\d,7802025006\d\d,7802025007\d\d
Output
1
7802315095\d\d,7802315098\d\d;7802025001\d\d,7802025002\d\d,7802025003\d\d,7802025004\d\d,7802025005\d\d,7802025006\d\d,7802025007\d\d
2
7802315095\d\d,7802025002\d\d,7802025003\d\d,7802025004\d\d,7802025005\d\d,7802025006\d\d,7802025007\d\d
Iam trying to fetch value of each key and again trying to split the comma seperated value from result hash , if i find a semicolon in any value i would want to store the left and right values in separate hash keys.
Something like below
1.#split the value of result_hash{$key} again by , and see whether any chunk is seperated by ;
2. #every chunk without ; and value on left with ; should be stored in
#{$final_hash{"eto"}} = ['7802315095\d\d','7802315098\d\d','7802025002\d\d','7802025003\d\d','7802025004\d\d','7802025005\d\d','7802025006\d\d','7802025007\d\d'] ;
3.#Anything found on the right side of ; has to be stored in
#{$final_hash{"pro"}} = ['7802025001\d\d'] ;
Is there a way that i can handle everything in the subroutine? Can i make the code more simpler
Update :
I tried splitting the string in a single shot, but its just picking the values with semicolon and ignoring everything
foreach my $key (sort keys %result_hash ){
# print "$key \n";
# print "$result_hash{$key} \n";
my ($o,$t) = split(/,|;/, $result_hash{$key});
print "Left : $o \n";
print "Left : $t \n";
#push #{$final_hash{"eto"}}, $o;
#push #{$final_hash{"pro"}} ,$t;
}
}
My updated code after help
sub Compare_results
{
open my $fh, '<', 'Data_File.txt' or die $!;
# split by colon and further split by , and ; if any (done in insert_array)
my %result_hash = map { chomp; split ':', $_ } <$fh> ;
foreach ( sort { $a <=> $b } (keys %result_hash) )
{
($_ < 21)
? insert_array($result_hash{$_}, "west")
: insert_array($result_hash{$_}, "east");
}
}
sub insert_array()
{
my ($val,$key) = #_;
foreach my $field (split ',', $val)
{
$field =~ s/^\s+|\s+$//g; # / turn off editor coloring
if ($field !~ /;/) {
push #{ $file_data{"pto"}{$key} }, $field ;
}
else {
my ($left, $right) = split ';', $field;
push #{$file_data{"pto"}{$key}}, $left if($left ne '') ;
push #{$file_data{"ero"}{$key}}, $right if($right ne '') ;
}
}
}
Thanks
Update Added a two-pass regex, at the end
Just proceed systematically, analyze the string step by step. The fact that you need consecutive splits and a particular separation rule makes it unwieldy to do in one shot. Better have a clear method than a monster statement.
use warnings 'all';
use strict;
use feature 'say';
my (%result_hash, %final_hash);
Compare_results();
say "$_ => $result_hash{$_}" for sort keys %result_hash;
say '---';
say "$_ => [ #{$final_hash{$_}} ]" for sort keys %final_hash;
sub Compare_results
{
%result_hash = map { chomp; split ':', $_ } <DATA>;
my (#eto, #pro);
foreach my $val (values %result_hash)
{
foreach my $field (split ',', $val)
{
if ($field !~ /;/) { push #eto, $field }
else {
my ($left, $right) = split ';', $field;
push #eto, $left;
push #pro, $right;
}
}
}
$final_hash{eto} = \#eto;
$final_hash{pro} = \#pro;
return 1; # but add checks above
}
There are some inefficiencies here, and no error checking, but the method is straightforward. If your input is anything but smallish please change the above to process line by line, what you clearly know how to do. It prints
1 => ... (what you have in the question)
---
eto => [ 7802315095\d\d 7802315098\d\d 7802025002\d\d 7802025003\d\ d ...
pro => [ 7802025001\d\d ]
Note that your data does have one loose \d\ d.
We don't need to build the whole hash %result_hash for this but only need to pick the part of the line after :. I left the hash in since it is declared global so you may want to have it around. If it in fact isn't needed on its own this simplifies
sub Compare_results {
my (#eto, #pro);
while (<DATA>) {
my ($val) = /:(.*)/;
foreach my $field (split ',', $val)
# ... same
}
# assign to %final_hash, return from sub
}
Thanks to ikegami for comments.
Just for the curiosity's sake, here it is in two passes with regex
sub compare_rx {
my #data = map { (split ':', $_)[1] } <DATA>;
$final_hash{eto} = [ map { /([^,;]+)/g } #data ];
$final_hash{pro} = [ map { /;([^,;]+)/g } #data ];
return 1;
}
This picks all characters which are not , or ;, using the negated character class, [^,;]. So that is up to the first either of them, left to right. It does this globally, /g, so it keeps going through the string, collecting all fields that are "left of" , or ;. Then it cheats a bit, picking all [^,;] that are right of ;. The map is used to do this for all lines of data.
If %result_hash is needed build it instead of #data and then pull the values from it with my #values = values %hash_result and feed the map with #values.
Or, broken line by line (again, you can build %result_hash instead of taking $data directly)
my (#eto, #pro);
while (<DATA>) {
my ($data) = /:(.*)/;
push #eto, $data =~ /([^,;]+)/g;
push #pro, $data =~ /;([^,;]+)/g;
}

Alternate between upper and lowercase, PERL

I want to alternate between upper and lower case, however I only managed to get the whole string upper or lower, or the first character.
I have not found a proper function to execute what I need. Please have a look and help me out. Cheers.
#!/usr/bin/perl
my $mystring = "this is my string I want each character to alternate between upper and lowercase";
my #myarray = split("", $mystring);
print ucfirst("#myarray");
A more general approach using function factory
use strict;
use warnings;
sub periodic {
my #subs = #_;
my $i = 0;
return sub {
$i = 0 if $i > $#subs;
return $subs[$i++]->(#_);
};
}
my $mystring = "this is my string I want each character to alternate between upper and lowercase";
my $f = periodic(
sub { uc pop },
sub { lc pop },
# sub { .. },
# sub { .. },
);
$mystring =~ s/([a-z])/ $f->($1) /egi;
print $mystring, "\n";
output
ThIs Is My StRiNg I wAnT eAcH cHaRaCtEr To AlTeRnAtE bEtWeEn UpPeR aNd LoWeRcAsE
How about:
my $mystring = "this is my string I want each character to alternate between upper and lowercase";
my #myarray = split("", $mystring);
my $cnt = 1;
for (#myarray) {
next unless /[a-z]/i;
$_ = ($cnt%2 ? uc($_) : lc($_));
$cnt++;
}
say join('',#myarray);
Output:
ThIs Is My StRiNg I wAnT eAcH cHaRaCtEr To AlTeRnAtE bEtWeEn UpPeR aNd LoWeRcAsE
My first thought was to use a regex substitution. Try this:
use strict;
use warnings;
my $str = "this string, I will change";
# Ignore whitespace and punctuation.
$str =~ s/(\w)(\w)/\L$1\U$2/g;
# Or include all characters in the uc/lc alternation.
# $str =~ s/(.)(.)/\L$1\U$2/g;
print $str, "\n";
If, for some reason, you wish to avoid regexes, try:
my $str = "this string, I will change";
my #ary;
my $count = 0;
for my $glyph ( split //, lc $str ) {
$glyph = uc $glyph if $count % 2;
push #ary, $glyph;
$count++;
}
print join( "", #ary ), "\n";
Try this:
use strict;
use warnings;
use 5.016;
use Data::Dumper;
my $str = 'hello';
my $x = 0;
$str =~ s/(.)/($x++ % 2 == 0) ? "\U$1" : "\L$1"/eg;
say $str;
--output:--
HeLlO
Save script below with name alter.pl
#!/usr/bin/perl
print#ARGV[0]=~s/([a-z])([^a-z]*)([a-z])/uc($1).$2.lc$3/egri
And run script by command
$ perl alter.pl "this is my string I want each character to alternate between upper and lowercase"
Output
ThIs Is My StRiNg I wAnT eAcH cHaRaCtEr To AlTeRnAtE bEtWeEn UpPeR aNd LoWeRcAse
You have some good answers already but I thought I'd chip in because I hadn't seen map yet.
print map { $c++ % 2 ? lc : uc } split ( //, $mystring );
splits $mystring into characters (split //);
uses map to apply a function to each letter.
uses $c++ to autoincrement, then take a modulo 2 to decide if this should be uppercase or lower case.
join the resultant array.
Gives:
#!c:\Strawberry\perl\bin
use strict;
use warnings;
my $mystring = "this is my string I want each character to alternate between upper and lowercase";
my $c;
print join ( "", map { $c++ % 2 ? lc : uc } split ( //, $mystring ));
Prints:
ThIs iS My sTrInG I WaNt eAcH ChArAcTeR To aLtErNaTe bEtWeEn uPpEr aNd lOwErCaSe
map is a useful function that applies some code to each element in a list, and then 'returns' the list that's produced. So if we treat your string as a list of characters, it works nicely.
Try this. simple if else condition enough for this
my $mystring = "this is my string I want each character to alternate between upper and lowercase";
#xz = split( '', $mystring );
for ( $i = 0; $i < scalar #xz; $i++ ) {
if ( $i % 2 ) {
print uc "$xz[$i]";
}
else {
print "$xz[$i]";
}
}

Perl list all keys in hash with identical values

If I have a colon-delimited file name FILE and I do:
cat FILE|perl -F: -lane 'my %hash = (); $hash{#F[0]} = #F[2]'
to assign the first and 3rd tokens as the key => value pairs for the hash..
1) Is that a sane way to assign key value pairs to a hash?
2) What is the simplest way to now find all keys with shared values and list them?
Assume FILE looks like:
Mike:34:Apple:Male
Don:23:Corn:Male
Jared:12:Apple:Male
Beth:56:Maize:Female
Sam:34:Apple:Male
David:34:Apple:Male
Desired Output: Keys with value "Apple": Mike,Jared,David,Sam
Your example won't work as you want because the -n option puts a while loop around your one-line program, so the hash you declare is created and destoyed for every record in the file. You could get around that by not declaring the hash, and so making it a persistent package variable which will retain all values stored in it.
You can then write push #{ $hash{$F[2]} }, $F[0] but notice that it should be $F[0] etc. and not #F[0], and I have used push to create a list of column 1 values for each column 3 value instead of just a list of one-to-one values relating each column 1 value with its column 3 value.
To clarify, your method produces a hash looking like this, which has to be searched to produce the display that you want.
(
Beth => "Maize",
David => "Apple",
Don => "Corn",
Jared => "Apple",
Mike => "Apple",
Sam => "Apple",
)
while mine creates this, which as you can see is pretty much already in the form you want.
(
Apple => ["Mike", "Jared", "Sam", "David"],
Corn => ["Don"],
Maize => ["Beth"],
)
But I think this problem is a bit too big to be solved with a one-line Perl program. The solution below expects the path to the input file as a command-line parameter, like this
> perl prog.pl colons.csv
but it will default to myfile.csv if no file is specified.
use strict;
use warnings;
our #ARGV = 'myfile.csv' unless #ARGV;
my %data;
while (<>) {
my #fields = split /:/;
push #{ $data{$fields[2]} }, $fields[0];
}
while (my ($k, $v) = each %data) {
next unless #$v > 1;
printf qq{Keys with value "%s": %s\n}, $k, join ', ', #$v;
}
output
Keys with value "Apple": Mike, Jared, Sam, David
use strict;
use warnings;
open my $in, '<', 'in.txt';
my %data;
while(<$in>){
chomp;
my #split = split/:/;
$data{$split[0]} = $split[2];
}
my $query = 'Apple';
print "Keys with value $query = ";
foreach my $name (keys %data){
print "$name " if $data{$name} eq $query;
}
print "\n";
Arrays are used to hold list of values, so use an array.
perl -F: -lane'
push #{ $h{$F[2]} }, $F[0];
END {
for my $fruit (keys %h) {
next if #{ $h{$fruit} } < 2;
print "$fruit: ", join(",", #{ $h{$fruit} });
}
}
' FILE
The END block is executed on exit. In it, we iterate over the keys of the hash. If the value of the current hash element is an array with only one element, it's skipped. Otherwise, we prints the key followed by contents of the array referenced by the hash element.
Here is another way:
perl -F: -lane'
push #{ $h{$F[2]} }, $F[0];
}{
print "$_: ", join(",", #{ $h{$_} }) for grep { #{$h{$_}} > 1 } keys %h;
' file
We read each line and create hash of arrays using third column as key and first column as list of values for matching key. In the END block we iterate over our hash using grep and filter keys whose array count greater than 1 and print the key followed by array elements.
It doesn't have to be a one liner,
Good. It's not going to be...
Is that a sane way to assign key value pairs to a hash?
You're simply assigning the key value pairs as:
$hash{"key"} = "value";
Which is about as simple as it gets. There might be a way of doing it via map. However, the main issue I see is what should happen if you have duplicate keys.
Let's say your file looks like this:
Mike:34:Apple:Male
Don:23:Corn:Male
Jared:12:Apple:Male
Beth:56:Maize:Female
Sam:34:Apple:Male
David:34:Apple:Male # Note this entry is here twice!
David:35:Wheat:Male # Note this entry is here twice!
Let's do a simple assignment loop:
my %hash;
while my $line ( <$fh> ) {
chomp $line;
my ($name, $age, $category, $sex) = split /:/, $line;
$hash{$name} = $category;
}
When you get to $hash{David}, it will first be set to Apple, but then you change the value to Wheat. There are four ways you can handle this:
Use whatever the last value is. No change in the loop.
Use the first value and ignore subsequent values. Simple enough to do.
If that happens, it's an error. Abort the program and report the error.
Keep all values.
This last one is the most interesting because it involves a reference to an array as the values for your hash:
my %hash;
while my $line ( <$fh> ) {
chomp $line;
my ($name, $age, $category, $sex) = split /:/, $line;
$hash{$name} = [] if not exists $hash{$name}; # I'm making this an array reference
push #{ $hash{$name} }, $category;
}
Now, each value in my hash is a reference to an array:
my #values = #{ $hash{David} ); # The values of David...
print "David is in categories " . join ( ", ", #values ) . "\n";
This will print out David is in categories Wheat, Apple
What is the simplest way to now find all keys with shared values and list them?
The easiest way is to create a second hash that's keyed by your value. In this hash, you will need to use an array reference. Let's assume no duplicate names for now:
my %hash;
my %indexed_hash;
while my $line ( <$fh> ) {
chomp $line;
my ($name, $age, $category, $sex) = split /:/, $line;
$hash{$name} = $category;
my $indexed_hash{$category} = [] if not exist $indexed_hash{$category};
push #{ $indexed_hash{$category} }, $name;
}
Now, if I want to find all the duplicates of Apple:
my #names = #{ $indexed_hash{Apple} };
print "The following are in 'Apple': " . join ( ", " #names ) . "\n";
Since we're getting into references, we could take things a step further and store all of your values of your file in your hash. Again, for simplicity, I am assuming that you will have one and only one entry per name:
my %hash;
while my $line ( <$fh> ) {
chomp $line;
my ($name, $age, $category, $sex) = split /:/, $line;
$hash{$name}->{AGE} = $age;
$hash{$name}->{CATEGORY} = $category;
$hash{$name}->{SEX} = $sex;
}
for my $name ( sort keys %hash ) {
print "$name Information:\n";
print " Age: " . $hash{$name}->{AGE} . "\n";
printf "Category: %s\n", $hash{$name}->{CATEGORY};
print " Sex: #{[$hash{$name}->{SEX}]}\n\n";
}
That last two statements are easier ways of interpolating complex data structures into a string. The printf is fairly clear. The second #{[...]} is a neat little trick.
What have you tried?
If you reverse the hash into a list of value => key pairs then use List::Util's pairs() against the list, you can transform the hash into a hash of values => key arrayrefs. i.e. ( foo => [ 'bar', 'baz' ] ), grep {#{$hash{$_}} > 1} keys %hash, and print the results.

Parsing a string into a hash structure in perl

I have the following string:
$str = "list
XYZ
status1 : YES
value1 : 100
status2 : NO
value2 : 200
Thats all";
I want to convert it into a hash using a function which takes this string as input and returns a hash with status1 as key and YES as value for example.
How to do so?
And how to reference the returned hash?
Like always, there's more than one way to do it. Here come five.
Pure regular expressions (YEAH!)
I think this is the coolest one. The regex returns a list of all captures which is exactly the list we want to initialize the hash with:
my %regex = $str =~ /(\S+)\s*:\s*(\S+)/g;
Iterative
This is the most straightforward way for most programmers, I think:
my #lines = split /\R/ => $str;
my %iterative = ();
for (#lines) {
next unless /(\S+)\s*:\s*(\S+)/;
$iterative{$1} = $2;
}
Nothing to explain here. I first split the string in lines, then iterate over them, leaving out lines that don't look like foo : bar. Done.
List processing
Writing everything as a big list expression feels a little bit hackish, but maybe this is interesting to learn more ways to express stuff:
my %list = map { /(\S+)\s*:\s*(\S+)/ and $1 => $2 }
grep { /:/ }
split /\R/ => $str;
Read from right to left: Like in the example above we start with splitting the string in lines. grep filters the lines for : and in the final map I transform matching line strings in a list of length two, with a key and a value.
List reducing
Non-trivial use-cases of List::Util's reduce function are very rare. Here's one, based on the list approach from above, returning a hash reference:
my $reduced = reduce {
$a = { $a =~ /(\S+)\s*:\s*(\S+)/ } unless ref $a;
$a->{$1} = $2 if $b =~ /(\S+)\s*:\s*(\S+)/;
return $a;
} grep { /:/ } split /\R/ => $str;
State machine
Here's a funny one with regex usage for white-space separation only. It needs to keep track of a state:
# preparations
my $state = 'idle';
my $buffer = undef;
my %state = ();
my #words = split /\s+/ => $str;
# loop over words
for my $word (#words) {
# last word was a key
if ($state eq 'idle' and $word eq ':') {
$state = 'got_key';
}
# this is a value for the key in buffer
elsif ($state eq 'got_key') {
$state{$buffer} = $word;
$state = 'idle';
$buffer = undef;
}
# remember this word
else {
$buffer = $word;
}
}
Just for fun (note that I recommend using one of memowe's) here is one that (ab)uses the YAML:
#!/usr/bin/env perl
use strict;
use warnings;
use YAML;
my $str = "list
XYZ
status1 : YES
value1 : 100
status2 : NO
value2 : 200
Thats all";
$str = join "\n", grep { /:/ } split "\n", $str;
my $hash = Load "$str\n";
#!/usr/bin/perl
use warnings;
$\="\n";
sub convStr {
my $str = $_[0];
my %h1=();
while ($str =~m/(\w+)\s+:\s+(\w+)/g) {
$h1{$1} =$2;
}
return \%h1;
}
my $str = "list
XYZ
status1 : YES
value1 : 100
status2 : NO
value2 : 200
Thats all";
my $href=convStr($str);
foreach (keys(%$href)) {
print $_ , "=>", $href->{$_};
}
On running this, I get:
status2=>NO
value1=>100
status1=>YES
value2=>200
my %hhash;
my #lines = split /\s+\n/, $str;
foreach (#lines)
{
$_=~s/^\s+//g;
if(/:/)
{
$key=(split(/:/))[0];
$value=(split(/:/))[1];
$hhash{$key}=$value;
}
}

How to matching sequential words in a sentence using Perl?

Is there better way to match words other than this method, im trying to find the word in the array that occur in any of the sentences.
my $count = 0;
my #strings = (
"i'm going to find the occurrence of two words going if possible",
"i'm going to find the occurrence of two words if impossible",
"to find a solution to this problem",
"i will try my best for a way to match this problem"
);
#neurot = qw(going match possible);
my $com_neu = '\b'.join('\b|\b', #neurot).'\b';
foreach my $sentence (#string){
#l = $sentence =~ /($com_neu)/gi;
foreach my $list (#l){
if($list =~ m/\w['\w-]*/){
print $list;
$count++;
}
}
print $count;
}
Output:
String 1: going going possible
String 2: going
String 3:
String 4: match
please help me with a faster way.
Thanks.
Another approach could be to use hash to match the words:
my %neurot_hash = map { lc($_) => 1 } qw(going match possible);
for my $sentence (#strings) {
for my $found (grep { $neurot_hash{ lc($_) } } $sentence =~ /\w['\w-]*/gi) {
print $found, " ";
}
print "\n";
}
For data you provided this method is ~ 7% faster. But keep in mind that the data set is very small, so YMMV.
what about the 'smart-match' operator?
foreach my $elem (#neurot){
if(/$elem/i ~~ #strings){
print "Found $elem\n";
}
}
The same as bvr answer, but perhaps cleaner
my %neurot_hash = map { lc($_) => 1 } qw(going match possible);
for my $sentence (#strings) {
my #words = split /[^\w']/, $sentence;
#I am not sure if you want to take "i'm" as a separate word.
#Apparently, stackoverflow does not like '.
my #found = grep { exists $neurot_hash{ lc($_) } } #words;
print join (" ", #found);
print "\n";
}