Parsing text file. F# and C#.

Данный проект демонстрирует, как можно построить приложение, используя языки F Sharp и C Sharp. Приложение читает текстовый файл, в котором находятся экспортированные из IBM Lotus Domino данные, преобразовывает их и записывает в базу данных MS SQL.
Изначально данные в файле имеют формат: <имя поля> : <значение поля>. Несложная задача состоит в том, чтобы отобрать нужные поля, убрать дублирование полей, сформировать кортеж и записать его в базу данных. Однако вторая и, возможно, главная причина данной работы состояла в том, чтобы понять насколько удобно использовать два языка.
Все, что относиться к построению GUI интерфейса выполнено с применением C Sharp, а вот разбор текстового файла, преобразование и запись в MS SQL выполнено средствами F Sharp. Ну, а чтобы интерфейс пользователя был "живым", преобразование и запись в базу данных выполняется в фоновом потоке с использованием класса BackgroundWorker и класса IterativeBackgroundWorker описанного в книге Don Syme Expert F# 3.0.
Интерес, вероятно, представляет и взаимодействие между языками. Например, как вызвать в C# метод класса, созданного на F#, или, как написать обработчики событий в C# для событий, происходящих в классе, созданном на F#. Однако, делается все очень просто - языки проникают друг в друга без лишних конструкций. Проект предлагается "как есть", и его можно свободно загрузить.
1. F#. Класс ParseBackgroundWorker.
Во-первых, отметим, что весь F# код оформлен в виде dll библиотеки и находится в namespace LibraryFSharp.ParceTxtFileKmis.
Класс ParseBackgroundWorker создан на основе класса BackgroundWorker, который в свою очередь изначально спроектирован для выполнения задач в отдельном, выделенном потоке.
Обратите внимание, как создаются в F# классе новые события. Позже Вы увидите, как для этих событий в C# коде создаются обработчики. Отметим, далее, что вся логика приложения: чтение входного файла с диска, выделения полей и преобразование, запись в выходной файл и в базу данных, все это запускается по событию DoWork. А генерируется это событие вызовом из C# метода RunWorkerAsync(). Процесс выполнения задачи отображается на форме приложения с использованием событий ProgressChanged, которые генерируются вызовом метода ReportProgress.
Завершение процесса, отмена выполнения процесса, потенциально возможные ошибки формируют событие RunWorkerCompleted. А вот логика преобразования данных, которая является фрагментом реального приложения, наверное, не требует детального описания.
Code
(* 15.01.2014.
1. Разбор текстового файла сформированного при экспорте данных из Lotus Domino.
2. За основу взят класс BackgroundWorker и кое-что из класса IterativeBackgroundWorker, что в книге
    Don Syme Expert F# 3.0.
3. Программа читает текстовый файл, анализирует его, переформировывает, записывает в новом виде на диск
    в файл и в базу данных MSSQL.
4. with _ as err ->  failwith err.Message - прерывает работу программы
*)
namespace LibraryFSharp.ParceTxtFileKmis
    open System
    open System.IO
    open System.Text.RegularExpressions
    open Microsoft.FSharp.Data.TypeProviders
    
    open System.ComponentModel
    open System.Windows.Forms
    open System.Threading

    //Определим исключение
    exception ReadFileException of string

    // Определим делегат для работы с C#
    type Delegate = delegate of obj * System.EventArgs -> unit

    // Определим тип для сформированных записей
    type Record = {
        mutable FIO_DR      : string
        mutable strComp     : string
        mutable numPolis    : string
        }

    // Строка подключения к провайдеру SQL
    type SqlConnection =
        Microsoft.FSharp.Data.TypeProviders.SqlDataConnection<ConnectionString =
       // @"Data Source=(LocalDB)\v11.0;Initial Catalog=KMIS;Integrated Security=True;
       // Encrypt=False;TrustServerCertificate=False">
        @"Data Source=(localdb)\Projects;Initial Catalog=KMIS;Integrated Security=True;Connect Timeout=30;Encrypt=False;
        TrustServerCertificate=False">

    // Создадим класс на основе BackgroundWorker
    type ParseBackgroundWorker(inpFileName : string, outFileName: string) =
        class
            let worker = new BackgroundWorker(WorkerReportsProgress = true, WorkerSupportsCancellation = true)
       
            // Создадим события на все случаи
            let completed   = new Event<Delegate, System.EventArgs>()
            let error       = new Event<Delegate, System.EventArgs>()
            let cancelled   = new Event<Delegate, System.EventArgs>()
            let progress    = new Event<Delegate, System.EventArgs>()
            let started     = new Event<_>()

            // Выберем нужные поля
            let predicat = @"field1:|field2:|field3:"
            let repl str  = Regex.Replace(str,predicat, "")
             // Создадим пустую запись
            let mutable client = {FIO_DR = ""; strComp = ""; numPolis= ""}

            // Флаги начала и конца полей для одного человека
            let mutable flagStart = false
            let mutable flagStop  = false
            // Индекс для выходногоо массива
            let mutable j = 0

            // Функция выделения нужных полей
            let readFile (fileName : string) =
                try              
                    fileName
                    |> File.ReadLines
                    |> Seq.filter(fun str -> Regex.IsMatch(str, predicat))
                  //|> Seq.map (fun  s -> Regex.Replace(s,predicat, ""))
                    |> Seq.toArray                    
                with | _ -> raise (ReadFileException("Ошибка чтения файла :" + inpFileName ))

           // Событие запускается при вызове метода RunWorkerAsync()
            do worker.DoWork.Add(fun args ->
                 // Прочитаем файл  с диска             
                let data =
                    try
                        readFile inpFileName
                    with _ as err ->  failwith err.Message

                // Если все хорошо
                let sizeArrayRecord = data.Length/10
                let arrayRecord: (string array) = Array.zeroCreate sizeArrayRecord
                
                // Цикл анализа масссива data                             
                let select =
                    let rec parse i =
                        if i < (data.Length - 1) then
                            if worker.CancellationPending then args.Cancel <- true
                                else
                                let percent = int ((float (i + 1) / float (data.Length - 1)) * 100.0)                        
                                worker.ReportProgress(percent)
                                // Выделение полей
                                match data.[i] with
                                | var1 when     var1.Contains(@"field1:")   -> client.FIO_DR    <- data.[i]; flagStart <- true
                                | var1 when     var1.Contains(@"field2:")   -> client.strComp   <- data.[i]                     
                                | var1 when     var1.Contains(@"field3:")   -> client.numPolis  <- data.[i]; if (flagStart = true) then flagStop  <- true
                                | _ -> () //printfn "Нет ничего"
                            
                                // Запись в массив arrayRecord
                                if flagStart && flagStop then
                                    // Переведем запись в строку
                                    let all  = repl client.FIO_DR + " ;" + repl client.strComp + " ;" +  repl client.numPolis
                                   // Запишем в выходной массив
                                    arrayRecord.[j]  <- all
                                    j <- j + 1
                                    flagStart <- false
                                    flagStop  <- false
                                parse (i+1)
                    parse 0                                                 
                    arrayRecord

                // Запишем сформированный массив в выходной файл
                let nameFile = outFileName    
                try     File.WriteAllLines(nameFile, select)               
                with _  as err ->  failwith err.Message

                // Получим контекст базы данных
                let db = SqlConnection.GetDataContext()

                // Функция записи в базу данных
                let rec writeDataBase i =
                    if i < j then
                        let percent = int ((float (i + 1) / float (j- 1)) * 200.0)                        
                        worker.ReportProgress(percent)
                        let newRecord = new SqlConnection.ServiceTypes.Clients(Id = i, Fio = arrayRecord.[i], Strax="www", Polis ="334")
                        db.Clients.InsertOnSubmit(newRecord)
                        try
                            db.DataContext.SubmitChanges()
                            if worker.CancellationPending then args.Cancel <- true
                                else
                                writeDataBase (i + 1)
                        // Up trigger Error
                        with _ as e -> failwith e.Message
                writeDataBase 0
                // Конец процесса преобразования
                args.Result <- box "O.K."
                        )
                                   
            // Это событие возникает по завершению метода DoWork
            do worker.RunWorkerCompleted.Add(fun args ->
                if args.Cancelled then cancelled.Trigger(args.Error, System.EventArgs())
                elif args.Error <> null then error.Trigger (args.Error, System.EventArgs())
                else completed.Trigger (args.Result,System.EventArgs()))

            // Отображение процесса
            do worker.ProgressChanged.Add(fun args ->
                progress.Trigger (args.ProgressPercentage,System.EventArgs()))
      
            // Публикация методов
            [<CLIEvent>]
            member x.WorkerCompleted = completed.Publish
            [<CLIEvent>]
            member x.WorkerCancelled = cancelled.Publish
            [<CLIEvent>]
            member x.WorkerError = error.Publish
            [<CLIEvent>]
            member x.ProgressChanged = progress.Publish
            member x.Started = started.Publish

            member x.RunWorkerAsync() = worker.RunWorkerAsync()
            member x.CancelAsync() = worker.CancelAsync()

        end
        
