Lean  $LEAN_TAG$
StringExtensions.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.Globalization;
18 using System.Text;
19 
20 namespace QuantConnect
21 {
22  /// <summary>
23  /// Provides extension methods for properly parsing and serializing values while properly using
24  /// an IFormatProvider/CultureInfo when applicable
25  /// </summary>
26  public static class StringExtensions
27  {
28  private static readonly CultureInfo CultureInfo = CultureInfo.InvariantCulture;
29  private static readonly IFormatProvider FormatProvider = CultureInfo;
30  private static readonly StringComparison StringComparison = StringComparison.InvariantCulture;
31 
32  /// <summary>
33  /// Converts the provided <paramref name="value"/> as <typeparamref name="T"/>
34  /// using <see cref="CultureInfo"/>
35  /// </summary>
36  public static T ConvertInvariant<T>(this object value)
37  {
38  return (T) value.ConvertInvariant(typeof(T));
39  }
40 
41  /// <summary>
42  /// Converts the provided <paramref name="value"/> as <paramref name="conversionType"/>
43  /// using <see cref="CultureInfo"/>
44  /// </summary>
45  /// <remarks>
46  /// This implementation uses the Convert.ToXXX methods. This causes null values to be converted to the default value
47  /// for the provided <paramref name="conversionType"/>. This is in contrast to directly calling <see cref="IConvertible.ToType"/>
48  /// which results in an <see cref="InvalidCastException"/> or a <see cref="FormatException"/>. Since existing code is
49  /// dependent on this null -> default value conversion behavior, it has been preserved in this method.
50  /// </remarks>
51  public static object ConvertInvariant(this object value, Type conversionType)
52  {
53  switch (Type.GetTypeCode(conversionType))
54  {
55  // these cases are purposefully ordered to ensure the compiler can generate a jump table vs a binary tree
56  case TypeCode.Empty:
57  throw new ArgumentException(Messages.StringExtensions.ConvertInvariantCannotConvertTo(TypeCode.Empty));
58 
59  case TypeCode.Object:
60  var convertible = value as IConvertible;
61  if (convertible != null)
62  {
63  return convertible.ToType(conversionType, FormatProvider);
64  }
65 
66  return Convert.ChangeType(value, conversionType, FormatProvider);
67 
68  case TypeCode.DBNull:
69  throw new ArgumentException(Messages.StringExtensions.ConvertInvariantCannotConvertTo(TypeCode.DBNull));
70 
71  case TypeCode.Boolean:
72  return Convert.ToBoolean(value, FormatProvider);
73 
74  case TypeCode.Char:
75  return Convert.ToChar(value, FormatProvider);
76 
77  case TypeCode.SByte:
78  return Convert.ToSByte(value, FormatProvider);
79 
80  case TypeCode.Byte:
81  return Convert.ToByte(value, FormatProvider);
82 
83  case TypeCode.Int16:
84  return Convert.ToInt16(value, FormatProvider);
85 
86  case TypeCode.UInt16:
87  return Convert.ToUInt16(value, FormatProvider);
88 
89  case TypeCode.Int32:
90  return Convert.ToInt32(value, FormatProvider);
91 
92  case TypeCode.UInt32:
93  return Convert.ToUInt32(value, FormatProvider);
94 
95  case TypeCode.Int64:
96  return Convert.ToInt64(value, FormatProvider);
97 
98  case TypeCode.UInt64:
99  return Convert.ToUInt64(value, FormatProvider);
100 
101  case TypeCode.Single:
102  return Convert.ToSingle(value, FormatProvider);
103 
104  case TypeCode.Double:
105  return Convert.ToDouble(value, FormatProvider);
106 
107  case TypeCode.Decimal:
108  return Convert.ToDecimal(value, FormatProvider);
109 
110  case TypeCode.DateTime:
111  return Convert.ToDateTime(value, FormatProvider);
112 
113  case TypeCode.String:
114  return Convert.ToString(value, FormatProvider);
115 
116  default:
117  return Convert.ChangeType(value, conversionType, FormatProvider);
118  }
119  }
120 
121  /// <summary>
122  /// Non-extension method alias for <see cref="FormattableString.Invariant"/>
123  /// This supports the <code>using static QuantConnect.StringExtensions</code> syntax
124  /// and is aimed at ensuring all formatting is piped through this class instead of
125  /// alternatively piping through directly to <see cref="FormattableString.Invariant"/>
126  /// </summary>
127  public static string Invariant(FormattableString formattable)
128  {
129  return FormattableString.Invariant(formattable);
130  }
131 
132  /// <summary>
133  /// Converts the provided value to a string using <see cref="CultureInfo"/>
134  /// </summary>
135  public static string ToStringInvariant(this IConvertible convertible)
136  {
137  if (convertible == null)
138  {
139  return string.Empty;
140  }
141 
142  return convertible.ToString(FormatProvider);
143  }
144 
145  /// <summary>
146  /// Formats the provided value using the specified <paramref name="format"/> and
147  /// <see cref="CultureInfo"/>
148  /// </summary>
149  public static string ToStringInvariant(this IFormattable formattable, string format)
150  {
151  if (formattable == null)
152  {
153  return string.Empty;
154  }
155 
156  // if we have a colon, this implies there's a width parameter in the format it seems this isn't handled
157  // as one would expect. For example, specifying something like $"{value,10:0.00}" would force the string
158  // to be at least 10 characters wide with extra padding in the front, but passing the string '10:0.00' or
159  // ',10:0.00' doesn't work. If we are able to detect a colon in the format and the values preceding the colon,
160  // are numeric, then we know it starts with a width parameter and we can pipe it into a custom-formed
161  // string.format call to get the correct output
162  if (format != null)
163  {
164  var indexOfColon = format.IndexOfInvariant(":");
165  if (indexOfColon != -1)
166  {
167  int padding;
168  var beforeColon = format.Substring(0, indexOfColon);
169  if (int.TryParse(beforeColon, out padding))
170  {
171  return string.Format(FormatProvider, $"{{0,{format}}}", formattable);
172  }
173  }
174  }
175 
176  return formattable.ToString(format, FormatProvider);
177  }
178 
179  /// <summary>
180  /// Provides a convenience methods for converting a <see cref="DateTime"/> to an invariant ISO-8601 string
181  /// </summary>
182  public static string ToIso8601Invariant(this DateTime dateTime)
183  {
184  return dateTime.ToStringInvariant("O");
185  }
186 
187  /// <summary>
188  /// Checks if the string starts with the provided <paramref name="beginning"/> using <see cref="CultureInfo"/>
189  /// while optionally ignoring case.
190  /// </summary>
191  public static bool StartsWithInvariant(this string value, string beginning, bool ignoreCase = false)
192  {
193  return value.StartsWith(beginning, ignoreCase, CultureInfo);
194  }
195 
196  /// <summary>
197  /// Checks if the string ends with the provided <paramref name="ending"/> using <see cref="CultureInfo"/>
198  /// while optionally ignoring case.
199  /// </summary>
200  public static bool EndsWithInvariant(this string value, string ending, bool ignoreCase = false)
201  {
202  return value.EndsWith(ending, ignoreCase, CultureInfo);
203  }
204 
205  /// <summary>
206  /// Gets the index of the specified <paramref name="character"/> using <see cref="StringComparison"/>
207  /// </summary>
208  public static int IndexOfInvariant(this string value, char character)
209  {
210  return value.IndexOf(character);
211  }
212 
213  /// <summary>
214  /// Gets the index of the specified <paramref name="substring"/> using <see cref="StringComparison"/>
215  /// or <see cref="System.StringComparison.InvariantCultureIgnoreCase"/> when <paramref name="ignoreCase"/> is true
216  /// </summary>
217  public static int IndexOfInvariant(this string value, string substring, bool ignoreCase = false)
218  {
219  return value.IndexOf(substring, ignoreCase
220  ? StringComparison.InvariantCultureIgnoreCase
221  : StringComparison
222  );
223  }
224 
225  /// <summary>
226  /// Gets the index of the specified <paramref name="substring"/> using <see cref="StringComparison"/>
227  /// or <see cref="System.StringComparison.InvariantCultureIgnoreCase"/> when <paramref name="ignoreCase"/> is true
228  /// </summary>
229  public static int LastIndexOfInvariant(this string value, string substring, bool ignoreCase = false)
230  {
231  return value.LastIndexOf(substring, ignoreCase
232  ? StringComparison.InvariantCultureIgnoreCase
233  : StringComparison
234  );
235  }
236 
237  /// <summary>
238  /// Provides a shorthand for avoiding the more verbose ternary equivalent.
239  /// Consider the following:
240  /// <code>
241  /// string.IsNullOrEmpty(str) ? (decimal?)null : Convert.ToDecimal(str, CultureInfo.InvariantCulture)
242  /// </code>
243  /// Can be expressed as:
244  /// <code>
245  /// str.IfNotNullOrEmpty&lt;decimal?&gt;(s => Convert.ToDecimal(str, CultureInfo.InvariantCulture))
246  /// </code>
247  /// When combined with additional methods from this class, reducing further to a declarative:
248  /// <code>
249  /// str.IfNotNullOrEmpty&lt;decimal?&gt;(s => s.ParseDecimalInvariant())
250  /// str.IfNotNullOrEmpty&lt;decimal?&gt;(s => s.ConvertInvariant&lt;decimal&gt;())
251  /// </code>
252  /// </summary>
253  /// <paramref name="value">The string value to check for null or empty</paramref>
254  /// <paramref name="defaultValue">The default value to use if null or empty</paramref>
255  /// <paramref name="func">Function run on non-null string w/ length > 0</paramref>
256  public static T IfNotNullOrEmpty<T>(this string value, T defaultValue, Func<string, T> func)
257  {
258  if (string.IsNullOrEmpty(value))
259  {
260  return defaultValue;
261  }
262 
263  return func(value);
264  }
265 
266  /// <summary>
267  /// Provides a shorthand for avoiding the more verbose ternary equivalent.
268  /// Consider the following:
269  /// <code>
270  /// string.IsNullOrEmpty(str) ? (decimal?)null : Convert.ToDecimal(str, CultureInfo.InvariantCulture)
271  /// </code>
272  /// Can be expressed as:
273  /// <code>
274  /// str.IfNotNullOrEmpty&lt;decimal?&gt;(s => Convert.ToDecimal(str, CultureInfo.InvariantCulture))
275  /// </code>
276  /// When combined with additional methods from this class, reducing further to a declarative:
277  /// <code>
278  /// str.IfNotNullOrEmpty&lt;decimal?&gt;(s => s.ParseDecimalInvariant())
279  /// str.IfNotNullOrEmpty&lt;decimal?&gt;(s => s.ConvertInvariant&lt;decimal&gt;())
280  /// </code>
281  /// </summary>
282  /// <paramref name="value">The string value to check for null or empty</paramref>
283  /// <paramref name="func">Function run on non-null string w/ length > 0</paramref>
284  public static T IfNotNullOrEmpty<T>(this string value, Func<string, T> func)
285  {
286  return value.IfNotNullOrEmpty(default(T), func);
287  }
288 
289  /// <summary>
290  /// Retrieves a substring from this instance. The substring starts at a specified
291  /// character position and has a specified length.
292  /// </summary>
293  /// <paramref name="startIndex">The zero-based starting character position of a substring in this instance</paramref>
294  /// <paramref name="length">The number of characters in the substring</paramref>
295  public static string SafeSubstring(this string value, int startIndex, int length)
296  {
297  if (string.IsNullOrEmpty(value))
298  {
299  return value;
300  }
301 
302  if (startIndex > value.Length - 1)
303  {
304  return string.Empty;
305  }
306 
307  if (startIndex < - 1)
308  {
309  startIndex = 0;
310  }
311 
312  return value.Substring(startIndex, Math.Min(length, value.Length - startIndex));
313  }
314 
315  /// <summary>
316  /// Truncates a string to the specified maximum length
317  /// </summary>
318  /// <param name="value">The string</param>
319  /// <param name="maxLength">The maximum allowed string</param>
320  /// <returns>
321  /// A new string with <paramref name="maxLength"/> characters if the original one's length was greater than the maximum allowed length.
322  /// Otherwise, the original string is returned.
323  /// </returns>
324  public static string Truncate(this string value, int maxLength)
325  {
326  if (value.Length > maxLength)
327  {
328  return value.Substring(0, maxLength);
329  }
330  return value;
331  }
332  }
333 }