Разделить строку с несколькими разделителями, используя только методы String

Я хочу разбить строку на токены.

Я разорвал другой вопрос о переполнении стека - Эквивалентно StringTokenizer с несколькими разделителями символов, но я хочу знать, можно ли это сделать только с помощью строковых методов (.equals(),.startsWith() и т.д.). Я не хочу использовать RegEx, класс StringTokenizer, шаблоны, сопоставления или что-то другое, кроме String, если на то пошло.

Например, так я хочу вызвать метод

String[] delimiters = {" ", "==", "=", "+", "+=", "++", "-", "-=", "--", "/", "/=", "*", "*=", "(", ")", ";", "/**", "*/", "\t", "\n"};
        String splitString[] = tokenizer(contents, delimiters);

И это код, который я разорвал на другой вопрос (я не хочу этого делать).

    private String[] tokenizer(String string, String[] delimiters) {
        // First, create a regular expression that matches the union of the
        // delimiters
        // Be aware that, in case of delimiters containing others (example &&
        // and &),
        // the longer may be before the shorter (&& should be before &) or the
        // regexpr
        // parser will recognize && as two &.
        Arrays.sort(delimiters, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return -o1.compareTo(o2);
            }
        });
        // Build a string that will contain the regular expression
        StringBuilder regexpr = new StringBuilder();
        regexpr.append('(');
        for (String delim : delimiters) { // For each delimiter
            if (regexpr.length() != 1)
                regexpr.append('|'); // Add union separator if needed
            for (int i = 0; i < delim.length(); i++) {
                // Add an escape character if the character is a regexp reserved
                // char
                regexpr.append('\\');
                regexpr.append(delim.charAt(i));
            }
        }
        regexpr.append(')'); // Close the union
        Pattern p = Pattern.compile(regexpr.toString());

        // Now, search for the tokens
        List<String> res = new ArrayList<String>();
        Matcher m = p.matcher(string);
        int pos = 0;
        while (m.find()) { // While there a delimiter in the string
            if (pos != m.start()) {
                // If there something between the current and the previous
                // delimiter
                // Add it to the tokens list
                res.add(string.substring(pos, m.start()));
            }
            res.add(m.group()); // add the delimiter
            pos = m.end(); // Remember end of delimiter
        }
        if (pos != string.length()) {
            // If it remains some characters in the string after last delimiter
            // Add this to the token list
            res.add(string.substring(pos));
        }
        // Return the result
        return res.toArray(new String[res.size()]);
    }
    public static String[] clean(final String[] v) {
        List<String> list = new ArrayList<String>(Arrays.asList(v));
        list.removeAll(Collections.singleton(" "));
        return list.toArray(new String[list.size()]);
    }

Изменить: я ТОЛЬКО хочу использовать строковые методы charAt, equals, equalsIgnoreCase, indexOf, length и substring

Ответ 1

ИЗМЕНИТЬ: Мой первоначальный ответ не совсем сделал трюк, он не включал разделители в результирующий массив и использовал метод String.split(), который не был разрешен.

Здесь мое новое решение, которое разбивается на 2 метода:

/**
 * Splits the string at all specified literal delimiters, and includes the delimiters in the resulting array
 */
private static String[] tokenizer(String subject, String[] delimiters)  { 

    //Sort delimiters into length order, starting with longest
    Arrays.sort(delimiters, new Comparator<String>() {
        @Override
        public int compare(String s1, String s2) {
          return s2.length()-s1.length();
         }
      });

    //start with a list with only one string - the whole thing
    List<String> tokens = new ArrayList<String>();
    tokens.add(subject);

    //loop through the delimiters, splitting on each one
    for (int i=0; i<delimiters.length; i++) {
        tokens = splitStrings(tokens, delimiters, i);
    }

    return tokens.toArray(new String[] {});
}

/**
 * Splits each String in the subject at the delimiter
 */
private static List<String> splitStrings(List<String> subject, String[] delimiters, int delimiterIndex) {

    List<String> result = new ArrayList<String>();
    String delimiter = delimiters[delimiterIndex];

    //for each input string
    for (String part : subject) {

        int start = 0;

        //if this part equals one of the delimiters, don't split it up any more
        boolean alreadySplit = false;
        for (String testDelimiter : delimiters) {
            if (testDelimiter.equals(part)) {
                alreadySplit = true;
                break;
            }
        }

        if (!alreadySplit) {
            for (int index=0; index<part.length(); index++) {
                String subPart = part.substring(index);
                if (subPart.indexOf(delimiter)==0) {
                    result.add(part.substring(start, index));   // part before delimiter
                    result.add(delimiter);                      // delimiter
                    start = index+delimiter.length();           // next parts starts after delimiter
                }
            }
        }
        result.add(part.substring(start));                      // rest of string after last delimiter          
    }
    return result;
}

Оригинальный ответ

Я замечаю, что вы используете Pattern, когда вы сказали, что хотите использовать только методы String.

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

Здесь код:

