anchor.diff:          | _ -> None
       Seq.unfold unfoldNextEntry (lines |> List.ofArray)
   
  +
  +type SrtBlockExtractor(entries: SrtBlockExtractorEntry seq, entryGenerator: SrtBlockExtractorEntry -> SrtEntry array) = 
  +    static member private DefaultEntryGenerator(rootPath: string)(entry: SrtBlockExtractorEntry): SrtEntry array = 
  +        File.ReadAllLines(Path.Combine(rootPath, entry.RelativePath)) |> parseSrtSubtitles |> Array.ofSeq
  +
  +    private new (rootPath: string, csvLines: string array) = 
  +        let tryParseAsHex(s: string): int64 = 
  +            if (s.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)) then
  +                Int64.Parse(s.Substring(2), NumberStyles.HexNumber)
  +            else
  +                Int64.Parse(s)
  +
  +        // Allow for sparse specification of some fields.
  +        let mutable lastRelativePath = ""
  +        let mutable lastID = ""
  +        let getOrLast(s: string, os: string) =
  +            if String.IsNullOrWhiteSpace(s) then
  +                os
  +            else
  +                s
  +
  +        let csvEntries = csvLines |> Array.map(fun line ->
  +                let fields = line.Split(',') |> Array.map(fun f -> f.Trim())
  +                
  +                lastRelativePath <- getOrLast(fields.[0], lastRelativePath)
  +                lastID <- getOrLast(fields.[2], lastID)
  +
  +                {
  +                    SrtBlockExtractorEntry.RelativePath = lastRelativePath
  +                    OverrideBaseKey = fields.[1]
  +                    Id = tryParseAsHex(lastID)
  +                    Languages = fields.[3]
  +                    SubtitleIdStart = Int32.Parse(fields.[4])
  +                    SubtitleIdEnd = Int32.Parse(fields.[5])
  +                }
  +            )
  +        new SrtBlockExtractor(csvEntries, SrtBlockExtractor.DefaultEntryGenerator(rootPath))
  +
  +    new(rootPath: string, sourceCsvStream: Stream) = 
  +        let toRead = sourceCsvStream.Length - sourceCsvStream.Position
  +        let csvBytes = Array.zeroCreate<byte>(int toRead)
  +        let csvRead = sourceCsvStream.Read(csvBytes, 0, int toRead)
  +        let csvText = Encoding.UTF8.GetString(csvBytes)
  +        let csvLines = csvText.Split([| Environment.NewLine |], StringSplitOptions.RemoveEmptyEntries) |> Array.skip(1)
  +        new SrtBlockExtractor(rootPath, csvLines)
  +
  +    new(rootPath: string, sourceCsvPath: string) = 
  +        // read in the source csv, and skip the header line
  +        let csvLines = File.ReadAllLines(sourceCsvPath) |> Array.skip(1)
  +        new SrtBlockExtractor(rootPath, csvLines)
  +
  +    member this.Extract(): SrtBlockExtractedEntry seq = 
  +        let createExtractedEntry(e: SrtBlockExtractorEntry, t: string, l: string) = 
  +            {
  +                SrtBlockExtractedEntry.Entry = e
  +                Language = l
  +                Text = t 
  +            }
  +
  +        // Basic idea: split the sequence of entries into ordered subsequences per file. (Order has to be preserved in
  +        // order to ensure that the strings are read and assigned in the correct order.) We can't use groupBy because
  +        // it doesn't guarantee ordering of the subsequences.
  +        let foldNextByFile s e = 
  +            match (s |> Map.tryFind(e.RelativePath)) with
  +            | Some(l) -> s |> Map.add e.RelativePath (e :: l)
  +            | _ -> s |> Map.add e.RelativePath [e]
  +
  +        let entriesByFile = 
  +            entries 
  +            |> Array.ofSeq
  +            |> Array.fold foldNextByFile Map.empty
  +            |> Map.map (fun _ v -> v |> List.rev) |> Map.toArray
  +
  +        // unfold function. the previous starting offset is passed in -- if it hasn't changed for this entry,
  +        // then we skip seeking the stream, and instead just continue to read.
  +        let createEntriesForLanguages(e: SrtBlockExtractorEntry, s: string) = 
  +            e.Languages.Split([| ';' |], StringSplitOptions.RemoveEmptyEntries)
  +            |> Array.map(fun l -> createExtractedEntry(e, s, l.Trim()))
  +
  +        let readNext(entries: SrtEntry array) = fun(state: SrtBlockExtractorEntry list) -> 
  +            match state with
  +            | n :: rest -> 
  +                let nextSubtitleEntries = Array.sub entries n.SubtitleIdStart (n.SubtitleIdEnd - n.SubtitleIdStart + 1)
  +                let nextString = String.Join(" ", nextSubtitleEntries |> Array.map(fun s -> s.Subtitle) |> Array.filter(String.IsNullOrWhiteSpace >> not))
  +                Some((createEntriesForLanguages(n, nextString), rest))
  +            | [] -> None
  +
  +        let extractEntriesForFile(s: string * SrtBlockExtractorEntry list) = 
  +            match s with
  +            | (fn: string, entries: SrtBlockExtractorEntry list) -> 
  +                let srtEntries = entryGenerator(entries |> List.head)
  +                List.unfold (readNext(srtEntries)) entries |> List.toArray |> Array.collect(fun t -> t)
  +
  +        entriesByFile |> (Array.map extractEntriesForFile) |> Array.collect(fun t -> t) |> Seq.ofArray
  +
  +let convertStringBlockExtractedEntriesToCardRecords(lid: int)(extracted: SrtBlockExtractedEntry seq): CardRecord seq =
  +    let createCardRecordForEntry(e: SrtBlockExtractedEntry) = 
  +        let baseKey = e.Entry.RelativePath + "\\" + e.Entry.Id.ToString()
  +        {
  +            CardRecord.ID = 0
  +            LessonID = lid
  +            Text = e.Text
  +            Gender = "masculine"
  +            Key = baseKey
  +            GenderlessKey = baseKey
  +            KeyHash = 0
  +            GenderlessKeyHash = 0
  +            SoundResource = ""
  +            LanguageTag = e.Language
  +            Reversible = true
  +        }
  +
  +    extracted |> Seq.map createCardRecordForEntry
  +
  
positive.code:     with
          override self.ToString() = 
              "SrtEntry(id = " + self.SubtitleId + ", timecodes = " + self.Timecodes + ", subtitle = " + self.Subtitle + ")"
  
  let parseSrtSubtitles(lines: string array) = 
      let unfoldNextEntry(s: string list): (SrtEntry * string list) option = 
  
negative.code: ﻿module SrtTools
  
  open LLDatabase
  open System
  open System.Globalization
  open System.IO
  open System.Text
  
  // Tools for dealing with .srt subtitle files.
  type SrtEntry = {
  