以下是一个专注于自定义验证和为每个字段处理错误的解决方案。对于仅包含数字数据的数据文件,这可能有些多余!
首先,对于这类问题,我喜欢使用Microsoft.VisualBasic.dll
中的解析器,因为它已经可以在不使用NuGet的情况下使用。
对于每一行,我们可以返回字段数组和行号(用于错误报告)。
// for each row, return the line number and the fields
let parserReadAllFields fieldWidths textReader =
let parser = new Microsoft.VisualBasic.FileIO.TextFieldParser(reader=textReader)
parser.SetFieldWidths fieldWidths
parser.TextFieldType <- Microsoft.VisualBasic.FileIO.FieldType.FixedWidth
seq {while not parser.EndOfData do
yield parser.LineNumber,parser.ReadFields() }
接下来,我们需要一个小的错误处理库(请参见http://fsharpforfunandprofit.com/rop/获取更多信息)。
type Result<'a> =
| Success of 'a
| Failure of string list
module Result =
let succeedR x =
Success x
let failR err =
Failure [err]
let mapR f xR =
match xR with
| Success a -> Success (f a)
| Failure errs -> Failure errs
let applyR fR xR =
match fR,xR with
| Success f,Success x -> Success (f x)
| Failure errs,Success _ -> Failure errs
| Success _,Failure errs -> Failure errs
| Failure errs1, Failure errs2 -> Failure (errs1 @ errs2)
接下来定义你的领域模型。在这种情况下,它是记录类型,每个字段都对应文件中的一个字段。
type MyRecord =
{id:int
然后,您可以定义特定领域的解析代码。对于每个字段,我都创建了一个验证函数(validateId
、validateName
等)。
不需要验证的字段可以通过原始数据传递(validateDescription
)。
在fieldsToRecord
中,使用应用程序样式(<!>
和<*>
)组合各种字段。
有关更多信息,请参见http://fsharpforfunandprofit.com/posts/elevated-world-3/#validation。
最后,readRecords
将每个输入行映射到记录结果,并仅选择成功的记录。失败的记录将在handleResult
中写入日志。
module MyFileParser =
open Result
let createRecord id name description =
{id=id; name=name; description=description}
let validateId (lineNo:int64) (fields:string[]) =
let rawId = fields.[0]
match System.Int32.TryParse(rawId) with
| true, id -> succeedR id
| false, _ -> failR (sprintf "[%i] Can't parse id '%s'" lineNo rawId)
let validateName (lineNo:int64) (fields:string[]) =
let rawName = fields.[1]
if System.String.IsNullOrWhiteSpace rawName then
failR (sprintf "[%i] Name cannot be blank" lineNo )
else
succeedR rawName
let validateDescription (lineNo:int64) (fields:string[]) =
let rawDescription = fields.[2]
succeedR rawDescription // no validation
let fieldsToRecord (lineNo,fields) =
let (<!>) = mapR
let (<*>) = applyR
let validatedId = validateId lineNo fields
let validatedName = validateName lineNo fields
let validatedDescription = validateDescription lineNo fields
createRecord <!> validatedId <*> validatedName <*> validatedDescription
/// print any errors and only return good results
let handleResult result =
match result with
| Success record -> Some record
| Failure errs -> printfn "ERRORS %A" errs; None
/// return a sequence of records
let readRecords parserOutput =
parserOutput
|> Seq.map fieldsToRecord
|> Seq.choose handleResult
以下是实际解析的示例:
// Set up some sample text
let text = """01name1description1
02name2description2
xxname3badid-------
yy badidandname
"""
// create a low-level parser
let textReader = new System.IO.StringReader(text)
let fieldWidths = [| 2
let parserOutput = parserReadAllFields fieldWidths textReader
// convert to records in my domain
let records =
parserOutput
|> MyFileParser.readRecords
|> Seq.iter (printfn "RECORD %A") // print each record
输出结果将会是这样的:
RECORD {id = 1;
name = "name1";
description = "description";}
RECORD {id = 2;
name = "name2";
description = "description";}
ERRORS ["[3] Can't parse id 'xx'"]
ERRORS ["[4] Can't parse id 'yy'"; "[4] Name cannot be blank"]
这绝不是解析文件的最有效方式(我认为NuGet上有一些CSV解析库可以在解析时进行验证),但它确实展示了如何完全控制验证和错误处理,如果需要的话。