PageRenderTime 49ms CodeModel.GetById 15ms app.highlight 15ms RepoModel.GetById 15ms app.codeStats 0ms

/Raptorious.SharpMT940Lib/Mt940Parser.cs

https://bitbucket.org/raptux/sharpmt940lib
C# | 458 lines | 290 code | 55 blank | 113 comment | 51 complexity | 449e80fcd45899dc3a22cfa07acb519a MD5 | raw file
  1/*
  2* Copyright (c) 2012 Jaco Adriaansen
  3* This code is distributed under the MIT (for details please see license.txt)
  4*/
  5
  6using Raptorious.SharpMt940Lib.Mt940Format;
  7using System;
  8using System.Collections.Generic;
  9using System.Globalization;
 10using System.IO;
 11using System.Linq;
 12using System.Text;
 13
 14namespace Raptorious.SharpMt940Lib
 15{
 16    /// <summary>
 17    ///
 18    /// </summary>
 19    public static class Mt940Parser
 20    {
 21        /// <summary>
 22        /// Opens the given file, reads it and returns a collection of CustomerStatementMessages using the current culture.
 23        /// </summary>
 24        /// <param name="file">File to read.</param>
 25        /// <param name="format">Bank specific format of this file.</param>
 26        /// <param name="mt940Params">Additional mt940 parameters</param>
 27        /// <returns>Returns a collection of customer statement messages, populated by the data of the given file.</returns>
 28        [Obsolete("Please use the Parse method with CultureInfo")]
 29        public static ICollection<CustomerStatementMessage> Parse(IMt940Format format, string file, Parameters mt940Params = null)
 30        {
 31            if (format == null)
 32            {
 33                throw new ArgumentNullException(nameof(format));
 34            }
 35
 36            if (String.IsNullOrWhiteSpace(file))
 37            {
 38                throw new ArgumentException("file can not be empty", file);
 39            }
 40
 41            return Parse(format, file, CultureInfo.CurrentCulture, mt940Params);
 42        }
 43
 44        /// <summary>
 45        /// Opens the given file, reads it and returns a collection of CustomerStatementMessages.
 46        /// </summary>
 47        /// <param name="file">File to read.</param>
 48        /// <param name="format">Bank specific format of this file.</param>
 49        /// <param name="cultureInfo">Specifies the culture to use</param>
 50        /// <param name="mt940Parameters">Additional mt940 parameters</param>
 51        /// <returns>Returns a collection of customer statement messages, populated by the data of the given file.</returns>
 52        public static ICollection<CustomerStatementMessage> Parse(IMt940Format format, string file, CultureInfo cultureInfo, Parameters mt940Parameters = null)
 53        {
 54            if (format == null)
 55            {
 56                throw new ArgumentNullException(nameof(format));
 57            }
 58
 59            if (String.IsNullOrWhiteSpace(file))
 60            {
 61                throw new ArgumentException("file can not be empty");
 62            }
 63
 64            if (cultureInfo == null)
 65            {
 66                throw new ArgumentNullException(nameof(cultureInfo));
 67            }
 68
 69
 70            if (!File.Exists(file))
 71            {
 72                throw new FileNotFoundException("Can not find file.", file);
 73            }
 74
 75            using (StreamReader reader = new StreamReader(File.OpenRead(file)))
 76            {
 77                return Parse(format, reader, cultureInfo, mt940Parameters);
 78            }
 79        }
 80
 81        /// <summary>
 82        /// Reads the given string to the end and parses the data to Customer Statement Messages.
 83        /// </summary>
 84        /// <param name="fileStream">Filestream to read.</param>
 85        /// <param name="format">Bank specific format of this file.</param>
 86        /// <param name="mt940Params">Additional mt940 parameters</param>
 87        /// <returns></returns>
 88        [Obsolete("Please use the Parse method with CultureInfo")]
 89        public static ICollection<CustomerStatementMessage> Parse(IMt940Format format, TextReader fileStream, Parameters mt940Params = null)
 90        {
 91            if (format == null)
 92            {
 93                throw new ArgumentNullException(nameof(format));
 94            }
 95
 96            if (fileStream == null)
 97            {
 98                throw new ArgumentNullException(nameof(fileStream));
 99            }
100
101            return Parse(format, fileStream, CultureInfo.CurrentCulture, mt940Params);
102        }
103
104        /// <summary>
105        /// Reads the given string to the end and parses the data to Customer Statement Messages.
106        /// </summary>
107        /// <param name="fileStream">Filestream to read.</param>
108        /// <param name="format">Bank specific format of this file.</param>
109        /// <param name="cultureInfo">Specifies the culture information to use</param>
110        /// <param name="mt940Parameters">Additional mt940 parameters</param>
111        /// <returns></returns>
112        public static ICollection<CustomerStatementMessage> Parse(IMt940Format format, TextReader fileStream, CultureInfo cultureInfo, Parameters mt940Parameters = null)
113        {
114            if (format == null)
115            {
116                throw new ArgumentNullException(nameof(format));
117            }
118
119            if (fileStream == null)
120            {
121                throw new ArgumentNullException(nameof(fileStream));
122            }
123
124            if (cultureInfo == null)
125            {
126                throw new ArgumentNullException(nameof(cultureInfo));
127            }
128
129            var completeFile = fileStream.ReadToEnd();
130            return ParseData(format, completeFile, cultureInfo, mt940Parameters);
131        }
132
133        /// <summary>
134        /// Reads the given string to the end and parses the data to Customer Statement Messages using the current culture
135        /// </summary>
136        /// <param name="format">Bank specific format of this file.</param>
137        /// <param name="data">String containing the MT940 file.</param>
138        /// <param name="mt940Params">Additional mt940 parameters</param>
139        /// <returns></returns>
140        [Obsolete("Please use the Parse method with CultureInfo")]
141        public static ICollection<CustomerStatementMessage> ParseData(IMt940Format format, string data, Parameters mt940Params = null)
142        {
143            if (format == null)
144            {
145                throw new ArgumentNullException(nameof(format));
146            }
147
148            if (String.IsNullOrWhiteSpace(data))
149            {
150                throw new ArgumentException("data can not be empty", nameof(data));
151            }
152
153            if (mt940Params == null)
154            {
155                // default values
156                mt940Params = new Parameters();
157            }
158
159            return ParseData(format, data, CultureInfo.CurrentCulture, mt940Params);
160        }
161
162        /// <summary>
163        /// Reads the given string to the end and parses the data to Customer Statement Messages
164        /// </summary>
165        /// <param name="format"></param>
166        /// <param name="data"></param>
167        /// <param name="cultureInfo">Specifies the culture information to use</param>
168        /// <param name="mt940Parameters">Additional mt940 parameters</param>
169        /// <returns></returns>
170        public static ICollection<CustomerStatementMessage> ParseData(IMt940Format format, string data, CultureInfo cultureInfo, Parameters mt940Parameters = null)
171        {
172            if (format == null)
173            {
174                throw new ArgumentNullException(nameof(format));
175            }
176
177            if (String.IsNullOrWhiteSpace(data))
178            {
179                throw new ArgumentException("data can not be empty", nameof(data));
180            }
181
182            if (mt940Parameters == null)
183            {
184                // default values
185                mt940Parameters = new Parameters();
186            }
187
188            var listData = CreateStringTransactions(format, data);
189            return CreateObjectTransactions(format, listData, cultureInfo, mt940Parameters);
190        }
191
192        /// <summary>
193        /// 
194        /// </summary>
195        /// <param name="format">IMt940Format implementation</param>
196        /// <param name="data">A collection of string arrays formatted by CreateStringTransactions()</param>
197        /// <param name="cultureInfo">The culture to use</param>
198        /// <param name="mt940Params">Additional mt940 parameters</param>
199        /// <see cref="CreateStringTransactions"></see>
200        /// <returns></returns>
201        /// TODO: This method is way to complex. It should be simplified.
202        private static ICollection<CustomerStatementMessage> CreateObjectTransactions(
203            IMt940Format format,
204            ICollection<String[]> data,
205            CultureInfo cultureInfo,
206            Parameters mt940Params)
207        {
208            // Create a new list.
209            var customerStatementList = new List<CustomerStatementMessage>();
210
211
212            // For each string collection of commands.
213            foreach (String[] line in data)
214            {
215                int transactionPointer = 0; // Start of the transaction.
216
217                // Skip the header (for some reason)
218                transactionPointer += format.Header.LineCount; // SWIFT HEADER.
219
220                var transaction = default(Transaction); // Set transaction to its default (null).
221                var customerStatementMessage = new CustomerStatementMessage();
222
223                // ReSharper disable AccessToModifiedClosure
224                Action addAndNullTransactionIfPresent = () =>
225                {
226                    if (customerStatementMessage != null && transaction != null)
227                        customerStatementMessage.Transactions.Add(transaction);
228                    transaction = null;
229                };
230                // ReSharper restore AccessToModifiedClosure
231
232                // Loop through the array.
233                for (; transactionPointer < line.Length; transactionPointer++)
234                {
235                    // Set transactionLine to the current line.
236                    string transactionLine = line[transactionPointer];
237
238                    // Skip if null, CreateObjectTransactions kinda leaves a mess.
239                    if (transactionLine != null)
240                    {
241                        // Get the command number.
242                        var tag = transactionLine.Substring(transactionLine.IndexOf(':'), transactionLine.IndexOf(':', 1) + 1);
243
244                        // Get the command data.
245                        var transactionData = transactionLine.Substring(tag.Length);
246
247                        // Fill the object the right data.
248                        switch (tag)
249                        {
250                            case ":20:":
251                                customerStatementMessage = customerStatementMessage.SetTransactionReference(transactionData);
252                                break;
253                            case ":21:":
254                                customerStatementMessage = customerStatementMessage.SetRelatedMessage(transactionData);
255                                break;
256                            case ":25:":
257                                customerStatementMessage = customerStatementMessage.SetAccount(transactionData, mt940Params.ClearAccount);
258                                break;
259                            case ":28:":
260                            case ":28C:":
261                                customerStatementMessage.SetSequenceNumber(transactionData, cultureInfo);
262                                break;
263                            case ":60m:":
264                            case ":60F:":
265                            case ":60M:":
266                                customerStatementMessage = customerStatementMessage.SetOpeningBalance(new TransactionBalance(transactionData, cultureInfo));
267                                break;
268                            case ":61:":
269                                addAndNullTransactionIfPresent();
270                                transaction = new Transaction(transactionData, customerStatementMessage.OpeningBalance.Currency, cultureInfo);
271                                break;
272                            case ":86:":
273                                /* 
274                                * If the previous line was a 61 (ie, we have a transaction), the 'Information to Account Owner'
275                                * applies to the transaction, otherwise it applies to the whole message.
276                                */
277                                if (transaction == null)
278                                    customerStatementMessage = customerStatementMessage.SetDescription(transactionData);
279                                else
280                                {
281                                    transaction.Description = transactionData;
282                                }
283                                addAndNullTransactionIfPresent();
284                                break;
285                            case ":62F:":
286                                addAndNullTransactionIfPresent();
287                                customerStatementMessage = customerStatementMessage.SetClosingBalance(new TransactionBalance(transactionData, cultureInfo));
288                                break;
289                            case ":62m:":
290                            case ":62M:":
291                                customerStatementMessage = customerStatementMessage.SetClosingBalance(new TransactionBalance(transactionData, cultureInfo));
292                                break;
293                            case ":64:":
294                                addAndNullTransactionIfPresent();
295                                customerStatementMessage = customerStatementMessage.SetClosingAvailableBalance(new TransactionBalance(transactionData, cultureInfo));
296                                break;
297                            case ":65:":
298                                customerStatementMessage = customerStatementMessage.SetForwardAvailableBalance(new TransactionBalance(transactionData, cultureInfo));
299                                break;
300                        }
301                    }
302                }
303
304                customerStatementList.Add(customerStatementMessage);
305            }
306
307            // geting :86: details
308            if (mt940Params.Codes.Count > 0)
309            {
310                var constantCode = mt940Params.ConstantCode();
311
312                foreach (var item in customerStatementList)
313                {
314                    foreach (var tr in item.Transactions)
315                    {
316                        Mt940Parser.Parse86Details(tr, mt940Params, constantCode);
317                    }
318                }
319            }
320
321            return customerStatementList;
322        }
323
324        private static void Parse86Details(Transaction tr, Parameters mt940Params, bool constantCode)
325        {
326            if (constantCode)
327            {
328                throw new NotImplementedException("to do...");
329            }
330            else
331            {
332                string data = tr.Description;
333                char separator = mt940Params.Codes.First().Value[0][0];
334                foreach (var item in mt940Params.Codes)
335                {
336                    string[] _codes = item.Value;
337                    string value = "";
338                    foreach (var cs in _codes)
339                    {
340                        var startIndex = data.IndexOf(cs, StringComparison.CurrentCulture);
341                        if (startIndex > -1)
342                        {
343                            startIndex += cs.Length;
344                            var endIndex = data.IndexOf(separator, startIndex);
345                            var line = data.Substring(startIndex, endIndex - startIndex).Trim();
346                            if (mt940Params.MaxLineLength > 0 && line.Length >= mt940Params.MaxLineLength)
347                                value += line;
348                            else
349                                value += line + " ";
350                        }
351                    }
352                    value = value.Trim();
353                    if (!string.IsNullOrEmpty(value))
354                    {
355                        switch (item.Key)
356                        {
357                            case TransactionDetail.Account:
358                                tr.Details.Account = value;
359                                break;
360                            case TransactionDetail.Name:
361                                tr.Details.Name = value;
362                                break;
363                            case TransactionDetail.Description:
364                                tr.Details.Description = value;
365                                break;
366                            case TransactionDetail.Empty:
367                            default:
368                                break;
369                        }
370                    }
371                }
372
373            }
374        }
375
376        /// <summary>
377        /// This method accepts mt940 data file given as a string. The string 
378        /// is split by Environment.NewLine as each line contains a command.
379        /// 
380        /// Every line that starts with a ':' is a mt940 'command'. Lines that 
381        /// does not start with a ':' belongs to the previous command. 
382        /// 
383        /// The method returns a collection of string arrays. Every item in 
384        /// the collection is a mt940 message. 
385        /// </summary>
386        /// <param name="data">A string of MT940 data to parse.</param>
387        /// <param name="format">Specifies the bank specific format</param>
388        /// <returns>A collection :)</returns>
389        private static ICollection<String[]> CreateStringTransactions(IMt940Format format, string data)
390        {
391            // Split on the new line seperator. In a MT940 messsage, every command is on a seperate line.
392            // Assumption is made it is in the same format as the enviroments new line.
393            var tokenized = data.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
394
395            // Create  an empty list of string arrays.
396            var transactions = new List<String[]>();
397
398
399            // Offset pointer, starts a the first line (zero based index).
400            int pointer = 0;
401
402            // Loop trough the entire file?
403            while (pointer < tokenized.Length)
404            {
405                // Seperator, this is the Trailer! We split messages based on trailer! - Right, check.
406                var trailerIndex = Array.IndexOf(tokenized, format.Trailer.Data, pointer);
407
408                // When we found a trailer.. then..
409                if (trailerIndex >= 0)
410                {
411                    // Create a new array the holds the correct number of elements.
412                    String[] currentTransaction = new String[trailerIndex - pointer];
413
414                    // Copy the data from the source array to our current transaction.
415                    Array.Copy(tokenized, pointer, currentTransaction, 0, currentTransaction.Length);
416
417                    // Walk trough the current message. Start at the current 
418                    // index and stop at the separator.
419                    for (int index = currentTransaction.Length - 1;
420                         index > format.Header.LineCount;
421                         index--)
422                    {
423                        // 
424                        String transactionItem = currentTransaction[index];
425
426                        System.Diagnostics.Debug.Assert(transactionItem != null);
427
428                        // If the transactionItem doesn't start with : then the
429                        // current line belongs to the previous one.
430                        if (!transactionItem.StartsWith(":", StringComparison.Ordinal))
431                        {
432                            // Append ths current line to the previous line seperated by
433                            // and NewLine.
434                            currentTransaction[index - 1] += Environment.NewLine;
435                            currentTransaction[index - 1] += transactionItem;
436
437                            // Set the current item to null, it doesn't exist anymore.
438                            currentTransaction[index] = null;
439                        }
440                    }
441
442                    // Add the current transaction.
443                    transactions.Add(currentTransaction);
444
445                    // Next up!
446                    pointer = (trailerIndex + 1);
447                }
448                else
449                {
450                    // Message doesn't contain a trailer. So it is invalid!
451                    throw new InvalidDataException("Can not find trailer!");
452                }
453            }
454
455            return transactions;
456        }
457    }
458}