views:

537

answers:

8

Is there any comparable function like Pos that is not case-sensitive in D2010 (unicode)?

I know I can use Pos(AnsiUpperCase(FindString), AnsiUpperCase(SourceString)) but that adds a lot of processing time by converting the strings to uppercase every time the function is called.

For example, on a 1000000 loop, Pos takes 78ms while converting to uppercase takes 764ms.

str1 := 'dfkfkL%&/s"#<.676505';
  for i := 0 to 1000000 do
    PosEx('#<.', str1, 1); // Takes 78ms

  for i := 0 to 1000000 do
    PosEx(AnsiUpperCase('#<.'), AnsiUpperCase(str1), 1); // Takes 764ms

I know that to improve the performance of this specific example I can convert the strings to uppercase first before the loop, but the reason why I'm looking to have a Pos-like function that is not case-sensitive is to replace one from FastStrings. All the strings I'll be using Pos for will be different so I will need to convert each and every one to uppercase.

Is there any other function that might be faster than Pos + convert the strings to uppercase?

A: 

The Jedi Code Library has StrIPos and thousands of other useful functions to complement Delphi's RTL. When I still worked a lot in Delphi, JCL and its visual brother JVCL were among the first things I added to a freshly installed Delphi.

fvu
+9  A: 

The built-in Delphi function to do that is in both the AnsiStrings.ContainsText for AnsiStrings and StrUtils.ContainsText for Unicode strings.

In the background however, they use logic very similar to your logic.

No matter in which library, functions like that will always be slow: especially to be as compatible with Unicode as possible, they need to have quite a lot of overhead. And since they are inside the loop, that costs a lot.

The only way to circumvent that overhead, is to do those conversions outside the loop as much as possible.

So: follow your own suggestion, and you have a really good solution.

--jeroen

Jeroen Pluimers
-1: AnsiStrings.ContainsText does not return the position of the substring, only whether the sub-string exists within the string.
Steve
OK: I can't vote you down, but I would have. :-)
Steve
Sorry - I misread the original post in that it was only interested if it contained the position. Anyhow, ContainsText is similar enough to shows how the Delphi team solved the issue, hence the conclusion in my message still holds.
Jeroen Pluimers
Steve, in my version, the `ContainsText` function is implemented by changing the case of the two input arguments, calling `Pos`, and then checking that the result is greater than zero. Return the result instead of checking that it's positive, and you have a case-insensitive `Pos`.
Rob Kennedy
+1  A: 

Here's one that I wrote and have been using for years:

function XPos( const cSubStr, cString :string ) :integer;
var
  nLen0, nLen1, nCnt, nCnt2 :integer;
  cFirst :Char;
begin
  nLen0 := Length(cSubStr);
  nLen1 := Length(cString);

  if nLen0 > nLen1 then
    begin
      // the substr is longer than the cString
      result := 0;
    end

  else if nLen0 = 0 then
    begin
      // null substr not allowed
      result := 0;
    end

  else

    begin

      // the outer loop finds the first matching character....
      cFirst := UpCase( cSubStr[1] );
      result := 0;

      for nCnt := 1 to nLen1 - nLen0 + 1 do
        begin

          if UpCase( cString[nCnt] ) = cFirst then
            begin
              // this might be the start of the substring...at least the first
              // character matches....
              result := nCnt;

              for nCnt2 := 2 to nLen0 do
                begin

                  if UpCase( cString[nCnt + nCnt2 - 1] ) <> UpCase( cSubStr[nCnt2] ) then
                    begin
                      // failed
                      result := 0;
                      break;
                    end;

                end;

            end;


          if result > 0 then
            break;
        end;


    end;
end;