private String[] tokenizer(String string, String[] delimiters)  {       

    //replace all specified delimiters with one
    for (String delimiter : delimiters) {
        while (string.indexOf(delimiter)!=-1) {
            string = string.replace(delimiter, "{split}");
        }
    }

    //now split at the new delimiter
    return string.split("\\{split\\}");

}

Мне нужно использовать String.replace(), а не String.replaceAll(), потому что replace() принимает литеральный текст, а replaceAll() принимает аргумент regex, а предоставленные разделители имеют литеральный текст.

Вот почему мне также нужен цикл while, чтобы заменить все экземпляры каждого разделителя.

Ответ 2

Использование только не-регулярных методов String... Я использовал метод startsWith (...), который не был включен в исключительный список перечисленных вами методов, поскольку он просто сравнивает строки, а не сравнение регулярных выражений.

Следующий impl:

public static void main(String ... params) {
    String haystack = "abcdefghijklmnopqrstuvwxyz";
    String [] needles = new String [] { "def", "tuv" };
    String [] tokens = splitIntoTokensUsingNeedlesFoundInHaystack(haystack, needles);
    for (String string : tokens) {
        System.out.println(string);
    }
}

private static String[] splitIntoTokensUsingNeedlesFoundInHaystack(String haystack, String[] needles) {
    List<String> list = new LinkedList<String>();
    StringBuilder builder = new StringBuilder();
    for(int haystackIndex = 0; haystackIndex < haystack.length(); haystackIndex++) {
        boolean foundAnyNeedle = false;
        String substring = haystack.substring(haystackIndex);
        for(int needleIndex = 0; (!foundAnyNeedle) && needleIndex < needles.length; needleIndex ++) {
            String needle = needles[needleIndex];
            if(substring.startsWith(needle)) {
                if(builder.length() > 0) {
                    list.add(builder.toString());
                    builder = new StringBuilder();
                }
                foundAnyNeedle = true;
                list.add(needle);
                haystackIndex += (needle.length() - 1);
            }
        }
        if( ! foundAnyNeedle) {
            builder.append(substring.charAt(0));
        }
    }
    if(builder.length() > 0) {
        list.add(builder.toString());
    }
    return list.toArray(new String[]{});
}

выходы

abc
def
ghijklmnopqrs
tuv
wxyz

Примечание... Этот код является демонстрационным. В случае, если одним из разделителей является любая пустая строка, она будет плохо себя вести и в конечном итоге сбой с OutOfMemoryError: кучей Java-памяти после потребления большого количества процессоров.

Ответ 3

Насколько я понял вашу проблему, вы можете сделать что-то вроде этого -

public Object[] tokenizer(String value, String[] delimeters){
    List<String> list= new ArrayList<String>();
    for(String s:delimeters){
        if(value.contains(s)){
            String[] strArr=value.split("\\"+s);
            for(String str:strArr){
                list.add(str);
                if(!list.contains(s)){
                    list.add(s);
                }
            }
        }
    }
    Object[] newValues=list.toArray();
    return newValues;
}

Теперь в основном методе вызывается эта функция -

String[] delimeters = {" ", "{", "==", "=", "+", "+=", "++", "-", "-=", "--", "/", "/=", "*", "*=", "(", ")", ";", "/**", "*/", "\t", "\n"};
    Object[] obj=st.tokenizer("ge{ab", delimeters); //st is the reference of the other class. Edit this of your own.
    for(Object o:obj){
        System.out.println(o.toString());
    }

Ответ 4

Предложение:

  private static int INIT_INDEX_MAX_INT = Integer.MAX_VALUE;

  private static String[] tokenizer(final String string, final String[] delimiters) {
    final List<String> result = new ArrayList<>();

    int currentPosition = 0;
    while (currentPosition < string.length()) {
      // plan: search for the nearest delimiter and its position
      String nextDelimiter = "";
      int positionIndex = INIT_INDEX_MAX_INT;
      for (final String currentDelimiter : delimiters) {
        final int currentPositionIndex = string.indexOf(currentDelimiter, currentPosition);
        if (currentPositionIndex < 0) { // current delimiter not found, go to the next
          continue;
        }
        if (currentPositionIndex < positionIndex) { // we found a better one, update
          positionIndex = currentPositionIndex;
          nextDelimiter = currentDelimiter;
        }
      }
      if (positionIndex == INIT_INDEX_MAX_INT) { // we found nothing, finish up
        final String finalPart = string.substring(currentPosition, string.length());
        result.add(finalPart);
        break;
      }
      // we have one, add substring + delimiter to result and update current position
      // System.out.println(positionIndex + ":[" + nextDelimiter + "]"); // to follow the internals
      final String stringBeforeNextDelimiter = string.substring(currentPosition, positionIndex);
      result.add(stringBeforeNextDelimiter);
      result.add(nextDelimiter);
      currentPosition += stringBeforeNextDelimiter.length() + nextDelimiter.length();
    }

    return result.toArray(new String[] {});
  }

