所以我正在使用解析器组合器构建一个词法分析器/解析器对,这给我留下了一些有趣的问题。现在这个问题的具体问题是我已经解决了,但我对我的解决方案并不完全满意。
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()
答案 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”可以解释为两个换行符..),所以你需要得到第一个结果......
(从您的问题来看,目前尚不清楚您是否真的想要使用某些解析器组合库,所以我没有详细说明该主题 - 如果您有兴趣,可以询问更多细节.. 。)