-
Notifications
You must be signed in to change notification settings - Fork 4
/
ConvertUtility.cs
347 lines (301 loc) · 10.8 KB
/
ConvertUtility.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
namespace Menees
{
#region Using Directives
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Text;
#endregion
/// <summary>
/// Methods for converting data types from one form to another.
/// </summary>
public static class ConvertUtility
{
#region Private Data Members
private static readonly HashSet<string> FalseValues = new(
new string[] { "false", "f", "no", "n", "0" }, StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> TrueValues = new(
new string[] { "true", "t", "yes", "y", "1" }, StringComparer.OrdinalIgnoreCase);
#endregion
#region Public Methods
/// <summary>
/// Converts a value into the specified type.
/// </summary>
/// <typeparam name="T">The type to convert the value into.</typeparam>
/// <param name="value">The value to convert.</param>
/// <returns>The converted value.</returns>
/// <exception cref="InvalidCastException">If <paramref name="value"/>
/// can't be converted into type T.</exception>
[return: MaybeNull]
[return: NotNullIfNotNull("value")]
public static T ConvertValue<T>(object? value)
{
T? result = (value is T typedValue) ? typedValue : (T?)ConvertValue(value, typeof(T));
return result;
}
/// <summary>
/// Converts a value into the specified type.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="resultType">The type to convert the value into.</param>
/// <returns>The converted value.</returns>
/// <exception cref="InvalidCastException">If <paramref name="value"/>
/// can't be converted into type T.</exception>
public static object? ConvertValue(object? value, Type resultType)
{
Conditions.RequireReference(resultType, nameof(resultType));
bool converted = false;
object? result = null;
if (value != null)
{
// Try using a TypeConverter instead of Convert.ChangeType because TypeConverter supports
// a lot more types (including enums and nullable types). Convert.ChangeType only supports
// IConvertible (see http://aspalliance.com/852).
TypeConverter converter = TypeDescriptor.GetConverter(resultType);
if (converter.CanConvertFrom(value.GetType()))
{
try
{
object? fromTypeConverter = converter.ConvertFrom(value);
result = fromTypeConverter;
converted = true;
}
catch (Exception ex)
{
// .NET's System.ComponentModel.BaseNumberConverter.ConvertFrom method catches
// all exceptions and then rethrows a System.Exception, which is HORRIBLE! We'll try to
// undo Microsoft's mistake by re-throwing the original exception, so callers can catch
// specific exception types.
Exception? inner = ex.InnerException;
if (inner != null)
{
ExceptionDispatchInfo.Capture(inner).Throw();
}
throw;
}
}
}
else if (!resultType.IsValueType || Nullable.GetUnderlyingType(resultType) != null)
{
// http://stackoverflow.com/questions/374651/how-to-check-if-an-object-is-nullable
result = null;
converted = true;
}
if (!converted)
{
// The value was null (for a value type) or the type converter couldn't convert it,
// so we have to fall back to ChangeType.
result = Convert.ChangeType(value, resultType);
}
return result;
}
/// <summary>
/// Gets whether a value is a null reference or DBNull.Value.
/// </summary>
/// <param name="value">The value to check.</param>
/// <returns>True if the value is null or DBNull.Value. False otherwise.</returns>
public static bool IsNull([NotNullWhen(false)] object? value)
{
bool result = value == null || value == DBNull.Value;
return result;
}
/// <summary>
/// Converts a string value to a bool.
/// </summary>
/// <param name="value">The string to convert.</param>
/// <returns>True if the string case-insensitively matches "True", "T", "Yes", "Y", or "1". False otherwise.</returns>
public static bool ToBoolean(string? value) => ToBoolean(value, false);
/// <summary>
/// Converts a string value to a bool.
/// </summary>
/// <param name="value">The string to convert.</param>
/// <param name="defaultValue">The default value to use if the string can't be converted.</param>
/// <returns>
/// True if the string case-insensitively matches "True", "T", "Yes", "Y", or "1".
/// False if the string case-insensitively matches "False", "F", "No", "N", or "0".
/// If the value is not one of the true values and not one of the false values,
/// then this returns the defaultValue. For example, ToBoolean("P", true) will return true,
/// and ToBoolean("Q", false) will return false.
/// </returns>
public static bool ToBoolean(string? value, bool defaultValue)
{
bool result = defaultValue;
if (value != null)
{
if (TrueValues.Contains(value))
{
result = true;
}
else if (FalseValues.Contains(value))
{
result = false;
}
}
return result;
}
/// <summary>
/// Converts a string value to an int.
/// </summary>
/// <param name="value">The string to convert.</param>
/// <param name="defaultValue">The default value to use if the string can't be converted.</param>
/// <returns>The int value.</returns>
public static int ToInt32(string? value, int defaultValue)
{
int result = defaultValue;
if (int.TryParse(value, out int parsed))
{
result = parsed;
}
return result;
}
/// <summary>
/// Converts a sequence of bytes into hexadecimal nibbles with an optional "0x" prefix.
/// </summary>
/// <param name="value">The sequence of bytes to convert.</param>
/// <param name="options">Options affecting a "0x" prefix and whether to use lowercase hex characters.</param>
/// <returns>The encoded hex bytes.</returns>
[return: NotNullIfNotNull("value")]
public static string? ToHex(IEnumerable<byte>? value, ToHexOptions options = ToHexOptions.None)
{
string? result = null;
if (value != null)
{
string prefix = options.HasFlag(ToHexOptions.Include0xPrefix) ? "0x" : string.Empty;
StringBuilder sb = new(prefix, prefix.Length + (2 * value.Count()));
string format = options.HasFlag(ToHexOptions.Lowercase) ? "{0:x2}" : "{0:X2}";
foreach (byte entry in value)
{
sb.AppendFormat(CultureInfo.InvariantCulture, format, entry);
}
result = sb.ToString();
}
return result;
}
/// <summary>
/// Tries to parse a string of hexadecimal characters into a byte array.
/// </summary>
/// <param name="value">A string of hex characters. This can optionally
/// start with a "0x" prefix and contain colons or whitespace.</param>
/// <param name="throwOnError">Whether an exception should be thrown for invalid input.
/// If false, then a null result will be returned for invalid input.
/// </param>
/// <exception cref="ArgumentException">Thrown if the input is invalid and
/// <paramref name="throwOnError"/> is true.</exception>
/// <returns>A byte array if <paramref name="value"/> can be parsed.
/// Or null if <paramref name="value"/>can't be parsed and <paramref name="throwOnError"/> is false.</returns>
[return: NotNullIfNotNull("value")]
public static byte[]? FromHex(string? value, bool throwOnError = true)
{
byte[]? result = null;
if (value != null)
{
string? errorMessage = null;
// Ignore leading and trailing whitespace, embedded whitespace, and colon separators (used in certificate hashes).
List<char> chars = value.Where(ch => !char.IsWhiteSpace(ch) && ch != ':').ToList();
// Skip a "0x" prefix.
int charCount = chars.Count;
int startIndex = (charCount >= 2 && chars[0] == '0' && (chars[1] == 'x' || chars[1] == 'X')) ? 2 : 0;
// If we see an odd number of nibbles (e.g., in 0x123), then add a leading 0 to make it 0x0123.
if (charCount % 2 != 0)
{
chars.Insert(startIndex, '0');
charCount++;
}
// Use a MemoryStream instead of List<byte> so we can grab its internal buffer at the end
// without a realloc if we specify an initial capacity that matches its final length.
int capacity = (charCount - startIndex) / 2;
using (MemoryStream stream = new(capacity))
{
byte[] buffer = new byte[1];
for (int i = startIndex; i < charCount; i += 2)
{
// This isn't super-efficient, but it's simple to understand.
string byteText = $"{chars[i]}{chars[i + 1]}";
if (!byte.TryParse(byteText, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out buffer[0]))
{
errorMessage = "Invalid hex byte representation: " + byteText;
break;
}
else
{
stream.Write(buffer, 0, 1);
}
}
if (string.IsNullOrEmpty(errorMessage))
{
result = (stream.Length == stream.Capacity) ? stream.GetBuffer() : stream.ToArray();
}
}
if (!string.IsNullOrEmpty(errorMessage) && throwOnError)
{
throw Exceptions.NewArgumentException(errorMessage!);
}
}
return result;
}
/// <summary>
/// Rounds the fractional seconds from a TimeSpan value to the nearest whole second.
/// </summary>
/// <param name="value">The value to truncate.</param>
/// <returns>The value truncated to whole seconds.</returns>
public static TimeSpan RoundToSeconds(TimeSpan value)
{
long fractionalTicks = value.Ticks % TimeSpan.TicksPerSecond;
TimeSpan result = TruncateToSeconds(value);
if (Math.Abs(fractionalTicks) >= (TimeSpan.TicksPerSecond / 2))
{
result += TimeSpan.FromSeconds(1 * Math.Sign(fractionalTicks));
}
return result;
}
/// <summary>
/// Removes the fractional seconds from a TimeSpan value.
/// </summary>
/// <param name="value">The value to truncate.</param>
/// <returns>The value truncated to whole seconds.</returns>
public static TimeSpan TruncateToSeconds(TimeSpan value) => TimeSpan.FromTicks(value.Ticks - (value.Ticks % TimeSpan.TicksPerSecond));
#endregion
#region Internal Methods
internal static T GetValue<T>(string? textValue, T defaultValue)
where T : struct
{
T result = defaultValue;
if (!string.IsNullOrEmpty(textValue))
{
Type type = typeof(T);
// Enums are the most common case, so handle them specially.
if (type.IsEnum)
{
if (Enum.TryParse(textValue, out T parsedValue))
{
result = parsedValue;
}
}
else
{
// Handle other simple types like Double and DateTime along with types
// that support TypeConverters (e.g., System.Windows.Forms.Color).
try
{
result = ConvertValue<T>(textValue);
}
catch (Exception ex)
{
if (!(ex is ArgumentException || ex is ArithmeticException || ex is FormatException || ex is IndexOutOfRangeException))
{
throw;
}
}
}
}
return result;
}
#endregion
}
}