Migrate to dotnet8 and fix warnings

This commit is contained in:
2024-02-18 00:46:45 +01:00
parent a688f2a692
commit 66536657e2
15 changed files with 492 additions and 470 deletions

11
.idea/.idea.CsvView/.idea/avalonia.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AvaloniaProject">
<option name="projectPerEditor">
<map>
<entry key="CsvView/App.axaml" value="CsvView/CsvView.csproj" />
<entry key="CsvView/MainWindow.axaml" value="CsvView/CsvView.csproj" />
</map>
</option>
</component>
</project>

View File

@@ -7,60 +7,75 @@ public class ByteArraySearcherTests
[Fact] [Fact]
public void Contains__EmptyNeedle_ReturnsTrue() public void Contains__EmptyNeedle_ReturnsTrue()
{ {
byte[] haystack = { 1, 2, 3, 4, 5 }; // --- Arrange
byte[] haystack = [1, 2, 3, 4, 5,];
byte[] needle = Array.Empty<byte>(); byte[] needle = Array.Empty<byte>();
ByteArraySearcher searcher = new(needle); ByteArraySearcher searcher = new(needle);
// --- Act
bool result = searcher.Contains(haystack); bool result = searcher.Contains(haystack);
// --- Assert
Assert.True(result); Assert.True(result);
} }
[Fact] [Fact]
public void Contains__NeedleAtBeginning_ReturnsTrue() public void Contains__NeedleAtBeginning_ReturnsTrue()
{ {
byte[] haystack = { 1, 2, 3, 4, 5 }; // --- Arrange
byte[] needle = { 1, 2, 3 }; byte[] haystack = [1, 2, 3, 4, 5,];
byte[] needle = [1, 2, 3,];
ByteArraySearcher searcher = new(needle); ByteArraySearcher searcher = new(needle);
// --- Act
bool result = searcher.Contains(haystack); bool result = searcher.Contains(haystack);
// --- Assert
Assert.True(result); Assert.True(result);
} }
[Fact] [Fact]
public void Contains__NeedleInMiddle_ReturnsTrue() public void Contains__NeedleInMiddle_ReturnsTrue()
{ {
byte[] haystack = { 1, 2, 3, 4, 5 }; // --- Arrange
byte[] needle = { 3, 4 }; byte[] haystack = [1, 2, 3, 4, 5,];
byte[] needle = [3, 4,];
ByteArraySearcher searcher = new(needle); ByteArraySearcher searcher = new(needle);
// --- Act
bool result = searcher.Contains(haystack); bool result = searcher.Contains(haystack);
// --- Assert
Assert.True(result); Assert.True(result);
} }
[Fact] [Fact]
public void Contains__NeedleAtEnd_ReturnsTrue() public void Contains__NeedleAtEnd_ReturnsTrue()
{ {
byte[] haystack = { 1, 2, 3, 4, 5 }; // --- Arrange
byte[] needle = { 4, 5 }; byte[] haystack = [1, 2, 3, 4, 5,];
byte[] needle = [4, 5,];
ByteArraySearcher searcher = new(needle); ByteArraySearcher searcher = new(needle);
// --- Act
bool result = searcher.Contains(haystack); bool result = searcher.Contains(haystack);
// --- Assert
Assert.True(result); Assert.True(result);
} }
[Fact] [Fact]
public void Contains__NeedleNotPresent_ReturnsFalse() public void Contains__NeedleNotPresent_ReturnsFalse()
{ {
byte[] haystack = { 1, 2, 3, 4, 5 }; // --- Arrange
byte[] needle = { 5, 6, 7 }; byte[] haystack = [1, 2, 3, 4, 5,];
byte[] needle = [5, 6, 7,];
ByteArraySearcher searcher = new(needle); ByteArraySearcher searcher = new(needle);
// --- Act
bool result = searcher.Contains(haystack); bool result = searcher.Contains(haystack);
// --- Assert
Assert.False(result); Assert.False(result);
} }
} }

View File

