Lean  $LEAN_TAG$
StreamReaderExtensions.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.IO;
18 using System.Text;
19 using System.Globalization;
20 using System.Collections.Concurrent;
21 using System.Runtime.CompilerServices;
22 
23 namespace QuantConnect.Util
24 {
25  /// <summary>
26  /// Extension methods to fetch data from a <see cref="StreamReader"/> instance
27  /// </summary>
28  /// <remarks>The value of these methods is performance. The objective is to avoid using
29  /// <see cref="StreamReader.ReadLine"/> and having to create intermediate substrings, parsing and splitting</remarks>
30  public static class StreamReaderExtensions
31  {
32  // we use '-1' value as a flag to determine whether we have decimal places or not, so we avoid having another variable required
33  private const int NoDecimalPlaces = -1;
34  private const char NoMoreData = unchecked((char)-1);
35  private const char DefaultDelimiter = ',';
36 
37  /// <summary>
38  /// Gets a decimal from the provided stream reader
39  /// </summary>
40  /// <param name="stream">The data stream</param>
41  /// <param name="delimiter">The data delimiter character to use, default is ','</param>
42  /// <returns>The decimal read from the stream</returns>
43  [MethodImpl(MethodImplOptions.AggressiveInlining)]
44  public static decimal GetDecimal(this StreamReader stream, char delimiter = DefaultDelimiter)
45  {
46  return GetDecimal(stream, out _, delimiter);
47  }
48 
49  /// <summary>
50  /// Gets a decimal from the provided stream reader
51  /// </summary>
52  /// <param name="stream">The data stream</param>
53  /// <param name="delimiter">The data delimiter character to use, default is ','</param>
54  /// <param name="pastEndLine">True if end line was past, useful for consumers to know a line ended</param>
55  /// <returns>The decimal read from the stream</returns>
56  [MethodImpl(MethodImplOptions.AggressiveInlining)]
57  public static decimal GetDecimal(this StreamReader stream, out bool pastEndLine, char delimiter = DefaultDelimiter)
58  {
59  long value = 0;
60  var decimalPlaces = NoDecimalPlaces;
61  var current = (char)stream.Read();
62 
63  while (current == ' ')
64  {
65  current = (char)stream.Read();
66  }
67 
68  var isNegative = current == '-';
69  if (isNegative)
70  {
71  current = (char)stream.Read();
72  }
73 
74  pastEndLine = current == '\n' || current == '\r' && (stream.Peek() != '\n' || stream.Read() == '\n') || current == NoMoreData;
75  while (!(current == delimiter || pastEndLine || current == ' '))
76  {
77  if (current == '.')
78  {
79  decimalPlaces = 0;
80  }
81  else
82  {
83  value = value * 10 + (current - '0');
84  if (decimalPlaces != NoDecimalPlaces)
85  {
86  decimalPlaces++;
87  }
88  }
89  current = (char)stream.Read();
90  pastEndLine = current == '\n' || current == '\r' && (stream.Peek() != '\n' || stream.Read() == '\n') || current == NoMoreData;
91  }
92 
93  var lo = (int)value;
94  var mid = (int)(value >> 32);
95  return new decimal(lo, mid, 0, isNegative, (byte)(decimalPlaces != NoDecimalPlaces ? decimalPlaces : 0));
96  }
97 
98  /// <summary>
99  /// Gets a date time instance from a stream reader
100  /// </summary>
101  /// <param name="stream">The data stream</param>
102  /// <param name="format">The format in which the date time is</param>
103  /// <param name="delimiter">The data delimiter character to use, default is ','</param>
104  /// <returns>The date time instance read</returns>
105  [MethodImpl(MethodImplOptions.AggressiveInlining)]
106  public static DateTime GetDateTime(this StreamReader stream, string format = DateFormat.TwelveCharacter, char delimiter = DefaultDelimiter)
107  {
108  var current = (char)stream.Read();
109  while (current == ' ')
110  {
111  current = (char)stream.Read();
112  }
113 
114  var index = 0;
115  // we know the exact format we want to parse so we can allocate the char array and not use an expensive string builder
116  var data = new char[format.Length];
117  while (!(current == delimiter || current == '\n' || current == '\r' && (stream.Peek() != '\n' || stream.Read() == '\n') || current == NoMoreData))
118  {
119  data[index++] = current;
120  current = (char)stream.Read();
121  }
122 
123  return DateTime.ParseExact(data,
124  format,
125  CultureInfo.InvariantCulture);
126  }
127 
128  /// <summary>
129  /// Gets an integer from a stream reader
130  /// </summary>
131  /// <param name="stream">The data stream</param>
132  /// <param name="delimiter">The data delimiter character to use, default is ','</param>
133  /// <returns>The integer instance read</returns>
134  [MethodImpl(MethodImplOptions.AggressiveInlining)]
135  public static int GetInt32(this StreamReader stream, char delimiter = DefaultDelimiter)
136  {
137  var result = 0;
138  var current = (char)stream.Read();
139 
140  while (current == ' ')
141  {
142  current = (char)stream.Read();
143  }
144 
145  var isNegative = current == '-';
146  if (isNegative)
147  {
148  current = (char)stream.Read();
149  }
150 
151  while (!(current == delimiter || current == '\n' || current == '\r' && (stream.Peek() != '\n' || stream.Read() == '\n') || current == NoMoreData || current == ' '))
152  {
153  result = (current - '0') + result * 10;
154  current = (char)stream.Read();
155  }
156  return isNegative ? result * -1 : result;
157  }
158 
159  private readonly static ConcurrentBag<StringBuilder> StringBuilders = new();
160 
161  /// <summary>
162  /// Gets a string from a stream reader
163  /// </summary>
164  /// <param name="stream">The data stream</param>
165  /// <param name="delimiter">The data delimiter character to use, default is ','</param>
166  /// <returns>The string instance read</returns>
167  [MethodImpl(MethodImplOptions.AggressiveInlining)]
168  public static string GetString(this StreamReader stream, char delimiter = DefaultDelimiter)
169  {
170  if (!StringBuilders.TryTake(out var builder))
171  {
172  builder = new();
173  }
174 
175  try
176  {
177  var current = (char)stream.Read();
178 
179  while (!(current == delimiter || current == '\n' || current == '\r' && (stream.Peek() != '\n' || stream.Read() == '\n') || current == NoMoreData))
180  {
181  builder.Append(current);
182  current = (char)stream.Read();
183  }
184  return builder.ToString();
185  }
186  finally
187  {
188  builder.Clear();
189  StringBuilders.Add(builder);
190  }
191  }
192  }
193 }