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

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

Мотивация для этого - написание расширения для редактора 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

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

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). Отбрасывание или добавление более 2-х уровней одновременно (обычно свернутые конечные скобки) встречается очень редко.

Неуместное бормотание

Единственная проблема, которую я вижу, заключается в том, что в старом.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)

  • Для каждой строки в файле
    • Если отступ больше предыдущего, добавьте разницу в список
      • отменить, если> 12, это, вероятно, продолжение строки
  • Создайте таблицу частот из # в списке
  • № 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

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

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

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

Алгоритм будет выглядеть так:

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

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

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

Может быть, сделать что-то вроде...

  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. список = [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, так что это, вероятно, ширина вкладки.

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

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

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

  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()))

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

#!/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),

Другие вопросы по тегам