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

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

#2071 improved memory efficiency in TableFileParser by removing duplicate storage of all columns, added heuristic to estimate the necessary capacity of columns

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