Какая хорошая эвристика для определения ширины табуляции, используемой в исходном файле?

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

Мотивация к этому - написание расширения для редактора SubEthaEdit. SubEthaEdit, к сожалению, не делает ширину табуляции доступной для сценариев, поэтому я собираюсь угадать ее на основе текста.

Подходящая эвристика должна:

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

Некоторые упрощающие факторы:

  • Можно предположить, что по крайней мере одна строка имеет отступ.
  • Предполагается, что ширина табуляции составляет не менее двух пробелов.
  • Можно с уверенностью предположить, что отступ делается только с пробелами. Дело не в том, что я имею что-то против вкладок - наоборот, я сначала проверю, используются ли какие-либо вкладки для отступов, и обработаю их отдельно. Это действительно означает, что табуляции и пробелы, смешанные с отступами, могут не обрабатываться должным образом, но я не считаю это важным.
  • Можно предположить, что нет строк, содержащих только пробелы.
  • Не со всеми языками нужно обращаться правильно. Например, успех или неудача с такими языками, как lisp и go, будут совершенно неактуальными, поскольку они обычно не имеют отступа вручную.
  • Совершенства не требуется. Мир не закончится, если несколько строк нужно будет время от времени корректировать вручную.

Какой подход вы бы выбрали, и в чем вы видите его преимущества и недостатки?

Если вы хотите указать рабочий код в своем ответе, лучшим подходом, вероятно, будет использование сценария оболочки, который считывает исходный файл из stdin и записывает ширину табуляции в stdout. Псевдокод или четкое описание словами тоже подойдут.

Некоторые результаты

Чтобы протестировать разные стратегии, мы можем применять разные стратегии к файлам в стандартных библиотеках для языковых дистрибутивов, поскольку они предположительно соответствуют стандартным для языка отступам. Я рассмотрю библиотеки Python 2.7 и Ruby 1.8 (системный фреймворк устанавливается в Mac OS X 10.7), для которых ожидаемая ширина вкладок составляет 4 и 2 соответственно. Исключаются те файлы, строки которых начинаются с символа табуляции или не имеют строк, начинающихся как минимум с двух пробелов.

Python:

                     Right  None  Wrong
Mode:                 2523     1    102
First:                2169     1    456
No-long (12):         2529     9     88
No-long (8):          2535    16     75
LR (changes):         2509     1    116
LR (indent):          1533     1   1092
Doublecheck (10):     2480    15    130
Doublecheck (20):     2509    15    101

Рубин:

                     Right  None  Wrong
Mode:                  594    29     51
First:                 578     0     54
No-long (12):          595    29     50
No-long (8):           597    29     48
LR (changes):          585     0     47
LR (indent):           496     0    136
Doublecheck (10):      610     0     22
Doublecheck (20):      609     0     23

В этих таблицах «Право» следует рассматривать как определение ширины табуляции по стандарту языка, «Неправильно» - как ненулевую ширину табуляции, не равную ширине стандартной табуляции, а «Нет» как нулевую ширину табуляции или ее отсутствие. отвечать. «Режим» - это стратегия выбора наиболее часто встречающегося изменения отступа; «Первый» - это отступ первой строки с отступом; «No-long» - это стратегия FastAl по исключению строк с большим отступом и переходу в режим с числом, указывающим максимально допустимое изменение отступа; «LR» - это стратегия Patrick87, основанная на линейной регрессии, с вариантами, основанными на изменении отступа между строками и на абсолютном отступе строк; «Doublecheck» (не мог устоять перед каламбуром!) - это модификация Марком стратегии FastAl, ограничивающая возможную ширину табуляции и проверяющую, часто ли встречается половина модального значения, с двумя разными порогами для выбора меньшей ширины.


person Michael J. Barber    schedule 04.08.2011    source источник
comment
ИМО, разумный подход: если ts = 8 не работает, отклонить файл и пожаловаться автору.   -  person William Pursell    schedule 04.08.2011
comment
@ Уильям Перселл. Это немного строже, чем я ожидал. ;)   -  person Michael J. Barber    schedule 04.08.2011
comment
Назначение награды, чтобы попытаться получить еще несколько идей. Я также добавлю базовый ответ, который нужно делать не хуже.   -  person Michael J. Barber    schedule 17.08.2011


Ответы (7)


Ваш выбор (реально) 2,3,4,5,6,7,8.

