Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

can anybody suggest a faster alternative to this regex algorithm?

I need to call this function pretty frequently. Basically, it replaces all accented characters by its unaccented equivalents, changes spaces to "_", converts to lowercase, and strips the rest of alien codes, so it is safe to use as filenames/url paths/etc.. The problem is, as you see, it looks terrible from the performance point of view. Can anybody think of a cleaner, faster alternative?

public static String makeValidPathName(String rawString) {
    if (rawString==null) return null;
    rawString = rawString.replaceAll("[ÁÀÂÄáàäaàáâãäå]","a");
    rawString = rawString.replaceAll("æ","ae");
    rawString = rawString.replaceAll("çÇ","c");
    rawString = rawString.replaceAll("[ÈÉÊËèéêë]","e");
    rawString = rawString.replaceAll("[ìíîïÍÌÎÏ]","i");
    rawString = rawString.replaceAll("ñÑ","n");                            
    rawString = rawString.replaceAll("[ÓÓÖÔòóôõö]","o");
    rawString = rawString.replaceAll("œ","oe");
    rawString = rawString.replaceAll("[ÙÚÛÜùúûü]", "u");
    rawString = rawString.replaceAll("[ýÿ]","y");
    rawString= rawString.replaceAll("[^a-z A-Z 0-9 \\_ \\+]","");
    rawString = rawString.replaceAll(" ","_");
    return rawString.toLowerCase();
}

--- EDIT

Ok, guys ... I did the performance tests on all 4 cases:

  • Case 1) The original routine as it is posted here.
  • Case 2) The improvement suggested by @WChargin
  • Case 3) The Lookup Table suggested by @devconsole with my optimizations for SparseArray
  • Case 4) The Normalizer approach suggested by @Erik Pragt

And ... the winner is ... TADAAA .....

D/REPLACEMENT_TEST(18563): *** Running Tests (1000 iterations)
D/REPLACEMENT_TEST(18563): Original REGEX  : 13533 ms
D/REPLACEMENT_TEST(18563): Compiled REGEX  : 12563 ms
D/REPLACEMENT_TEST(18563): Character LUT   : 1840 ms
D/REPLACEMENT_TEST(18563): Java NORMALIZER : 2416 ms
  • It's interesting the pattern compilation optimization doesn't help much.
  • I see I was also completely wrong about my assumptions on the speed on REGEXES, and devconsole was right in his educated guess on Normalizer outperforming regexes.
  • It is amazing how slow REGEXES are. Differences by an order of magnitude really surprise me. I will try to avoid them on Java.
  • The lookup table is the fastest option by short margin. I'll stick with this solution, because w/ the Normalizer one, I'd still have to replace some chars by hand (spaces into "_"), then convert to lowercase.

The tests were done in a Samsung Galaxy Tab v1 10.1.

Please find attached the source code for the test cases.

public class Misc { 

    /* Test 2 (@WCChargin's Regex compilation) initialization */

static Map<Pattern, String> patterns = new HashMap<Pattern, String>();

static {
        patterns.put(Pattern.compile("[ÁÀÂÄáàäaàáâãäå]") ,"a");
        patterns.put(Pattern.compile("æ"),"ae");
        patterns.put(Pattern.compile("çÇ"),"c");
        patterns.put(Pattern.compile("[ÈÉÊËèéêë]"),"e");
        patterns.put(Pattern.compile("[ìíîïÍÌÎÏ]"),"i");
        patterns.put(Pattern.compile("ñÑ"),"n");                            
        patterns.put(Pattern.compile("[ÓÓÖÔòóôõö]"),"o");
        patterns.put(Pattern.compile("œ"),"oe");
        patterns.put(Pattern.compile("[ÙÚÛÜùúûü]"), "u");
        patterns.put(Pattern.compile("[ýÿ]"),"y");
        patterns.put(Pattern.compile("[^a-z A-Z 0-9 \\_ \\+]"),"");
        patterns.put(Pattern.compile(" "),"_");
}

