Что такое хорошая эвристика для определения ширины табуляции, используемой в исходном файле?
Я хотел бы определить ширину закладки, используемую в исходных файлах с отступом с пробелами.
Это не сложно для файлов с особенно регулярным отступом, где ведущие пробелы используются только для отступов, всегда в кратных ширине табуляции и с отступом, увеличивающимся на один уровень в момент времени.
Но у многих файлов будет некоторый отход от такого рода регулярных отступов, как правило, для некоторой формы вертикального выравнивания. Таким образом, я ищу хорошую эвристику, чтобы оценить, какая ширина табуляции была использована, что дает возможность нерегулярного отступа.
Мотивация для этого заключается в написании расширения для редактора SubEthaEdit. К сожалению, SubEthaEdit не делает ширину табуляции доступной для сценариев, поэтому я собираюсь угадать ее на основе текста.
Подходящая эвристика должна:
- Выполните достаточно хорошо для интерактивного использования. Я не думаю, что это будет проблемой, и только часть текста может быть использована в случае необходимости.
- Не зависит от языка.
- Верните самую длинную подходящую ширину вкладки. Например, любой файл с шириной табуляции в четыре пробела также может быть файлом с вкладками с двумя пробелами, если каждый отступ был фактически вдвое большим количеством уровней. Очевидно, что четыре места будут правильным выбором.
- Всегда делайте это правильно, если отступ полностью правильный.
Некоторые упрощающие факторы:
- По крайней мере, одна строка может считаться отступом.
- Ширина закладки может считаться как минимум двумя пробелами.
- Безопасно предположить, что отступ делается только с пробелами. Это не то, что у меня есть что-либо против вкладок - совсем наоборот, я сначала проверю, есть ли какие-либо вкладки, используемые для отступов, и обрабатывать их отдельно. Это означает, что вкладки и пробелы смещения отступа могут обрабатываться неправильно, но я не считаю это важным.
- Можно предположить, что нет строк, содержащих только пробелы.
- Не все языки должны обрабатываться правильно. Например, успех или неудача с такими языками, как lisp и go, будут совершенно неактуальны, поскольку они обычно не отступаются вручную.
- Совершенство не требуется. Мир не собирается заканчиваться, если несколько строк иногда необходимо отрегулировать вручную.
Какой подход вы возьмете, и что вы видите в его преимуществах и недостатках?
Если вы хотите предоставить рабочий код в своем ответе, лучше всего использовать оболочку script, которая читает исходный файл из 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
Ruby:
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
В этих таблицах "Правильно" следует принимать за определение ширины табуляции по языку "Неправильно" в качестве ненулевой ширины табуляции, не равной ширине языкового стандарта, и "Нет" в качестве нулевой табуляции, ширину или отсутствие ответа. "Режим" - это стратегия выбора наиболее часто встречающихся изменений в отступе; "Первый" принимает отступы первой строки с отступом; "Нет-долго" - это стратегия FastAl для исключения строк с большим отступом и с использованием режима, с указанием номера, указывающего максимально допустимое изменение отступа; "LR" - это стратегия Patrick87, основанная на линейной регрессии, с вариантами, основанными на изменении отступов между линиями и абсолютном отступе линий; "Doublecheck" (не может противостоять каламбуре!) Является модификацией Mark стратегии FastAl, ограничивающей возможную ширину табуляции и проверяющей частоту появления половины модального значения с двумя разными порогами для выбора меньшей ширины.
Ответы
Ответ 1
Ваш выбор (реалистично) 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)
Ответ 2
- Для каждой строки в файле
- Если с отступом больше предыдущего, добавьте разницу в список
- отменить if > 12, это, вероятно, продолжение строки
- Создайте частотную таблицу #s в списке
- # 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
Надеюсь, что это поможет.
Ответ 3
Хорошо, поскольку вы хотите использовать языковое агностическое решение, мы не сможем использовать какие-либо синтаксические подсказки. Хотя вы сказали, что не хотите идеального решения, вот один, который отлично работает с большинством языков.
Мне действительно пришлось решить аналогичную проблему в криптографии, чтобы получить правильную длину кодового слова в полиалфавитный шифр. Такое шифрование является основным цезарем-chiffre (каждая буква алфавита перемещается на n букв), где криптослово используется для перемещения букв по-разному (n-я буква чистого текста перемещается по модулю (n-я, длина (cryptword)) буква криптового слова). Оружие выбора autocorrelation.
Алгоритм будет таким:
- разделите все символы после того, как пробелы в начале строки закончились - оставьте маркеры конца строки неповрежденными.
- удалить строки с нулевым пробелом (поскольку это только пустые строки)
- Подсчитайте ширину пробела для каждой строки и сохраните ее в массиве длиной
- Автокорреляция: цикл до максимального оценочного числа - может быть довольно высоким, как 32 или что-то - текущая итерация должна быть i. Для каждой итерации вычислите расстояние между каждой записью и i-й записью. Подсчитайте количество расстояний = 0 (те же значения для n-й и (n + i)-й записей), сохраните в массиве для ключа i.
- Теперь у вас есть массив одинаковых пар-событий. Вычислите среднее значение этого массива и удалите все значения вблизи этого значения (оставляя спайки автокорреляции). Шипы будут кратными наименьшему значению, которое будет искомым числом пробелов, используемых для отступов.
Автокорреляция - очень приятная функция, пригодная для любой ситуации, в которой вы хотите обнаружить повторяющиеся значения в потоке данных. Он сильно используется в обработке сигналов и очень быстро (в зависимости от предполагаемого максимального расстояния повторов сигнала).
И да, тогда я взломал полиалфавитный зашифрованный текст с автокорреляцией.;)
Ответ 4
Может быть, что-то вроде...
- получить список всех ширины табуляции в файле
- удалить 50% наименее часто используемых записей
- сортировать остальные записи в порядке возрастания
- вычислить список (a, b) пар, где b находится в списке ширины табуляции, а a дать ранг этой ширины табуляции.
- построить линию наилучшего соответствия
- Наклон линии наилучшего соответствия - это предположение о ширине табуляции. округлить до ближайшего целого числа.
Пример:
- list = [4, 4, 6, 8, 8, 4, 4, 4, 8, 8, 12, 5, 11, 13, 12, 12]
- list = [4, 4, 4, 4, 4, 8, 8, 8]
- уже отсортировано
- [(1, 4), (1, 4), (1, 4), (1, 4), (1, 4), (2, 8), (2, 8), (2, 8 )]
- лучшая линия соответствия - это b = 4a + 0 (R ^ 2 = 0)
- Наклон равен 4, так что это, вероятно, ширина табуляции.
Ответ 5
Для каждого langauge, который вы хотите поддержать, вам нужно немного разобрать:
1) исключить комментарии (как по строкам, так и по блокам, возможно, также вложенные?)
2) найти отверстия субблока ({
в C-подобных языках, begin
в pascal, do
в оболочке и т.д.)
Затем просто посмотрите, сколько увеличивается количество пробелов после открытия подблока. Сделайте несколько простых статистических данных - чтобы найти наиболее частое значение, максимальное и минимальное значение, среднее значение. Таким образом вы также можете увидеть, является ли отступы регулярными или нет и сколько.
Ответ 6
В качестве базовой линии можно было просто рассчитать все увеличения отступов и наиболее часто увеличивать их в виде ширины табуляции. В качестве оболочки script, написанной с небольшими действиями на этапе конвейера, она может выглядеть так:
#!/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)
.
Ответ 7
Эвристический:
- Получить список всех изменений отступа от строки до следующей строки, которая равнa > 0.
- Сделать таблицу частот всех значений в этом списке.
- Возьмите значение с максимальной частотой.
Python script, принимает имена файлов или stdin и печатает наилучший номер отступа:
#!/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()))