12

I have a list with some Belgian cities with diacritic characters: (Liège, Quiévrain, Franière, etc.) and I would like to transform these special characters to compare with a list containing the same names in upper case, but without the diacritical marks (LIEGE, QUIEVRAIN, FRANIERE)

What i first tried to do was to use the upper case:

LIEGE.contentEqual(Liège.toUpperCase()) but that doesn't fit because the Upper case of Liège is LIÈGE and not LIEGE.

I have some complicated ideas like replacing each character, but that sounds stupid and a long process.

Any ideas on how to do this in a smart way?

g t
  • 6,576
  • 6
  • 44
  • 83
Waza_Be
  • 39,545
  • 47
  • 176
  • 256
  • Similar question including a Java answer http://stackoverflow.com/questions/249087/how-do-i-remove-diacritics-accents-from-a-string-in-net – Rup Jul 09 '10 at 11:03
  • possible duplicate of [Converting Symbols, Accent Letters to English Alphabet.](http://stackoverflow.com/questions/1008802/converting-symbols-accent-letters-to-english-alphabet) – Pentium10 Jul 09 '10 at 11:18
  • Sigh! Used to be you could do this with a single `TR` instruction on 360, but things have gotten a bit more complex since then. – Hot Licks Oct 16 '12 at 21:05

8 Answers8

15

As of Java 6, you can use java.text.Normalizer:

public String unaccent(String s) {
    String normalized = Normalizer.normalize(s, Normalizer.Form.NFD);
    return normalized.replaceAll("[^\\p{ASCII}]", "");
}

Note that in Java 5 there is also a sun.text.Normalizer, but its use is strongly discouraged since it's part of Sun's proprietary API and has been removed in Java 6.

Eric Darchis
  • 18,107
  • 4
  • 22
  • 48
Stijn Van Bael
  • 4,764
  • 2
  • 27
  • 38
  • Unfortunately, I guess that the Android SDK does not provide me the latest Java 6 feature... I get this message: "Normalizer cannot be resolved" and I cannot import java.text.Normalizer – Waza_Be Jul 09 '10 at 11:38
  • 1
    just for reference Java 1.5 is on Android, so no Normalizer – Pentium10 Jul 09 '10 at 12:24
  • nice ! I didn't know this API (but I'm still working with Java 1.5) thanks – Jean-Philippe Caruana Jul 09 '10 at 18:20
  • 2
    For those who still need Java < 1.6, [Apache Commons Lang](http://commons.apache.org/lang/api/org/apache/commons/lang3/StringUtils.html#stripAccents(java.lang.String)) has a `stripAccents` method that uses `java.text.Normalizer` in Java 6, and `sun.text.Normalizer` for earlier versions. (I don't know if the latter is available on Android though.) – Chris R. Donnelly Aug 15 '12 at 15:28
8

Check out this method in Java

private static final String PLAIN_ASCII = "AaEeIiOoUu" // grave
            + "AaEeIiOoUuYy" // acute
            + "AaEeIiOoUuYy" // circumflex
            + "AaOoNn" // tilde
            + "AaEeIiOoUuYy" // umlaut
            + "Aa" // ring
            + "Cc" // cedilla
            + "OoUu" // double acute
    ;

    private static final String UNICODE = "\u00C0\u00E0\u00C8\u00E8\u00CC\u00EC\u00D2\u00F2\u00D9\u00F9"
            + "\u00C1\u00E1\u00C9\u00E9\u00CD\u00ED\u00D3\u00F3\u00DA\u00FA\u00DD\u00FD"
            + "\u00C2\u00E2\u00CA\u00EA\u00CE\u00EE\u00D4\u00F4\u00DB\u00FB\u0176\u0177"
            + "\u00C3\u00E3\u00D5\u00F5\u00D1\u00F1"
            + "\u00C4\u00E4\u00CB\u00EB\u00CF\u00EF\u00D6\u00F6\u00DC\u00FC\u0178\u00FF"
            + "\u00C5\u00E5" + "\u00C7\u00E7" + "\u0150\u0151\u0170\u0171";

    /**
     * remove accented from a string and replace with ascii equivalent
     */
    public static String removeAccents(String s) {
        if (s == null)
            return null;
        StringBuilder sb = new StringBuilder(s.length());
        int n = s.length();
        int pos = -1;
        char c;
        boolean found = false;
        for (int i = 0; i < n; i++) {
            pos = -1;
            c = s.charAt(i);
            pos = (c <= 126) ? -1 : UNICODE.indexOf(c);
            if (pos > -1) {
                found = true;
                sb.append(PLAIN_ASCII.charAt(pos));
            } else {
                sb.append(c);
            }
        }
        if (!found) {
            return s;
        } else {
            return sb.toString();
        }
    }
Pentium10
  • 190,605
  • 114
  • 394
  • 474
  • This method seem long and complicated, but that's the only one I succeed to use! 2 others seem better but doesn't work. Thank a lot. – Waza_Be Jul 09 '10 at 11:43
  • How can you say the Collator doesn't work ? With it, you don't have to use equals, but you have to compare with 0. – Jean-Philippe Caruana Jul 09 '10 at 12:28
  • Never tried, if you have better, propose it. Make sure it does removeAccents and not compares on them. – Pentium10 Jul 09 '10 at 20:13
  • StringUtils.stripAccents does this http://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/StringUtils.html – cquezel May 31 '13 at 19:24
6

This is the simplest solution I've found so far and it works perfectly in our applications.

Normalizer.normalize(string, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", ""); 

But I don't know if the Normalizer is available on the Android platform.

janb
  • 1,007
  • 1
  • 9
  • 10
  • just for reference Java 1.5 is on Android, so no Normalizer – Pentium10 Jul 09 '10 at 12:25
  • The two steps are combined into one by StringUtils.stripAccents which makes use of sun.text.Normalizer before java 6 http://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/StringUtils.html – cquezel May 31 '13 at 19:26
3

If you still need that for Android API 8 or lower (Android 2.2, Java 1.5) where you don't have Normalizer class, here's my code, I think better to modify than Pentium10 answer:

public class StringAccentRemover {

    @SuppressWarnings("serial")
    private static final HashMap<Character, Character> accents  = new HashMap<Character, Character>(){
        {
            put('Ą', 'A');
            put('Ę', 'E');
            put('Ć', 'C');
            put('Ł', 'L');
            put('Ń', 'N');
            put('Ó', 'O');
            put('Ś', 'S');
            put('Ż', 'Z');
            put('Ź', 'Z');

            put('ą', 'a');
            put('ę', 'e');
            put('ć', 'c');
            put('ł', 'l');
            put('ń', 'n');
            put('ó', 'o');
            put('ś', 's');
            put('ż', 'z');
            put('ź', 'z');
        }
    };
    /**
     * remove accented from a string and replace with ascii equivalent
     */
    public static String removeAccents(String s) {
        char[] result = s.toCharArray();
        for(int i=0; i<result.length; i++) {
            Character replacement = accents.get(result[i]);
            if (replacement!=null) result[i] = replacement;
        }
        return new String(result);
    }

}
tutejszy
  • 530
  • 6
  • 22
1

I don't know if it is avaible on Android but on the JVM, you should not reimplement it in your project and reuse already existing code: just use org.apache.commons.lang3.StringUtils#stripAccents

numéro6
  • 3,254
  • 1
  • 16
  • 18
1

The Collator class is a good way to do it (see corresponding javadoc). Here is a unit test that shows how to use it :

import static org.junit.Assert.assertEquals;

import java.text.Collator;
import java.util.Locale;

import org.junit.Test;

public class CollatorTest {
    @Test public void liege() throws Exception {
        Collator compareOperator = Collator.getInstance(Locale.FRENCH);
        compareOperator.setStrength(Collator.PRIMARY);

        assertEquals(0, compareOperator.compare("Liege", "Liege")); // no accent
        assertEquals(0, compareOperator.compare("Liège", "Liege")); // with accent
        assertEquals(0, compareOperator.compare("LIEGE", "Liege")); // case insensitive
        assertEquals(0, compareOperator.compare("LIEGE", "Liège")); // case insensitive with accent

        assertEquals(1, compareOperator.compare("Liege", "Bruxelles"));
        assertEquals(-1, compareOperator.compare("Bruxelles", "Liege"));
    }
}

EDIT : sorry to see my answer did not meet your needs ; maybe it's beause I've presented it as unit test ? Is this ok for you ? I personnaly find it better because it's short and it uses the SDK (no need for String replacement)

Collator compareOperator = Collator.getInstance(Locale.FRENCH);
compareOperator.setStrength(Collator.PRIMARY);
if (compareOperator.compare("Liège", "Liege") == 0) {
    // if we are here, then it's the "same" String
}

hope this helps

Jean-Philippe Caruana
  • 2,530
  • 4
  • 23
  • 46
  • The question related to transforming, i.e. removing diacritics, not just comparing. – Mike Keskinov Aug 07 '12 at 15:41
  • That's not the way I read the question : the final goal is to *compare* strings but he only thinks in terms of transformation before comparing: "I would like to transform these special characters to compare". In my answer, you don't have to transform to compare' that's why I still think my answer is helpfull. – Jean-Philippe Caruana Sep 05 '12 at 09:58
1

For those looking for a clean java solution, use apache commons:

StringUtils.stripAccents("Liège").toUpperCase();

this will return

LIEGE
Laurens
  • 2,474
  • 3
  • 26
  • 40
0

Since class Normalizer is not supported in Froyo or previous Android versions, I have combined this and this (which I both voted up), and optimized it, obtaining a couple of helper methods. Method unaccentify simply converts diacritic chars to plain chars, while method slugify generates a slug for the input string. Hope it can be useful to someone. Here is the source code:

import java.util.Arrays;
import java.util.Locale;  
import java.util.regex.Pattern;  

public class SlugFroyo {
    private static final Pattern STRANGE = Pattern.compile("[^a-zA-Z0-9-]");
    private static final Pattern WHITESPACE = Pattern.compile("[\\s]");

    private static final String DIACRITIC_CHARS = "\u00C0\u00E0\u00C8\u00E8\u00CC\u00EC\u00D2\u00F2\u00D9\u00F9"
            + "\u00C1\u00E1\u00C9\u00E9\u00CD\u00ED\u00D3\u00F3\u00DA\u00FA\u00DD\u00FD"
            + "\u00C2\u00E2\u00CA\u00EA\u00CE\u00EE\u00D4\u00F4\u00DB\u00FB\u0176\u0177"
            + "\u00C3\u00E3\u00D5\u00F5\u00D1\u00F1"
            + "\u00C4\u00E4\u00CB\u00EB\u00CF\u00EF\u00D6\u00F6\u00DC\u00FC\u0178\u00FF"
            + "\u00C5\u00E5" + "\u00C7\u00E7" + "\u0150\u0151\u0170\u0171";

    private static final String PLAIN_CHARS = "AaEeIiOoUu" // grave
            + "AaEeIiOoUuYy" // acute
            + "AaEeIiOoUuYy" // circumflex
            + "AaOoNn" // tilde
            + "AaEeIiOoUuYy" // umlaut
            + "Aa" // ring
            + "Cc" // cedilla
            + "OoUu"; // double acute

    private static char[] lookup = new char[0x180];

    static {
        Arrays.fill(lookup, (char) 0);
        for (int i = 0; i < DIACRITIC_CHARS.length(); i++)
            lookup[DIACRITIC_CHARS.charAt(i)] = PLAIN_CHARS.charAt(i);
    }

    public static String slugify(String s) {
        String nowhitespace = WHITESPACE.matcher(s).replaceAll("-");
        String unaccented = unaccentify(nowhitespace);
        String slug = STRANGE.matcher(unaccented).replaceAll("");
        return slug.toLowerCase(Locale.ENGLISH);
    }

    public static String unaccentify(String s) {
        StringBuilder sb = new StringBuilder(s);
        for (int i = 0; i < sb.length(); i++) {
            char c = sb.charAt(i);
            if (c > 126 && c < lookup.length) {
                char replacement = lookup[c];
                if (replacement > 0)
                    sb.setCharAt(i, replacement);
            }
        }
        return sb.toString();
    }
}
Community
  • 1
  • 1
Giorgio Barchiesi
  • 5,707
  • 3
  • 29
  • 33