Я бы просканировал первые 50-100 строк или около того, используя что-то вроде того, что предлагал @FastAl. Я бы, вероятно, склонился к тому, чтобы просто слепо вытаскивать количество пробелов из начала любой строки с текстом и подсчитывать длину строки пробелов. Левая линия обрезки и двойная длина бега кажутся пустой тратой, если у вас есть регулярное выражение. Кроме того, я бы сделал System.Math.abs(indent - previndent), чтобы вы получили данные для снятия отступа. Регулярное выражение будет таким:

row.matches('^( +)[^ ]') # grab all the spaces from line start to non-space.

Как только вы получите статистику, у которой из 7 вариантов наибольшее количество, используйте ее в качестве первого предположения. Для 8, 6 и 4 вы должны проверить, есть ли также значительный счет (2-е место или более 10% или какой-либо другой дешевый эвристический метод) для 4 и 2, 3 или 2. Если много 12 ( или 9s), которые могут намекать, что 4 (или 3) также лучший выбор, чем 8 (или 6). Падение или добавление более двух уровней за раз (обычно закрытые конечные скобки) очень редко.

Нерелевантное бормотание

Единственная проблема, которую я вижу, заключается в том, что в старом коде .c, в частности, присутствует этот неприятный паттерн:

code level 0
/* Fancy comments get weird spacing because there 
 * is an extra space beyond the *
 * looks like one space!
 */
  code indent (2 spaces)
  /* Fancy comments get weird spacing because there 
   * is an extra space beyond the *
   * looks like three spaces!
   */

code level 0
  code indent (2 spaces)
  /* comment at indent level 1
     With no stars you wind up with 2 spaces + 3 spaces.
  */

Фу. Я не знаю, как вы относитесь к таким стандартам комментариев. Для кода с буквой «c» вам, возможно, придется иметь дело с комментариями, специально предназначенными для версии 2.0 ... но я бы просто проигнорировал это сейчас.

Ваша последняя проблема связана со строками, которые не соответствуют вашим предположениям. Я бы посоветовал «вставить» их в глубину, а затем оставить лишние пробелы на месте. Если нужно исправить, я сделаю так: rowtabdepth = ceiling((rowspacecount - (tabwidth/2)) / tabwidth)

person Mark    schedule 24.08.2011
comment
Это дает хорошее улучшение для стандартной библиотеки ruby, но на самом деле небольшую потерю для python - в абсолютном выражении это выглядит больше, но в процентном отношении выигрыш для ruby ​​перевешивает потери для python. Просматривая, где Python ошибается, оказывается, что файлов для исправления не так много больше, чем это делает no-long8. Использование порога в 20% кажется немного лучше, чем ваше предположение в 10%. Я нашел ваше описание немного неясным, читая, как будто вы работаете с абсолютным отступом, но ссылаетесь на FastAl, который касается различий; возможно, нужно немного отредактировать. - person Michael J. Barber; 25.08.2011
comment
Продуманный макет, такой как C, о котором вы говорите, - это именно то, почему я подчеркнул не все языки, совершенство не требуется. Даже с точной шириной табуляции будет сложно вставить текст, соответствующий форматированию: лучше вызвать indent или что-то подобное. - person Michael J. Barber; 25.08.2011
comment
Вы правы, я плохо совмещал два ответа. : - / Я подправлю ответ, чтобы сдвинуть его в сторону относительной табуляции, как у @ FastAl. - person Mark; 26.08.2011

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

На самом деле мне пришлось решить аналогичную проблему в криптографии, чтобы получить правильную длину кодового слова в полиалфавитном шифре . Этот вид шифрования представляет собой базовое шифрование Цезаря (каждая буква алфавита перемещается на n букв), где криптографическое слово используется для различного перемещения букв (n-е буква открытого текста перемещается на букву mod (nth, length (cryptword)) криптографического слова). Лучшее оружие - автокорреляция.

Алгоритм будет таким:

  1. Удалите все символы после того, как пробел в начале строки закончился - оставьте маркеры конца строки нетронутыми.
  2. удалить строки с нулевым пробелом (так как это только пустые строки)
  3. Подсчитайте ширину пробелов для каждой строки и сохраните ее в массиве lengths.
  4. Автокорреляция: цикл до максимального оценочного числа - может быть довольно большим, например 32 или около того - текущая итерация должна быть i. Для каждой итерации рассчитайте расстояние между каждой записью и ой записью. Подсчитайте количество расстояний = 0 (одинаковые значения для записей nth и (n + i) th), сохраните в массиве для ключа i .
  5. Теперь у вас есть массив совпадений с одинаковыми парами. Вычислите среднее значение этого массива и удалите все значения, близкие к этому среднему (оставляя пики автокорреляции). Пики будут кратны наименьшему значению, которое будет искомым количеством пробелов, используемых для отступа.

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