    /* Test 3 (@devconsole's Lookup table) initialization */
static SparseArray<String> homeBrewPatterns=new SparseArray<String>();
/** helper function to fill the map where many different chars map to the same replacement */
static void fillMap(String chars, String replacement) { for (int i=0,len=chars.length(); i<len; i++) homeBrewPatterns.put(chars.charAt(i), replacement); }
static {
    // fill the sparsearray map with all possible substitutions: Any char code gets its equivalent, ie, ä->a. a->a. A->a
    // this also does the toLowerCase automatically. If a char is not in the list, it is forbidden and we skip it.
    fillMap("ÁÀÂÄáàäaàáâãäå","a");
    fillMap("æ","ae");
    fillMap("çÇ","c");
    fillMap("ÈÉÊËèéêë","e");
    fillMap("ìíîïÍÌÎÏ","i");
    fillMap("ñÑ","n");
    fillMap("ÓÓÖÔòóôõö","o");
    fillMap("œ","oe");
    fillMap("ÙÚÛÜùúûü","u");
    fillMap("ýÿ","y");
    fillMap(" ","_");
    for (char c='a'; c<='z'; c++) fillMap(""+c,""+c); // fill standard ASCII lowercase -> same letter 
    for (char c='A'; c<='Z'; c++) fillMap(""+c,(""+c).toLowerCase()); // fill standard ASCII uppercase -> uppercase
    for (char c='0'; c<='9'; c++) fillMap(""+c,""+c); // fill numbers
}

    /** FUNCTION TO TEST #1: Original function, no pattern compilation */ 
public static String makeValidPathName(String rawString) {
    if (rawString==null) return null;
    rawString = rawString.replaceAll("[ÁÀÂÄáàäaàáâãäå]","a");
    rawString = rawString.replaceAll("æ","ae");
    rawString = rawString.replaceAll("çÇ","c");
    rawString = rawString.replaceAll("[ÈÉÊËèéêë]","e");
    rawString = rawString.replaceAll("[ìíîïÍÌÎÏ]","i");
    rawString = rawString.replaceAll("ñÑ","n");                            
    rawString = rawString.replaceAll("[ÓÓÖÔòóôõö]","o");
    rawString = rawString.replaceAll("œ","oe");
    rawString = rawString.replaceAll("[ÙÚÛÜùúûü]", "u");
    rawString = rawString.replaceAll("[ýÿ]","y");
    rawString = rawString.replaceAll("[^a-z A-Z 0-9 \\_ \\+]","");
    rawString = rawString.replaceAll(" ","_");
    return rawString.toLowerCase();
}
/** FUNCTION TO TEST #2: @WCChargin's suggestion: Compile patterns then iterate a map */
public static String makeValidPathName_compiled(String rawString) {
    for (Map.Entry<Pattern, String> e : patterns.entrySet()) {
        rawString=e.getKey().matcher(rawString).replaceAll(e.getValue());
    }
    return rawString.toLowerCase();
}

    /** FUNCTION TO TEST #3: @devconsole's suggestion: Create a LUT with all equivalences then append to a stringbuilder */
public static String makeValidPathName_lut(String rawString) {
    StringBuilder purified=new StringBuilder(rawString.length()); // to avoid resizing
    String aux; // to avoid creating objects inside the for
    for (int i=0, len=rawString.length(); i<len; i++) {
        aux=homeBrewPatterns.get(rawString.charAt(i));
        if (aux!=null) purified.append(aux);
    }
    return purified.toString();
}

    /** FUNCTION TO TEST #4: @Erik Pragt's suggestion on the use of a Normalizer */
public static String makeValidPathName_normalizer(String rawString) {
        return rawString == null ? null
            : Normalizer.normalize(rawString, Form.NFD)
                .replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
}

