Jak sprawdzić, czy ciąg znaków zawiera inny ciąg znaków w Javie bez rozróżniania wielkości liter?


386

Powiedzmy, że mam dwie struny

String s1 = "AbBaCca";
String s2 = "bac";

Chcę wykonać zwrot czeku, który s2jest zawarty w s1. Mogę to zrobić za pomocą:

return s1.contains(s2);

Jestem pewien, że contains()rozróżniana jest wielkość liter, jednak nie mogę tego ustalić na podstawie lektury dokumentacji. Jeśli tak, to sądzę, że moją najlepszą metodą byłoby coś takiego:

return s1.toLowerCase().contains(s2.toLowerCase());

Pomijając wszystko, czy istnieje inny (być może lepszy) sposób na osiągnięcie tego bez dbania o rozróżnianie wielkości liter?


DrJava byłby niezwykle łatwym sposobem na przetestowanie tego, gdy dokumentacja Cię zawiedzie. Po prostu wpisz kilka przypadków testowych w oknie Interakcje i powinieneś się dowiedzieć.
EfForEffort

17
Myślę, że odpowiedziałeś na własne pytanie. Nie sądzę, aby którekolwiek z poniższych rozwiązań było lepsze niż to. Ale są zdecydowanie wolniejsze.
Nikolay Dimitrov

7
Twoje rozwiązanie jest prostsze niż którekolwiek z odpowiedzi
LobsterMan

2
Odpowiedź, której szukam ja i wielu tutaj, znajduje się w twoim pytaniu.
Lalit Fauzdar,

1
Twój przykład jest najprostszym, najbardziej czytelnym i prawdopodobnie najlepszym sposobem na zrobienie tego - lepszym niż jakakolwiek odpowiedź, którą widzę.
user1258361,

Odpowiedzi:


320

Tak, zawiera rozróżnia małe i wielkie litery. Możesz użyć java.util.regex.Pattern z flagą CASE_INSENSITIVE do dopasowywania bez rozróżniania wielkości liter:

Pattern.compile(Pattern.quote(wantedStr), Pattern.CASE_INSENSITIVE).matcher(source).find();

EDYCJA: Jeśli s2 zawiera regexowe znaki specjalne (których jest wiele), ważne jest, aby zacytować je jako pierwsze. Poprawiłem odpowiedź, ponieważ ludzie widzą ją po raz pierwszy, ale głosujcie Mattowi Quailowi, odkąd to zauważył.


23
Jak stwierdzono w dokumentacji Pattern.CASE_INSENSITIVE, działa to tylko dla znaków ASCII (tzn. „Ę” nie pasuje do „ä”). Aby UNICODE_CASEto osiągnąć, należy dodatkowo określić flagę.
Philipp Wendler

72
czy to podejście wykorzystuje Patternbardziej wydajne niż s1.toLowerCase().contains(s2.toLowerCase())?
Rajat Gupta,

6
@ user01 Przeprowadziłem analizę prędkości. Zobacz moją odpowiedź na wyniki (pokazałem też szybsze rozwiązanie): stackoverflow.com/a/25379180/1705598
icza

10
Wyjaśniłbym, co się dzieje, gdybyśmy mieli lepsze nazwy zmiennych:Pattern.compile(Pattern.quote(needle), Pattern.CASE_INSENSITIVE).matcher(haystack).find()
John Bowers,

5
@ user01 poprawność pojawia się przed wydajnością, a użycie toLowerCase da potencjalnie niepoprawne wyniki (na przykład przy porównywaniu pewnego greckiego tekstu zawierającego literę Sigma, która ma dwie małe litery dla tej samej wielkiej litery).
Klitos Kyriacou,

266

Jednym z problemów z odpowiedzią Dave'a L. jest to, że s2 zawiera znaczniki regularne, takie jak \ditp.

Chcesz wywołać Pattern.quote () na s2:

Pattern.compile(Pattern.quote(s2), Pattern.CASE_INSENSITIVE).matcher(s1).find();

1
Niezły chwyt Matt. Jestem ciekawy, która metoda jest bardziej wydajna - małe litery zawierają lub twoje rozwiązanie wzorcowe. Czy stosowanie wzoru nie jest mniej wydajne dla pojedynczego porównania, ale bardziej wydajne dla wielu porównań?
Aaron