Примечания:

  • Я добавил больше комментариев, чем необходимо. Думаю, это помогло бы в этом случае.
  • Выполнение этого довольно плохо (можно улучшить с помощью древовидных структур и хэшей). Это не было частью спецификации.
  • Приоритет оператора не указан (см. мой комментарий к вопросу). Это не было частью спецификации.

Я ТОЛЬКО хочу использовать строковые методы charAt, equals, equalsIgnoreCase, indexOf, length и substring

Check. Функция использует только indexOf(), length() и substring()

Нет, я имею в виду возвращенные результаты. Например, если мой разделитель был {, а строка была ge{ab, мне нужен массив с ge, { и ab

Check:

  private static void test() {
    final String[] delimiters = { "{" };
    final String contents = "ge{ab";
    final String splitString[] = tokenizer(contents, delimiters);
    final String joined = String.join("", splitString);
    System.out.println(Arrays.toString(splitString));
    System.out.println(contents.equals(joined) ? "ok" : "wrong: [" + contents + "]#[" + joined + "]");
  }
  // [ge, {, ab]
  // ok

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

Ответ 5

Возможно, я не полностью понял вопрос, но у меня создается впечатление, что вы хотите переписать метод Java String split(). Я бы посоветовал вам взглянуть на эту функцию, посмотреть, как это сделать и начать оттуда.

Ответ 6

Честно говоря, вы можете использовать Apache Commons Lang. Если вы проверите исходный код библиотеки, вы заметите, что он не использует Regex. В методе [StringUtils.split] используется только String и множество флагов (http://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/StringUtils.html#split(java.lang.String, java.lang.String)).

В любом случае, посмотрите код, используя Apache Commons Lang.

import org.apache.commons.lang.StringUtils;
import org.junit.Assert;
import org.junit.Test;

public class SimpleTest {

    @Test
    public void testSplitWithoutRegex() {
        String[] delimiters = {"==", "+=", "++", "-=", "--", "/=", "*=", "/**", "*/",
            " ", "=", "+", "-", "/", "*", "(", ")", ";", "\t", "\n"};

        String finalDelimiter = "#";

        //check if demiliter can be used
        boolean canBeUsed = true;

        for (String delimiter : delimiters) {
            if (finalDelimiter.equals(delimiter)) {
                canBeUsed = false;
                break;
            }
        }

        if (!canBeUsed) {
            Assert.fail("The selected delimiter can't be used.");
        }

        String s = "Assuming that we have /** or /* all these signals like == and; / or * will be replaced.";
        System.out.println(s);

        for (String delimiter : delimiters) {
            while (s.indexOf(delimiter) != -1) {
                s = s.replace(delimiter, finalDelimiter);
            }
        }

        String[] splitted = StringUtils.split(s, "#");

        for (String s1 : splitted) {
            System.out.println(s1);
        }

    }
}

Надеюсь, это поможет.

Ответ 7

Проще, как я мог его получить...

public class StringTokenizer {
    public static String[] split(String s, String[] tokens) {
        Arrays.sort(tokens, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.length()-o1.length();
            }
        });

        LinkedList<String> result = new LinkedList<>();

        int j=0;
        for (int i=0; i<s.length(); i++) {
            String ss = s.substring(i);

            for (String token : tokens) {
                if (ss.startsWith(token)) {
                    if (i>j) {
                        result.add(s.substring(j, i));
                    }

                    result.add(token);

                    j = i+token.length();
                    i = j-1;

                    break;
                }
            }
        }

        result.add(s.substring(j));

        return result.toArray(new String[result.size()]);
    }
}

Он создает много новых объектов - и может быть оптимизирован путем написания пользовательской реализации startsWith(), которая сравнивает char с char строки.

@Test
public void test() {
    String[] split = StringTokenizer.split("this==is the most>complext<=string<<ever", new String[] {"=", "<", ">", "==", ">=", "<="});

    assertArrayEquals(new String[] {"this", "==", "is the most", ">", "complext", "<=", "string", "<", "<", "ever"}, split);
}

проходит нормально:)

Ответ 8

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

public static String[] tokenizer(String text, String[] delims) {
    for(String delim : delims) {
        int i = text.indexOf(delim);

        if(i >= 0) {

            // recursive call
            String[] tail = tokenizer(text.substring(i + delim.length()), delims);

            // return [ head, middle, tail.. ]
            String[] list = new String[tail.length + 2];
            list[0] = text.substring(0,i);
            list[1] = delim;
            System.arraycopy(tail, 0, list, 2, tail.length);
            return list;
        }
    }
    return new String[] { text };
}

Протестировано с использованием того же модульного теста из другого ответа

public static void main(String ... params) {
    String haystack = "abcdefghijklmnopqrstuvwxyz";
    String [] needles = new String [] { "def", "tuv" };
    String [] tokens = tokenizer(haystack, needles);
    for (String string : tokens) {
        System.out.println(string);
    }
}

Выход

abc
def
ghijklmnopqrs
tuv
wxyz

Было бы немного более элегантно, если бы у Java была лучшая поддержка собственных массивов.