Сплит-верблюд с обсаженной или змеей

Я хочу реализовать метод, который, учитывая некоторые верблюды или подчеркнутые String, вернет список отдельных слов, которые составляют это String. Примеры:

  • ISomeCamelCasedString -> {I, Some, Camel, Cased, String}
  • UNDERSCORED_STRING -> {UNDERSCORED, STRING}
  • camelCased_and_UNDERSCORED -> {camel, Cased, и, UNDERSCORED}

Мой подход к решению этого заключается в следующем: я добавляю пробел между каждыми двумя словами, которые должны быть разделены, а затем разделить их на список, используя StringTokenizer

public static List<String> split(String string) {
    StringBuilder separatedWords = new StringBuilder();

    for (int i=0; i<string.length(); i++) {
        char c = string.charAt(i);

        if (i > 0) {    
            char previousC = string.charAt(i-1);

            if ((!Character.isLowerCase(c) && !Character.isUpperCase(previousC)) //  UpperCamelCase, UPPER_DASHED
                    || !Character.isLetterOrDigit(previousC)                    // lower_dashed
                    || (i < string.length() - 1) && Character.isLowerCase(string.charAt(i+1))  && Character.isUpperCase(c)  ) // IAttribute
            {       
                separatedWords.append(" ");
            }
        }       
        if (Character.isLetterOrDigit(c)) {
            separatedWords.append(c);
        }
    }

    List<String> tokens = new ArrayList<String>();

    StringTokenizer tokenizer = new StringTokenizer(separatedWords.toString());

    while(tokenizer.hasMoreTokens()) {
        tokens.add(tokenizer.nextToken());
    }
    return tokens;
}

Как вы можете видеть, это выглядит довольно сложно. Как я могу улучшить это?

11 голосов | спросил Kao 10 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowWed, 10 Sep 2014 11:53:08 +0400 2014, 11:53:08

3 ответа


6

Мое первое впечатление:

  • Реализация выглядит длинной и сложной для чего-то простого.
  • Вы предоставили несколько примеров, чтобы проверить правильность, что очень хорошо.

Моя первая реакция заключается в том, чтобы воплотить примеры тестовых примеров в правильные модульные тесты, а затем заменить реализацию на другой подход и сделать тесты, которые я снова нарушил.

Единичные тесты, прямо из ваших примеров:

@Test
public void testCamelCased() {
    assertEquals(Arrays.asList("I", "Some", "Camel", "Cased", "String"), split("ISomeCamelCasedString"));
}

@Test
public void testSnakeCased() {
    assertEquals(Arrays.asList("UNDERSCORED", "STRING"), split("UNDERSCORED_STRING"));
}

@Test
public void testMixed() {
    assertEquals(Arrays.asList("camel", "Cased", "and", "UNDERSCORED"), split("camelCased_and_UNDERSCORED"));
}

Затем я подумал, что это можно упростить, разбив на какое-нибудь умное регулярное выражение. Я не regex wiz, но их много в Stack Overflow, и я нашел подходящую нить о разделении camelCase на свой слова здесь . Адаптация его к разделению на подчеркивания была относительно легкой, хотя, вероятно, не идеальной, в результате чего:

private static final String RE_CAMELCASE_OR_UNDERSCORE =
        "(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])|_";

public static List<String> split(String string) {
    List<String> words = new ArrayList<String>();
    for (String word : string.split(RE_CAMELCASE_OR_UNDERSCORE)) {
        if (!word.isEmpty()) {
            words.add(word);
        }
    }
    return words;
}

Это, вероятно, не идеально, но намного короче, чем оригинал, и легче понять, как это работает. Если кто-то может понять, как получить RE_CAMELCASE_OR_UNDERSCORE, чтобы он не создавал пустые элементы, тогда метод можно сократить до простого: /р>

return Arrays.asList(string.split(RE_CAMELCASE_OR_UNDERSCORE));

PS: this_is_usually_called_snake_cased, а не «подчеркнуто».

ответил janos 10 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowWed, 10 Sep 2014 16:35:36 +0400 2014, 16:35:36
5