    /** Test Runner as a Runnable, just do a Handler.post() to run it */

public static Runnable msStringReplacementTest=new Runnable() {

    public void run() {


    String XTAG="REPLACEMENT_TEST";

    Log.d(XTAG, "*** Running Tests");

    int ITERATIONS=1000;

    String[] holder=new String[4];

            /* http://www.adhesiontext.com/ to generate dummy long text ... its late n im tired :) */

    String tester="e arte nací valse ojales i demediada cesé entrañan domó reo ere fiaréis cinesiterapia fina pronto mensuraré la desatufases adulantes toree fusca ramiro hez apolíneo insalvable atas no enorme mí ojo trola chao fas eh borda no consignataria uno ges no arenque ahuyento y daca pío veló tenle baúl ve birria filo rho fui tañe mean taz raicita alimentarías ano defunciones u reabráis repase apreciaran cantorales ungidas naftalina ex guió abomba o escribimos consultarás des usó saudí mercó yod aborrecieses guiri lupia ña adosado jeringara fe cabalgadura tú descasar diseñe amar limarme escobero latamente e oreó lujuria niñez fabularios da inviernen vejé estañarán dictará sil mírales emoción zar claudiquéis ó e ti ñ veintén dañen ríase esmeraras acató noté as mancharlos avisen chocarnos divertidas y relata nuera usé fié élitro baba upa cu enhornan da toa hechizase genesíacos sol fija aplicó gafa pi enes fin asé deal rolar recurran cacen ha id pis pisó democristiano oes eras lañó ch pino fijad ñ quita hondazos ñ determinad vid corearan corrompimiento pamema meso fofas ocio eco amagados pian bañarla balan cuatrimestrales pijojo desmandara merecedor nu asimiladores denunciándote jada ñudos por descifraseis oré pelote ro botó tu sí mejorado compatibilizaciones enyerba oyeses atinado papa borbón pe dé ñora semis prosada luces leí aconteciesen doy colmará o ve te modismos virola garbillen apostabas abstenido ha bajá le osar cima ají adormecéis ñu mohecí orden abrogándote dan acanilladas uta emú ha emporcara manila arribeña hollejo ver puntead ijadeáis chalanesca pechugón silbo arabescos e i o arenar oxidas palear ce oca enmaderen niñez acude topó aguanieves i aconsejaseis lago él roía grafito ceñir jopo nitritos mofe botáis nato compresores ñu asilo amerizan allanándola cuela ó han ice puya alta lío son de sebo antieconómicas alá aceza latitud faca lavé colocándolos concebirlo miserea ñus gro mearé enchivarse";

    long time0=System.currentTimeMillis();
    for (int i=0; i<ITERATIONS; i++) holder[0]=makeValidPathName(tester); // store in an array to avoid possible JIT optimizations
    long elapsed0=System.currentTimeMillis()-time0;

    Log.d(XTAG, "Original REGEX  : "+elapsed0+" ms");

    long time1=System.currentTimeMillis();
    for (int i=0; i<ITERATIONS; i++) holder[1]=makeValidPathName_compiled(tester); // store in an array to avoid possible JIT optimizations
    long elapsed1=System.currentTimeMillis()-time1;

    Log.d(XTAG, "Compiled REGEX  : "+elapsed1+" ms");

    long time2=System.currentTimeMillis();
    for (int i=0; i<ITERATIONS; i++) holder[2]=makeValidPathName_lut(tester); // store in an array to avoid possible JIT optimizations
    long elapsed2=System.currentTimeMillis()-time2;

    Log.d(XTAG, "Character LUT   : "+elapsed2+" ms");

    long time3=System.currentTimeMillis();
    for (int i=0; i<ITERATIONS; i++) holder[3]=makeValidPathName_normalizer(tester); // store in an array to avoid possible JIT optimizations
    long elapsed3=System.currentTimeMillis()-time3;

    Log.d(XTAG, "Java NORMALIZER : "+elapsed3+" ms");

    Log.d(XTAG, "Output 0: "+holder[0]);
    Log.d(XTAG, "Output 1: "+holder[1]);
    Log.d(XTAG, "Output 2: "+holder[2]);
    Log.d(XTAG, "Output 3: "+holder[3]);
    }
};

Guys, thank you very much for your suggestions :) I love stackoverflow :)

like image 302
rupps Avatar asked May 08 '13 23:05

rupps


2 Answers

Build a static Map<Character,String> that maps a character to its replacement string, that is map 'Á' to "a", 'ä' to "a", etc. Also include one-to-one correspondencies, that is map 'a' to "a", and so forth. You get a map with a couple of hundred entries at most.

Now iterate over the characters of the input string and look for a replacement string in your static map. Skip the character if the map does not contain an entry, otherwise append the replacement to a StringBuilder.

like image 52
devconsole Avatar answered Oct 19 '22 07:10

devconsole


If you must use regex, you could precompile your patterns:

Map<Pattern, String> patterns = new HashMap<Pattern, String>();
{ // initializer block (you could do this in constructor also)
    patterns.put(Pattern.compile("[ÁÀÂÄáàäaàáâãäå]") ,"a");
    rawString = rawString.replaceAll("æ","ae");
    // etc.
}

// later...
for (Map.Entry<Pattern, String> e : patterns) {
    rawString = e.getValue().matcher(rawString).replaceAll(e.getKey());
}

The line in the for loop is the key. Here's the dissection:

  • e.getValue() gets the pattern from the map key
  • .matcher(rawString) gets a Matcher object for the pattern, to match instances of the regex to the given string (the raw string)
  • .replaceAll works just like the String method (I believe String uses this, actually)
  • e.getKey() gets the value to be replaced from the map key

Links for further reading:

  • Pattern
  • Matcher and its method replaceAll
  • Map.Entry
like image 2
wchargin Avatar answered Oct 19 '22 09:10

wchargin