确定源文件中使用的选项卡宽度有什么好的启发式算法?

时间:2011-08-04 10:37:37

标签: language-agnostic indentation heuristics

我想确定源文件中使用的标签宽度用空格缩进。 这对于具有特别规则缩进的文件并不难,其中前导空格仅用于缩进,始终是制表符宽度的倍数,并且缩进在时间上增加一个级别。 但是许多文件会偏离这种常规缩进,通常用于某种形式的垂直对齐。因此,我正在寻找一种良好的启发式算法,以估计使用的标签宽度,允许一些不规则压痕的可能性。

这样做的动机是为SubEthaEdit编辑器编写扩展。不幸的是,SubEthaEdit没有使标签宽度可用于编写脚本,所以我将根据文本猜测它。

合适的启发式应该:

  • 表现良好,可以互动使用。我不认为这会是一个问题,如果需要,只能使用一部分文本。
  • 与语言无关。
  • 返回最长的合适标签宽度。例如,任何选项卡宽度为四个空格的文件也可以是具有两个空格选项卡的文件,如果每个缩进实际上是两倍的级别。显然,四个空间将是正确的选择。
  • 如果压痕完全正常,请务必正确使用。

一些简化因素:

  • 可以假设至少有一行缩进。
  • 标签宽度可以假设为至少两个空格。
  • 可以安全地假设缩进仅使用空格。这并不是说我有任何反对标签的东西 - 恰恰相反,我会首先检查是否有任何用于缩进的标签并单独处理它。这确实意味着缩进混合标签和空格可能无法正确处理,但我认为这不重要。
  • 可以假设没有包含空格的行。
  • 并非所有语言都需要正确处理。例如,使用像lisp和go这样的语言的成功或失败将完全无关紧要,因为它们通常不会手工缩进。
  • 不需要完美。如果偶尔需要手动调整几条线,世界就不会结束。

你会采取什么方法,你认为它的优点和缺点是什么?

如果要在答案中提供工作代码,最好的方法可能是使用从stdin读取源文件的shell脚本,并将选项卡宽度写入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

在这些表中,“Right”应作为语言标准制表符宽度的确定,“错误”作为非零标签宽度不等于语言标准宽度,“无”作为零标签 - 宽度还是没有答案。 “模式”是选择最常出现的缩进变化的策略; “第一个”是第一个缩进线的缩进; “No-long”是FastA的排除大缩进线并采用该模式的策略,其中数字表示允许的最大缩进变化; “LR”是Patrick87基于线性回归的策略,其变体基于线之间的压痕变化和线的绝对压痕; “Doublecheck”(无法抗拒双关语!)是Mark对FastAl策略的修改,限制了可能的标签宽度,并检查模态值的一半是否也经常发生,有两个不同的阈值来选择较小的宽度。

7 个答案:

答案 0 :(得分:3)

  • 对于文件中的每一行
    • 如果缩进比上一个更多,请将差异添加到列表中
      • 如果>丢弃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

希望有所帮助。

答案 1 :(得分:3)

好的,因为你想要一个与语言无关的解决方案,我们将无法使用任何语法提示。虽然你说,你不想要一个完美的解决方案,但这里有一个,这对大多数语言都很有效。

我实际上必须解决密码学中的类似问题,才能在polyalphabetic cipher中获得正确的代码字长。这种加密是基本的Caesar-chiffre(字母表的每个字母都由 n 字母移动),其中密码用于以不同方式移动字母( nth 明文的字母由 mod(密码的第n,长度(密码))字母移动。选择的武器是autocorrelation

算法如下:

  1. 在一行开头的空格后删除所有字符 - 保持行尾标记不变。
  2. 删除零空格的行(因为它们只是空行)
  3. 计算每行的空白宽度并将其保存在数组中 length
  4. 自相关:循环直到最大估计数 - 可能相当高,如32或其他 - 当前迭代应为 i 。对于每次迭代,计算每个条目与 ith 条目之间的距离。计算距离的数量= 0( nth (n + i)th 条目的相同值),保存在数组中以获取密钥 i
  5. 您现在拥有一系列相同对的出现。计算此数组的平均值,并删除该均值附近的所有值(保留自相关的峰值)。峰值将是最低值的倍数,这将是用于缩进的搜索空格数。
  6. 自相关是一个非常好的函数,可用于您想要检测数据流中的重复值的每种情况。它大量用于信号处理并且速度非常快(取决于估计的信号重复的最大距离)。

    是的,那时我用自相关破解了多字母密文。 ;)

答案 2 :(得分:1)

对于您想要支持的每个语言,您需要进行一些解析:
1)排除评论(按行或按块,也可以嵌套?)
2)找到子块的开头({在C语言中,begin在pascal中,do在shell等中。)

然后看看在打开子块之后空间的数量增加了多少。做一些简单的统计 - 找出最常见的值,最大值和最小值,平均值。通过这种方式,您还可以查看缩进是否正常以及缩进程度。

答案 3 :(得分:1)

也许做点什么......

  1. 获取文件
  2. 中所有标签宽度的列表
  3. 删除50%最不常用的条目
  4. 按升序对其余条目进行排序
  5. 计算(a,b)对的列表,其中b位于标签宽度列表中,a表示该标签宽度的等级。
  6. 绘制最合适的线
  7. 最佳拟合线的斜率是标签宽度的猜测。舍入到最接近的整数。
  8. 示例:

    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,所以这可能是标签宽度。

答案 4 :(得分:1)

您的选择(实际上)是2,3,4,5,6,7,8。

我会使用@FastAl建议的东西扫描前50-100行左右。我可能只是盲目地从文本的任何行的前面盲目地拉空间数并计算空白字符串的长度。如果你有正则表达式,左边的修剪线和两次运行长度似乎是浪费。另外,我会System.Math.abs(indent - previndent),所以你得到去缩进数据。正则表达式是这样的:

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

一旦你得到了7个选项中哪个选项具有最高计数的统计数据,请将其作为第一个猜测运行。对于8,6和4你应该检查4和2,3或2是否也有重要的计数(第二位或超过10%或其他一些廉价启发式)。如果有很多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)

答案 5 :(得分:0)

作为基线,可以简单地计算所有缩进增量,并将最频繁的增加作为标签宽度。作为一个shell脚本,编写为每个管道阶段都有小动作,它可能如下所示:

#!/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)中轻松完成。

答案 6 :(得分:0)

启发式:

  1. 获取从一行到下一行的所有缩进更改的列表,其中&gt; 0
  2. 制作此列表中所有值的频率表。
  3. 取最高频率的值。
  4. Python脚本,获取文件名或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()))