Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to emulate word boundary when using unicode character properties?

From my previous questions Why under locale-pragma word characters do not match? and How to change nested quotes I learnt that when dealing with UTF-8 data you can't trust \w as word-char and you must use the Unicode character property \p{Word}. Now I am in a situation where I found that zero-width word boundary \b also does not work with UTF-8 (with locale enabled), but I did not find any equivalent in Unicode character properties. I thought I may construct it myself like: (?<=\P{Word})(\p{Word}+)(?=\P{Word}), it should be equivalent to \b(\w+)\b.

In the test script below I have two arrays to test two different regexes. The first based on \b works fine when locale is not enabled. To get it to also work with locales I wrote another version with emulating boundary (?=\P{Word}) but it does not work as I expected (I show expected results in script too).

Do you see what is wrong and how to get emulated regex work as first with ASCII (or without locale)?

#!/usr/bin/perl

use 5.010;
use utf8::all;
use locale; # et_EE.UTF-8 in my case
$| = 1;

my @test_boundary = (  # EXPECTED RESULT:
  '"abc def"',         # '«abc def»'
  '"abc "d e f" ghi"', # '«abc «d e f» ghi»'
  '"abc "d e f""',     # '«abc «d e f»»'
  '"abc "d e f"',      # '«abc "d e f»'
  '"abc "d" "e" f"',   # '«abc «d» «e» f»'
  # below won't work with \b when locale enabled
  '"100 Естонiï"',     #  '«100 Естонiï»'
  '"äöõ "ä õ ü" ï"',   # '«äöõ «ä õ ü» ï»'
  '"äöõ "ä õ ü""',     # '«äöõ «ä õ ü»»'
  '"äöõ "ä õ ü"',      # '«äöõ «ä õ ü»'
  '"äöõ "ä" "õ" ï"',   # '«äöõ «ä» «õ» ï»'
);

my @test_emulate = (   # EXPECTED RESULT:
  '"100 Естонiï"',     # '«100 Естонiï»'
  '"äöõ "ä õ ü" ï"',   # '«äöõ «ä õ ü» ï»'
  '"äöõ "ä õ ü""',     # '«äöõ «ä õ ü»»'
  '"äöõ "ä õ ü"',      # '«äöõ "ä õ ü»'
  '"äöõ "ä" "õ" ï"',   # '«äöõ «ä» «õ» ï»'
);

say "BOUNDARY";
for my $sentence ( @test_boundary ) {
  my $quote_count = ( $sentence =~ tr/"/"/ );

  for ( my $i = 0 ; $i <= $quote_count ; $i += 2 ) {
    $sentence =~ s/
      "(                          # first qoute, start capture
        [\p{Word}\.]+?            # suva word-char
        .*?\b[\.,?!»]*?           # any char followed boundary + opt. punctuation
      )"                          # stop capture, ending quote
      /«$1»/xg;                   # change to fancy
  }
  say $sentence;
}

say "EMULATE";
for my $sentence ( @test_emulate ) {
  my $quote_count =  ( $sentence =~ tr/"/"/ );

  for ( my $i = 0 ; $i <= $quote_count ; $i += 2 ) {
    $sentence =~ s/
      "(                         # first qoute, start capture
      [\p{Word}\.]+?             # at least one word-char or point
      .*?(?=\P{Word})            # any char followed boundary 
      [\.,?!»]*?                 # optional punctuation
      )"                         # stop capture, ending quote
      /«$1»/gx;                  # change to fancy
  }
  say $sentence;
}
like image 526
w.k Avatar asked Feb 18 '13 18:02

w.k


2 Answers

Since the character after the position of the \b is either some punctuation or " (to be safe, please double check that \p{Word} does not match any of them), it falls into the case \b\W. Therefore, we can emulate \b with:

(?<=\p{Word})

I am not familiar with Perl, but from what I tested here, it seems that \w (and \b) also works nicely when the encoding is set to UTF-8.

$sentence =~ s/
  "(
    [\w\.]+?
    .*?\b[\.,?!»]*?
  )"
  /«$1»/xg;

If you move up to Perl 5.14 and above, you can set the character set to Unicode with u flag.


You can use this general strategy to construct a boundary corresponding to a character class. (Like how \b word boundary definition is based on the definition of \w).

Let C be the character class. We would like to define a boundary that is based on the character class C.

The construction below will emulate boundary in front when you know the current character belongs to C character class (equivalent to (\b\w)):

(?<!C)C

Or behind (equivalent to \w\b):

C(?!C)

Why negative look-around? Because positive look-around (with the complementary character class) will also assert that there must be a character ahead/behind (assert width ahead/behind at least 1). Negative look-around will allow for the case of beginning/ending of the string without writing a cumbersome regex.


For \B\w emulation:

(?<=C)C

and similarly \w\B:

C(?=C)

\B is the direct opposite of \b, therefore, we can just flip the positive/negative look-around to emulate the effect. It also makes sense - a non-boundary can only be formed when there are more character ahead/behind.


Other emulations (let c be the complement character class of C):

  • \b\W: (?<=C)c
  • \W\b: c(?=C)
  • \B\W: (?<!C)c
  • \W\B: c(?!C)

For the emulation of a standalone boundary (equivalent to \b):

(?:(?<!C)(?=C)|(?<=C)(?!C))

And standalone non-boundary (equivalent to \B):

(?:(?<!C)(?!C)|(?<=C)(?=C))
like image 56
nhahtdh Avatar answered Oct 30 '22 03:10

nhahtdh


You should be using negative lookarounds:

(?<!\p{Word})(\p{Word}+)(?!\p{Word})

The positive lookarounds fail at the start or end of the string because they require a non-word character to be present. The negative lookarounds work in both cases.

like image 20
Tim Pietzcker Avatar answered Oct 30 '22 05:10

Tim Pietzcker