Это представляет интересную проблему. Ваше решение касается меня, потому что оно не решает вещи так, как я. Это само по себе не проблема, но мне пришлось просмотреть ваш код и выяснить, почему вы делаете что-то. У вашего решения есть несколько неожиданных и полезных побочных эффектов.

  • Ваш метод определен, чтобы разделить слово на токены, которые выделены CamelCase или Underscore, но на самом деле расщепляется на любую другую пунктуацию, включая пробел (что-то, что не является Character.isLetterOrDigit(..), поэтому ваше решение получает «привет» и «мир» от «! Hello! .... world !!!»

  • Ваше решение также удачно разбивает некоторые сложные примеры CamelCase, такие как thisIsHTMLInCamelCase, что меня удивило.

Итак, я решил предложить, почему ваше решение было проблемой, но потом обнаружило, что оно работает лучше, чем я думал.

Это заинтриговало меня, поэтому я приступил к пониманию вашего решения, и, в итоге, вы выполняете текстовую разметку при пунктуации и перед очередными капиталами ....

По сути, вы разделяете слово с пробелами между всеми последовательностями символов, а затем добавляете пробел перед столицей комбинации Xx и перед капиталом комбинации xX (или обоих).

Процесс, который вы используете, действительно уродлив. Регулярные выражения - лучший инструмент для работы. Иногда их трудно понять, но они «скомпилируются» до того, что называется Детерминированный конечный автомат .

Таким образом, ваш код делает намного больше, чем описание, поскольку он может обрабатывать пробелы и пунктуации разделенных слов (т. е. предложений), но он делает это медленным способом и требует промежуточных результатов (строка, разделенная пробелом ), который затем обрабатывается после слов.

Я взял эту систему и реализовал ее как регулярные выражения как упражнение:

Сначала я скомпилировал два шаблона: один разделить содержимое на слова, другой - разбить слова в CamelCase и т. д.

private static final Pattern PUNCTSPACE = Pattern.compile("[ \\p{Punct}]+");
private static final Pattern TRANSITION = Pattern.compile("(?<=[^\\p{Lu}])(?=[\\p{Lu}])|(?=[\\p{Lu}][^\\p{Lu}])");

Обратите внимание, что шаблон PUNCTSPACE содержит символ подчеркивания.

Второй шаблон TRANSITION содержит две альтернативы lookahead /lookbehind с нулевой длиной. С CamelCase вы разделяете до первой Столицы после нижней буквы, и вы также разбиваете перед столицей до буквы в нижнем регистре (или в конце слова). Рассмотрим CamelHTMLCase, где мы разделяем до H и до C случая. Они отмечены переходами, один из нижних-верхних, другой - сверху вниз. В каждом случае мы разбиваем перед верхним.

Обратите внимание, что \p{Lu} является регулярным выражением идентификатор для всех букв Юникода в верхнем регистре . \ должен быть экранирован в константе String, поэтому в константе вы увидите два шаблона: [\\p{Lu}] и [^\\p{Lu}] Первый представляет любой символ верхнего регистра, второй представляет любой символ верхнего регистра (включая цифры и другую пунктуацию).

Итак, в шаблоне TRANSITION есть две части:

  • (?<=[^\\p{Lu}])(?=[\\p{Lu}]) - этот первый принимает заданное пятно между двумя символами, и если символ перед пятном не верхний, а символ после пятно является верхним, затем оно расщепляется в этом месте.
  • (?=[\\p{Lu}][^\\p{Lu}]) - этот шаблон занимает пятно перед символом, и, если символ является верхним, а после этого символа ниже, тогда split на месте.

Теперь я не предлагаю, чтобы это было легко, но он работает, и он будет работать быстро.

При расщеплении исходного предложения возможно, что есть какое-то ведущее пространство, пунктуация или другой барахл. Это может привести к тому, что в слове раскол будет начальная пустая строка, поэтому мынужно игнорировать пустые слова.

Объединяя это, вы получаете:

private static final Pattern PUNCTSPACE = Pattern.compile("[ \\p{Punct}]+");
private static final Pattern TRANSITION = Pattern.compile("(?<=[^\\p{Lu}])(?=[\\p{Lu}])|(?=[\\p{Lu}][^\\p{Lu}])");

public static final List<String> deHump(String text) {
    List<String> result = new ArrayList<String>();
    for (String word : PUNCTSPACE.split(text)) {
        if (word.isEmpty()) {
            continue;
        }
        for (String part : TRANSITION.split(word)) {
            result.add(part);
        }
    }
    return result;
}

Теперь, это здорово, он заполнит список, но в мире Java8 с выражениями лямбда это тоже «весело»:

public static final List<String> deHumpLambda(String text) {
    return Arrays.stream(PUNCTSPACE.split(text))
                 .filter(word -> !word.isEmpty())
                 .flatMap(word -> Arrays.asList(TRANSITION.split(word)).stream())
                 .collect(Collectors.toList());
}

Вышеуказанное разбивает входные слова на слова, основанные на пунктуации и символе _, затем принимает каждое непустое слово и разбивает его снова на переходах CamelCase. Он накапливает результаты в списке и возвращает это.

ответил rolfl 10 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowWed, 10 Sep 2014 20:13:17 +0400 2014, 20:13:17
3

Первое, что я сделал бы, - это удалить StringTokenizer и идею вставки разделителя. Если мы знаем, где вставить разделитель, мы можем просто создать токены, используя StringBuilder.

List<String> tokens = new ArrayList<>();
StringBuilder token = new StringBuilder();

for (int i = 0; i < string.length(); i++) {
  char c = string.charAt(i);

  if (i > 0) {    
    char previousC = string.charAt(i-1);

    if ((!Character.isLowerCase(c) && !Character.isUpperCase(previousC)) //  UpperCamelCase, UPPER_DASHED
        || !Character.isLetterOrDigit(previousC)                    // lower_dashed
        || (i < string.length() - 1) && Character.isLowerCase(string.charAt(i+1))  && Character.isUpperCase(c)  ) // IAttribute
    {       
      if (token.length() > 0) {
        tokens.add(token.toString());
        token.setLength(0);
      }
    }
  }       
  if (Character.isLetterOrDigit(c)) {
    token.append(c);
  }
}

if (token.length() > 0) {
  tokens.add(token.toString());
}

return tokens;

Теперь, когда if (i > 0) выглядит уродливым. Переместим специальный случай из цикла.

List<String> tokens = new ArrayList<>();
if (string.length() == 0) {
  return tokens;
}

StringBuilder token = new StringBuilder().append(string.charAt(0));
for (int i = 1; i < string.length(); i++) {
  char c = string.charAt(i);
  char prev = string.charAt(i - 1);
  if ((!Character.isLowerCase(c) && !Character.isUpperCase(prev)) //  UpperCamelCase, UPPER_DASHED
      || !Character.isLetterOrDigit(prev)                    // lower_dashed
      || (i < string.length() - 1) && Character.isLowerCase(string.charAt(i+1)) && Character.isUpperCase(c)) // IAttribute
  {       
    if (token.length() > 0) {
      tokens.add(token.toString());
      token.setLength(0);
    }
  }
  if (Character.isLetterOrDigit(c)) {
    token.append(c);
  }
}

if (token.length() > 0) {
  tokens.add(token.toString());
}

return tokens;

Поскольку условие достаточно длинное, чтобы потребовать три комментария, пришло время переместить его в отдельный метод.

List<String> tokens = new ArrayList<>();
if (string.length() == 0) {
  return tokens;
}

StringBuilder token = new StringBuilder().append(string.charAt(0));
for (int i = 1; i < string.length(); i++) {
  if (token.length() > 0 && isTransition(string, i)) {       
    tokens.add(token.toString());
    token.setLength(0);
  }

  char c = string.charAt(i);
  if (Character.isLetterOrDigit(c)) {
    token.append(c);
  }
}

if (token.length() > 0) {
  tokens.add(token.toString());
}

return tokens;
ответил mjolka 10 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowWed, 10 Sep 2014 17:08:13 +0400 2014, 17:08:13

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132