Free cookie consent management tool by TermsFeed Policy Generator

source: branches/3040_VectorBasedGP/HeuristicLab.Problems.Instances.DataAnalysis/3.3/TableFileParser.cs @ 17414

Last change on this file since 17414 was 17414, checked in by pfleck, 5 years ago

#3040 Started adding UCI time series regression benchmarks.
Adapted parser (extracted format options & added parsing for double vectors).

File size: 29.1 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 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
22using System;
23using System.Collections;
24using System.Collections.Generic;
25using System.Diagnostics.Contracts;
26using System.Globalization;
27using System.IO;
28using System.Linq;
29using System.Text;
30using HeuristicLab.Problems.DataAnalysis;
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
42    private Encoding encoding = Encoding.Default;
43
44    public Encoding Encoding {
45      get { return encoding; }
46      set {
47        if (value == null) throw new ArgumentNullException("Encoding");
48        encoding = value;
49      }
50    }
51
52
53    private int rows;
54    public int Rows {
55      get { return rows; }
56      set { rows = value; }
57    }
58
59    private int columns;
60    public int Columns {
61      get { return columns; }
62      set { columns = value; }
63    }
64
65    private List<IList> values;
66    public List<IList> Values {
67      get {
68        return values;
69      }
70    }
71
72    private List<string> variableNames;
73    public IEnumerable<string> VariableNames {
74      get {
75        if (variableNames.Count > 0) return variableNames;
76        else {
77          string[] names = new string[columns];
78          for (int i = 0; i < names.Length; i++) {
79            names[i] = "X" + i.ToString("000");
80          }
81          return names;
82        }
83      }
84    }
85
86    public TableFileParser() {
87      variableNames = new List<string>();
88    }
89
90    public bool AreColumnNamesInFirstLine(string fileName) {
91      var formatOptions = DetermineFileFormat(fileName);
92      using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
93        return AreColumnNamesInFirstLine(stream, formatOptions);
94      }
95    }
96
97    public bool AreColumnNamesInFirstLine(Stream stream) {
98      var formatOptions = new TableFileFormatOptions {
99        NumberFormat = NumberFormatInfo.InvariantInfo,
100        DateTimeFormat = DateTimeFormatInfo.InvariantInfo,
101        ColumnSeparator = ','
102      };
103      return AreColumnNamesInFirstLine(stream, formatOptions);
104    }
105
106    public bool AreColumnNamesInFirstLine(string fileName, TableFileFormatOptions formatOptions) {
107      using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
108        return AreColumnNamesInFirstLine(stream, formatOptions);
109      }
110    }
111
112    public bool AreColumnNamesInFirstLine(Stream stream, TableFileFormatOptions formatOptions) {
113      using (StreamReader reader = new StreamReader(stream, Encoding)) {
114        tokenizer = new Tokenizer(reader, formatOptions);
115        return (tokenizer.PeekType() != TokenTypeEnum.Double);
116      }
117    }
118
119    /// <summary>
120    /// Parses a file and determines the format first
121    /// </summary>
122    /// <param name="fileName">file which is parsed</param>
123    /// <param name="columnNamesInFirstLine"></param>
124    public void Parse(string fileName, bool columnNamesInFirstLine, int lineLimit = -1) {
125      var formatOptions = DetermineFileFormat(fileName);
126      EstimateNumberOfLines(fileName);
127      Parse(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), formatOptions, columnNamesInFirstLine, lineLimit);
128    }
129
130    /// <summary>
131    /// Parses a file with the given formats
132    /// </summary>
133    /// <param name="fileName">file which is parsed</param>
134    /// <param name="numberFormat">Format of numbers</param>
135    /// <param name="dateTimeFormatInfo">Format of datetime</param>
136    /// <param name="separator">defines the separator</param>
137    /// <param name="columnNamesInFirstLine"></param>
138    public void Parse(string fileName, TableFileFormatOptions formatOptions, bool columnNamesInFirstLine, int lineLimit = -1) {
139      EstimateNumberOfLines(fileName);
140      using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
141        Parse(stream, formatOptions, columnNamesInFirstLine, lineLimit);
142      }
143    }
144
145    // determines the number of newline characters in the first 64KB to guess the number of rows for a file
146    private void EstimateNumberOfLines(string fileName) {
147      var len = new System.IO.FileInfo(fileName).Length;
148      var buf = new char[1024 * 1024];
149      using (var reader = new StreamReader(fileName, Encoding)) {
150        reader.ReadBlock(buf, 0, buf.Length);
151      }
152      int numNewLine = 0;
153      int charsInCurrentLine = 0, charsInFirstLine = 0; // the first line (names) and the last line (incomplete) are not representative
154      foreach (var ch in buf) {
155        charsInCurrentLine++;
156        if (ch == '\n') {
157          if (numNewLine == 0) charsInFirstLine = charsInCurrentLine; // store the number of chars in the first line
158          charsInCurrentLine = 0;
159          numNewLine++;
160        }
161      }
162      if (numNewLine <= 1) {
163        // fail -> keep the default setting
164        return;
165      } else {
166        double charsPerLineFactor = (buf.Length - charsInFirstLine - charsInCurrentLine) / ((double)numNewLine - 1);
167        double estimatedLines = len / charsPerLineFactor;
168        estimatedNumberOfLines = (int)Math.Round(estimatedLines * 1.1); // pessimistic allocation of 110% to make sure that the list is very likely large enough
169      }
170    }
171
172    /// <summary>
173    /// Takes a Stream and parses it with default format. NumberFormatInfo.InvariantInfo, DateTimeFormatInfo.InvariantInfo and separator = ','
174    /// </summary>
175    /// <param name="stream">stream which is parsed</param>
176    /// <param name="columnNamesInFirstLine"></param>
177    public void Parse(Stream stream, bool columnNamesInFirstLine, int lineLimit = -1) {
178      var formatOptions = new TableFileFormatOptions {
179        NumberFormat = NumberFormatInfo.InvariantInfo,
180        DateTimeFormat = DateTimeFormatInfo.InvariantInfo,
181        ColumnSeparator = ','
182      };
183      Parse(stream, formatOptions, columnNamesInFirstLine, lineLimit);
184    }
185
186    /// <summary>
187    /// Parses a stream with the given formats.
188    /// </summary>
189    /// <param name="stream">Stream which is parsed</param>   
190    /// <param name="numberFormat">Format of numbers</param>
191    /// <param name="dateTimeFormatInfo">Format of datetime</param>
192    /// <param name="separator">defines the separator</param>
193    /// <param name="columnNamesInFirstLine"></param>
194    public void Parse(Stream stream, TableFileFormatOptions formatOptions, bool columnNamesInFirstLine, int lineLimit = -1) {
195      if (lineLimit > 0) estimatedNumberOfLines = lineLimit;
196
197      using (var reader = new StreamReader(stream)) {
198        tokenizer = new Tokenizer(reader, formatOptions);
199        var strValues = new List<List<string>>();
200        values = new List<IList>();
201        Prepare(columnNamesInFirstLine, strValues);
202
203        int nLinesParsed = 0;
204        int colIdx = 0;
205        while (tokenizer.HasNext() && (lineLimit < 0 || nLinesParsed < lineLimit)) {
206          if (tokenizer.PeekType() == TokenTypeEnum.NewLine) {
207            tokenizer.Skip();
208
209            // all rows have to have the same number of values
210            // the first row defines how many elements are needed
211            if (colIdx > 0 && values.Count != colIdx) {
212              // read at least one value in the row (support for skipping empty lines)
213              Error("The first row of the dataset has " + values.Count + " columns." + Environment.NewLine +
214                    "Line " + tokenizer.CurrentLineNumber + " has " + colIdx + " columns.", "",
215                tokenizer.CurrentLineNumber);
216            }
217            OnReport(tokenizer.BytesRead);
218
219            nLinesParsed++;
220            colIdx = 0;
221          } else {
222            // read one value
223            TokenTypeEnum type;
224            string strVal;
225            double dblVal;
226            DateTime dateTimeVal;
227            tokenizer.Next(out type, out strVal, out dblVal, out dateTimeVal);
228
229            if (colIdx == values.Count) {
230              Error("The first row of the dataset has " + values.Count + " columns." + Environment.NewLine +
231                    "Line " + tokenizer.CurrentLineNumber + " has more columns.", "",
232                tokenizer.CurrentLineNumber);
233            }
234            if (!IsColumnTypeCompatible(values[colIdx], type)) {
235              values[colIdx] = strValues[colIdx];
236            }
237
238            // add the value to the column
239            AddValue(type, values[colIdx], strVal, dblVal, dateTimeVal);
240            if (!(values[colIdx] is List<string>)) { // optimization: don't store the string values in another list if the column is list<string>
241              strValues[colIdx].Add(strVal);
242            }
243            colIdx++;
244          }
245        }
246      }
247
248      if (!values.Any() || values.First().Count == 0)
249        Error("Couldn't parse data values. Probably because of incorrect number format " +
250              "(the parser expects english number format with a '.' as decimal separator).", "", tokenizer.CurrentLineNumber);
251
252      this.rows = values.First().Count;
253      this.columns = values.Count;
254
255      // see if any string column can be converted to vectors
256      if (formatOptions.VectorSeparator != null) {
257        for (int i = 0; i < values.Count; i++) {
258          if (!(values[i] is List<string> stringList)) continue;
259
260          var strings = new string[stringList.Count][];
261          var doubles = new double[strings.Length][];
262          bool allDoubles = true;
263          for (int j = 0; j < strings.Length && allDoubles; j++) {
264            strings[j] = stringList[j].Split(formatOptions.VectorSeparator.Value);
265            doubles[j] = new double[strings[j].Length];
266            for (int k = 0; k < doubles[j].Length && allDoubles; k++) {
267              allDoubles = double.TryParse(strings[j][k], NumberStyles.Float, formatOptions.NumberFormat, out doubles[j][k]);
268            }
269          }
270
271          if (allDoubles) {
272            var vectorList = new List<DoubleVector>(stringList.Count);
273            for (int j = 0; j < doubles.Length; j++) {
274              vectorList.Add(new DoubleVector(doubles[j]));
275            }
276
277            values[i] = vectorList;
278          }
279        }
280      }
281
282      // replace lists with undefined type (object) with double-lists
283      for (int i = 0; i < values.Count; i++) {
284        if (values[i] is List<object>) {
285          values[i] = Enumerable.Repeat(double.NaN, rows).ToList();
286        }
287      }
288
289      // after everything has been parsed make sure the lists are as compact as possible
290      foreach (var l in values) {
291        var dblList = l as List<double>;
292        var byteList = l as List<byte>;
293        var dateList = l as List<DateTime>;
294        var stringList = l as List<string>;
295        var objList = l as List<object>;
296        var vecList = l as List<DoubleVector>;
297        if (dblList != null) dblList.TrimExcess();
298        if (byteList != null) byteList.TrimExcess();
299        if (dateList != null) dateList.TrimExcess();
300        if (stringList != null) stringList.TrimExcess();
301        if (objList != null) objList.TrimExcess();
302        if (vecList != null) vecList.TrimExcess();
303      }
304
305      // for large files we created a lot of memory pressure, cannot hurt to run GC.Collect here (TableFileParser is called seldomly on user interaction)
306      GC.Collect(2, GCCollectionMode.Forced);
307    }
308
309    private void Prepare(bool columnNamesInFirstLine, List<List<string>> strValues) {
310      if (columnNamesInFirstLine) {
311        ParseVariableNames();
312        if (!tokenizer.HasNext())
313          Error(
314            "Couldn't parse data values. Probably because of incorrect number format (the parser expects english number format with a '.' as decimal separator).",
315            "", tokenizer.CurrentLineNumber);
316      }
317      // read first line to determine types and allocate specific lists
318      // read values... start in first row
319      int colIdx = 0;
320      while (tokenizer.PeekType() != TokenTypeEnum.NewLine) {
321        // read one value
322        TokenTypeEnum type; string strVal; double dblVal; DateTime dateTimeVal;
323        tokenizer.Next(out type, out strVal, out dblVal, out dateTimeVal);
324
325        // initialize column
326        values.Add(CreateList(type, estimatedNumberOfLines));
327        if (type == TokenTypeEnum.String)
328          strValues.Add(new List<string>(0)); // optimization: don't store the string values in another list if the column is list<string>
329        else
330          strValues.Add(new List<string>(estimatedNumberOfLines));
331
332        AddValue(type, values[colIdx], strVal, dblVal, dateTimeVal);
333        if (type != TokenTypeEnum.String)
334          strValues[colIdx].Add(strVal);
335        colIdx++;
336      }
337      tokenizer.Skip(); // skip newline
338    }
339
340    #region type-dependent dispatch
341    private bool IsColumnTypeCompatible(IList list, TokenTypeEnum tokenType) {
342      return (list is List<object>) || // unknown lists are compatible to everything (potential conversion)
343             (list is List<string>) || // all tokens can be added to a string list
344             (tokenType == TokenTypeEnum.Missing) || // empty entries are allowed in all columns
345             (tokenType == TokenTypeEnum.Double && list is List<double>) ||
346             (tokenType == TokenTypeEnum.DateTime && list is List<DateTime>);
347    }
348
349    // all columns are converted to string columns when we find an non-empty value that has incorrect type
350    private IList ConvertToStringColumn(IList list) {
351      var dblL = list as List<double>;
352      if (dblL != null) {
353        var l = new List<string>(dblL.Capacity);
354        l.AddRange(dblL.Select(dbl => dbl.ToString()));
355        return l;
356      }
357
358      var dtL = list as List<DateTime>;
359      if (dtL != null) {
360        var l = new List<string>(dtL.Capacity);
361        l.AddRange(dtL.Select(dbl => dbl.ToString()));
362        return l;
363      }
364
365      if (list is List<string>) return list;
366
367      throw new InvalidProgramException(string.Format("Cannot convert column of type {0} to string column", list.GetType()));
368    }
369
370    private void AddValue(TokenTypeEnum type, IList list, string strVal, double dblVal, DateTime dateTimeVal) {
371      // Add value if list has a defined type
372      var dblList = list as List<double>;
373      if (dblList != null) {
374        AddValue(type, dblList, dblVal);
375        return;
376      }
377      var strList = list as List<string>;
378      if (strList != null) {
379        AddValue(type, strList, strVal);
380        return;
381      }
382      var dtList = list as List<DateTime>;
383      if (dtList != null) {
384        AddValue(type, dtList, dateTimeVal);
385        return;
386      }
387
388      // Undefined list-type
389      if (type == TokenTypeEnum.Missing) {
390        // add null to track number of missing values
391        list.Add(null);
392      } else { // first non-missing value for undefined list-type
393        var newList = ConvertList(type, list, estimatedNumberOfLines);
394        // replace list
395        var idx = values.IndexOf(list);
396        values[idx] = newList;
397        // recursively call AddValue
398        AddValue(type, newList, strVal, dblVal, dateTimeVal);
399      }
400    }
401
402    private static void AddValue(TokenTypeEnum type, List<double> list, double dblVal) {
403      Contract.Assert(type == TokenTypeEnum.Missing || type == TokenTypeEnum.Double);
404      list.Add(type == TokenTypeEnum.Missing ? double.NaN : dblVal);
405    }
406
407    private static void AddValue(TokenTypeEnum type, List<string> list, string strVal) {
408      // assumes that strVal is always set to the original token read from the input file
409      list.Add(type == TokenTypeEnum.Missing ? string.Empty : strVal);
410    }
411
412    private static void AddValue(TokenTypeEnum type, List<DateTime> list, DateTime dtVal) {
413      Contract.Assert(type == TokenTypeEnum.Missing || type == TokenTypeEnum.DateTime);
414      list.Add(type == TokenTypeEnum.Missing ? DateTime.MinValue : dtVal);
415    }
416
417    private static IList CreateList(TokenTypeEnum type, int estimatedNumberOfLines) {
418      switch (type) {
419        case TokenTypeEnum.String:
420          return new List<string>(estimatedNumberOfLines);
421        case TokenTypeEnum.Double:
422          return new List<double>(estimatedNumberOfLines);
423        case TokenTypeEnum.DateTime:
424          return new List<DateTime>(estimatedNumberOfLines);
425        case TokenTypeEnum.Missing: // List<object> represent list of unknown type
426          return new List<object>(estimatedNumberOfLines);
427        default:
428          throw new InvalidOperationException();
429      }
430    }
431
432    private static IList ConvertList(TokenTypeEnum type, IList list, int estimatedNumberOfLines) {
433      var newList = CreateList(type, estimatedNumberOfLines);
434      object missingValue = GetMissingValue(type);
435      for (int i = 0; i < list.Count; i++)
436        newList.Add(missingValue);
437      return newList;
438    }
439    private static object GetMissingValue(TokenTypeEnum type) {
440      switch (type) {
441        case TokenTypeEnum.String: return string.Empty;
442        case TokenTypeEnum.Double: return double.NaN;
443        case TokenTypeEnum.DateTime: return DateTime.MinValue;
444        default: throw new ArgumentOutOfRangeException("type", type, "No missing value defined");
445      }
446    }
447    #endregion
448
449    public static TableFileFormatOptions DetermineFileFormat(string path) {
450      return DetermineFileFormat(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
451    }
452
453    public static TableFileFormatOptions DetermineFileFormat(Stream stream) {
454      using (StreamReader reader = new StreamReader(stream)) {
455        // skip first line
456        reader.ReadLine();
457        // read a block
458        char[] buffer = new char[BUFFER_SIZE];
459        int charsRead = reader.ReadBlock(buffer, 0, BUFFER_SIZE);
460        // count frequency of special characters
461        Dictionary<char, int> charCounts = buffer.Take(charsRead)
462          .GroupBy(c => c)
463          .ToDictionary(g => g.Key, g => g.Count());
464
465        // depending on the characters occuring in the block
466        // we distinghish a number of different cases based on the the following rules:
467        // many points => it must be English number format, the other frequently occuring char is the separator
468        // no points but many commas => this is the problematic case. Either German format (real numbers) or English format (only integer numbers) with ',' as separator
469        //   => check the line in more detail:
470        //            English: 0, 0, 0, 0
471        //            German:  0,0 0,0 0,0 ...
472        //            => if commas are followed by space => English format
473        // no points no commas => English format (only integer numbers) use the other frequently occuring char as separator
474        // in all cases only treat ' ' as separator if no other separator is possible (spaces can also occur additionally to separators)
475        if (OccurrencesOf(charCounts, '.') > 10) {
476          return new TableFileFormatOptions {
477            NumberFormat = NumberFormatInfo.InvariantInfo,
478            DateTimeFormat = DateTimeFormatInfo.InvariantInfo,
479            ColumnSeparator = POSSIBLE_SEPARATORS
480              .Where(c => OccurrencesOf(charCounts, c) > 10)
481              .OrderBy(c => -OccurrencesOf(charCounts, c))
482                .DefaultIfEmpty(' ')
483              .First()
484          };
485        } else if (OccurrencesOf(charCounts, ',') > 10) {
486          // no points and many commas
487          // count the number of tokens (chains of only digits and commas) that contain multiple comma characters
488          int tokensWithMultipleCommas = 0;
489          for (int i = 0; i < charsRead; i++) {
490            int nCommas = 0;
491            while (i < charsRead && (buffer[i] == ',' || Char.IsDigit(buffer[i]))) {
492              if (buffer[i] == ',') nCommas++;
493              i++;
494            }
495            if (nCommas > 2) tokensWithMultipleCommas++;
496          }
497          if (tokensWithMultipleCommas > 1) {
498            // English format (only integer values) with ',' as separator
499            return new TableFileFormatOptions {
500              NumberFormat = NumberFormatInfo.InvariantInfo,
501              DateTimeFormat = DateTimeFormatInfo.InvariantInfo,
502              ColumnSeparator = ','
503            };
504          } else {
505            char[] disallowedSeparators = new char[] { ',' }; // n. def. contains a space so ' ' should be disallowed to, however existing unit tests would fail
506            // German format (real values)
507            return new TableFileFormatOptions {
508              NumberFormat = NumberFormatInfo.GetInstance(new CultureInfo("de-DE")),
509              DateTimeFormat = DateTimeFormatInfo.GetInstance(new CultureInfo("de-DE")),
510              ColumnSeparator = POSSIBLE_SEPARATORS
511                .Except(disallowedSeparators)
512                .Where(c => OccurrencesOf(charCounts, c) > 10)
513                .OrderBy(c => -OccurrencesOf(charCounts, c))
514                .DefaultIfEmpty(' ')
515                .First()
516            };
517          }
518        } else {
519          // no points and no commas => English format
520          return new TableFileFormatOptions {
521            NumberFormat = NumberFormatInfo.InvariantInfo,
522            DateTimeFormat = DateTimeFormatInfo.InvariantInfo,
523            ColumnSeparator = POSSIBLE_SEPARATORS
524              .Where(c => OccurrencesOf(charCounts, c) > 10)
525              .OrderBy(c => -OccurrencesOf(charCounts, c))
526              .DefaultIfEmpty(' ')
527              .First()
528          };
529        }
530      }
531    }
532
533    private static int OccurrencesOf(Dictionary<char, int> charCounts, char c) {
534      return charCounts.ContainsKey(c) ? charCounts[c] : 0;
535    }
536
537    #region tokenizer
538    // the tokenizer reads full lines and returns separated tokens in the line as well as a terminating end-of-line character
539    internal enum TokenTypeEnum {
540      NewLine, String, Double, DateTime, Missing
541    }
542
543    internal class Tokenizer {
544      private StreamReader reader;
545      // we assume that a buffer of 1024 tokens for a line is sufficient most of the time (the buffer is increased below if necessary)
546      private TokenTypeEnum[] tokenTypes = new TokenTypeEnum[1024];
547      private string[] stringVals = new string[1024];
548      private double[] doubleVals = new double[1024];
549      private DateTime[] dateTimeVals = new DateTime[1024];
550      private int tokenPos;
551      private int numTokens;
552      private NumberFormatInfo numberFormatInfo;
553      private DateTimeFormatInfo dateTimeFormatInfo;
554      private char separator;
555
556      // arrays for string.Split()
557      private readonly char[] whiteSpaceSeparators = new char[0]; // string split uses separators as default
558      private readonly char[] separators;
559
560      private int currentLineNumber = 0;
561      public int CurrentLineNumber {
562        get { return currentLineNumber; }
563        private set { currentLineNumber = value; }
564      }
565      private string currentLine;
566      public string CurrentLine {
567        get { return currentLine; }
568        private set { currentLine = value; }
569      }
570      public long BytesRead {
571        get;
572        private set;
573      }
574
575      public Tokenizer(StreamReader reader, TableFileFormatOptions formatOptions) {
576        this.reader = reader;
577        this.numberFormatInfo = formatOptions.NumberFormat;
578        this.dateTimeFormatInfo = formatOptions.DateTimeFormat;
579        this.separator = formatOptions.ColumnSeparator;
580        this.separators = new char[] { separator };
581        ReadNextTokens();
582      }
583
584      public bool HasNext() {
585        return numTokens > tokenPos || !reader.EndOfStream;
586      }
587
588      public TokenTypeEnum PeekType() {
589        return tokenTypes[tokenPos];
590      }
591
592      public void Skip() {
593        // simply skips one token without returning the result values
594        tokenPos++;
595        if (numTokens == tokenPos) {
596          ReadNextTokens();
597        }
598      }
599
600      public void Next(out TokenTypeEnum type, out string strVal, out double dblVal, out DateTime dateTimeVal) {
601        type = tokenTypes[tokenPos];
602        strVal = stringVals[tokenPos];
603        dblVal = doubleVals[tokenPos];
604        dateTimeVal = dateTimeVals[tokenPos];
605        Skip();
606      }
607
608      private void ReadNextTokens() {
609        if (!reader.EndOfStream) {
610          CurrentLine = reader.ReadLine();
611          CurrentLineNumber++;
612          if (reader.BaseStream.CanSeek) {
613            BytesRead = reader.BaseStream.Position;
614          } else {
615            BytesRead += CurrentLine.Length + 2; // guess
616          }
617          int i = 0;
618          if (!string.IsNullOrWhiteSpace(CurrentLine)) {
619            foreach (var tok in Split(CurrentLine)) {
620              TokenTypeEnum type;
621              double doubleVal;
622              DateTime dateTimeValue;
623              type = TokenTypeEnum.String; // default
624              stringVals[i] = tok.Trim();
625              if (double.TryParse(tok, NumberStyles.Float, numberFormatInfo, out doubleVal)) {
626                type = TokenTypeEnum.Double;
627                doubleVals[i] = doubleVal;
628              } else if (DateTime.TryParse(tok, dateTimeFormatInfo, DateTimeStyles.NoCurrentDateDefault, out dateTimeValue)
629                && (dateTimeValue.Year > 1 || dateTimeValue.Month > 1 || dateTimeValue.Day > 1)// if no date is given it is returned as 1.1.0001 -> don't allow this
630                ) {
631                type = TokenTypeEnum.DateTime;
632                dateTimeVals[i] = dateTimeValue;
633              } else if (string.IsNullOrWhiteSpace(tok)) {
634                type = TokenTypeEnum.Missing;
635              }
636
637              // couldn't parse the token as an int or float number or datetime value so return a string token
638
639              tokenTypes[i] = type;
640              i++;
641
642              if (i >= tokenTypes.Length) {
643                // increase buffer size if necessary
644                IncreaseCapacity(ref tokenTypes);
645                IncreaseCapacity(ref doubleVals);
646                IncreaseCapacity(ref stringVals);
647                IncreaseCapacity(ref dateTimeVals);
648              }
649            }
650          }
651          tokenTypes[i] = TokenTypeEnum.NewLine;
652          numTokens = i + 1;
653          tokenPos = 0;
654        }
655      }
656
657      private IEnumerable<string> Split(string line) {
658        return separator == WHITESPACECHAR ?
659          line.Split(whiteSpaceSeparators, StringSplitOptions.RemoveEmptyEntries) :
660          line.Split(separators);
661      }
662
663      private static void IncreaseCapacity<T>(ref T[] arr) {
664        int n = (int)Math.Floor(arr.Length * 1.7); // guess
665        T[] arr2 = new T[n];
666        Array.Copy(arr, arr2, arr.Length);
667        arr = arr2;
668      }
669    }
670    #endregion
671
672    #region parsing
673
674    private void ParseVariableNames() {
675      // the first line must contain variable names
676      List<string> varNames = new List<string>();
677
678      TokenTypeEnum type;
679      string strVal;
680      double dblVal;
681      DateTime dateTimeVal;
682
683      tokenizer.Next(out type, out strVal, out dblVal, out dateTimeVal);
684
685      // the first token must be a variable name
686      if (type != TokenTypeEnum.String)
687        throw new ArgumentException("Error: Expected " + TokenTypeEnum.String + " got " + type);
688      varNames.Add(strVal);
689
690      while (tokenizer.HasNext() && tokenizer.PeekType() != TokenTypeEnum.NewLine) {
691        tokenizer.Next(out type, out strVal, out dblVal, out dateTimeVal);
692        varNames.Add(strVal);
693      }
694      ExpectType(TokenTypeEnum.NewLine);
695
696      variableNames = varNames;
697    }
698
699    private void ExpectType(TokenTypeEnum expectedToken) {
700      if (tokenizer.PeekType() != expectedToken)
701        throw new ArgumentException("Error: Expected " + expectedToken + " got " + tokenizer.PeekType());
702      tokenizer.Skip();
703    }
704
705    private void Error(string message, string token, int lineNumber) {
706      throw new IOException(string.Format("Error while parsing. {0} (token: {1} lineNumber: {2}).", message, token, lineNumber));
707    }
708    #endregion
709  }
710}
Note: See TracBrowser for help on using the repository browser.