41
Metoda .toLowerCase (). Zawiera () będzie prawdopodobnie w większości przypadków szybsza. Prawdopodobnie wolałbym ten styl również dla mniejszej złożoności.
Matt Quail

3
@AaronFerguson Tak, rzeczywiście, toLowerCase().contains()jest szybszy. Przeprowadziłem analizę prędkości, zobacz moją odpowiedź dla wyników: stackoverflow.com/a/25379180/1705598
icza

2
@MattQuail nie ma sensu być szybszy, jeśli może być niepoprawny. Na przykład grecka sigma wielka ma dwie małe litery (w zależności od tego, czy pojawia się na końcu słowa, czy nie), a przy próbie dopasowania rozróżniania wielkości liter bez rozróżniania wielkości liter, gdzie podłańcuch kończy się sigmą, łatwo można się pomylić wyniki.
Klitos Kyriacou,

Myślę, że powinniśmy również dodać Pattern.UNICODE_CASEflagę. Czy możesz to potwierdzić?
Thariq Nugrohotomo

160

Możesz użyć

org.apache.commons.lang3.StringUtils.containsIgnoreCase("AbBaCca", "bac");

Biblioteka Apache Commons jest bardzo przydatna do tego typu rzeczy. A ten konkretny może być lepszy niż wyrażenia regularne, ponieważ wyrażenie regularne jest zawsze drogie pod względem wydajności.


1
Czy ktoś wie, czy to szanuje lokalizację?
Charles Wood

