Обертка слов с регулярными выражениями

РЕДАКТИРОВАТЬ ДЛЯ ЯЗЫКА - Я знаю, что есть способы сделать это несколькими шагами, или используя LINQ или манипуляцию с символами линии С#. Причина, по которой я использую один вызов регулярного выражения, заключается в том, что я хотел практиковать со сложными шаблонами регулярных выражений. - END EDIT

Я пытаюсь написать одно регулярное выражение, которое будет выполнять перенос слов. Он очень близок к желаемому результату, но я не могу заставить его работать.

Regex.Replace(text, @"(?<=^|\G)(.{1,20}(\s|$))", "$1\r\n", RegexOptions.Multiline)

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

Ввод

"This string is really long. There are a lot of words in it.\r\nHere another line in the string that also very long."

Ожидаемый результат

"This string is \r\nreally long. There \r\nare a lot of words \r\nin it.\r\nHere another line \r\nin the string that \r\nalso very long."

Фактический выход

"This string is \r\nreally long. There \r\nare a lot of words \r\nin it.\r\n\r\nHere another line \r\nin the string that \r\nalso very long.\r\n"

Обратите внимание на двойную "\ r\n" между предложениями, в которых вход уже имел разрыв строки, и добавленный "\ r\n" , который был помещен в конец.

Возможно, существует способ условно применить различные шаблоны замены? И.Е. Если совпадение заканчивается на "\ r\n" , используйте шаблон замены "$ 1", в противном случае используйте шаблон замены "$ 1\r\n".

Здесь ссылка на аналогичный вопрос для обертывания строки без пробела, которую я использовал в качестве отправной точки. Регулярное выражение для поиска несломанного текста и вставки пространства

Ответы

Ответ 1

Это было проверено на Perl.

Изменить. Этот код регулярных выражений имитирует используемое слово wrap (хорошее или плохое) в MS-Windows Notepad.exe

 # MS-Windows  "Notepad.exe Word Wrap" simulation
 # ( N = 16 )
 # ============================
 # Find:     @"(?:((?>.{1,16}(?:(?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|.{1,16})(?:\r?\n)?|(?:\r?\n|$))"
 # Replace:  @"$1\r\n"
 # Flags:    Global     

 # Note - Through trial and error discovery, it apparears Notepad accepts an extra whitespace
 # (possibly in the N+1 position) to help alignment. This matters not because thier viewport hides it.
 # There is no trimming of any whitespace, so the wrapped buffer could be reconstituted by inserting/detecting a
 # wrap point code which is different than a linebreak.
 # This regex works on un-wrapped source, but could probably be adjusted to produce/work on wrapped buffer text.
 # To reconstitute the source all that is needed is to remove the wrap code which is probably just an extra "\r".

 (?:
      # -- Words/Characters 
      (                       # (1 start)
           (?>                     # Atomic Group - Match words with valid breaks
                .{1,16}                 #  1-N characters
                                        #  Followed by one of 4 prioritized, non-linebreak whitespace
                (?:                     #  break types:
                     (?<= [^\S\r\n] )        # 1. - Behind a non-linebreak whitespace
                     [^\S\r\n]?              #      ( optionally accept an extra non-linebreak whitespace )
                  |  (?= \r? \n )            # 2. - Ahead a linebreak
                  |  $                       # 3. - EOS
                  |  [^\S\r\n]               # 4. - Accept an extra non-linebreak whitespace
                )
           )                       # End atomic group
        |  
           .{1,16}                 # No valid word breaks, just break on the N'th character
      )                       # (1 end)
      (?: \r? \n )?           # Optional linebreak after Words/Characters
   |  
      # -- Or, Linebreak
      (?: \r? \n | $ )        # Stand alone linebreak or at EOS
 )

