Ejemplo n.º 1
0
        /// <summary>
        /// Initialisiert ein neues <see cref="CsvReader"/>-Objekt.
        /// </summary>
        /// <param name="fileName">Dateipfad der CSV-Datei.</param>
        /// <param name="hasHeaderRow"><c>true</c>, wenn die CSV-Datei eine Kopfzeile mit den Spaltennamen hat.</param>
        /// <param name="options">Optionen für das Lesen der CSV-Datei.</param>
        /// <param name="fieldSeparator">Das Feldtrennzeichen, das in der CSV-Datei Verwendung findet.</param>
        /// <param name="textEncoding">Die zum Einlesen der CSV-Datei zu verwendende Textenkodierung oder <c>null</c> für <see cref="Encoding.UTF8"/>.</param>
        /// <exception cref="ArgumentNullException"><paramref name="fileName"/> ist <c>null</c>.</exception>
        /// <exception cref="ArgumentException"><paramref name="fileName"/> ist kein gültiger Dateipfad.</exception>
        /// <exception cref="IOException">Es kann nicht auf den Datenträger zugegriffen werden.</exception>
        public CsvReader(
            string fileName,
            bool hasHeaderRow     = true,
            CsvOptions options    = CsvOptions.Default,
            char fieldSeparator   = ',',
            Encoding?textEncoding = null)
        {
            StreamReader streamReader = InitializeStreamReader(fileName, textEncoding);

            this._options      = options;
            this._reader       = new CsvStringReader(streamReader, fieldSeparator, options.HasFlag(CsvOptions.ThrowOnEmptyLines));
            this._hasHeaderRow = hasHeaderRow;
        }
Ejemplo n.º 2
0
        public void FieldStartsWithEmptyLineTest()
        {
            string input = Environment.NewLine + "Hello";

            string csv = "\"" + input + "\"";

            using var stringReader = new StringReader(csv);
            using var reader       = new CsvStringReader(stringReader, ',', true);

            string?field = reader.First().First();

            Assert.IsNotNull(field);
            Assert.AreEqual(field, input);
        }
Ejemplo n.º 3
0
        /// <summary>
        /// Initialisiert ein neues <see cref="CsvReader"/>-Objekt.
        /// </summary>
        /// <param name="reader">Der <see cref="TextReader"/>, mit dem die CSV-Datei gelesen wird.</param>
        /// <param name="hasHeaderRow"><c>true</c>, wenn die CSV-Datei eine Kopfzeile mit den Spaltennamen hat.</param>
        /// <param name="options">Optionen für das Lesen der CSV-Datei.</param>
        /// <param name="fieldSeparator">Das Feldtrennzeichen.</param>
        /// <exception cref="ArgumentNullException"><paramref name="reader"/> ist <c>null</c>.</exception>
        public CsvReader(
            TextReader reader,
            bool hasHeaderRow   = true,
            CsvOptions options  = CsvOptions.Default,
            char fieldSeparator = ',')
        {
            if (reader is null)
            {
                throw new ArgumentNullException(nameof(reader));
            }

            this._options      = options;
            this._reader       = new CsvStringReader(reader, fieldSeparator, options.HasFlag(CsvOptions.ThrowOnEmptyLines));
            this._hasHeaderRow = hasHeaderRow;
        }