2. C# проект ParseTextFile создан как Windows Forms Application.
Исходный текст абсолютно стандартен. Однако обратите внимание на то, как создается объект класса ParseBackgroundWorker и обработчики для событий, происходящих в F# классе.
Code

/* 15.01.2014 года.

 * 1. C# часть проекта Parsing Text File.

 * 2. Работа с GUI интерфесом пользователя.

 * 3. Рработа с клаасом ParseBackgroundWorker на стороне C#.

 */

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Windows.Forms;

using LibraryFSharp.ParceTxtFileKmis;

 

namespace ParseTextFile

{

    public partial class mainForm : Form

    {

        private ParseBackgroundWorker   lib;

        private OpenFileDialog          openFileDialog;

        private SaveFileDialog          saveFileDialog;

        private string                  inputFileName;

        private string                  outputFileName;

 

        public mainForm()

        {      

                InitializeComponent();

        }

 

        // Отмена работы

        public void handleCancelAsync(object sender, EventArgs e)

        {

            progress.Visible = false;

            label.Text = "Parsing canceled ...";

        }

 

        // Ошибка при выполнении класса IterativeBackgroundWorker

        public void handleError(object sender, EventArgs e)

        {

            progress.Visible = false;

            label.Text = "Error parsing ..";

        }

 