12
@CharlesWood To deleguje do String.regionMatches, który wykorzystuje konwersje znakowe, więc nie. Ponadto containsIgnoreCase("ß", "ss")zwraca -1, co jest niepoprawne w każdym locale (niemiecki „sharp s”
zmienia się

Jaki byłby właściwy sposób porównywania niemieckich słów? Wydaje się, że jest to jeden język, który komplikuje każdy sposób porównywania ciągów: P
chomp

1
BTW: język niemiecki został oficjalnie rozszerzony o dużą ß w 2017 r .: de.wikipedia.org/wiki/Gro%C3%9Fes_%C3%9F . Na niemieckich klawiaturach wpisz Shift + Alt Gr + ß -> test: ẞ 😁
Kawu

119

Szybsza implementacja: wykorzystanie String.regionMatches()

Korzystanie z wyrażenia regularnego może być stosunkowo wolne. To (powolne) nie ma znaczenia, jeśli chcesz tylko sprawdzić w jednym przypadku. Ale jeśli masz tablicę lub kolekcję tysięcy lub setek tysięcy ciągów, rzeczy mogą być dość powolne.

Przedstawione poniżej rozwiązanie nie używa wyrażeń regularnych ani toLowerCase()(co jest również powolne, ponieważ tworzy kolejne ciągi znaków i po prostu je wyrzuca po sprawdzeniu).

Rozwiązanie opiera się na metodzie String.regionMatches () , która wydaje się nieznana. Sprawdza, czy 2 Stringregiony pasują do siebie, ale ważne jest to, że ma również przeciążenie przydatnym ignoreCaseparametrem.

public static boolean containsIgnoreCase(String src, String what) {
    final int length = what.length();
    if (length == 0)
        return true; // Empty string is contained

    final char firstLo = Character.toLowerCase(what.charAt(0));
    final char firstUp = Character.toUpperCase(what.charAt(0));

    for (int i = src.length() - length; i >= 0; i--) {
        // Quick check before calling the more expensive regionMatches() method:
        final char ch = src.charAt(i);
        if (ch != firstLo && ch != firstUp)
            continue;

        if (src.regionMatches(true, i, what, 0, length))
            return true;
    }

    return false;
}

Analiza prędkości

Ta analiza prędkości nie oznacza nauki o rakietach, tylko przybliżony obraz szybkości różnych metod.

Porównuję 5 metod.

  1. Nasza metoda zawieraIgnoreCase () .
  2. Konwertując oba ciągi znaków na małe litery i wywołanie String.contains().
  3. Konwertując łańcuch źródłowy na małe litery i wywołuj String.contains()z wstępnie buforowanym podciągiem, małymi literami. To rozwiązanie nie jest już tak elastyczne, ponieważ testuje podciąg predefiend.
  4. Używanie wyrażenia regularnego (zaakceptowana odpowiedź Pattern.compile().matcher().find()...)
  5. Używanie wyrażenia regularnego, ale z wcześniej utworzonym i buforowanym Pattern. To rozwiązanie nie jest już tak elastyczne, ponieważ testuje predefiniowany podciąg.

Wyniki (wywołując metodę 10 milionów razy):

  1. Nasza metoda: 670 ms
  2. 2x toLowerCase () i zawiera (): 2829 ms
  3. 1x toLowerCase () i zawiera () z buforowanym podciągiem: 2446 ms
  4. Regexp: 7180 ms
  5. Regexp z pamięcią podręczną Pattern: 1845 ms

Wyniki w tabeli:

                                            RELATIVE SPEED   1/RELATIVE SPEED
 METHOD                          EXEC TIME    TO SLOWEST      TO FASTEST (#1)
------------------------------------------------------------------------------
 1. Using regionMatches()          670 ms       10.7x            1.0x
 2. 2x lowercase+contains         2829 ms        2.5x            4.2x
 3. 1x lowercase+contains cache   2446 ms        2.9x            3.7x
 4. Regexp                        7180 ms        1.0x           10.7x
 5. Regexp+cached pattern         1845 ms        3.9x            2.8x

Nasza metoda jest czterokrotnie szybsza w porównaniu do mniejszych liter i używania contains(), 10 razy szybsza w porównaniu do używania wyrażeń regularnych, a także 3 razy szybsza, nawet jeśli Patternjest wstępnie buforowana (i traci elastyczność sprawdzania dowolnego podciągu).


Kod testu analitycznego

Jeśli interesuje Cię sposób przeprowadzenia analizy, oto kompletna aplikacja do uruchomienia:

import java.util.regex.Pattern;

public class ContainsAnalysis {

    // Case 1 utilizing String.regionMatches()
    public static boolean containsIgnoreCase(String src, String what) {
        final int length = what.length();
        if (length == 0)
            return true; // Empty string is contained

        final char firstLo = Character.toLowerCase(what.charAt(0));
        final char firstUp = Character.toUpperCase(what.charAt(0));

        for (int i = src.length() - length; i >= 0; i--) {
            // Quick check before calling the more expensive regionMatches()
            // method:
            final char ch = src.charAt(i);
            if (ch != firstLo && ch != firstUp)
                continue;

            if (src.regionMatches(true, i, what, 0, length))
                return true;
        }

        return false;
    }

    // Case 2 with 2x toLowerCase() and contains()
    public static boolean containsConverting(String src, String what) {
        return src.toLowerCase().contains(what.toLowerCase());
    }

    // The cached substring for case 3
    private static final String S = "i am".toLowerCase();

    // Case 3 with pre-cached substring and 1x toLowerCase() and contains()
    public static boolean containsConverting(String src) {
        return src.toLowerCase().contains(S);
    }

    // Case 4 with regexp
    public static boolean containsIgnoreCaseRegexp(String src, String what) {
        return Pattern.compile(Pattern.quote(what), Pattern.CASE_INSENSITIVE)
                    .matcher(src).find();
    }

    // The cached pattern for case 5
    private static final Pattern P = Pattern.compile(
            Pattern.quote("i am"), Pattern.CASE_INSENSITIVE);

    // Case 5 with pre-cached Pattern
    public static boolean containsIgnoreCaseRegexp(String src) {
        return P.matcher(src).find();
    }

    // Main method: perfroms speed analysis on different contains methods
    // (case ignored)
    public static void main(String[] args) throws Exception {
        final String src = "Hi, I am Adam";
        final String what = "i am";

        long start, end;
        final int N = 10_000_000;

        start = System.nanoTime();
        for (int i = 0; i < N; i++)
            containsIgnoreCase(src, what);
        end = System.nanoTime();
        System.out.println("Case 1 took " + ((end - start) / 1000000) + "ms");

        start = System.nanoTime();
        for (int i = 0; i < N; i++)
            containsConverting(src, what);
        end = System.nanoTime();
        System.out.println("Case 2 took " + ((end - start) / 1000000) + "ms");

        start = System.nanoTime();
        for (int i = 0; i < N; i++)
            containsConverting(src);
        end = System.nanoTime();
        System.out.println("Case 3 took " + ((end - start) / 1000000) + "ms");

        start = System.nanoTime();
        for (int i = 0; i < N; i++)
            containsIgnoreCaseRegexp(src, what);
        end = System.nanoTime();
        System.out.println("Case 4 took " + ((end - start) / 1000000) + "ms");

        start = System.nanoTime();
        for (int i = 0; i < N; i++)
            containsIgnoreCaseRegexp(src);
        end = System.nanoTime();
        System.out.println("Case 5 took " + ((end - start) / 1000000) + "ms");
    }

}

6
+1, ale zauważ, że to się nie udaje dla ß(niemieckie ostre S; wielkie litery do SS), a także dla niektórych innych postaci (patrz źródło String.regionMatches, które próbuje obu konwersji).
maaartinus

2
Zawsze testujesz te same ciągi, co nie jest tak naprawdę uczciwym porównaniem. „Jestem” jest zawsze w środku, co może, ale nie musi mieć znaczenia dla różnych metod wyszukiwania. Lepiej byłoby generować losowe ciągi, a także raportować prędkość, gdy nie ma podłańcucha.

2
To wydaje się bardzo zbliżone do metody Apache StringUtils: grepcode.com/file/repo1.maven.org/maven2/org.apache.commons/…
alain.janinm

1
@ alain.janinm Nie widzę podobieństw. Jedyną rzeczą, która wydaje się „bliska”, StringUtils.containsIgnoreCase()jest to, że zarówno moje rozwiązanie, jak i Apache używają regionMatches()metody (w cyklu), ale nawet to nie jest to samo, co wywołuję String.regionMatches()i wywołuje Apache CharSequenceUtils.regionMatches().
icza

2
@icza CharSequenceUtils.regionMatcheswłaśnie dzwoni String.regionMatches. W każdym razie, moim celem było podanie informacji, że jeśli ktoś już korzysta z StringUtils lib, może po prostu to nazwać, ponieważ wydaje się, że jest to skuteczny sposób, jak to udowodnisz za pomocą testu porównawczego. Gdybym nie korzystał z biblioteki Apache lib, zdecydowanie
użyłbym

22

Prostszym sposobem na zrobienie tego (bez obawy o dopasowanie wzorca) byłoby przekonwertowanie obu Strings na małe litery:

String foobar = "fooBar";
String bar = "FOO";
if (foobar.toLowerCase().contains(bar.toLowerCase()) {
    System.out.println("It's a match!");
}

4
Wielkość liter zależy od języka, co oznacza, że ​​będzie działać na twoim komputerze, ale nie powiedzie się klientowi :). patrz komentarz @Adriaan Koster.
kroiz

1
@kroiz, to zależy od tego, skąd pochodził String. Porównanie „foobar” i „FOO” zawsze będzie pasować, jednak jeśli porównujesz informacje wprowadzone przez użytkownika lub treści specyficzne dla języka, masz rację - programista powinien zachować ostrożność.
Phil

16

Tak, można to osiągnąć:

String s1 = "abBaCca";
String s2 = "bac";

String s1Lower = s1;

//s1Lower is exact same string, now convert it to lowercase, I left the s1 intact for print purposes if needed

s1Lower = s1Lower.toLowerCase();

String trueStatement = "FALSE!";
if (s1Lower.contains(s2)) {

    //THIS statement will be TRUE
    trueStatement = "TRUE!"
}

return trueStatement;

Ten kod zwróci ciąg „PRAWDA!” ponieważ okazało się, że twoje postacie były zamknięte.


12
Dużą wadą używania toLowerCase () jest to, że wynik zależy od bieżących ustawień regionalnych. Zobacz: javapapers.com/core-java/…
Adriaan Koster

4
Pytanie faktycznie zawiera lepsze rozwiązanie, ponieważ to nie udaje się dla małych liter s2. Nie mówiąc o takich szczegółach, że ten się nie kompiluje, a jeśli tak, zwróci ciąg.
maaartinus


3

Oto kilka przyjaznych dla Unicode, które możesz zrobić, jeśli włączysz ICU4j. Wydaje mi się, że „ignoruj ​​wielkość liter” jest wątpliwa w przypadku nazw metod, ponieważ chociaż pierwotne porównania siły ignorują wielkość liter, opisuje się je jako zależne od ustawień regionalnych. Ale miejmy nadzieję, że zależy to od lokalizacji w sposób, jakiego mógłby oczekiwać użytkownik.

public static boolean containsIgnoreCase(String haystack, String needle) {
    return indexOfIgnoreCase(haystack, needle) >= 0;
}

public static int indexOfIgnoreCase(String haystack, String needle) {
    StringSearch stringSearch = new StringSearch(needle, haystack);
    stringSearch.getCollator().setStrength(Collator.PRIMARY);
    return stringSearch.first();
}

3

Zrobiłem test, szukając dopasowania łańcucha bez rozróżniania wielkości liter. Mam wektor 150 000 obiektów z ciągiem jako jednym polem i chciałem znaleźć podzbiór pasujący do ciągu. Wypróbowałem trzy metody:

  1. Konwertuj wszystko na małe litery

    for (SongInformation song: songs) {
        if (song.artist.toLowerCase().indexOf(pattern.toLowercase() > -1) {
                ...
        }
    }
  2. Użyj metody String Match ()

    for (SongInformation song: songs) {
        if (song.artist.matches("(?i).*" + pattern + ".*")) {
        ...
        }
    }
  3. Używaj wyrażeń regularnych

    Pattern p = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
    Matcher m = p.matcher("");
    for (SongInformation song: songs) {
        m.reset(song.artist);
        if (m.find()) {
        ...
        }
    }

Wyniki pomiaru czasu są następujące:

  • Brak próby dopasowania: 20 ms

  • Aby obniżyć dopasowanie: 182 ms

  • Ciągi znaków: 278 ms

  • Wyrażenie regularne: 65 ms

Wyrażenie regularne wydaje się najszybsze w tym przypadku użycia.


Dobrze, że umieściłeś wyniki pomiaru czasu. Wszyscy mówią, jak powolny jest regex, ale w rzeczywistości jest bardzo szybki, jeśli trzeba go skompilować tylko raz.
woot

1

Istnieje prosty, zwięzły sposób, używając flagi wyrażenia regularnego (bez rozróżniania wielkości liter {i}):

 String s1 = "hello abc efg";
 String s2 = "ABC";
 s1.matches(".*(?i)"+s2+".*");

/*
 * .*  denotes every character except line break
 * (?i) denotes case insensitivity flag enabled for s2 (String)
 * */

0

Nie jestem pewien, jakie jest twoje główne pytanie, ale tak, w .contains rozróżniana jest wielkość liter.


0
String container = " Case SeNsitive ";
String sub = "sen";
if (rcontains(container, sub)) {
    System.out.println("no case");
}

public static Boolean rcontains(String container, String sub) {

    Boolean b = false;
    for (int a = 0; a < container.length() - sub.length() + 1; a++) {
        //System.out.println(sub + " to " + container.substring(a, a+sub.length()));
        if (sub.equalsIgnoreCase(container.substring(a, a + sub.length()))) {
            b = true;
        }
    }
    return b;
}

Zasadniczo jest to metoda, która wymaga dwóch ciągów. Powinna to być wersja rozróżniająca wielkość liter (). Korzystając z metody zawiera, chcesz sprawdzić, czy jeden ciąg znaków jest zawarty w drugim.

Ta metoda pobiera ciąg „sub” i sprawdza, czy jest równy podciągowi ciągu kontenera o długości równej „sub”. Jeśli spojrzysz na forpętlę, zobaczysz, że iteruje się w podciągach (które są długością „sub”) nad ciągiem kontenera.

Każda iteracja sprawdza, czy podłańcuch łańcucha kontenera jest equalsIgnoreCasepodrzędny.


w zasadzie jest to metoda, która wymaga dwóch ciągów. przypuszcza się, że nie zawiera rozróżnianej wielkości liter zawiera (). Korzystając z metody zawiera, chcesz sprawdzić, czy jeden ciąg znaków jest zawarty w drugim. ta metoda pobiera ciąg znaków, który jest „sub” i sprawdza, czy jest on równy ciągom podrzędnym ciągu kontenera o długości równej „sub”. jeśli spojrzysz na pętlę for, zobaczysz, że iteruje się w ciągach podrzędnych (które są długością „pod”) nad ciągiem kontenera. każda iteracja sprawdza, czy łańcuch podrzędny łańcucha kontenera jest równy z literą podrzędną.
seth

@ Prawdopodobnie powinieneś dodać to do swojej odpowiedzi.
Facet z kapeluszem

2
Jest to najwolniejsza metoda w historii ... a także zawodzi w przypadku języka niemieckiego.
maaartinus

0

Jeśli musisz wyszukać ciąg ASCII w innym ciągu ASCII, takim jak adres URL , moje rozwiązanie będzie lepsze. Testowałem metodę icza i moją pod kątem prędkości i oto wyniki:

  • Przypadek 1 zajął 2788 ms - regiony
  • Przypadek 2 zajął 1520 ms - mój

Kod:

public static String lowerCaseAscii(String s) {
    if (s == null)
        return null;

    int len = s.length();
    char[] buf = new char[len];
    s.getChars(0, len, buf, 0);
    for (int i=0; i<len; i++) {
        if (buf[i] >= 'A' && buf[i] <= 'Z')
            buf[i] += 0x20;
    }

    return new String(buf);
}

public static boolean containsIgnoreCaseAscii(String str, String searchStr) {
    return StringUtils.contains(lowerCaseAscii(str), lowerCaseAscii(searchStr));
}

0
import java.text.Normalizer;

import org.apache.commons.lang3.StringUtils;

public class ContainsIgnoreCase {

    public static void main(String[] args) {

        String in = "   Annulée ";
        String key = "annulee";

        // 100% java
        if (Normalizer.normalize(in, Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").toLowerCase().contains(key)) {
            System.out.println("OK");
        } else {
            System.out.println("KO");
        }

        // use commons.lang lib
        if (StringUtils.containsIgnoreCase(Normalizer.normalize(in, Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", ""), key)) {
            System.out.println("OK");
        } else {
            System.out.println("KO");
        }

    }

}

Dziękujemy za ten fragment kodu, który może zapewnić ograniczoną krótkotrwałą pomoc. Właściwe wyjaśnienie znacznie poprawiłoby jego długoterminową wartość, pokazując, dlaczego jest to dobre rozwiązanie problemu i uczyniłoby to bardziej użytecznym dla przyszłych czytelników z innymi podobnymi pytaniami. Proszę edytować swoją odpowiedź dodać kilka wyjaśnień, w tym założeń już wykonanych.
Toby Speight

0
"AbCd".toLowerCase().contains("abcD".toLowerCase())

2
Czy możesz poprawić swoją odpowiedź, wyjaśniając, w jaki sposób Twój kod rozwiązuje problem?
Isuka

1
Ta odpowiedź została już zasugerowana w wielu innych, bardziej szczegółowych odpowiedziach na to pytanie udzielonych przez innych. Nie sądzę, żeby ta odpowiedź służyła temu celowi.
DaveyDaveDave

0

Możemy używać strumienia z anyMatch i zawiera Java 8

public class Test2 {
    public static void main(String[] args) {

        String a = "Gina Gini Protijayi Soudipta";
        String b = "Gini";

        System.out.println(WordPresentOrNot(a, b));
    }// main

    private static boolean WordPresentOrNot(String a, String b) {
    //contains is case sensitive. That's why change it to upper or lower case. Then check
        // Here we are using stream with anyMatch
        boolean match = Arrays.stream(a.toLowerCase().split(" ")).anyMatch(b.toLowerCase()::contains);
        return match;
    }

}

0

lub możesz zastosować proste podejście i po prostu przekonwertować wielkość ciągu na wielkość ciągu, a następnie użyć metody


-1
String x="abCd";
System.out.println(Pattern.compile("c",Pattern.CASE_INSENSITIVE).matcher(x).find());

-1

Możesz po prostu zrobić coś takiego:

String s1 = "AbBaCca";
String s2 = "bac";
String toLower = s1.toLowerCase();
return toLower.contains(s2);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.