I'm parsing log files (entries starting with debug/warning/info) in a line-by-line manner and have encountered rare occurrences where a single log entry spans multiple lines - eg, the warning here:
debug: Preparing movie /movies/menu_background.sfd: 272204
info: /init
debug: Playing movie c:\program files (x86)\steam\steamapps\common\supreme commander forged alliance\movies\menu_background.sfd: 272204
warning: Error running OnDestroy script in Entity bsa0001 at 2f8b6908: ...gramdata\faforever\gamedata\lua.nx2\lua\sim\unit.lua(5494): attempt to call method `Destroy' (a nil value)
stack traceback:
...gramdata\faforever\gamedata\lua.nx2\lua\sim\unit.lua(5494): in function <...gramdata\faforever\gamedata\lua.nx2\lua\sim\unit.lua:5489>
...orever\gamedata\lua.nx2\lua\sim\units\mobileunit.lua(65): in function `DestroyAllTrashBags'
My current code is below - which ignores the additional lines (puts them in misclist) - is not what I want. I would like to amend the code to include the additional lines with the prior info/warning/debug entry. My C# brain can do this procedurally but I'm trying to do this in a more functional manner as I learn F#.
let rec sortLines (lines: string list) (warninglist: string list) (debuglist: string list) (infolist: string list) (misclist: string list) : string list list =
match lines with
| (h::t) when h.StartsWith("debug") ->
sortLines t warninglist (h :: debuglist) infolist misclist
| (h::t) when h.StartsWith("info") ->
sortLines t warninglist debuglist (h :: infolist) misclist
| (h::t) when h.StartsWith("warning") ->
sortLines t (h :: warninglist) debuglist infolist misclist
| (h::t) ->
sortLines t warninglist debuglist infolist (h :: misclist)
| [] ->
[warninglist; debuglist; infolist; misclist]
When you need to maintain state while iterating through a collection, fold
is often your friend. I would suggest a solution like this:
type Log =
{
Info : List<string>
Debug : List<string>
Warning : List<string>
}
module Log =
let empty =
{
Info = []
Debug = []
Warning = []
}
let logInfo line log =
{ log with Info = line :: log.Info }
let logDebug line log =
{ log with Debug = line :: log.Debug }
let logWarning line log =
{ log with Warning = line :: log.Warning }
let logInvalid line log =
failwith "Invalid state"
let read lines =
let log, _ =
Seq.fold (fun (log, logger) (line : string) ->
let logger' =
if line.StartsWith("info") then logInfo
elif line.StartsWith("debug") then logDebug
elif line.StartsWith("warning") then logWarning
else logger
let log' = logger' line log
log', logger') (empty, logInvalid) lines
log
The read
function loops through the incoming lines, assigning each one to its correct level. log
is the current state of the log, and logger
is a function that will add a single line to the log.
If you prefer recursion, here's an equivalent implementation:
let read lines =
let rec loop log logger lines =
match lines with
| (line : string) :: tail ->
let logger' =
if line.StartsWith("info") then logInfo
elif line.StartsWith("debug") then logDebug
elif line.StartsWith("warning") then logWarning
else logger
let log' = logger' line log
loop log' logger' tail
| [] -> log
loop empty logInvalid lines
You can test it like this:
let lines =
[
@"debug: Preparing movie /movies/menu_background.sfd: 272204"
@"info: /init"
@"debug: Playing movie c:\program files (x86)\steam\steamapps\common\supreme commander forged alliance\movies\menu_background.sfd: 272204"
@"warning: Error running OnDestroy script in Entity bsa0001 at 2f8b6908: ...gramdata\faforever\gamedata\lua.nx2\lua\sim\unit.lua(5494): attempt to call method `Destroy' (a nil value)"
@" stack traceback:"
@" ...gramdata\faforever\gamedata\lua.nx2\lua\sim\unit.lua(5494): in function <...gramdata\faforever\gamedata\lua.nx2\lua\sim\unit.lua:5489>"
@" ...orever\gamedata\lua.nx2\lua\sim\units\mobileunit.lua(65): in function `DestroyAllTrashBags'"
]
let log = Log.read lines
printfn "Info:"; for line in List.rev log.Info do printfn " %s" line
printfn "Debug:"; for line in List.rev log.Debug do printfn " %s" line
printfn "Warning:"; for line in List.rev log.Warning do printfn " %s" line
Output is:
Info:
info: /init
Debug:
debug: Preparing movie /movies/menu_background.sfd: 272204
debug: Playing movie c:\program files (x86)\steam\steamapps\common\supreme commander forged alliance\movies\menu_background.sfd: 272204
Warning:
warning: Error running OnDestroy script in Entity bsa0001 at 2f8b6908: ...gramdata\faforever\gamedata\lua.nx2\lua\sim\unit.lua(5494): attempt to call method `Destroy' (a nil value)
stack traceback:
...gramdata\faforever\gamedata\lua.nx2\lua\sim\unit.lua(5494): in function <...gramdata\faforever\gamedata\lua.nx2\lua\sim\unit.lua:5489>
...orever\gamedata\lua.nx2\lua\sim\units\mobileunit.lua(65): in function `DestroyAllTrashBags'