@@ -5,7 +5,6 @@ namespace CvsLib;
public class CsvFieldIndexerTests public class CsvFieldIndexerTests
{ {
#region GenerateIndex #region GenerateIndex
[Fact] [Fact]
@@ -51,10 +50,11 @@ public class CsvFieldIndexerTests
public void GenerateIndex__TwoLinesOfPainText__TwoRows() public void GenerateIndex__TwoLinesOfPainText__TwoRows()
{ {
// --- Arrange // --- Arrange
StringReader sr = new(""" StringReader sr = new(
Hello World """
Hello World Hello World
"""); Hello World
""");
// --- Act // --- Act
CsvFieldIndexer indexer = new(); CsvFieldIndexer indexer = new();
@@ -80,10 +80,11 @@ public class CsvFieldIndexerTests
public void GenerateIndex__TwoLinesOfQuotedText__TwoRows() public void GenerateIndex__TwoLinesOfQuotedText__TwoRows()
{ {
// --- Arrange // --- Arrange
StringReader sr = new(""" StringReader sr = new(
"Hello World" """
"Hello World" "Hello World"
"""); "Hello World"
""");
// --- Act // --- Act
CsvFieldIndexer indexer = new(); CsvFieldIndexer indexer = new();
@@ -109,10 +110,11 @@ public class CsvFieldIndexerTests
public void GenerateIndex__TwoLinesWithTwoQuotedColumns__TwoRowsTwoFields() public void GenerateIndex__TwoLinesWithTwoQuotedColumns__TwoRowsTwoFields()
{ {
// --- Arrange // --- Arrange
StringReader sr = new(""" StringReader sr = new(
"Hello","World" """
"Hello","World" "Hello","World"
"""); "Hello","World"
""");
// --- Act // --- Act
CsvFieldIndexer indexer = new(); CsvFieldIndexer indexer = new();
@@ -142,10 +144,11 @@ public class CsvFieldIndexerTests
public void GenerateIndex__TwoLinesWithTwoQuotedColumnsWithUnicode__TwoRowsTwoFields() public void GenerateIndex__TwoLinesWithTwoQuotedColumnsWithUnicode__TwoRowsTwoFields()
{ {
// --- Arrange // --- Arrange
StringReader sr = new(""" StringReader sr = new(
"Hélló","Wórld" """
"Hélló","Wórld" "Hélló","Wórld"
"""); "Hélló","Wórld"
""");
// --- Act // --- Act
CsvFieldIndexer indexer = new(); CsvFieldIndexer indexer = new();
@@ -174,15 +177,16 @@ public class CsvFieldIndexerTests
#endregion GenerateIndex #endregion GenerateIndex
#region Search #region Search
[Fact] [Fact]
public void Search__TwoLinesWithTwoQuotedColumns__OneIndexFirstRow() public void Search__TwoLinesWithTwoQuotedColumns__OneIndexFirstRow()
{ {
// --- Arrange // --- Arrange
string strText = """ const string strText =
"Hello","test" """
"Hello","World" "Hello","test"
"""; "Hello","World"
""";
StringReader sr = new(strText); StringReader sr = new(strText);
CsvFieldIndexer indexer = new(); CsvFieldIndexer indexer = new();
indexer.GenerateIndex(sr); indexer.GenerateIndex(sr);
@@ -202,10 +206,11 @@ public class CsvFieldIndexerTests
public void Search__TwoLinesWithTwoQuotedColumns__OneIndexSecondRow() public void Search__TwoLinesWithTwoQuotedColumns__OneIndexSecondRow()
{ {
// --- Arrange // --- Arrange
string strText = """ const string strText =
"Hello","World" """
"Hello","test" "Hello","World"
"""; "Hello","test"
""";
StringReader sr = new(strText); StringReader sr = new(strText);
CsvFieldIndexer indexer = new(); CsvFieldIndexer indexer = new();
indexer.GenerateIndex(sr); indexer.GenerateIndex(sr);
@@ -225,10 +230,11 @@ public class CsvFieldIndexerTests
public void Search__TwoLinesWithTwoQuotedColumnsTwoMatches__OneIndexSecondRow() public void Search__TwoLinesWithTwoQuotedColumnsTwoMatches__OneIndexSecondRow()
{ {
// --- Arrange // --- Arrange
string strText = """ const string strText =
"Hello","World" """
"test","test" "Hello","World"
"""; "test","test"
""";
StringReader sr = new(strText); StringReader sr = new(strText);
CsvFieldIndexer indexer = new(); CsvFieldIndexer indexer = new();
indexer.GenerateIndex(sr); indexer.GenerateIndex(sr);
@@ -243,7 +249,7 @@ public class CsvFieldIndexerTests
Assert.Single(indexes); Assert.Single(indexes);
Assert.Equal(16, indexes[0]); Assert.Equal(16, indexes[0]);
} }
#endregion Search #endregion Search
} }

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>CvsLib</RootNamespace> <RootNamespace>CvsLib</RootNamespace>

View File

@@ -1,57 +1,52 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
namespace CsvLib namespace CsvLib;
public class BufferedTextReader : TextReader
{ {
public class BufferedTextReader : TextReader private readonly TextReader _baseReader;
private readonly StringBuilder _sbBuffer = new();
private readonly Encoding _currentEncoding = Encoding.Default;
public BufferedTextReader(TextReader baseReader)
{ {
private readonly TextReader _baseReader; _baseReader = baseReader;
private int _position; if (baseReader is StreamReader streamReader)
private readonly StringBuilder _sbBuffer = new();
private readonly Encoding _currentEncoding = Encoding.Default;
public BufferedTextReader(TextReader baseReader)
{ {
_baseReader = baseReader; _currentEncoding = streamReader.CurrentEncoding;
if (baseReader is StreamReader streamReader)
{
_currentEncoding = streamReader.CurrentEncoding;
}
}
public override int Read()
{
int read = _baseReader.Read();
if (read > 127)
{
int count = _currentEncoding.GetByteCount(((char)read).ToString());
_position += count;
}
else
{
_position++;
}
if (read != -1)
{
_sbBuffer.Append((char)read);
}
return read;
}
public int Position
{
get { return _position; }
}
public string GetBuffer()
{
return _sbBuffer.ToString();
}
public void CleanBuffer()
{
_sbBuffer.Clear();
} }
} }
}
public override int Read()
{
int read = _baseReader.Read();
if (read > 127)
{
int count = _currentEncoding.GetByteCount(((char)read).ToString());
Position += count;
}
else
{
Position++;
}
if (read != -1)
{
_sbBuffer.Append((char)read);
}
return read;
}
public int Position { get; private set; }
public string GetBuffer()
{
return _sbBuffer.ToString();
}
public void CleanBuffer()
{
_sbBuffer.Clear();
}
}

View File

@@ -1,5 +1,3 @@
#nullable enable
namespace CsvLib; namespace CsvLib;
public class ByteArraySearcher public class ByteArraySearcher

View File

@@ -3,322 +3,319 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
namespace CsvLib namespace CsvLib;
public class CsvFieldIndexer
{ {
public class CsvFieldIndexer private bool _insideString;
private Encoding _currentEncoding = Encoding.Default;
private readonly char _separator;
private readonly char _quoteChar;
private readonly char _escapeChar;
public CsvFieldIndexer(char separator = ',', char quoteChar = '"', char escapeChar = '\\')
{ {
private bool _insideString; _separator = separator;
_quoteChar = quoteChar;
_escapeChar = escapeChar;
}
private Encoding _currentEncoding = Encoding.Default; private List<long> _index = new();
private readonly char _separator; public List<long> Index { get { return _index; } }
private readonly char _quoteChar;
private readonly char _escapeChar;
public CsvFieldIndexer(char separator = ',', char quoteChar = '"', char escapeChar = '\\') private List<List<long>> _fieldIndex = new();
public List<List<long>> FieldIndex { get { return _fieldIndex; } }
private void DummyParser(string line)
{
for (int i = 0; i < line.Length; i++)
{ {
_separator = separator; char c = line[i];
_quoteChar = quoteChar; if (c == _separator && _insideString == false)
_escapeChar = escapeChar;
}
private List<long> _index = new();
public List<long> Index { get { return _index; } }
private List<List<long>> _fieldIndex = new();
public List<List<long>> FieldIndex { get { return _fieldIndex; } }
private void DummyParser(string line)
{
for (int i = 0; i < line.Length; i++)
{ {
char c = line[i]; continue;
if (c == _separator && _insideString == false) }
{ if (c == _quoteChar && _insideString == false)
continue; {
} _insideString = true;
if (c == _quoteChar && _insideString == false) continue;
{ }
_insideString = true; if (c == _quoteChar && _insideString)
continue; {
} _insideString = false;
if (c == _quoteChar && _insideString) continue;
{ }
_insideString = false; if (c == _escapeChar && _insideString)
continue; {
} i++;
if (c == _escapeChar && _insideString)
{
i++;
}
} }
} }
}
private List<long> ParseLineIndex(string line, long lineOffset) private List<long> ParseLineIndex(string line, long lineOffset)
{
List<long> fieldPositions = new();
long? fieldStartPosition = null;
long? fieldEndPosition = null;
int unicodeDelta = 0;
for (int i = 0; i < line.Length; i++)
{ {
List<long> fieldPositions = new(); char c = line[i];
long? fieldStartPosition = null; if (c == _separator && _insideString == false)
long? fieldEndPosition = null;
int unicodeDelta = 0;
for (int i = 0; i < line.Length; i++)
{ {
char c = line[i]; if (fieldStartPosition != null && fieldEndPosition != null)
if (c == _separator && _insideString == false)
{
if (fieldStartPosition != null)
{
fieldPositions.Add((long)fieldStartPosition);
fieldPositions.Add((long)fieldEndPosition);
}
fieldStartPosition = null;
fieldEndPosition = null;
}
else if (c == _quoteChar && _insideString == false)
{
_insideString = true;
}
else if (c == _quoteChar && _insideString)
{
_insideString = false;
}
else if (c == _escapeChar && _insideString)
{
i++;
}
else if ((c == '\n' || c == '\r') && _insideString == false)
{
break;
}
else
{
if (c > 127)
{
unicodeDelta += _currentEncoding.GetByteCount(c.ToString()) - 1;
}
long absolutePosition = lineOffset + i + unicodeDelta;
fieldStartPosition ??= absolutePosition;
fieldEndPosition = absolutePosition;
}
}
if (_insideString == false)
{
if (fieldStartPosition != null)
{ {
fieldPositions.Add((long)fieldStartPosition); fieldPositions.Add((long)fieldStartPosition);
fieldPositions.Add((long)fieldEndPosition); fieldPositions.Add((long)fieldEndPosition);
} }
fieldStartPosition = null;
fieldEndPosition = null;
} }
return fieldPositions; else if (c == _quoteChar && _insideString == false)
}
private void GenerateIndex(string file)
{
using FileStream stream = new(file, FileMode.Open);
using StreamReader streamReader = new(stream, Encoding.Default, true, 4096);
GenerateIndex(streamReader);
stream.Close();
}
public void GenerateIndex(TextReader textReader)
{
_insideString = false;
_index.Clear();
_index.Add(0);
int idxRow = 0;
if (textReader is StreamReader streamReader)
{ {
_currentEncoding = streamReader.CurrentEncoding; _insideString = true;
} }
using BufferedTextReader reader = new(textReader); else if (c == _quoteChar && _insideString)
string currentLine;
while ((currentLine = reader.ReadLine()) != null)
{ {
DummyParser(currentLine); _insideString = false;
if (_insideString) { continue; }
string fullLine = reader.GetBuffer();
reader.CleanBuffer();
List<long> fieldIndexes = ParseLineIndex(fullLine, _index[idxRow]);
_fieldIndex.Add(fieldIndexes);
_index.Add(reader.Position);
idxRow++;
} }
} else if (c == _escapeChar && _insideString)
private const byte FileFormatVersion = 1;
private void SaveFile(string indexFile)
{
if (indexFile == null) { return; }
if (File.Exists(indexFile))
{ {
File.Delete(indexFile); i++;
} }
Stream streamOut = File.Open(indexFile, FileMode.Create); else if ((c == '\n' || c == '\r') && _insideString == false)
using (BinaryWriter binWriter = new(streamOut))
{ {
binWriter.Write((byte)'C'); break;
binWriter.Write((byte)'S'); }
binWriter.Write((byte)'V'); else
{
binWriter.Write(FileFormatVersion); if (c > 127)
binWriter.Write(_index.Count);
foreach (long currentIndex in _index)
{ {
binWriter.Write(currentIndex); unicodeDelta += _currentEncoding.GetByteCount(c.ToString()) - 1;
} }
binWriter.Write(_fieldIndex.Count); long absolutePosition = lineOffset + i + unicodeDelta;
foreach (List<long> currentFieldIndex in _fieldIndex) fieldStartPosition ??= absolutePosition;
{ fieldEndPosition = absolutePosition;
binWriter.Write(currentFieldIndex.Count);
for (int i = 0; i < currentFieldIndex.Count; i++)
{
binWriter.Write(currentFieldIndex[i]);
}
}
}
streamOut.Close();
}
private bool LoadFile(string indexFile)
{
if (File.Exists(indexFile) == false)
{
return false;
}
List<long> tempIndex;
List<List<long>> tempFieldIndex;
Stream streamIn = File.Open(indexFile, FileMode.Open);
try
{
using BinaryReader binReader = new(streamIn);
byte magik0 = binReader.ReadByte();
byte magik1 = binReader.ReadByte();
byte magik2 = binReader.ReadByte();
if (magik0 != (byte)'C' || magik1 != (byte)'S' || magik2 != (byte)'V') { return false; }
byte fileVersion = binReader.ReadByte();
if (fileVersion != FileFormatVersion) { return false; }
int numIndexes = binReader.ReadInt32();
tempIndex = new List<long>(numIndexes);
for (int i = 0; i < numIndexes; i++)
{
long value = binReader.ReadInt64();
tempIndex.Add(value);
}
int numFieldIndexes = binReader.ReadInt32();
tempFieldIndex = new List<List<long>>(numFieldIndexes);
for (int j = 0; j < numFieldIndexes; j++)
{
int numCurrentFieldIndexes = binReader.ReadInt32();
List<long> currentFieldIndex = new(numCurrentFieldIndexes);
for (int i = 0; i < numCurrentFieldIndexes; i++)
{
long value = binReader.ReadInt64();
currentFieldIndex.Add(value);
}
tempFieldIndex.Add(currentFieldIndex);
}
}
catch (Exception)
{
// NON NON NOM
return false;
}
finally
{
streamIn.Close();
}
_index = tempIndex;
_fieldIndex = tempFieldIndex;
return true;
}
public void LoadIndexOfFile(string file)
{
DateTime dtFile = File.GetCreationTime(file);
string indexFile = $"{file}.idx";
if (File.Exists(indexFile) && File.GetCreationTime(indexFile) > dtFile)
{
if (LoadFile(indexFile)) { return; }
}
// Generate index
DateTime dtNow = DateTime.UtcNow;
GenerateIndex(file);
TimeSpan tsGenIndex = DateTime.UtcNow - dtNow;
// Save Index if expensive generation
if (tsGenIndex.TotalSeconds > 2)
{
SaveFile(indexFile);
} }
} }
if (_insideString == false)
public List<long> Search(string fileName, string textToSearch, Action<float> notifyProgress = null)
{ {
List<long> index; if (fieldStartPosition != null && fieldEndPosition != null)
using FileStream streamIn = new(fileName, FileMode.Open);
try
{ {
index = Search(streamIn, textToSearch, notifyProgress); fieldPositions.Add((long)fieldStartPosition);
fieldPositions.Add((long)fieldEndPosition);
} }
finally
{
streamIn.Close();
}
return index ?? new List<long>();
} }
return fieldPositions;
public List<long> Search(Stream streamIn, string textToSearch, Action<float> notifyProgress = null) }
private void GenerateIndex(string file)
{
using FileStream stream = new(file, FileMode.Open);
using StreamReader streamReader = new(stream, Encoding.Default, true, 4096);
GenerateIndex(streamReader);
stream.Close();
}
public void GenerateIndex(TextReader textReader)
{
_insideString = false;
_index.Clear();
_index.Add(0);
int idxRow = 0;
if (textReader is StreamReader streamReader)
{ {
// TODO: Use MemoryMappedFile for better IO performance _currentEncoding = streamReader.CurrentEncoding;
DateTime datePrevious = DateTime.UtcNow; }
List<long> newIndexes = new(); using BufferedTextReader reader = new(textReader);
byte[] bText = Encoding.UTF8.GetBytes(textToSearch); while (reader.ReadLine() is { } currentLine)
ByteArraySearcher searcher = new(bText); {
byte[] buffer = new byte[1024]; DummyParser(currentLine);
for (int j = 0; j < _fieldIndex.Count; j++) if (_insideString) { continue; }
{
for (int i = 0; i < _fieldIndex[j].Count; i += 2)
{
TimeSpan tsElapsed = DateTime.UtcNow - datePrevious;
if (tsElapsed.TotalMilliseconds > 200)
{
datePrevious = DateTime.UtcNow;
notifyProgress?.Invoke(j/(float)_fieldIndex.Count);
}
long offset = _fieldIndex[j][i];
int length = (int)(_fieldIndex[j][i + 1] - offset) + 1;
if (buffer.Length < length)
{
buffer = new byte[length];
}
streamIn.Seek(offset, SeekOrigin.Begin);
int read = streamIn.Read(buffer, 0, length);
if (read != length) { throw new Exception($"Search: Expected {length} bytes, but read {read}"); }
bool matches = searcher.Contains(buffer, length); string fullLine = reader.GetBuffer();
if (matches == false) { continue; } reader.CleanBuffer();
List<long> fieldIndexes = ParseLineIndex(fullLine, _index[idxRow]);
newIndexes.Add(_index[j]); _fieldIndex.Add(fieldIndexes);
break;
}
}
return newIndexes; _index.Add(reader.Position);
idxRow++;
} }
} }
}
private const byte FileFormatVersion = 1;
private void SaveFile(string indexFile)
{
if (File.Exists(indexFile))
{
File.Delete(indexFile);
}
Stream streamOut = File.Open(indexFile, FileMode.Create);
using (BinaryWriter binWriter = new(streamOut))
{
binWriter.Write((byte)'C');
binWriter.Write((byte)'S');
binWriter.Write((byte)'V');
binWriter.Write(FileFormatVersion);
binWriter.Write(_index.Count);
foreach (long currentIndex in _index)
{
binWriter.Write(currentIndex);
}
binWriter.Write(_fieldIndex.Count);
foreach (List<long> currentFieldIndex in _fieldIndex)
{
binWriter.Write(currentFieldIndex.Count);
foreach (long fieldIndex in currentFieldIndex)
{
binWriter.Write(fieldIndex);
}
}
}
streamOut.Close();
}
private bool LoadFile(string indexFile)
{
if (File.Exists(indexFile) == false)
{
return false;
}
List<long> tempIndex;
List<List<long>> tempFieldIndex;
Stream streamIn = File.Open(indexFile, FileMode.Open);
try
{
using BinaryReader binReader = new(streamIn);
byte magik0 = binReader.ReadByte();
byte magik1 = binReader.ReadByte();
byte magik2 = binReader.ReadByte();
if (magik0 != (byte)'C' || magik1 != (byte)'S' || magik2 != (byte)'V') { return false; }
byte fileVersion = binReader.ReadByte();
if (fileVersion != FileFormatVersion) { return false; }
int numIndexes = binReader.ReadInt32();
tempIndex = new List<long>(numIndexes);
for (int i = 0; i < numIndexes; i++)
{
long value = binReader.ReadInt64();
tempIndex.Add(value);
}
int numFieldIndexes = binReader.ReadInt32();
tempFieldIndex = new List<List<long>>(numFieldIndexes);
for (int j = 0; j < numFieldIndexes; j++)
{
int numCurrentFieldIndexes = binReader.ReadInt32();
List<long> currentFieldIndex = new(numCurrentFieldIndexes);
for (int i = 0; i < numCurrentFieldIndexes; i++)
{
long value = binReader.ReadInt64();
currentFieldIndex.Add(value);
}
tempFieldIndex.Add(currentFieldIndex);
}
}
catch (Exception)
{
// NON NON NOM
return false;
}
finally
{
streamIn.Close();
}
_index = tempIndex;
_fieldIndex = tempFieldIndex;
return true;
}
public void LoadIndexOfFile(string file)
{
DateTime dtFile = File.GetCreationTime(file);
string indexFile = $"{file}.idx";
if (File.Exists(indexFile) && File.GetCreationTime(indexFile) > dtFile)
{
if (LoadFile(indexFile)) { return; }
}
// Generate index
DateTime dtNow = DateTime.UtcNow;
GenerateIndex(file);
TimeSpan tsGenIndex = DateTime.UtcNow - dtNow;
// Save Index if expensive generation
if (tsGenIndex.TotalSeconds > 2)
{
SaveFile(indexFile);
}
}
public List<long> Search(string fileName, string textToSearch, Action<float>? notifyProgress = null)
{
List<long> index;
using FileStream streamIn = new(fileName, FileMode.Open);
try
{
index = Search(streamIn, textToSearch, notifyProgress);
}
finally
{
streamIn.Close();
}
return index;
}
public List<long> Search(Stream streamIn, string textToSearch, Action<float>? notifyProgress = null)
{
// TODO: Use MemoryMappedFile for better IO performance
DateTime datePrevious = DateTime.UtcNow;
List<long> newIndexes = new();
byte[] bText = Encoding.UTF8.GetBytes(textToSearch);
ByteArraySearcher searcher = new(bText);
byte[] buffer = new byte[1024];
for (int j = 0; j < _fieldIndex.Count; j++)
{
for (int i = 0; i < _fieldIndex[j].Count; i += 2)
{
TimeSpan tsElapsed = DateTime.UtcNow - datePrevious;
if (tsElapsed.TotalMilliseconds > 200)
{
datePrevious = DateTime.UtcNow;
notifyProgress?.Invoke(j/(float)_fieldIndex.Count);
}
long offset = _fieldIndex[j][i];
int length = (int)(_fieldIndex[j][i + 1] - offset) + 1;
if (buffer.Length < length)
{
buffer = new byte[length];
}
streamIn.Seek(offset, SeekOrigin.Begin);
int read = streamIn.Read(buffer, 0, length);
if (read != length) { throw new Exception($"Search: Expected {length} bytes, but read {read}"); }
bool matches = searcher.Contains(buffer, length);
if (matches == false) { continue; }
newIndexes.Add(_index[j]);
break;
}
}
return newIndexes;
}
}

View File

@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<LangVersion>11</LangVersion> <LangVersion>11</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -2,102 +2,100 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
namespace CsvLib namespace CsvLib;
public class CsvParser
{ {
public class CsvParser private bool _insideString;
private readonly char _separator;
private readonly char _quoteChar;
private readonly char _escapeChar;
public CsvParser(char separator = ',', char quoteChar = '"', char escapeChar = '\\')
{ {
private bool _insideString; _separator = separator;
_quoteChar = quoteChar;
_escapeChar = escapeChar;
}
private readonly char _separator; private List<List<string>> _data = new();
private readonly char _quoteChar;
private readonly char _escapeChar;
public CsvParser(char separator = ',', char quoteChar = '"', char escapeChar = '\\') private List<string>? _currentReg;
private StringBuilder? _currentCell;
public List<List<string>> Data
{
get { return _data; }
}
private void ParseLine(string line)
{
_currentReg ??= new List<string>();
_currentCell ??= new StringBuilder();
for (int i = 0; i < line.Length; i++)
{ {
_separator = separator; char c = line[i];
_quoteChar = quoteChar; if (c == _separator && _insideString == false)
_escapeChar = escapeChar;
}
private List<List<string>> _data = new();
private List<string> _currentReg;
StringBuilder _currentCell;
public List<List<string>> Data
{
get { return _data; }
}
private void ParseLine(string line)
{
_currentReg ??= new List<string>();
_currentCell ??= new StringBuilder();
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == _separator && _insideString == false)
{
_currentReg.Add(_currentCell.ToString());
_currentCell.Clear();
continue;
}
if (c == _quoteChar && _insideString == false)
{
_insideString = true;
continue;
}
if (c == _quoteChar && _insideString)
{
_insideString = false;
continue;
}
if (c == _escapeChar && _insideString)
{
i++;
if (i == line.Length) { break; }
c = line[i];
}
_currentCell.Append(c);
}
if (_insideString)
{
_currentCell.Append('\n');
}
else
{ {
_currentReg.Add(_currentCell.ToString()); _currentReg.Add(_currentCell.ToString());
_currentCell.Clear(); _currentCell.Clear();
_data.Add(_currentReg); continue;
_currentReg = null;
} }
if (c == _quoteChar && _insideString == false)
{
_insideString = true;
continue;
}
if (c == _quoteChar && _insideString)
{
_insideString = false;
continue;
}
if (c == _escapeChar && _insideString)
{
i++;
if (i == line.Length) { break; }
c = line[i];
}
_currentCell.Append(c);
} }
public void ParseFile(string file, long offset = 0, int count = 0)
if (_insideString)
{ {
_insideString = false; _currentCell.Append('\n');
_data = new List<List<string>>(); }
else
{
_currentReg.Add(_currentCell.ToString());
_currentCell.Clear();
_data.Add(_currentReg);
_currentReg = null; _currentReg = null;
FileStream stream = new(file, FileMode.Open); }
stream.Seek(offset, SeekOrigin.Begin); }
using (StreamReader reader = new(stream, Encoding.Default, true, 4096))
public void ParseFile(string file, long offset = 0, int count = 0)
{
_insideString = false;
_data = new List<List<string>>();
_currentReg = null;
FileStream stream = new(file, FileMode.Open);
stream.Seek(offset, SeekOrigin.Begin);
using (StreamReader reader = new(stream, Encoding.Default, true, 4096))
{
while (reader.ReadLine() is { } currentLine)
{ {
string currentLine; ParseLine(currentLine);
while ((currentLine = reader.ReadLine()) != null) if (count > 0 && Data.Count == count)
{ {
ParseLine(currentLine); break;
if (count > 0 && Data.Count == count)
{
break;
}
} }
} }
stream.Close();
} }
stream.Close();
} }
}
}

View File

@@ -1,3 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/usr/share/dotnet/sdk/7.0.107/MSBuild.dll</s:String> <s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/usr/share/dotnet/sdk/7.0.107/MSBuild.dll</s:String>
<s:Int64 x:Key="/Default/Environment/Hierarchy/Build/BuildTool/MsbuildVersion/@EntryValue">4294967293</s:Int64></wpf:ResourceDictionary> <s:Int64 x:Key="/Default/Environment/Hierarchy/Build/BuildTool/MsbuildVersion/@EntryValue">4294967293</s:Int64>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -1,7 +1,7 @@
<Application xmlns="https://github.com/avaloniaui" <Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="CsvView.App" x:Class="CsvView.App"
RequestedThemeVariant="Default"> RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles> <Application.Styles>

View File

@@ -1,14 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.2"/> <PackageReference Include="Avalonia" Version="11.0.2"/>
<PackageReference Include="Avalonia.Desktop" Version="11.0.2"/> <PackageReference Include="Avalonia.Desktop" Version="11.0.2"/>
@@ -17,8 +16,7 @@
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.2"/> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.2"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CsvLib\CsvLib.csproj" /> <ProjectReference Include="..\CsvLib\CsvLib.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -9,12 +9,12 @@
Width="800" Width="800"
Height="600" Height="600"
Title="CsvView"> Title="CsvView">
<Grid RowDefinitions="Auto,Auto,Auto,*"> <Grid RowDefinitions="Auto,Auto,Auto,*" Margin="5">
<Grid ColumnDefinitions="Auto,*" Grid.Row="0"> <Grid ColumnDefinitions="Auto,*" Grid.Row="0" Margin="0 0 0 5">
<Button Grid.Column="0" Name="BtnLoad" Click="BtnLoad_OnClick">...</Button> <Button Grid.Column="0" Name="BtnLoad" Click="BtnLoad_OnClick">...</Button>
<TextBox Grid.Column="1" Name="TxtFileName" IsReadOnly="true" /> <TextBox Grid.Column="1" Name="TxtFileName" IsReadOnly="true" />
</Grid> </Grid>
<Grid ColumnDefinitions="*,Auto" Grid.Row="1"> <Grid ColumnDefinitions="*,Auto" Grid.Row="1" Margin="0 0 0 5">
<TextBox Grid.Column="0" Name="TxtSearch" /> <TextBox Grid.Column="0" Name="TxtSearch" />
<Button Grid.Column="1" Name="BtnSearch" Click="BtnSearch_OnClick">🔍</Button> <Button Grid.Column="1" Name="BtnSearch" Click="BtnSearch_OnClick">🔍</Button>
</Grid> </Grid>
@@ -22,7 +22,7 @@
<Button Name="BtnFirst" Click="BtnFirst_OnClick">|◁</Button> <Button Name="BtnFirst" Click="BtnFirst_OnClick">|◁</Button>
<Button Name="BtnPrevious" Click="BtnPrevious_OnClick">◁</Button> <Button Name="BtnPrevious" Click="BtnPrevious_OnClick">◁</Button>
<TextBox Name="TxtIndex" Text="{Binding Index}" TextChanged="TxtIndex_OnTextChanged"></TextBox> <TextBox Name="TxtIndex" Text="{Binding Index}" TextChanged="TxtIndex_OnTextChanged"></TextBox>
<TextBlock>/</TextBlock> <TextBlock VerticalAlignment="Center">/</TextBlock>
<TextBox Name="TxtMaxIndex" Text="{Binding MaxIndex}" IsReadOnly="true"></TextBox> <TextBox Name="TxtMaxIndex" Text="{Binding MaxIndex}" IsReadOnly="true"></TextBox>
<Button Name="BtnNext" Click="BtnNext_OnClick">▷</Button> <Button Name="BtnNext" Click="BtnNext_OnClick">▷</Button>
<Button Name="BtnLast" Click="BtnLast_OnClick">▷|</Button> <Button Name="BtnLast" Click="BtnLast_OnClick">▷|</Button>

View File

@@ -28,8 +28,8 @@ public partial class MainWindow : Window
AllowMultiple = false, AllowMultiple = false,
FileTypeFilter = new List<FilePickerFileType> FileTypeFilter = new List<FilePickerFileType>
{ {
new("CSV Files") { Patterns = new[] { "*.csv" } }, new("CSV Files") { Patterns = new[] { "*.csv", }, },
new("Any File") { Patterns = new[] { "*" } }, new("Any File") { Patterns = new[] { "*", }, },
}, },
}); });
@@ -89,6 +89,8 @@ public partial class MainWindow : Window
private void Search(string? textToSearch) private void Search(string? textToSearch)
{ {
if (textToSearch == null) { return; }
// TODO: Loading animation // TODO: Loading animation
CsvFieldIndexer csvIndexer = new(); CsvFieldIndexer csvIndexer = new();
csvIndexer.LoadIndexOfFile(_loadedFile); csvIndexer.LoadIndexOfFile(_loadedFile);

View File

@@ -3,7 +3,7 @@ using System;
namespace CsvView; namespace CsvView;
class Program static class Program
{ {
// Initialization code. Don't use any Avalonia, third-party APIs or any // Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
@@ -13,7 +13,7 @@ class Program
.StartWithClassicDesktopLifetime(args); .StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer. // Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp() private static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>() => AppBuilder.Configure<App>()
.UsePlatformDetect() .UsePlatformDetect()
.WithInterFont() .WithInterFont()