И да, тогда я взломал полиалфавитный шифртекст с автокорреляцией. ;)

person Lars    schedule 23.08.2011
comment
Очень интересный подход. Прошло много времени с тех пор, как я занимался какой-либо обработкой сигналов, но я думаю, что могу понять, как это работает. По сути, вы предлагаете способ сделать дешевое преобразование Фурье, основанное на предположении, что преобладают низкие частоты (то есть ограничение на шаге 4 является формой фильтра нижних частот). На пятом шаге отбрасываются значения, которые мало влияют на спектр мощности в частотной области. Это звучит примерно правильно? - person Michael J. Barber; 23.08.2011
comment
Реализуя это, я не нахожу ваш шаг 4 очень ясным: что представляет собой i? Кажется, это разница между индексом сравниваемых строк, но как это преобразовать в ширину табуляции в конце? Может быть, это должна быть двухмерная автокорреляция? - person Michael J. Barber; 23.08.2011
comment
@ michael-j-barber звучит примерно правильно, но, честно говоря, обработка сигналов - не лучшая область моих знаний. Я также читал о сходстве с БПФ. В конце концов, вы пытаетесь усилить всплески, сравнивая сигнал с самим собой со смещением. Представьте себе синусоидальную волну, которую вы копируете и многократно увеличиваете смещение, пока две волны снова не совпадут. Это заметно усиливает сигнал, и, таким образом, вы можете определить длину волны, посмотрев на смещение. То же самое работает и с зашифрованным текстом, если вы возьмете буквенное число в алфавите в качестве значения волны, но теперь это действительно не по теме. ;) - person Lars; 23.08.2011
comment
@ Michael-j-barber: i - проверяемая итерация или текущее смещение. Взгляните на Индекс совпадения для подробного объяснения решения полиалфавитного шифра. Может быть, это прояснит ситуацию лучше, чем я могу предоставить в 500 символов. - person Lars; 23.08.2011
comment
Я посмотрю статью, надеюсь, она прояснит ситуацию. Прямо сейчас точки 4 и 5, кажется, говорят о том, чтобы подсчитать, сколько пар линий со смещениями i имеют одинаковые отступы, и выбрать смещения с большими счетчиками. Но это проигнорирует фактический отступ без возможности его восстановления. Не забудьте, что вы можете отредактировать свой ответ: ограничение в 500 символов - не проблема! - person Michael J. Barber; 24.08.2011
comment
получил точку - я посмотрю, найду ли я время, чтобы написать какой-нибудь реальный код на этом завтра. - person Lars; 24.08.2011
comment
Автокорреляция - это излишек для этой проблемы, к тому же вам нужно оценить / угадать наибольшее число, а для этой простой задачи это довольно медленно. - person Jürgen Strobel; 24.08.2011

  • For each line in the file
    • If indented more than the previous, add the difference to a list
      • discard if > 12, it's probably a line continuation
  • Сгенерируйте частотную таблицу # в списке
  • №1, вероятно, ваш ответ.

редактировать

У меня открыт VB.Net (а вы? :-) Вот что я имею в виду:

    Sub Main()
        Dim lines = IO.File.ReadAllLines("ProveGodExists.c")
        Dim previndent As Integer = 0
        Dim indent As Integer
        Dim diff As Integer
        Dim Diffs As New Dictionary(Of Integer, Integer)
        For Each line In lines
            previndent = indent
            indent = Len(line) - Len(LTrim(line))
            diff = indent - previndent
            If diff > 0 And diff < 13 Then
                If Diffs.ContainsKey(diff) Then
                    Diffs(diff) += 1
                Else
                    Diffs.Add(diff, 1)
                End If
            End If
        Next
        Dim freqtbl = From p In Diffs Order By p.Value Descending
        Console.WriteLine("Dump of frequency table:")
        For Each item In freqtbl
            Console.WriteLine(item.Key.ToString & " " & item.Value.ToString)
        Next
        Console.WriteLine("My wild guess at tab setting: " & freqtbl(0).Key.ToString)
        Console.ReadLine()
    End Sub

Результаты:

Дамп таблицы частот:
4 748
8 22
12 12
2 2
9 2
3 1
6 1
Мое безумное предположение при настройке вкладки: 4

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