Steve
Doesn't this exacerbate the problem about which the OP asked? This will do the uppercase conversion inside the loop, perhaps multiple times.
Argalatyr
I disagree because the UpCase function is Char based only so there is no memory allocation involved. Remember, the slow part of most string functions is allocating the memory for the result and any local strings used within the function. If you look closely at my function you will realise that there is no memory allocation for strings. However, I would agrue that the proof of the pudding is in the eating, so let smartins benchmark indicate whether it is efficient of not.
Steve
-1: UpCase only handles characters in the range a..z: http://docwiki.embarcadero.com/VCL/en/System.UpCaseTo make this work for Unicode (this is Delphi 2010!) you need to use something else, for instance from the Character class: http://docwiki.embarcadero.com/RADStudio/en/Character_Manipulation_RoutinesSince you do the uppercase conversion inside the loop, it will be as slow as the routine from the original poster.
Jeroen Pluimers
Actually, I just tested it and it comes up at 93ms against 764ms of PosEx(AnsiUpperCase.... Would this be Unicode compatible?
smartins
I don't know what your benchmarking methodology is smartins, but this isn't what I found. Even removing the bounds checking of the parameters ("taking the safety off") this routine clocked in about 10% slower than Pos + Uppercase in my tests.
Deltics
However, I'm giving this +1 because the char-wise handling of the problem goes some way toward avoiding potential issues introduced by case conversion of strings that may affect the *length* of those strings, thereby causing errors in any char-position based results, w.r.t the input strings.
Deltics
I'm doing the following: for i := 0 to 1000000 do j := XPos(searchstr, str1); //296ms and: for i := 0 to 1000000 do j := Pos(AnsiUpperCase(searchstr), AnsiUpperCase(str1)); // 780ms I call GetTickCount before and after the loop to get the time it takes in ms
smartins
To make this routine fully Unicode compatible you will need to change the UpCase functions calls with ToUpper calls (defined in Character.pas). My benchmarks give more or less the same timings for my unicode/non-unicode versions, both of which are still faster than your original version.
Steve
Actually, my benchmark after replacing UpCase with ToUpper shows your function takes 3432ms to complete while "mine" takes 874ms.
smartins
Very strange?? My benchmarks for your code and my code and any variants are all pretty much in the same ballpark.
Steve
A: 

Instead 'AnsiUpperCase' you can use Table it is much faster. I have reshape my old code. It is very simple and also very fast. Check it:

type
  TAnsiUpCaseTable = array [AnsiChar] of AnsiChar;

var
  AnsiTable: TAnsiUpCaseTable;

procedure InitAnsiUpCaseTable(var Table: TAnsiUpCaseTable);
var
  n: cardinal;
begin
  for n := 0 to SizeOf(TAnsiUpCaseTable) -1 do
  begin
    AnsiTable[AnsiChar(n)] := AnsiChar(n);
    CharUpperBuff(@AnsiTable[AnsiChar(n)], 1);
  end;
end;

function UpCasePosEx(const SubStr, S: string; Offset: Integer = 1): Integer;
var
  n              :integer;
  SubStrLength   :integer;
  SLength        :integer;
label
  Fail;
begin
  SLength := length(s);
  if (SLength > 0) and (Offset > 0) then begin
    SubStrLength := length(SubStr);
    result := Offset;
    while SubStrLength <= SLength - result + 1 do begin
      for n := 1 to SubStrLength do
        if AnsiTable[SubStr[n]] <> AnsiTable[s[result + n -1]] then
          goto Fail;
      exit;
Fail:
      inc(result);
    end;
  end;
  result := 0;
end;

initialization
  InitAnsiUpCaseTable(AnsiTable);
end.
GJ
-1: Actually, contrary to the naming, AnsiUpperCase does support Unicode. Your function does not.
Jeroen Pluimers
I thought ansi* were MBCS safe, not unicode.
Marco van de Voort
@Jeroen Pluimers: You are wrong. Check my code again! My code at initialization call InitAnsiUpCaseTable which inside call API CharUpperBuff. So my function use at initialization current locale character set.
GJ
GJ: Jeroen is right in the sense that you don't handle any multibyte encodings properly. Be it the older MBCS (BIG-5 etc) or UTF-8.jeroen: I didn't see the D2010 bit originally. If the ansi* definition changed to UTF-8 nowadays, forget what I said.
Marco van de Voort
@Marco van de Voort: In fact the algorithem must work! In D2009 and D2010 size of char is 16bit in older versions 8bit. API call CharUpperBuff return Unicode uppercase character. In my code the size of TAnsiUpCaseTable is 128 Kbytes if compiled under D2009 or D2010, compiled under older versions the size is only 256 bytes and support only at initialization current locale character set. OK instead 'SizeOf(TAnsiUpCaseTable)' must be 'Length(Table)'. I didn't test code under D2009 but I believe that work.
GJ
GJ your code doesn't even compile in D2010. "E2010 Incompatible types: 'AnsiChar' and 'Char'" on AnsiTable[SubStr[n]] because SubStr is a string and AnsiTable is an array of AnsiChar.
smartins
GJ, as Smartins points out, you don't handle the conversion from UnicodeChar to AnsiChar. Even in non-Unicode Delphi, though, your code is wrong. In an AnsiString, a character can be represented by **multiple bytes**. Although you call CharUpperBuffer, you only handle a single byte, so if the character has multiple bytes, your code will fail to convert it properly.
Rob Kennedy
Rob Kennedy, I aggry, but the character size in D2010 is now 16bit and compiler will call API CharUpperBufferW instead CharUpperBufferA the UpCaseTable size is now 128KBytes. Check my second version.
GJ
+3  A: 

I have also faced the problem of converting FastStrings, which used a Boyer-Moore (BM) search to gain some speed, for D2009 and D2010. Since many of my searches are looking for a single character only, and most of these are looking for non-alphabetic characters, my D2010 version of SmartPos has an overload version with a widechar as the first argument, and does a simple loop through the string to find these. I use uppercasing of both arguments to handle the few non-case-sensitive case. For my applications, I believe the speed of this solution is comparable to FastStrings.

For the 'string find' case, my first pass was to use SearchBuf and do the uppercasing and accept the penalty, but I have recently been looking into the possibility of using a Unicode BM implementation. As you may be aware, BM does not scale well or easily to charsets of Unicode proportions, but there is a Unicode BM implementation at Soft Gems. This pre-dates D2009 and D2010, but looks as if it would convert fairly easily. The author, Mike Lischke, solves the uppercasing issue by including a 67kb Unicode uppercasing table, and this may be a step too far for my modest requirements. Since my search strings are usually short (though not as short as your single three-character example) the overhead for Unicode BM may also be a price not worth paying: the BM advantage increases with the length of the string being searched for.

This is definitely a situation where benchmarking with some real-world application-specific examples will be needed before incorporating that Unicode BM into my own applications.

Edit: some basic benchmarking shows that I was right to be wary of the "Unicode Tuned Boyer-Moore" solution. In my environment, UTBM results in bigger code, longer time. I might consider using it if I needed some of the extras this implementation provides (handling surrogates and whole-words only searches).

frogb
A: 

I think, converting to upper or lower case before Pos is the best way, but you should try to call AnsiUpperCase/AnsiLowerCase functions as less as possible.

samir105
A: 

On this occasion I couldn't find any approach that was even as good as, let alone better than Pos() + some form of string normalisation (upper/lowercase conversion).

This is not entirely surprising as when benchmarked the Unicode string handling in Delphi 2009 I found that the Pos() RTL routine has improved significantly since Delphi 7, explained in part by the fact that aspects of the FastCode libraries have been incorporated into the RTL for some time now.

The FastStrings library on the other hand has not - iirc - been significantly updated for a long time now. In tests I found that many FastStrings routines have in fact been overtaken by the equivalent RTL functions (with a couple of exceptions, explained by the unavoidable overhead incurred by the additional complications of Unicode).

The "Char-Wise" processing of the solution presented by Steve is the best so far imho.

Any approach that involves normalising the entire strings (both string and sub-string) risks introducing errors in any character-based position in the results due to the fact that with Unicode strings a case conversion may result in a change in the length of the string (some characters convert to more/fewer characters in a case conversion).

These may be rare cases but Steve's routine avoids them and is only about 10% slower than the already quite fast Pos + Uppercase (your benchmarking results don't tally with mine on that score).

