summaryrefslogtreecommitdiff
path: root/ThirdParty/CsvHelper-master/src/CsvHelper/Configuration/ConfigurationFunctions.cs
blob: 0cf725995742db3641830d129b486f30f43acca1 (plain)
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
// Copyright 2009-2022 Josh Close
// This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0.
// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0.
// https://github.com/JoshClose/CsvHelper
using CsvHelper.Delegates;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace CsvHelper.Configuration
{
	/// <summary>Holds the default callback methods for delegate members of <c>CsvHelper.Configuration.Configuration</c>.</summary>
	public static class ConfigurationFunctions
	{
		private static readonly char[] lineEndingChars = new char[] { '\r', '\n' };

		/// <summary>
		/// Throws a <see cref="ValidationException"/> if <see name="HeaderValidatedArgs.InvalidHeaders"/> is not empty.
		/// </summary>
		public static void HeaderValidated(HeaderValidatedArgs args)
		{
			if (args.InvalidHeaders.Count() == 0)
			{
				return;
			}

			var errorMessage = new StringBuilder();
			foreach (var invalidHeader in args.InvalidHeaders)
			{
				errorMessage.AppendLine($"Header with name '{string.Join("' or '", invalidHeader.Names)}'[{invalidHeader.Index}] was not found.");
			}

			if (args.Context.Reader.HeaderRecord != null)
			{
				foreach (var header in args.Context.Reader.HeaderRecord)
				{
					errorMessage.AppendLine($"Headers: '{string.Join("', '", args.Context.Reader.HeaderRecord)}'");
				}
			}

			var messagePostfix =
				$"If you are expecting some headers to be missing and want to ignore this validation, " +
				$"set the configuration {nameof(HeaderValidated)} to null. You can also change the " +
				$"functionality to do something else, like logging the issue.";
			errorMessage.AppendLine(messagePostfix);

			throw new HeaderValidationException(args.Context, args.InvalidHeaders, errorMessage.ToString());
		}

		/// <summary>
		/// Throws a <c>MissingFieldException</c>.
		/// </summary>
		public static void MissingFieldFound(MissingFieldFoundArgs args)
		{
			var messagePostfix = $"You can ignore missing fields by setting {nameof(MissingFieldFound)} to null.";

			// Get by index.

			if (args.HeaderNames == null || args.HeaderNames.Length == 0)
			{
				throw new MissingFieldException(args.Context, $"Field at index '{args.Index}' does not exist. {messagePostfix}");
			}

			// Get by name.

			var indexText = args.Index > 0 ? $" at field index '{args.Index}'" : string.Empty;

			if (args.HeaderNames.Length == 1)
			{
				throw new MissingFieldException(args.Context, $"Field with name '{args.HeaderNames[0]}'{indexText} does not exist. {messagePostfix}");
			}

			throw new MissingFieldException(args.Context, $"Field containing names '{string.Join("' or '", args.HeaderNames)}'{indexText} does not exist. {messagePostfix}");
		}

		/// <summary>
		/// Throws a <see cref="BadDataException"/>.
		/// </summary>
		public static void BadDataFound(BadDataFoundArgs args)
		{
			throw new BadDataException(args.Field, args.RawRecord, args.Context, $"You can ignore bad data by setting {nameof(BadDataFound)} to null.");
		}

		/// <summary>
		/// Throws the given <see name="ReadingExceptionOccurredArgs.Exception"/>.
		/// </summary>
		public static bool ReadingExceptionOccurred(ReadingExceptionOccurredArgs args)
		{
			return true;
		}

		/// <summary>
		/// Returns true if the field contains a <see cref="IWriterConfiguration.Quote"/>,
		/// starts with a space, ends with a space, contains \r or \n, or contains
		/// the <see cref="IWriterConfiguration.Delimiter"/>.
		/// </summary>
		/// <param name="args">The args.</param>
		/// <returns><c>true</c> if the field should be quoted, otherwise <c>false</c>.</returns>
		public static bool ShouldQuote(ShouldQuoteArgs args)
		{
			var config = args.Row.Configuration;

			var shouldQuote = !string.IsNullOrEmpty(args.Field) &&
			(
				args.Field.Contains(config.Quote) // Contains quote
				|| args.Field[0] == ' ' // Starts with a space
				|| args.Field[args.Field.Length - 1] == ' ' // Ends with a space
				|| (config.Delimiter.Length > 0 && args.Field.Contains(config.Delimiter)) // Contains delimiter
				|| !config.IsNewLineSet && args.Field.IndexOfAny(lineEndingChars) > -1 // Contains line ending characters
				|| config.IsNewLineSet && args.Field.Contains(config.NewLine) // Contains newline
			);

			return shouldQuote;
		}

		/// <summary>
		/// Returns the <see name="PrepareHeaderForMatchArgs.Header"/> as given.
		/// </summary>
		public static string PrepareHeaderForMatch(PrepareHeaderForMatchArgs args)
		{
			return args.Header;
		}

		/// <summary>
		/// Returns <c>true</c> if <paramref name="args.ParameterType"/>:
		/// 1. does not have a parameterless constructor
		/// 2. has a constructor
		/// 3. is not a value type
		/// 4. is not a primitive
		/// 5. is not an enum
		/// 6. is not an interface
		/// 7. TypeCode is an Object.
		/// </summary>
		public static bool ShouldUseConstructorParameters(ShouldUseConstructorParametersArgs args)
		{
			return !args.ParameterType.HasParameterlessConstructor()
				&& args.ParameterType.HasConstructor()
				&& !args.ParameterType.IsValueType
				&& !args.ParameterType.IsPrimitive
				&& !args.ParameterType.IsEnum
				&& !args.ParameterType.IsInterface
				&& Type.GetTypeCode(args.ParameterType) == TypeCode.Object;
		}

		/// <summary>
		/// Returns the type's constructor with the most parameters. 
		/// If two constructors have the same number of parameters, then
		/// there is no guarantee which one will be returned. If you have
		/// that situation, you should probably implement this function yourself.
		/// </summary>
		public static ConstructorInfo GetConstructor(GetConstructorArgs args)
		{
			return args.ClassType.GetConstructorWithMostParameters();
		}

		/// <summary>
		/// Returns the header name ran through <see cref="PrepareHeaderForMatch(PrepareHeaderForMatchArgs)"/>.
		/// If no header exists, property names will be Field1, Field2, Field3, etc.
		/// </summary>
		/// <param name="args">The args.</param>
		public static string GetDynamicPropertyName(GetDynamicPropertyNameArgs args)
		{
			if (args.Context.Reader.HeaderRecord == null)
			{
				return $"Field{args.FieldIndex + 1}";
			}

			var header = args.Context.Reader.HeaderRecord[args.FieldIndex];
			var prepareHeaderForMatchArgs = new PrepareHeaderForMatchArgs(header, args.FieldIndex);
			header = args.Context.Reader.Configuration.PrepareHeaderForMatch(prepareHeaderForMatchArgs);

			return header;
		}

		/// <summary>
		/// Detects the delimiter based on the given text.
		/// Return the detected delimiter or null if one wasn't found.
		/// </summary>
		/// <param name="args">The args.</param>
		public static string? GetDelimiter(GetDelimiterArgs args)
		{
			var text = args.Text;
			var config = args.Configuration;

			if (config.Mode == CsvMode.RFC4180)
			{
				// Remove text in between pairs of quotes.
				text = Regex.Replace(text, $"{config.Quote}.*?{config.Quote}", string.Empty, RegexOptions.Singleline);
			}
			else if (config.Mode == CsvMode.Escape)
			{
				// Remove escaped characters.
				text = Regex.Replace(text, $"({config.Escape}.)", string.Empty, RegexOptions.Singleline);
			}

			var newLine = config.NewLine;
			if ((new[] { "\r\n", "\r", "\n" }).Contains(newLine))
			{
				newLine = "\r\n|\r|\n";
			}

			var lineDelimiterCounts = new List<Dictionary<string, int>>();
			while (text.Length > 0)
			{
				// Since all escaped text has been removed, we can reliably read line by line.
				var match = Regex.Match(text, newLine);
				var line = match.Success ? text.Substring(0, match.Index + match.Length) : text;

				var delimiterCounts = new Dictionary<string, int>();
				foreach (var delimiter in config.DetectDelimiterValues)
				{
					// Escape regex special chars to use as regex pattern.
					var pattern = Regex.Replace(delimiter, @"([.$^{\[(|)*+?\\])", "\\$1");
					delimiterCounts[delimiter] = Regex.Matches(line, pattern).Count;
				}

				lineDelimiterCounts.Add(delimiterCounts);

				text = match.Success ? text.Substring(match.Index + match.Length) : string.Empty;
			}

			if (lineDelimiterCounts.Count > 1)
			{
				// The last line isn't complete and can't be used to reliably detect a delimiter.
				lineDelimiterCounts.Remove(lineDelimiterCounts.Last());
			}

			// Rank only the delimiters that appear on every line.
			var delimiters =
			(
				from counts in lineDelimiterCounts
				from count in counts
				group count by count.Key into g
				where g.All(x => x.Value > 0)
				let sum = g.Sum(x => x.Value)
				orderby sum descending
				select new
				{
					Delimiter = g.Key,
					Count = sum
				}
			).ToList();

			string? newDelimiter = null;
			if (delimiters.Any(x => x.Delimiter == config.CultureInfo.TextInfo.ListSeparator) && lineDelimiterCounts.Count > 1)
			{
				// The culture's separator is on every line. Assume this is the delimiter.
				newDelimiter = config.CultureInfo.TextInfo.ListSeparator;
			}
			else
			{
				// Choose the highest ranked delimiter.
				newDelimiter = delimiters.Select(x => x.Delimiter).FirstOrDefault();
			}

			if (newDelimiter != null)
			{
				config.Validate();
			}

			return newDelimiter ?? config.Delimiter;
		}
	}
}