        // Завершение работы

        public void nandleComplete(object sender, EventArgs e)

        {

            progress.Visible = false;

            progress.Value = 0;

            label.Text = "O.K.";

        }

 

        // Отображение процесса выполнения

        public void handleProgrees(object sender, EventArgs e)

        {

            label.Text = "Processing ...";

            progress.Visible = true;

            progress.Value = (int)sender;

        }

 

        // Начало процесса выполнения задания

        private void startParseToolStripMenuItem_Click(object sender, EventArgs e)

        {

            try

            {

                // Имена входного и выходного файлов

                inputFileName = inpFileName.Text.Trim();

                outputFileName = outFileName.Text.Trim();

                if (String.IsNullOrEmpty(inputFileName)) throw new ArgumentNullException("Value", "Input File Name is null");

                if (String.IsNullOrEmpty(outputFileName)) throw new ArgumentNullException("Value", "Output File Name is null");

 

                // Создадим обьект класса ParseBackgroundWorke и обработчики событий в классе.

                lib = new LibraryFSharp.ParceTxtFileKmis.ParseBackgroundWorker(inputFileName, outputFileName);

                lib.ProgressChanged += handleProgrees;

                lib.WorkerCompleted += nandleComplete;

                lib.WorkerError += handleError;

                lib.WorkerCancelled += handleCancelAsync;    

 

                label.Text = "Read File ...";

                lib.RunWorkerAsync();

            }

            catch (Exception error)

            {

                MessageBox.Show(error.Message);

            }

        }

 

        // Выбор входного файла   

        private void openFileToolStripMenuItem_Click(object sender, EventArgs e)

        {

            openFileDialog = new OpenFileDialog();

            openFileDialog.InitialDirectory = "c:\\";

            openFileDialog.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*";

            openFileDialog.FilterIndex = 2;

            openFileDialog.RestoreDirectory = true;

 

            if (openFileDialog.ShowDialog() == DialogResult.OK)

            {

                inpFileName.Text = openFileDialog.FileName;

            }  

        }

 

        // Отмена задания

        private void stopParseToolStripMenuItem_Click(object sender, EventArgs e)

        {

            lib.CancelAsync();

        }

 

        // Exit

        private void exitToolStripMenuItem_Click(object sender, EventArgs e)

        {

            System.Environment.Exit(0);

            Application.Exit();

        }

 

        // Выбор выходного файла

        private void writeFileToolStripMenuItem_Click(object sender, EventArgs e)

        {

            saveFileDialog = new SaveFileDialog();

            saveFileDialog.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*";

            saveFileDialog.FilterIndex = 2;

            saveFileDialog.RestoreDirectory = true;

 

            if (saveFileDialog.ShowDialog() == DialogResult.OK)

            {

                outFileName.Text = saveFileDialog.FileName;

            }

        }

 

    }

}



Microsoft Visual Studio 2012 проект приложения можно свободно загрузить.
C уважением, Евгений Вересов.
15.01.2014 года.