Deltics
+2  A: 

Smartins my idea about table is right!

OK I have repair code because you didn't. Code now works fine under D2007 and D2010. So both compilers can compile it. The difference is that under older version of compiler the CharUpCaseTable is 256 bites size and under D2010 is 128 KBytes. The reason is char size. Under older version of Delphi my code support only at initialisation current locale character set! My InsensPosEx is about 4 times faster than your code. Certainly is possible to do even faster code but we will lose simplicity.

type
  TCharUpCaseTable = array [Char] of Char;

var
  CharUpCaseTable: TCharUpCaseTable;

procedure InitCharUpCaseTable(var Table: TCharUpCaseTable);
var
  n: cardinal;
begin
  for n := 0 to Length(Table) - 1 do
    Table[Char(n)] := Char(n);
  CharUpperBuff(@Table, Length(Table));
end;

function InsensPosEx(const SubStr, S: string; Offset: Integer = 1): Integer;
var
  n              :integer;
  SubStrLength   :integer;
  SLength        :integer;
label
  Fail;
begin
  SLength := length(s);
  if (SLength > 0) and (Offset > 0) then begin
    SubStrLength := length(SubStr);
    result := Offset;
    while SubStrLength <= SLength - result + 1 do begin
      for n := 1 to SubStrLength do
        if CharUpCaseTable[SubStr[n]] <> CharUpCaseTable[s[result + n - 1]] then
          goto Fail;
      exit;
Fail:
      inc(result);
    end;
  end;
  result := 0;
end;

//...

initialization
  InitCharUpCaseTable(CharUpCaseTable);

Kind regards,

        GJ
GJ
Indeed, your updated version is faster than any of the other functions I tried. UpCasePosEx: 171ms, PosEx(AnsiUpperCase... 827ms, XPos: 343ms
smartins
And if you still need more speed you can (it is good idea) switch off in compiler options "String format checking" or put at beginning of the unit {$STRINGCHECKS OFF} you can read more about: http://www.micro-isv.asia/2008/10/needless-string-checks-with-ensureunicodestring/Now is about 10 times faster than your privius code.
GJ
Thanks for the heads-up, I'm already using that compile directive on all my code via a include file.
smartins
Strange, becuse as I have measured the XPos is faster (better algorithem I have use CharUpCaseTable with 16 bit char size) for about 25%. What about other compiler switches?
GJ