Math.NET Numerics
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

317 lines
13 KiB

// <copyright file="DelimitedReader.cs" company="Math.NET">
// Math.NET Numerics, part of the Math.NET Project
// http://numerics.mathdotnet.com
// http://github.com/mathnet/mathnet-numerics
// http://mathnetnumerics.codeplex.com
//
// Copyright (c) 2009-2015 Math.NET
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions;
using MathNet.Numerics.LinearAlgebra;
namespace MathNet.Numerics.Data.Text
{
/// <summary>
/// Creates a <see cref="Matrix{T}"/> from a delimited text file. If the user does not
/// specify a delimiter, then any whitespace is used.
/// </summary>
public static class DelimitedReader
{
/// <summary>
/// The base regular expression.
/// </summary>
const string RegexTemplate = "\\([^\\)]*\\)|'[^']*'|\"[^\"]*\"|^{0}{{1,}}|{0}$|{0}{{2,}}|[^{0}]*";
/// <summary>
/// Regular expression to match whitespace.
/// </summary>
const string WhiteSpaceRegexTemplate = "\\([^\\)]*\\)|'[^']*'|\"[^\"]*\"|[^\\s]*";
/// <summary>
/// Cached compiled regular expressions for various delimiters, as needed.
/// </summary>
static readonly ConcurrentDictionary<string, Regex> RegexCache = new ConcurrentDictionary<string, Regex>();
/// <summary>
/// Reads a <see cref="Matrix{TDataType}"/> from the given <see cref="TextReader"/>.
/// </summary>
/// <param name="reader">The <see cref="TextReader"/> to read the matrix from.</param>
/// <param name="sparse">Whether the returned matrix should be constructed as sparse (true) or dense (false). Default: false.</param>
/// <param name="delimiter">Number delimiter between numbers of the same line. Supports Regex groups. Default: "\s" (white space).</param>
/// <param name="hasHeaders">Whether the first row contains column headers or not. Default: false.</param>
/// <param name="formatProvider">The culture to use. Default: null.</param>
/// <param name="missingValue">The value to represent missing values. Default: NaN.</param>
/// <returns>A matrix containing the data from the <see cref="TextReader"/>.</returns>
/// <typeparam name="T">The data type of the Matrix. It can be either: double, float, Complex, or Complex32.</typeparam>
public static Matrix<T> Read<T>(TextReader reader, bool sparse = false, string delimiter = @"\s", bool hasHeaders = false, IFormatProvider formatProvider = null, T? missingValue = null)
where T : struct, IEquatable<T>, IFormattable
{
delimiter = CleanDelimiter(delimiter);
var mv = missingValue ?? (sparse ? Zero<T>() : NaN<T>());
var mvStr = mv.ToString("G", formatProvider);
var regex = delimiter == @"\s" ?
RegexCache.GetOrAdd(delimiter, d => new Regex(WhiteSpaceRegexTemplate, RegexOptions.Compiled)) :
RegexCache.GetOrAdd(delimiter, d => new Regex(string.Format(RegexTemplate, d), RegexOptions.Compiled));
var parse = CreateParser<T>(formatProvider);
var data = new List<T[]>();
// max is used to supports files like:
// 1,2
// 3,4,5,6
// 7
// this creates a 3x4 matrix:
// 1, 2, MISSING, MISSING
// 3, 4, 5, 6
// 7, MISSING, MISSING, MISSING
var max = -1;
var line = reader.ReadLine();
if (hasHeaders)
{
line = reader.ReadLine();
}
while (line != null)
{
line = line.Trim();
if (line.Length > 0)
{
var matches = regex.Matches(line);
if (delimiter == @"\s")
{
var row = (from Match match in matches where match.Length > 0 select parse(match.Value.StripOffQuotes())).ToArray();
max = Math.Max(max, row.Length);
data.Add(row);
}
else
{
var offset = 0;
var row = new List<T>();
foreach (var value in (from Match match in matches where match.Length > 0 select match.Value))
{
var delimterCount = value.StartsWith(delimiter) ? value.CountDelimiters(delimiter) : 0;
if (delimterCount > 0)
{
for (var i = offset; i < delimterCount; i++)
{
row.Add(parse(mvStr.StripOffQuotes()));
}
}
else
{
row.Add(parse((string.IsNullOrWhiteSpace(value) ? mvStr : value).StripOffQuotes()));
}
offset = 1;
}
max = Math.Max(max, row.Count);
data.Add(row.ToArray());
}
}
line = reader.ReadLine();
}
var matrix = sparse ? Matrix<T>.Build.Sparse(data.Count, max, mv) : Matrix<T>.Build.Dense(data.Count, max, mv);
var storage = matrix.Storage;
for (var i = 0; i < data.Count; i++)
{
var row = data[i];
for (var j = 0; j < row.Length; j++)
{
var value = row[j];
storage.At(i, j, value);
}
}
#if !NETSTANDARD1_3
reader.Close();
#endif
reader.Dispose();
return matrix;
}
/// <summary>
/// Reads a <see cref="Matrix{TDataType}"/> from the given file.
/// </summary>
/// <param name="filePath">The path and name of the file to read the matrix from.</param>
/// <param name="sparse">Whether the returned matrix should be constructed as sparse (true) or dense (false). Default: false.</param>
/// <param name="delimiter">Number delimiter between numbers of the same line. Supports Regex groups. Default: "\s" (white space).</param>
/// <param name="hasHeaders">Whether the first row contains column headers or not. Default: false.</param>
/// <param name="formatProvider">The culture to use. Default: null.</param>
/// <param name="missingValue">The value to represent missing values. Default: NaN.</param>
/// <returns>A matrix containing the data from the <see cref="TextReader"/>.</returns>
/// <typeparam name="T">The data type of the Matrix. It can be either: double, float, Complex, or Complex32.</typeparam>
public static Matrix<T> Read<T>(string filePath, bool sparse = false, string delimiter = @"\s", bool hasHeaders = false, IFormatProvider formatProvider = null, T? missingValue = null)
where T : struct, IEquatable<T>, IFormattable
{
using (var stream = File.OpenRead(filePath))
using (var reader = new StreamReader(stream))
{
return Read(reader, sparse, delimiter, hasHeaders, formatProvider, missingValue);
}
}
/// <summary>
/// Reads a <see cref="Matrix{TDataType}"/> from the given <see cref="Stream"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to read the matrix from.</param>
/// <param name="sparse">Whether the returned matrix should be constructed as sparse (true) or dense (false). Default: false.</param>
/// <param name="delimiter">Number delimiter between numbers of the same line. Supports Regex groups. Default: "\s" (white space).</param>
/// <param name="hasHeaders">Whether the first row contains column headers or not. Default: false.</param>
/// <param name="formatProvider">The culture to use. Default: null.</param>
/// <param name="missingValue">The value to represent missing values. Default: NaN.</param>
/// <returns>A matrix containing the data from the <see cref="TextReader"/>.</returns>
/// <typeparam name="T">The data type of the Matrix. It can be either: double, float, Complex, or Complex32.</typeparam>
public static Matrix<T> Read<T>(Stream stream, bool sparse = false, string delimiter = @"\s", bool hasHeaders = false, IFormatProvider formatProvider = null, T? missingValue = null)
where T : struct, IEquatable<T>, IFormattable
{
using (var reader = new StreamReader(stream))
{
return Read(reader, sparse, delimiter, hasHeaders, formatProvider, missingValue);
}
}
/// <summary>
/// NaN for the given numeric type.
/// </summary>
static T NaN<T>()
{
if (typeof (T) == typeof (double))
{
return (T)(object)double.NaN;
}
if (typeof (T) == typeof (float))
{
return (T)(object)float.NaN;
}
if (typeof (T) == typeof (Complex))
{
return (T)(object)new Complex(double.NaN, double.NaN);
}
if (typeof (T) == typeof (Complex32))
{
return (T)(object)new Complex32(float.NaN, float.NaN);
}
throw new NotSupportedException();
}
/// <summary>
/// Zero for the given numeric type.
/// </summary>
static T Zero<T>() where T : struct
{
return default(T);
}
static Func<string, T> CreateParser<T>(IFormatProvider formatProvider)
{
if (typeof (T) == typeof (double))
{
return number => (T)(object)double.Parse(number, NumberStyles.Any, formatProvider);
}
if (typeof (T) == typeof (float))
{
return number => (T)(object)float.Parse(number, NumberStyles.Any, formatProvider);
}
if (typeof (T) == typeof (Complex))
{
return number => (T)(object)number.ToComplex(formatProvider);
}
if (typeof (T) == typeof (Complex32))
{
return number => (T)(object)number.ToComplex32(formatProvider);
}
throw new NotSupportedException();
}
/// <summary>
/// Counts the number of delimiters in a row.
/// </summary>
static int CountDelimiters(this string obj, string demlimiter)
{
var pos = 0;
var count = 0;
while ((pos = obj.IndexOf(demlimiter, pos, StringComparison.OrdinalIgnoreCase)) != -1)
{
count++;
pos++;
}
return count;
}
/// <summary>
/// Returns a regex friendly delimiter.
/// </summary>
static string CleanDelimiter(string delimiter)
{
if (string.IsNullOrEmpty(delimiter))
{
return @"\s";
}
switch (delimiter)
{
case ".":
return "\\.";
case "\\":
return "\\\\";
default:
return delimiter;
}
}
/// <summary>
/// strip off quotes (TODO: can we replace this with trimming?)
/// </summary>
static string StripOffQuotes(this string obj)
{
return obj.Replace("'", string.Empty).Replace("\"", string.Empty);
}
}
}