person FastAl    schedule 18.08.2011
comment
Неплохо, но это не может, например, Определите, что ширина вкладки равна 8, если 45% ширины вкладки равны 7, а 55% - 9. Это интересно. - person Patrick87; 19.08.2011
comment
@ Patrick87 - если вы отсортируете таблицу частот, то эти # будут в последующих слотах. Но я не думаю, что ОП этого хотела; Я перечитываю вопрос и все же думаю, что ему просто нужен наиболее вероятный кандидат. - person FastAl; 19.08.2011
comment
@ Patrick87 Я бы не ожидал, что файл, в котором отступ никогда не меняется на 8, будет иметь ширину табуляции 8. Приведенные вами числа кажутся исключительным случаем, о котором не стоит особо беспокоиться. - person Michael J. Barber; 19.08.2011
comment
В частности, для этого ответа он во многом соответствует тому, что я ищу. В конце концов, если вы можете придумать хорошее правило для устранения ложных отступов, вы сможете очень хорошо справиться с простой стратегией выбора, такой как режим изменения отступов. Я реализую это позже и посмотрю, являются ли большие отступы хорошим тестом на наличие ложных отступов. - person Michael J. Barber; 19.08.2011
comment
Ваш выбор (реально) 2,3,4,5,6,7,8. Я бы просканировал первые 50-100 непустых строк этим методом и выбрал самую высокую. Если попадание равно 8, 6 или 4, я бы сделал вторую проверку, чтобы увидеть, были ли 4, 3 или 2 вторыми по величине, и вместо этого выбрал бы тот. Я бы выбрал схему рационализации для исправления глупостей для каждой из ваших 7 возможностей. - person Mark; 24.08.2011
comment
@Mark Я наградил этот ответ наградой, так как он пока лучший, и у меня больше нет времени на реализацию и тестирование другого ответа до истечения крайнего срока. Но, пожалуйста, оставьте свой комментарий в качестве ответа, я реализоваю его завтра и посмотрю, будет ли оно лучше. Я буду ждать ответа, пока не опробую и ваш. - person Michael J. Barber; 24.08.2011

