是否有更有效的方法从多行令牌计算下一行/列号?

时间:2010-09-09 20:01:09

标签: f#

所以我正在使用解析器组合器构建一个词法分析器/解析器对,这给我留下了一些有趣的问题。现在这个问题的具体问题是我已经解决了,但我对我的解决方案并不完全满意。

module Program =            

    type Token = { value:string; line:int; column:int; }

    let lineTerminators = set ['\u000A'; '\u000D'; '\u2028'; '\u2029']

    let main () =

        let token = { value = "/*\r\n *\r\n *\r\n \n */"; line = 1; column = 1; }

        let chars = token.value.ToCharArray()

        let totalLines =
            chars
            |> Array.mapi(
                fun i c ->
                    if not (lineTerminators.Contains c)  then 
                        0 
                    else
                        if c <> '\n' || i = 0 || chars.[i - 1] <> '\r' then 
                            1 
                        else 
                            0
            )
            |> Array.sum

        let nextLine = token.line + totalLines

        let nextColumn =
            if totalLines = 0 then 
                token.column + token.value.Length 
            else
                1 + (chars 
                     |> Array.rev 
                     |> Array.findIndex lineTerminators.Contains)


        System.Console.ReadKey true |> ignore

    main()

1 个答案:

答案 0 :(得分:2)

您的实现的一个问题是,它最初似乎期望所有行终止符都只是单个字符,但事实并非如此 - 如果您将“\ r \ n”视为单行终止符(由2个字符组成) ),那么情况应该更清楚。例如,我会声明这样的终结符:

let terminators = [ ['\r'; '\n']; ['\r']; ['\n'] ]

顺序很重要 - 如果我们首先找到“\ r \ n”,那么我们要跳过2个字符(这样我们就不会将下一个'\ n'char作为下一个终结符)。不幸的是,“跳过2个字符”有点棘手 - 使用mapi函数无法完成,它为每个元素调用函数。

使用递归函数的直接实现可能如下所示:

let input = "aaa\nbbb\r\nccc" |> List.ofSeq

// Returns Some() if input starts with token (and the returned
// value is the rest of the input with the starting token removed)
let rec equalStart input token = 
  match input, token with
  | _, [] -> Some(input) // End of recursion
  | a::input, b::token when a = b -> equalStart input token // Recursive call
  | _ -> None // Mismatch

// Counts the number of lines...
let rec countLines count input = 
  // Pick first terminator that matches with the beginning of the input (if any)
  let term = terminators |> List.tryPick (equalStart input)
  match term, input with 
  | None, _::input -> countLines count input  // Nothing found - continue
  | None, [] -> count + 1 // At the end, add the last line & return
  | Some(rest), _ -> countLines (count + 1) rest  // Found - add line & continue

如果您使用的是一些解析器组合库(例如FParsec),那么您可以使用内置解析器来完成大部分工作。我实际上并没有尝试这个,但这是一个粗略的草图 - 您可以将终结符列表存储为字符串列表并为每个字符串生成解析器:

let terminators = [ "\r\n"; "\r"; "\n" ]
let parsers = [ for t in terminators -> 
                  parser { let! _ = pstring t  
                           return 1 } ] // Return '1' because we found next line

这为您提供了一个解析器列表,当最后有一个终结符时返回1 - 现在您可以使用<|>(或组合器)聚合所有解析器,然后运行组合解析器。如果失败,您可以跳过第一个字符(将其与另一个解析器结合)并继续递归。唯一的问题是解析器组合器通常返回所有可能的派生(“\ r \ n”可以解释为两个换行符..),所以你需要得到第一个结果......

(从您的问题来看,目前尚不清楚您是否真的想要使用某些解析器组合库,所以我没有详细说明该主题 - 如果您有兴趣,可以询问更多细节.. 。)