Контрольный пример Ширина обертки N равна 16. Выход соответствует Notepad и по разным ширинам.

 $/ = undef;

 $string1 = <DATA>;

 $string1 =~ s/(?:((?>.{1,16}(?:(?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|.{1,16})(?:\r?\n)?|(?:\r?\n|$))/$1\r\n/g;

 print $string1;

 __DATA__
 hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh
 bbbbbbbbbbbbbbbbEDIT FOR CLARITY - I                    know there are  ways to do this in   multiple steps, or using LINQ or vanilla C#
 string manipulation. 

 The reason I am using a single regex call, is because I wanted practice. with complex
 regex patterns. - END EDIT
 pppppppppppppppppppUf

Выход →

 hhhhhhhhhhhhhhhh
 hhhhhhhhhhhhhhh
 bbbbbbbbbbbbbbbb
 EDIT FOR CLARITY 
 - I              
       know there 
 are  ways to do 
 this in   
 multiple steps, 
 or using LINQ or 
 vanilla C#
 string 
 manipulation. 

 The reason I am 
 using a single 
 regex call, is 
 because I wanted 
 practice. with 
 complex
 regex patterns. 
 - END EDIT
 pppppppppppppppp
 pppUf

Ответ 2

Я бы написал метод расширения, подобный этому.

var input = "This string is really long. There are a lot of words in it.\r\nHere another line in the string that also very long.";

var lines = input.SplitByLength(20).ToList();

public static partial class MyExtensions
{
    public static  IEnumerable<string> SplitByLength(this string input, int maxLen)
    {
        return Regex.Split(input, @"(.{1," + maxLen + @"})(?:\s|$)")
                    .Where(x => x.Length > 0)
                    .Select(x => x.Trim());
    }
}

OUTPUT

This string is
really long. There
are a lot of words
in it.
Here another line
in the string that's
also very long.

Ответ 3

Добавьте место для "\ r\n" в первый проход, затем замените любые значения \r\n'placeholder 'на\r\n, в конце концов сделайте третий проход и замените оставшиеся заполнители на\г\п.

Например, используя \u0000 в качестве заполнителя

Это, конечно, работает только в том случае, если исходные строки не содержат null

    string text = "This string is really long. There are a lot of words in it.\r\nHere another line in the string that also very long.";
    Console.WriteLine(text);

    text = Regex.Replace(text, @"(?<=^|\G)(.{1,20}(\s|$))", "$1\u0000", RegexOptions.Multiline);
    // break added after original
    text = Regex.Replace(text, "\r\n\u0000", "\r\n", RegexOptions.Multiline);
    text = Regex.Replace(text, "\u0000", "\r\n", RegexOptions.Multiline);
    Console.WriteLine(text);

Ответ 4

Так как вы не укажете, что вы хотите, если одно слово больше, чем количество символов в wordwrap, я решил разделить на максимальное количество символов (20 в этом случае), если слово длиннее чем 20:

resultString = Regex.Replace(subjectString, @"(.{1,19}\S)(?:\s+|$)|(.{20})", @"$1$2
", RegexOptions.Multiline);

После $1 $2 есть LF, не уверен, как он появится здесь. Возможно, вы сможете вставить туда \r\n, но что-то не работает на моем эмуляторе:

resultString = Regex.Replace(subjectString, @"(.{1,19}\S)(?:\s+|$)|(.{20})", @"$1$2\r\n", RegexOptions.Multiline);    

Ответ 5

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

# assuming a max line length of 16
(?:
    [^\r\n]{1,16}(?=\s|$)       # non-linebreaking characters followed by a space 
                                #    or end-of-string, up to the max line length
    |[^\r\n]{16}                # Or for really long words: a sequence of non-breaking  
                                #    characters exactly the line length
    |(?<=\n)\r?\n               # Or blank lines: a line break following another line break.  This works for \n or \r\n styles.
)

Подобно L.B Я помещаю регулярное выражение в метод расширения, WordWrap:

void Main()
{
    var lineLen = 25;
    var test1 = "Some random words like calendar boat and breathe.\nAnd an extra line.\n\n\nAnd here one that has to break in the middle because there are no spaces:\n"
        + String.Join("", Enumerable.Range(1, lineLen + 5).Select(i => (i % 10).ToString()));

    var test2 = test1.Replace("\n","\r\n");

    StringHelper.StringRuler(lineLen).Dump("ruler");
    String.Join("\n", test1.WordWrap(lineLen)).Dump("test 1");
    String.Join("\r\n", test2.WordWrap(lineLen)).Dump("test 2");
}

public static class StringHelper {

    public static IEnumerable<String> WordWrap(this string source, int lineLength) {
        return new Regex(
            @"(?:[^\r\n]{1,lineLength}(?=\s|$)|[^\r\n]{lineLength}|(?<=\n)\r?\n)"
                .Replace("lineLength", lineLength.ToString()))
            .Matches(source)
            .Cast<Match>()  // http://stackoverflow.com/a/7274451/555142
            .Select(m=>m.Value.Trim());
    }

    public static string StringRuler(int lineLength) {
        return 
            String.Join("", Enumerable.Range(1, lineLength)
                .Select(i => ((i % 10) == 0 ? (i / 10).ToString() : " "))) + "\n" 
            + String.Join("", Enumerable.Range(1, lineLength).Select(i => (i % 10).ToString())) + "\n" 
            + String.Join("", Enumerable.Range(1, lineLength).Select(i => "-")); 
    }

}

Тестирование с помощью LinqPad (Instant Share). Существует два теста: первый для разрывов строк \n, второй - для разрывов строки \r\n.

ruler

         1         2     
1234567890123456789012345
------------------------- 

test 1

Some random words like
calendar boat and
breathe.
And an extra line.


And here one that has
to break in the middle
because there are no
spaces:
1234567890123456789012345
67890 


test 2

Some random words like
calendar boat and
breathe.
And an extra line.


And here one that has
to break in the middle
because there are no
spaces:
1234567890123456789012345
67890 

Ответ 6

Мое решение в JS:

function wordWrap(s, width) {
  var r = '(?:(.{1,' + width + '})[ \\r\\t]+|(.{' + width + '}))(?!$)';
  r = new RegExp(r, 'g');
  // console.log(r);
  return s.replace(r, '$1$2\n');
}