Для каждого языка, который вы хотите поддерживать, вам необходимо выполнить небольшой синтаксический анализ:
1) исключить комментарии (построчные или блочные, возможно также вложенные?)
2) найти открытия подпрограмм -block ({ в языках типа C, begin в паскале, do в оболочке и т. д.)

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

person Tomas    schedule 09.08.2011

Может, сделаем что-нибудь вроде ...

  1. получить список всех значений ширины табуляции в файле
  2. удалить 50% наименее частых записей
  3. отсортировать оставшиеся записи в порядке возрастания
  4. вычислить список пар (a, b), где b находятся в списке ширины табуляции, а a задают ранг этой ширины табуляции.
  5. построить наиболее подходящую линию
  6. наклон наиболее подходящей линии - это предположение для ширины выступа. округлить до ближайшего целого числа.

Пример:

  1. list = [4, 4, 6, 8, 8, 4, 4, 4, 8, 8, 12, 5, 11, 13, 12, 12]
  2. list = [4, 4, 4, 4, 4, 8, 8, 8]
  3. уже отсортировано
  4. [(1, 4), (1, 4), (1, 4), (1, 4), (1, 4), (2, 8), (2, 8), (2, 8)]
  5. наиболее подходящая линия: b = 4a + 0 (R ^ 2 = 0)
  6. наклон равен 4, так что это, вероятно, ширина вкладки.
person Patrick87    schedule 09.08.2011
comment
Когда вы говорите о ширине табуляции, вы имеете в виду начальный отступ строк или изменение отступа между последовательными строками? - person Michael J. Barber; 17.08.2011
comment
Мой метод аппроксимирует оба варианта: ведущий отступ - это точка пересечения по оси Y, а изменение отступа - это наклон. В качестве альтернативы, эта строка будет давать функцию промежутков отступа в зависимости от глубины табуляции. - person Patrick87; 17.08.2011
comment
Хорошо, тогда я отвечу на вопросы и комментарии по обеим версиям. Что касается ширины отступа, кажется, что подход может ошибиться, даже если все изменения отступа имеют одинаковую величину; на практике это может не быть проблемой, и вполне может быть необходимо для улучшения общих результатов - это должно быть определено эмпирически. Замечу, что числа в вашем примере не имеют нулей --- это было намеренно? - person Michael J. Barber; 17.08.2011
comment
В случае изменений отступов, кажется, есть предположение, что большинство изменений кратны ширине табуляции, в чем я не уверен - опять же, к этому я обращусь эмпирически. В числах в вашем примере нет ни нулей, ни минусов. Есть ли намерение опустить уменьшение отступа? Чтобы использовать величины ненулевых изменений? - person Michael J. Barber; 17.08.2011
comment
Нет, можно добавить нули. Я не уверен, что понимаю, как это могло пойти не так. Это эмпирический вопрос, и подгонка кривой к данным - для отступов можно предположить, что линейная кривая наиболее подходит - стандартная практика. Единственный раз, когда я вижу эффектную неудачу этого метода, - это когда все уровни отступов одинаковы ... в этом случае вы скажете мне, какую схему отступов использовал этот парень! - person Patrick87; 17.08.2011
comment
Другой способ сказать это так: мой метод - лучшее предположение, которое вы можете сделать, глядя на данные ... чтобы добиться большего, вам потребуются предположения. Скажем, кто-то выбирает отступ на первом уровне вкладки и на третьем, с шириной вкладки 2. Затем есть много значений 2 и 6, и мой метод сказал бы, что ширина вкладки равна 4. Если этого недостаточно, вам нужен экстрасенс, а не алгоритм. Между прочим, точки данных - это общее количество ведущих пробелов в каждой строке ... Не какая-то межстрочная дельта. - person Patrick87; 17.08.2011
comment
Спасибо за дополнительные комментарии, я уверен, что теперь могу честно передать ваше намерение для целей тестирования. Кстати, использование ширины отступа может пойти не так, если у вас есть функция, состоящая из группы строк с отступом на один уровень, строки или двух с отступом на два уровня, а затем связкой строк с отступом на три уровня; двухуровневый отступ отбрасывается, и возвращается двойная фактическая ширина табуляции. Эта структура проявляется, например, в числовом коде, в котором вы перебираете оба индекса двумерной матрицы. - person Michael J. Barber; 17.08.2011

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

#!/bin/sh

grep -v -E '^[[:space:]]*$' | 
  sed 's/^\([[:space:]]*\).*/\1/' | 
    awk '{ print length($0) }' | 
      awk '$1 > prev { print $1 - prev } { prev = $1 }' | 
        sort | 
          uniq -c | 
            sort -k1nr | 
              awk '{ print $2 }' | 
                head -n 1

Эта реализация - O(n log(n)), где n - количество строк в файле, но это легко сделать в O(n).

person Michael J. Barber    schedule 17.08.2011
comment
Мне это нравится из-за его явной извращенности. После создания 9 процессов я не думаю, что нелинейное поведение сортировки O () является проблемой для типичных исходных файлов. - person Jürgen Strobel; 24.08.2011
comment
@ Jürgen Это было задумано как пошаговая иллюстрация с одним действием на каждом этапе конвейера, чтобы действовать как базовая линия, которую другие могли бы изменить без особых проблем - мне нужны были идеи больше, чем эффективная реализация. Это не обязательно приводит к быстрой реализации (два последовательных этапа awk выглядят особенно вопиюще, как и стратегия сортировки для получения максимума). Тем не менее, он работает с файлом из 10 тыс. Строк без заметной задержки по времени; достаточно быстрая для интерактивного использования не является большим ограничением! - person Michael J. Barber; 24.08.2011
comment
Я это полностью понимаю. Мой скрипт на Python использует почти ту же стратегию. - person Jürgen Strobel; 24.08.2011

Эвристический:

  1. Получите список всех изменений отступа от строки к следующей строке, которые> 0.
  2. Составьте частотную таблицу всех значений в этом списке.
  3. Возьмите значение с наибольшей частотой.

Скрипт Python берет имена файлов или стандартный ввод и выводит номер наилучшего отступа:

#!/usr/bin/env python

import fileinput, collections

def leadingSpaceLen(line):
    return len(line) - len(line.lstrip())

def indentChange(line1, line2):
    return leadingSpaceLen(line2) - leadingSpaceLen(line1)

def indentChanges(lines):
    return [indentChange(line1, line2)
        for line1, line2 in zip(lines[:-1], lines[1:])]

def bestIndent(lines):
    f = collections.defaultdict(lambda: 0)
    for change in indentChanges(lines):
        if change > 0:
            f[change] += 1
    return max(f.items(), key=lambda x: x[1])[0]

if __name__ == '__main__':
    print bestIndent(tuple(fileinput.input()))
person Jürgen Strobel    schedule 24.08.2011