/// <summary> /// Convert to a multi-line CSV text with column and/or row headers. /// /// Escapes separator with quotes if necessary. /// </summary> public override string ToString() { StringBuilder result = new StringBuilder(); // Validate table before serializing it to CSV string Validate(); // Add column headers, if any if (ColHeaders != null) { IEnumerable <string> firstRowTokens = null; if (RowHeaders != null) { // Prepend corner value if row headers are also present firstRowTokens = new string[1] { CornerHeader }.Concat(ColHeaders); } else { // No corner value firstRowTokens = ColHeaders; } string firstRowString = CsvUtil.TokensToLine(firstRowTokens); result.AppendLine(firstRowString); } int colOffset = RowHeaders != null ? 1 : 0; for (int rowIndex = 0; rowIndex < RowCount; ++rowIndex) { string[] tokens = new string[colOffset + ColCount]; if (colOffset != 0) { // Add row header, if present tokens[0] = RowHeaders[rowIndex]; } for (int colIndex = 0; colIndex < ColCount; ++colIndex) { // Add values with offset for row header, if present tokens[colOffset + colIndex] = this[rowIndex, colIndex].AsString(); } // Convert to CSV and append string csvLine = CsvUtil.TokensToLine(tokens); result.AppendLine(csvLine); } return(result.ToString()); }
/// <summary> /// Populate by parsing multi-line CSV text using the specified /// matrix layout and an array of column parser functions. /// /// If the data has more columns than the array of parsers, /// the last parser in the array is used for all additional /// columns. This permits ingesting CSV files with an unknown /// number of value columns of the same type, after an initial /// set of category columns that have other types. /// /// The specified parser is not used for row and column headers /// which are always dot delimited strings. /// </summary> public void ParseCsv(MatrixLayout layout, Func <string, T>[] colParsers, string csvText) { if (layout == MatrixLayout.Empty) { throw new Exception("Matrix layout passed to ParseCsv method is empty"); } Layout = layout; // Parse into a list of text lines string[] csvLines = CsvUtil.TextToLines(csvText); // Parse each line into tokens, keeping track of maximum // size which will determine the matrix size int rowCount = csvLines.Length; List <string[]> parsedRows = new List <string[]>(); int colCount = 0; foreach (string csvLine in csvLines) { string[] tokens = CsvUtil.LineToTokens(csvLine); if (colCount < tokens.Length) { colCount = tokens.Length; } parsedRows.Add(tokens); } int colOffset = 0; if (layout.HasRowHeaders()) { // First column is row headers, data has one less column colOffset = 1; colCount--; } int rowOffset = 0; if (layout.HasColHeaders()) { // First row is column headers, data has one less row rowOffset = 1; rowCount--; } // Resize Resize(layout, rowCount, colCount); // Parse column headers if present if (rowOffset != 0) { string[] rowTokens = parsedRows[0]; // Populate corner header if present if (colOffset != 0) { CornerHeader = rowTokens[0]; } // Populate column headers if present for (int colIndex = 0; colIndex < colCount; ++colIndex) { string token = rowTokens[colOffset + colIndex]; ColHeaders[colIndex] = token; } } for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) { string[] rowTokens = parsedRows[rowOffset + rowIndex]; // Set row header if present if (colOffset != 0) { RowHeaders[rowIndex] = rowTokens[0]; } // Set row values for (int colIndex = 0; colIndex < colCount; ++colIndex) { // If column index is outside the range of column parsers array, // take the last element of the array instead. This permits // parsing CSV data with unknown number of columns where trailing // columns have the same type int parserColIndex = colIndex < colParsers.Length ? colIndex : colParsers.Length - 1; Func <string, T> colParser = colParsers[parserColIndex]; string token = rowTokens[colOffset + colIndex]; values_[LinearIndex(rowIndex, colIndex)] = colParser(token); } } }