views:

132

answers:

4

Hi everyone, I am running into a strange regex issue.... I have a document where I am doing a replace... as an example I want to replace "DEXX" with "DEXX/AREX" and then with the next substitution replace... "AREX" with "AREX/CUBE"

DEXX and AREX are stored in a hash like so.... "DEXX" => "AREX", "AREX" => "CUBE"

The regex I have is this.....

foreach (keys %hashstore){
    $doc=~s!\b($_)\b!$1/$hashstore{$_}!ig;
}

What's happening is that "DEXX" is being replaced with "DEXX/AREX" ok but when "DEXX/AREX" is encountered the regex is replacing "DEXX/AREX" with "DEXX/AREX/CUBE" when it should only be replacing "AREX" when it finds it as a standalone word not as part of another combination like "DEXX/AREX"

It seems to detect "/" as a word boundary. Has anyone encountered this or know of a fix around it? Many thanks! Amy

+6  A: 

But / is a word boundary. From perldoc perlreref:

\b Match word boundary (between \w and \W).

In light of your comment below, you should avoid the loop:

#!/usr/bin/perl

use strict; use warnings;
use Regex::PreSuf;

my %lookup = (
    "DEXX" => "AREX",
    "AREX" => "CUBE",
);

my $doc = 'DEXX AREX AREX DEXX AREX DEXX DEXX DEXX AREX';
my $re = presuf keys %lookup;

$doc =~ s{($re)}{$1/$lookup{$1}}g;

print $doc, "\n";

Output:

DEXX/AREX AREX/CUBE AREX/CUBE DEXX/AREX AREX/CUBE DEXX/AREX DEXX/AREX DEXX/AREX
AREX/CUBE

Of course, you don't have to use Regex::PreSuf if you only have two keys:

s{(AREX|DEXX)}{$1/$lookup{$1}}g;

will also do. But, for a longer list of keys, I find Regex::PreSuf to be very handy.

Update: Of course, if the keys can occur in any case in the text, you can use uc to transform when looking up the replacement:

So, either

$doc =~ s{($re)}{join '/', uc($1), $lookup{uc $1}}eig;

or

$doc =~ s{($re)}{join '/', $1, $lookup{uc $1}}eig;

depending on what you need.

Also, ysth points out in the comments "With 5.10 and later, Regex::PreSuf generates a poorer regex than the naive alternation in most cases." So,

my $re = join '|', map quotemeta, sort { length($b) <=> length($a) } keys %lookup; 

might be better. The sort is needed if some keys might be initial substrings of other keys.

Sinan Ünür
Sinan, I have lurked and enjoyed your solutions. What I need is for "DEXX" to be replaced with "DEXX/AREX" and when "AREX" is encountered (standing alone as it's own word, "AREX" need to be "AREX/CUBE" but leave the previous "DEXX/AREX" alone. Thanks so much again for your advice. Amy
Amy Wilkins
@Ether, no Sinan is just recreating my problem. Thanks for your input.
Amy Wilkins
@Amy: This is why giving actual sample input and expected output is important.
Sinan Ünür
@Sinan, I realized that after Ether mentioned that. I re-edited the original question to be more clear. Thanks again by the way. This is my last problem I have to solve before going to bed. LOL
Amy Wilkins
@Sinan, WOW! Normally I would spend some time hacking away at these problems (partly from reviewing SO first, I lurk a lot). This, however, has me under a early morning deadline and I appreciate you pointing me in the right direction. Thank you SO Much again! Amy
Amy Wilkins
@Amy You are welcome. Good luck.
Sinan Ünür
@Sinan, one last question. This works great because I have over 7,000 words I have to match. They have to be case insensitive though and I discovered that using the modifier "ig" gives me an error. That is it matches and correctly handles "DEXX" turning it into "DEXX/AREX" but it turns lowercase "dexx" into "dexx/" instead of "dexx/AREX". Can you try running it on your end with ig, just as a sanity check on my end? You're awesome and I am so tired. LOL
Amy Wilkins
With 5.10 and later, Regex::PreSuf generates a poorer regex than the naive alternation in most cases.
ysth
@Sinan: sure enough, when I use the "i" modifier the error "Use of uninitialized value in concatenation (.) or string at .process.pl line 14" which corresponds to the line "$doc =~ s{($re)}{$1/$lookup{$1}}ig;" in our code above. I guess I'll have to somehow report that to the author Jarkko Hietaniemi. I hate being this close and yet so far. LOL!
Amy Wilkins
@Amy Wilkins: there's no bug to report, except that you are looking up a key in the lookup hash in a different case than the hash uses, so you get an undef
ysth
@ysth: thanks so much. I suppose I could create 3 more hashes that are just dupes of the first and make the keys respectively lower case, upper case and initial case. That's about the best work around I can figure out at the moment. I'm very lucky to have gotten so much help on this and I really appreciate it.
Amy Wilkins
@Amy `perldoc -f uc`
Sinan Ünür
+2  A: 

The word boundary is any time there's a transition between \w and \W, which is to say [a-zA-Z0-9_] and [^a-zA-Z0-9_] if you're dealing with ASCII.

You should be able to get around this problem by using a negative lookbehind:

foreach (keys %hashstore){
    $doc=~s!(?<!/)\b($_)\b!$1/$hashstore{$_}!ig;
}
Joe
Thanks Joe, not quite there yet. I did change the format from s!!! to s### so that the "!" in the negative look behind is read as it should be. I'll keep plugging and thank you so much for your help. I really appreciate it. Amy
Amy Wilkins
+3  A: 

\b is equivalent to (though more efficient than) (?:(?<!\w)(?=\w)|(?<=\w)(?!\w)). If you want a different set of word characters than the default, just use that but with \w replaced by an appropriate character class.

ysth
You have the `\b` equivalent regexp wrong. The `\W` s should all be lower case `\w`. The negation is provided by the look(ahead/behind), not by the character class: `(?:(?<!\w)(?=\w)|(?<=\w)(?!\w))`. You can replace the `\w`s with any character class that you want to consider your "word-constituent" characters.
cjm
Oops, you're right.
ysth
+1  A: 

First off I am indebted to Sinan (who isn't in regards to Perl on SO? I know I have been lurking a long time....) and ysth. Thanks to these two I have a better grasp of regexes. My solution however was the following...

my $pat = join '|', keys(%hashstore);
$doc =~ s!\b($pat)\b!$1/$hashstore{uc($1)}!ig;

Problem that I had was I replacing my replacements! Normally I really try to hash these things out but this was such a tight deadline and Sinan and ysth, you both rock severely! Amy

Amy Wilkins
if you may have keys with the same prefix (e.g. ABC and ABCD) you need to sort by length so that ABCD comes first. Also, if there are special characters, they need quoting: `join '|', map quotemeta, sort { length($b) <=> length($a) } keys %hashstore`
ysth