source: trunk/sources/HeuristicLab.Problems.Instances.DataAnalysis/3.3/TableFileParser.cs @ 13447

Last change on this file since 13447 was 13447, checked in by gkronber, 6 years ago

#2071 more refactoring of TableFileParser to remove boxing of double values and to remove production of separator tokens

File size: 24.9 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2015 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22
23using System;
24using System.Collections;
25using System.Collections.Generic;
26using System.Globalization;
27using System.IO;
28using System.Linq;
29using System.Runtime;
30using System.Runtime.Serialization;
31using System.Text;
32
33namespace HeuristicLab.Problems.Instances.DataAnalysis {
34  public class TableFileParser : Progress<long> { // reports the number of bytes read
35    private const int BUFFER_SIZE = 65536;
36    // char used to symbolize whitespaces (no missing values can be handled with whitespaces)
37    private const char WHITESPACECHAR = (char)0;
38    private static readonly char[] POSSIBLE_SEPARATORS = new char[] { ',', ';', '\t', WHITESPACECHAR };
39    private Tokenizer tokenizer;
40    private int estimatedNumberOfLines = 200; // initial capacity for columns, will be set automatically when data is read from a file
41
42    private int rows;
43    public int Rows {
44      get { return rows; }
45      set { rows = value; }
46    }
47
48    private int columns;
49    public int Columns {
50      get { return columns; }
51      set { columns = value; }
52    }
53
54    private List<IList> values;
55    public List<IList> Values {
56      get {
57        return values;
58      }
59    }
60
61    private List<string> variableNames;
62    public IEnumerable<string> VariableNames {
63      get {
64        if (variableNames.Count > 0) return variableNames;
65        else {
66          string[] names = new string[columns];
67          for (int i = 0; i < names.Length; i++) {
68            names[i] = "X" + i.ToString("000");
69          }
70          return names;
71        }
72      }
73    }
74
75    public TableFileParser() {
76      variableNames = new List<string>();
77    }
78
79    public bool AreColumnNamesInFirstLine(string fileName) {
80      NumberFormatInfo numberFormat;
81      DateTimeFormatInfo dateTimeFormatInfo;
82      char separator;
83      DetermineFileFormat(fileName, out numberFormat, out dateTimeFormatInfo, out separator);
84      using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
85        return AreColumnNamesInFirstLine(stream, numberFormat, dateTimeFormatInfo, separator);
86      }
87    }
88
89    public bool AreColumnNamesInFirstLine(Stream stream) {
90      NumberFormatInfo numberFormat = NumberFormatInfo.InvariantInfo;
91      DateTimeFormatInfo dateTimeFormatInfo = DateTimeFormatInfo.InvariantInfo;
92      char separator = ',';
93      return AreColumnNamesInFirstLine(stream, numberFormat, dateTimeFormatInfo, separator);
94    }
95
96    public bool AreColumnNamesInFirstLine(string fileName, NumberFormatInfo numberFormat,
97                                         DateTimeFormatInfo dateTimeFormatInfo, char separator) {
98      using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
99        return AreColumnNamesInFirstLine(stream, numberFormat, dateTimeFormatInfo, separator);
100      }
101    }
102
103    public bool AreColumnNamesInFirstLine(Stream stream, NumberFormatInfo numberFormat,
104                                          DateTimeFormatInfo dateTimeFormatInfo, char separator) {
105      using (StreamReader reader = new StreamReader(stream)) {
106        tokenizer = new Tokenizer(reader, numberFormat, dateTimeFormatInfo, separator);
107        return (tokenizer.PeekType() != TokenTypeEnum.Double);
108      }
109    }
110
111    /// <summary>
112    /// Parses a file and determines the format first
113    /// </summary>
114    /// <param name="fileName">file which is parsed</param>
115    /// <param name="columnNamesInFirstLine"></param>
116    public void Parse(string fileName, bool columnNamesInFirstLine, int lineLimit = -1) {
117      NumberFormatInfo numberFormat;
118      DateTimeFormatInfo dateTimeFormatInfo;
119      char separator;
120      DetermineFileFormat(fileName, out numberFormat, out dateTimeFormatInfo, out separator);
121      EstimateNumberOfLines(fileName);
122      Parse(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), numberFormat, dateTimeFormatInfo, separator, columnNamesInFirstLine, lineLimit);
123    }
124
125    /// <summary>
126    /// Parses a file with the given formats
127    /// </summary>
128    /// <param name="fileName">file which is parsed</param>
129    /// <param name="numberFormat">Format of numbers</param>
130    /// <param name="dateTimeFormatInfo">Format of datetime</param>
131    /// <param name="separator">defines the separator</param>
132    /// <param name="columnNamesInFirstLine"></param>
133    public void Parse(string fileName, NumberFormatInfo numberFormat, DateTimeFormatInfo dateTimeFormatInfo, char separator, bool columnNamesInFirstLine, int lineLimit = -1) {
134      EstimateNumberOfLines(fileName);
135      using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
136        Parse(stream, numberFormat, dateTimeFormatInfo, separator, columnNamesInFirstLine, lineLimit);
137      }
138    }
139
140    // determines the number of newline characters in the first 64KB to guess the number of rows for a file
141    private void EstimateNumberOfLines(string fileName) {
142      var len = new System.IO.FileInfo(fileName).Length;
143      var buf = new char[64 * 1024];
144      using (var reader = new StreamReader(fileName)) {
145        reader.ReadBlock(buf, 0, buf.Length);
146      }
147      int numNewLine = 0;
148      int charsInCurrentLine = 0, charsInFirstLine = 0; // the first line (names) and the last line (incomplete) are not representative
149      foreach (var ch in buf) {
150        charsInCurrentLine++;
151        if (ch == '\n') {
152          if (numNewLine == 0) charsInFirstLine = charsInCurrentLine; // store the number of chars in the first line
153          charsInCurrentLine = 0;
154          numNewLine++;
155        }
156      }
157      if (numNewLine <= 1) {
158        // fail -> keep the default setting
159        return;
160      } else {
161        double charsPerLineFactor = (buf.Length - charsInFirstLine - charsInCurrentLine) / ((double)numNewLine - 1);
162        double estimatedLines = len / charsPerLineFactor;
163        estimatedNumberOfLines = (int)Math.Round(estimatedLines * 1.1); // pessimistic allocation of 110% to make sure that the list is very likely large enough
164      }
165    }
166
167    /// <summary>
168    /// Takes a Stream and parses it with default format. NumberFormatInfo.InvariantInfo, DateTimeFormatInfo.InvariantInfo and separator = ','
169    /// </summary>
170    /// <param name="stream">stream which is parsed</param>
171    /// <param name="columnNamesInFirstLine"></param>
172    public void Parse(Stream stream, bool columnNamesInFirstLine, int lineLimit = -1) {
173      NumberFormatInfo numberFormat = NumberFormatInfo.InvariantInfo;
174      DateTimeFormatInfo dateTimeFormatInfo = DateTimeFormatInfo.InvariantInfo;
175      char separator = ',';
176      Parse(stream, numberFormat, dateTimeFormatInfo, separator, columnNamesInFirstLine, lineLimit);
177    }
178
179    /// <summary>
180    /// Parses a stream with the given formats.
181    /// </summary>
182    /// <param name="stream">Stream which is parsed</param>   
183    /// <param name="numberFormat">Format of numbers</param>
184    /// <param name="dateTimeFormatInfo">Format of datetime</param>
185    /// <param name="separator">defines the separator</param>
186    /// <param name="columnNamesInFirstLine"></param>
187    public void Parse(Stream stream, NumberFormatInfo numberFormat, DateTimeFormatInfo dateTimeFormatInfo, char separator, bool columnNamesInFirstLine, int lineLimit = -1) {
188      using (StreamReader reader = new StreamReader(stream)) {
189        tokenizer = new Tokenizer(reader, numberFormat, dateTimeFormatInfo, separator);
190        values = new List<IList>();
191        if (lineLimit > 0) estimatedNumberOfLines = lineLimit;
192
193        if (columnNamesInFirstLine) {
194          ParseVariableNames();
195          if (!tokenizer.HasNext())
196            Error(
197              "Couldn't parse data values. Probably because of incorrect number format (the parser expects english number format with a '.' as decimal separator).",
198              "", tokenizer.CurrentLineNumber);
199        }
200
201
202        // read values... start in first row
203        int nLinesParsed = 0;
204        int colIdx = 0;
205        int numValuesInFirstRow = columnNamesInFirstLine ? variableNames.Count : -1; // number of variables or inizialize based on first row of values (-1)
206        while (tokenizer.HasNext() && (lineLimit < 0 || nLinesParsed < lineLimit)) {
207          if (tokenizer.PeekType() == TokenTypeEnum.NewLine) {
208            tokenizer.Skip();
209
210            // all rows have to have the same number of values
211            // the first row defines how many samples are needed
212            if (numValuesInFirstRow < 0) numValuesInFirstRow = values.Count; // set to number of colums in the first row
213            else if (colIdx > 0 && numValuesInFirstRow != colIdx) { // read at least one value in the row (support for skipping empty lines)
214              Error("The first row of the dataset has " + numValuesInFirstRow + " columns." + Environment.NewLine +
215                    "Line " + tokenizer.CurrentLineNumber + " has " + colIdx + " columns.", "",
216                    tokenizer.CurrentLineNumber);
217            }
218            OnReport(tokenizer.BytesRead);
219
220            nLinesParsed++;
221            colIdx = 0;
222          } else {
223            // read one value
224            TokenTypeEnum type; string strVal; double dblVal; DateTime dateTimeVal;
225            tokenizer.Next(out type, out strVal, out dblVal, out dateTimeVal);
226
227            // initialize columns on the first row (fixing data types as presented in the first row...)
228            if (nLinesParsed == 0) {
229              values.Add(CreateList(type, estimatedNumberOfLines));
230            } else if (colIdx == values.Count) {
231              Error("The first row of the dataset has " + numValuesInFirstRow + " columns." + Environment.NewLine +
232                    "Line " + tokenizer.CurrentLineNumber + " has more columns.", "",
233                tokenizer.CurrentLineNumber);
234            }
235            // add the value to the column
236            AddValue(type, values[colIdx++], strVal, dblVal, dateTimeVal);
237          }
238        }
239
240        if (!values.Any() || values.First().Count == 0)
241          Error("Couldn't parse data values. Probably because of incorrect number format " +
242                "(the parser expects english number format with a '.' as decimal separator).", "", tokenizer.CurrentLineNumber);
243      }
244
245      this.rows = values.First().Count;
246      this.columns = values.Count;
247
248      // after everything has been parsed make sure the lists are as compact as possible
249      foreach (var l in values) {
250        var dblList = l as List<double>;
251        var byteList = l as List<byte>;
252        var dateList = l as List<DateTime>;
253        var stringList = l as List<string>;
254        var objList = l as List<object>;
255        if (dblList != null) dblList.TrimExcess();
256        if (byteList != null) byteList.TrimExcess();
257        if (dateList != null) dateList.TrimExcess();
258        if (stringList != null) stringList.TrimExcess();
259        if (objList != null) objList.TrimExcess();
260      }
261
262      // for large files we created a lot of memory pressure, cannot hurt to run GC.Collect here (TableFileParser is called seldomly on user interaction)
263      GC.Collect(2, GCCollectionMode.Forced);
264    }
265
266    #region type-dependent dispatch
267    private void AddValue(TokenTypeEnum type, IList list, string strVal, double dblVal, DateTime dateTimeVal) {
268      switch (type) {
269        case TokenTypeEnum.Double:
270          AddDoubleToList(list, dblVal);
271          break;
272        case TokenTypeEnum.String:
273          AddStringToList(list, strVal);
274          break;
275        case TokenTypeEnum.DateTime:
276          AddDateTimeToList(list, dateTimeVal);
277          break;
278        default:
279          throw new InvalidOperationException();
280      }
281    }
282
283    private void AddDoubleToList(IList list, double dblVal) {
284      var dblList = list as List<double>;
285      if (dblList != null) dblList.Add(dblVal);
286      else {
287        var strList = list as List<string>;
288        if (strList != null) strList.Add(dblVal.ToString());
289        else list.Add(null);
290      }
291    }
292
293    private void AddStringToList(IList list, string strVal) {
294      var strList = list as List<string>;
295      if (strList != null) strList.Add(strVal);
296      else {
297        var dblList = list as List<double>;
298        if (dblList != null) dblList.Add(double.NaN);
299        else list.Add(null);
300      }
301    }
302
303    private void AddDateTimeToList(IList list, DateTime dateTimeVal) {
304      var dateTimeList = list as List<DateTime>;
305      if (dateTimeList != null) dateTimeList.Add(dateTimeVal);
306      else {
307        var dblList = list as List<double>;
308        if (dblList != null) dblList.Add(double.NaN);
309        else {
310          var strList = list as List<string>;
311          if (strList != null) strList.Add(dateTimeVal.ToString());
312          else list.Add(null);
313        }
314      }
315    }
316
317    private IList CreateList(TokenTypeEnum type, int estimatedNumberOfLines) {
318      switch (type) {
319        case TokenTypeEnum.String:
320          return new List<string>(estimatedNumberOfLines);
321        case TokenTypeEnum.Double:
322          return new List<double>(estimatedNumberOfLines);
323        case TokenTypeEnum.DateTime:
324          return new List<DateTime>(estimatedNumberOfLines);
325        default:
326          throw new InvalidOperationException();
327      }
328    }
329    #endregion
330
331    public static void DetermineFileFormat(string path, out NumberFormatInfo numberFormat, out DateTimeFormatInfo dateTimeFormatInfo, out char separator) {
332      DetermineFileFormat(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), out numberFormat, out dateTimeFormatInfo, out separator);
333    }
334
335    public static void DetermineFileFormat(Stream stream, out NumberFormatInfo numberFormat, out DateTimeFormatInfo dateTimeFormatInfo, out char separator) {
336      using (StreamReader reader = new StreamReader(stream)) {
337        // skip first line
338        reader.ReadLine();
339        // read a block
340        char[] buffer = new char[BUFFER_SIZE];
341        int charsRead = reader.ReadBlock(buffer, 0, BUFFER_SIZE);
342        // count frequency of special characters
343        Dictionary<char, int> charCounts = buffer.Take(charsRead)
344          .GroupBy(c => c)
345          .ToDictionary(g => g.Key, g => g.Count());
346
347        // depending on the characters occuring in the block
348        // we distinghish a number of different cases based on the the following rules:
349        // many points => it must be English number format, the other frequently occuring char is the separator
350        // no points but many commas => this is the problematic case. Either German format (real numbers) or English format (only integer numbers) with ',' as separator
351        //   => check the line in more detail:
352        //            English: 0, 0, 0, 0
353        //            German:  0,0 0,0 0,0 ...
354        //            => if commas are followed by space => English format
355        // no points no commas => English format (only integer numbers) use the other frequently occuring char as separator
356        // in all cases only treat ' ' as separator if no other separator is possible (spaces can also occur additionally to separators)
357        if (OccurrencesOf(charCounts, '.') > 10) {
358          numberFormat = NumberFormatInfo.InvariantInfo;
359          dateTimeFormatInfo = DateTimeFormatInfo.InvariantInfo;
360          separator = POSSIBLE_SEPARATORS
361            .Where(c => OccurrencesOf(charCounts, c) > 10)
362            .OrderBy(c => -OccurrencesOf(charCounts, c))
363            .DefaultIfEmpty(' ')
364            .First();
365        } else if (OccurrencesOf(charCounts, ',') > 10) {
366          // no points and many commas
367          // count the number of tokens (chains of only digits and commas) that contain multiple comma characters
368          int tokensWithMultipleCommas = 0;
369          for (int i = 0; i < charsRead; i++) {
370            int nCommas = 0;
371            while (i < charsRead && (buffer[i] == ',' || Char.IsDigit(buffer[i]))) {
372              if (buffer[i] == ',') nCommas++;
373              i++;
374            }
375            if (nCommas > 2) tokensWithMultipleCommas++;
376          }
377          if (tokensWithMultipleCommas > 1) {
378            // English format (only integer values) with ',' as separator
379            numberFormat = NumberFormatInfo.InvariantInfo;
380            dateTimeFormatInfo = DateTimeFormatInfo.InvariantInfo;
381            separator = ',';
382          } else {
383            char[] disallowedSeparators = new char[] { ',' };
384            // German format (real values)
385            numberFormat = NumberFormatInfo.GetInstance(new CultureInfo("de-DE"));
386            dateTimeFormatInfo = DateTimeFormatInfo.GetInstance(new CultureInfo("de-DE"));
387            separator = POSSIBLE_SEPARATORS
388              .Except(disallowedSeparators)
389              .Where(c => OccurrencesOf(charCounts, c) > 10)
390              .OrderBy(c => -OccurrencesOf(charCounts, c))
391              .DefaultIfEmpty(' ')
392              .First();
393          }
394        } else {
395          // no points and no commas => English format
396          numberFormat = NumberFormatInfo.InvariantInfo;
397          dateTimeFormatInfo = DateTimeFormatInfo.InvariantInfo;
398          separator = POSSIBLE_SEPARATORS
399            .Where(c => OccurrencesOf(charCounts, c) > 10)
400            .OrderBy(c => -OccurrencesOf(charCounts, c))
401            .DefaultIfEmpty(' ')
402            .First();
403        }
404      }
405    }
406
407    private static int OccurrencesOf(Dictionary<char, int> charCounts, char c) {
408      return charCounts.ContainsKey(c) ? charCounts[c] : 0;
409    }
410
411    #region tokenizer
412    // the tokenizer reads full lines and returns separated tokens in the line as well as a terminating end-of-line character
413    internal enum TokenTypeEnum {
414      NewLine, String, Double, DateTime
415    }
416
417    internal class Tokenizer {
418      private StreamReader reader;
419      // we assume that a buffer of 1024 tokens for a line is sufficient most of the time (the buffer is increased below if necessary)
420      private TokenTypeEnum[] tokenTypes = new TokenTypeEnum[1024];
421      private string[] stringVals = new string[1024];
422      private double[] doubleVals = new double[1024];
423      private DateTime[] dateTimeVals = new DateTime[1024];
424      private int tokenPos;
425      private int numTokens;
426      private NumberFormatInfo numberFormatInfo;
427      private DateTimeFormatInfo dateTimeFormatInfo;
428      private char separator;
429
430      // arrays for string.Split()
431      private readonly char[] whiteSpaceSeparators = new char[0]; // string split uses separators as default
432      private readonly char[] separators;
433
434      private int currentLineNumber = 0;
435      public int CurrentLineNumber {
436        get { return currentLineNumber; }
437        private set { currentLineNumber = value; }
438      }
439      private string currentLine;
440      public string CurrentLine {
441        get { return currentLine; }
442        private set { currentLine = value; }
443      }
444      public long BytesRead {
445        get;
446        private set;
447      }
448
449      public Tokenizer(StreamReader reader, NumberFormatInfo numberFormatInfo, DateTimeFormatInfo dateTimeFormatInfo, char separator) {
450        this.reader = reader;
451        this.numberFormatInfo = numberFormatInfo;
452        this.dateTimeFormatInfo = dateTimeFormatInfo;
453        this.separator = separator;
454        this.separators = new char[] { separator };
455        ReadNextTokens();
456      }
457
458      public bool HasNext() {
459        return numTokens > tokenPos || !reader.EndOfStream;
460      }
461
462      public TokenTypeEnum PeekType() {
463        return tokenTypes[tokenPos];
464      }
465
466      public void Skip() {
467        // simply skips one token without returning the result values
468        tokenPos++;
469        if (numTokens == tokenPos) {
470          ReadNextTokens();
471        }
472      }
473
474      public void Next(out TokenTypeEnum type, out string strVal, out double dblVal, out DateTime dateTimeVal) {
475        type = tokenTypes[tokenPos];
476        strVal = stringVals[tokenPos];
477        dblVal = doubleVals[tokenPos];
478        dateTimeVal = dateTimeVals[tokenPos];
479        Skip();
480      }
481
482      private void ReadNextTokens() {
483        if (!reader.EndOfStream) {
484          CurrentLine = reader.ReadLine();
485          CurrentLineNumber++;
486          try {
487            BytesRead = reader.BaseStream.Position;
488          } catch (IOException) {
489            BytesRead += CurrentLine.Length + 2; // guess
490          } catch (NotSupportedException) {
491            BytesRead += CurrentLine.Length + 2;
492          }
493          int i = 0;
494          if (!string.IsNullOrWhiteSpace(CurrentLine)) {
495            foreach (var tok in Split(CurrentLine)) {
496              TokenTypeEnum type;
497              double doubleVal;
498              DateTime dateTimeValue;
499              type = TokenTypeEnum.String; // default
500              stringVals[i] = tok.Trim();
501              if (double.TryParse(tok, NumberStyles.Float, numberFormatInfo, out doubleVal)) {
502                type = TokenTypeEnum.Double;
503                doubleVals[i] = doubleVal;
504              } else if (DateTime.TryParse(tok, dateTimeFormatInfo, DateTimeStyles.None, out dateTimeValue)) {
505                type = TokenTypeEnum.DateTime;
506                dateTimeVals[i] = dateTimeValue;
507              }
508
509              // couldn't parse the token as an int or float number or datetime value so return a string token
510
511              tokenTypes[i] = type;
512              i++;
513
514              if (i >= tokenTypes.Length) {
515                // increase buffer size if necessary
516                IncreaseCapacity(ref tokenTypes);
517                IncreaseCapacity(ref doubleVals);
518                IncreaseCapacity(ref stringVals);
519                IncreaseCapacity(ref dateTimeVals);
520              }
521            }
522          }
523          tokenTypes[i] = TokenTypeEnum.NewLine;
524          numTokens = i + 1;
525          tokenPos = 0;
526        }
527      }
528
529      private IEnumerable<string> Split(string line) {
530        return separator == WHITESPACECHAR ?
531          line.Split(whiteSpaceSeparators, StringSplitOptions.RemoveEmptyEntries) :
532          line.Split(separators);
533      }
534
535      private static void IncreaseCapacity<T>(ref T[] arr) {
536        int n = (int)Math.Floor(arr.Length * 1.7); // guess
537        T[] arr2 = new T[n];
538        Array.Copy(arr, arr2, arr.Length);
539        arr = arr2;
540      }
541    }
542    #endregion
543
544    #region parsing
545
546    private void ParseVariableNames() {
547      // the first line must contain variable names
548      List<string> varNames = new List<string>();
549
550      TokenTypeEnum type;
551      string strVal;
552      double dblVal;
553      DateTime dateTimeVal;
554
555      tokenizer.Next(out type, out strVal, out dblVal, out dateTimeVal);
556
557      // the first token must be a variable name
558      if (type != TokenTypeEnum.String)
559        throw new ArgumentException("Error: Expected " + TokenTypeEnum.String + " got " + type);
560      varNames.Add(strVal);
561
562      while (tokenizer.HasNext() && tokenizer.PeekType() != TokenTypeEnum.NewLine) {
563        tokenizer.Next(out type, out strVal, out dblVal, out dateTimeVal);
564        varNames.Add(strVal);
565      }
566      ExpectType(TokenTypeEnum.NewLine);
567
568      variableNames = varNames;
569    }
570
571    private void ExpectType(TokenTypeEnum expectedToken) {
572      if (tokenizer.PeekType() != expectedToken)
573        throw new ArgumentException("Error: Expected " + expectedToken + " got " + tokenizer.PeekType());
574      tokenizer.Skip();
575    }
576
577    private void Error(string message, string token, int lineNumber) {
578      throw new DataFormatException("Error while parsing.\n" + message, token, lineNumber);
579    }
580    #endregion
581
582    [Serializable]
583    public class DataFormatException : Exception {
584      private int line;
585      public int Line {
586        get { return line; }
587      }
588      private string token;
589      public string Token {
590        get { return token; }
591      }
592      public DataFormatException(string message, string token, int line)
593        : base(message + "\nToken: " + token + " (line: " + line + ")") {
594        this.token = token;
595        this.line = line;
596      }
597
598      public DataFormatException(SerializationInfo info, StreamingContext context) : base(info, context) { }
599    }
600  }
601}
Note: See TracBrowser for help on using the repository browser.