Ejemplo n.º 4
0
        /// <summary>
        /// Analysiert die CSV-Datei, auf die <paramref name="fileName"/> verweist, und füllt die Eigenschaften des <see cref="CsvAnalyzer"/>-Objekts mit den
        /// Ergebnissen der Analyse.
        /// </summary>
        /// <param name="fileName">Dateipfad der CSV-Datei.</param>
        /// <param name="analyzedLinesCount">Höchstanzahl der in der CSV-Datei zu analysierenden Zeilen. Der Mindestwert
        /// ist <see cref="AnalyzedLinesMinCount"/>. Wenn die Datei weniger Zeilen hat als <paramref name="analyzedLinesCount"/>
        /// wird sie komplett analysiert. (Sie können <see cref="int.MaxValue">Int32.MaxValue</see> angeben, um in jedem Fall die gesamte Datei zu
        /// analysieren.)</param>
        /// <param name="textEncoding">Die zum Einlesen der CSV-Datei zu verwendende Textkodierung oder <c>null</c> für <see cref="Encoding.UTF8"/>.</param>
        ///
        /// <exception cref="ArgumentNullException"><paramref name="fileName"/> ist <c>null</c>.</exception>
        /// <exception cref="ArgumentException"><paramref name="fileName"/> ist kein gültiger Dateipfad.</exception>
        /// <exception cref="IOException">Es kann nicht auf den Datenträger zugegriffen werden.</exception>
        /// <remarks><para><see cref="CsvAnalyzer"/> führt auf der CSV-Datei eine statistische Analyse durch, um die geeigneten
        /// Parameter für das Lesen der Datei zu finden. Das Ergebnis der Analyse ist also immer nur eine Schätzung, deren
        /// Treffsicherheit mit der Zahl der analysierten Zeilen steigt.</para>
        /// <para>Die Analyse ist zeitaufwändig, da auf die CSV-Datei lesend zugegriffen werden muss.</para></remarks>
        public void Analyze(string fileName, int analyzedLinesCount = AnalyzedLinesMinCount, Encoding?textEncoding = null)
        {
            if (analyzedLinesCount < AnalyzedLinesMinCount)
            {
                analyzedLinesCount = AnalyzedLinesMinCount;
            }

            // Suche Feldtrennzeichen:
            using (StreamReader? reader = CsvReader.InitializeStreamReader(fileName, textEncoding))
            {
                const int COMMA_INDEX     = 0;
                const int SEMICOLON_INDEX = 1;
                const int HASH_INDEX      = 2;

                bool firstLine = true;

#if NET40
                char[] sepChars = new char[] { ',', ';', '#', '\t', ' ' };

                int sepCharsLength = sepChars.Length;

                int[] firstLineOccurrence   = new int[sepCharsLength];
                int[] sameOccurrence        = new int[sepCharsLength];
                int[] currentLineOccurrence = new int[sepCharsLength];
#else
                ReadOnlySpan <char> sepChars   = stackalloc[] { ',', ';', '#', '\t', ' ' };
                int        sepCharsLength      = sepChars.Length;
                Span <int> firstLineOccurrence = stackalloc int[sepCharsLength];
                firstLineOccurrence.Clear();
                Span <int> sameOccurrence = stackalloc int[sepCharsLength];
                sameOccurrence.Clear();
                Span <int> currentLineOccurrence = stackalloc int[sepCharsLength];
                currentLineOccurrence.Clear();
#endif

                for (int i = 0; i < analyzedLinesCount; i++)
                {
                    string?line = reader.ReadLine();

                    if (line is null)
                    {
                        break;
                    }

                    if (firstLine)
                    {
                        // Skip empty lines before the first line:
                        if (line.Length == 0)
                        {
                            Options = Options.Unset(CsvOptions.ThrowOnEmptyLines);
                            i--;
                            continue;
                        }

                        firstLine = false;

                        // Vergleich für Kopfzeile:
                        for (int charIndex = 0; charIndex < line.Length; charIndex++)
                        {
                            char ch = line[charIndex];

                            for (int sepCharIndex = 0; sepCharIndex < sepCharsLength; sepCharIndex++)
                            {
                                if (ch.Equals(sepChars[sepCharIndex]))
                                {
                                    firstLineOccurrence[sepCharIndex]++;
                                }
                            }
                        }

                        // wenn in der Kopfzeile Komma, Semikolon oder Raute auftauchen, werden Tabulator und Leerzeichen nicht mehr ausgewertet
                        if (firstLineOccurrence[COMMA_INDEX] != 0 || firstLineOccurrence[SEMICOLON_INDEX] != 0 || firstLineOccurrence[HASH_INDEX] != 0)
                        {
                            // lösche Whitespace-Werte
                            for (int j = 3; j < sepCharsLength; j++)
                            {
                                firstLineOccurrence[j] = 0;
                            }

                            sepCharsLength = 3;
                        }

                        continue;
                    }



                    // Vergleich für Datenzeile:
                    for (int charIndex = 0; charIndex < line.Length; charIndex++)
                    {
                        char ch = line[charIndex];

                        for (int sepCharIndex = 0; sepCharIndex < sepCharsLength; sepCharIndex++)
                        {
                            if (ch.Equals(sepChars[sepCharIndex]))
                            {
                                currentLineOccurrence[sepCharIndex]++;
                            }
                        }
                    }

                    for (int sepCharIndex = 0; sepCharIndex < sepCharsLength; sepCharIndex++)
                    {
                        if (currentLineOccurrence[sepCharIndex] == firstLineOccurrence[sepCharIndex])
                        {
                            sameOccurrence[sepCharIndex]++;
                        }
                    }

                    // Clear currentLineOccurrence
                    for (int sepCharIndex = 0; sepCharIndex < sepCharsLength; sepCharIndex++)
                    {
                        currentLineOccurrence[sepCharIndex] = 0;
                    }
                }//for

                this.FieldSeparator = sepChars[0];

                int sameOcc     = sameOccurrence[0];
                int probability = firstLineOccurrence[0] * (1 + sameOcc * sameOcc * sameOcc);

                // Formel für statistische Wahrscheinlichkeit:
                for (int sepCharIndex = 1; sepCharIndex < sepCharsLength; sepCharIndex++)
                {
                    sameOcc = sameOccurrence[sepCharIndex];

                    int newProbability = firstLineOccurrence[sepCharIndex] * (1 + sameOcc * sameOcc * sameOcc);

                    if (newProbability > probability)
                    {
                        this.FieldSeparator = sepChars[sepCharIndex];
                        probability         = newProbability;
                    }
                }
            }//using

            using (StreamReader? reader = CsvReader.InitializeStreamReader(fileName, textEncoding))
            {
                bool firstLine = true;

                int firstLineCount = 0;

                using var csvStringReader = new CsvStringReader(reader, FieldSeparator, !Options.IsSet(CsvOptions.ThrowOnEmptyLines));

                foreach (IEnumerable <string?>?row in csvStringReader)
                {
                    if (firstLine)
                    {
                        firstLine = false;

                        var firstLineFields = new List <string>();

                        bool hasHeader        = true;
                        bool hasMaybeNoHeader = false;

                        foreach (string?s in row)
                        {
                            firstLineCount++;

                            if (hasHeader)
                            {
                                // Nach RFC 4180 darf das letzte Feld einer Datenzeile hinter sich kein
                                // FieldSeparatorChar mehr haben: "The last field in the
                                // record must not be followed by a comma."
                                // Schlechte Implementierungen - wie Thunderbird - halten sich aber nicht daran.
                                if (hasMaybeNoHeader)
                                {
                                    hasHeader    = false;
                                    this.Options = this.Options.Unset(CsvOptions.TrimColumns);
                                }

                                if (s is null)
                                {
                                    hasMaybeNoHeader = true;
                                    continue;
                                }

                                string trimmed = s.Trim();

                                firstLineFields.Add(trimmed);

                                if (trimmed.Length != s.Length)
                                {
                                    this.Options = this.Options.Set(CsvOptions.TrimColumns);
                                }
                            }
                        }//foreach

                        if (hasHeader)
                        {
                            this.ColumnNames = new ReadOnlyCollection <string>(firstLineFields);
                        }

                        // Prüfe, ob sich zwei Spaltennamen nur durch Groß- und Kleinschreibung unterscheiden:
                        if (hasHeader)
                        {
                            // estimatedLength unterscheidet sich von firstLineCount, wenn nach dem letzten Feld
                            // ein FieldSeparatorChar stand (s.o.)
                            int estimatedLength = hasMaybeNoHeader ? firstLineCount - 1 : firstLineCount;

                            if (estimatedLength != firstLineFields.Distinct(StringComparer.OrdinalIgnoreCase).Count())
                            {
                                this.Options = this.Options.Set(CsvOptions.CaseSensitiveKeys);
                            }
                        }
                    }
                    else
                    {
                        int    currentLineCount = 0;
                        string?firstString      = null;

                        foreach (string?s in row)
                        {
                            if (currentLineCount == 0)
                            {
                                firstString = s;
                            }

                            currentLineCount++;
                        }

                        if (currentLineCount != firstLineCount)
                        {
                            this.Options = currentLineCount < firstLineCount
                                ? currentLineCount == 1 && firstString is null?this.Options.Unset(CsvOptions.ThrowOnEmptyLines) : this.Options.Unset(CsvOptions.ThrowOnTooFewFields)
                                               : this.Options.Unset(CsvOptions.ThrowOnTooMuchFields);
                        }
                    }
                }
            }//using
        }