0

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

5 Answers5

5

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
  • 116,958
  • 15
  • 196
  • 339
  • 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 Aug 12 '10 at 03:04
  • @Ether, no Sinan is just recreating my problem. Thanks for your input. – Amy Wilkins Aug 12 '10 at 03:26
  • @Amy: This is why giving actual sample input and expected output is important. – Sinan Ünür Aug 12 '10 at 03:57
  • @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 Aug 12 '10 at 03:59
  • 1
    @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 Aug 12 '10 at 04:29
  • @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 Aug 12 '10 at 04:47
  • With 5.10 and later, Regex::PreSuf generates a poorer regex than the naive alternation in most cases. – ysth Aug 12 '10 at 04:48
  • @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 Aug 12 '10 at 05:10
  • 2
    @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 Aug 12 '10 at 05:19
  • @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 Aug 12 '10 at 05:24
2

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
  • 2,547
  • 1
  • 18
  • 27
  • 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 Aug 12 '10 at 03:19
2

\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
  • 96,171
  • 6
  • 121
  • 214
  • 3
    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 Aug 12 '10 at 07:31
1

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

  • 4
    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 Aug 12 '10 at 14:34
0

Boundaries from the standpoint of \b are often not quite what you want, especially given that English words can contain apostrophes and dashes, and that these behave very differently than letters do when you put a \b next to them. See this answer for more explanation of this issue, and what to do about it.

Community
  • 1
  • 1
tchrist
  • 78,834
  • 30
  • 123
  • 180