如何返回 .csv 中的上一行?

How To Go Back To Previous Line In .csv?

我正在尝试弄清楚如何记录我所在的行,例如 line = 32,允许我在上一个记录按钮事件中添加 line-- 或查找更好的选择。

我目前有我的表单设置和工作,如果我点击 "Next Record" 按钮,文件会递增到下一行并在其关联的文本框中正确显示单元格,但是我如何创建一个按钮转到 .csv 文件中的上一行?

StreamReader csvFile;

public GP_Appointment_Manager()
{
    InitializeComponent();
}

private void buttonOpenFile_Click(object sender, EventArgs e)
{
    try
    {
        csvFile = new StreamReader("patients_100.csv");
        // Read First line and do nothing
        string line;
        if (ReadPatientLineFromCSV(out line))
        {
            // Read second line, first patient line and populate form
            ReadPatientLineFromCSV(out line);
            PopulateForm(line);
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

private bool ReadPatientLineFromCSV(out string line)
{
    bool result = false;
    line = "";
    if ((csvFile != null) && (!csvFile.EndOfStream))
    {
        line = csvFile.ReadLine();
        result = true;
    }
    else
    {
        MessageBox.Show("File has not been opened. Please open file before reading.");
    }
    return result;
}

private void PopulateForm(string patientDetails)
{
    string[] patient = patientDetails.Split(',');
    //Populates ID
    textBoxID.Text = patient[0];
    //Populates Personal 
    comboBoxSex.SelectedIndex = (patient[1] == "M") ? 0 : 1;
    dateTimePickerDOB.Value = DateTime.Parse(patient[2]);
    textBoxFirstName.Text = patient[3];
    textBoxLastName.Text = patient[4];
    //Populates Address 
    textboxAddress.Text = patient[5];
    textboxCity.Text = patient[6];
    textboxCounty.Text = patient[7];
    textboxTelephone.Text = patient[8];
    //Populates Kin
    textboxNextOfKin.Text = patient[9];
    textboxKinTelephone.Text = patient[10];
}

这是 "Next Record" 按钮的代码

private void buttonNextRecord_Click(object sender, EventArgs e)
{
    string patientInfo;
    if (ReadPatientLineFromCSV(out patientInfo))
    {
        PopulateForm(patientInfo);
    }
}

一般做法如下:

像这样添加文本文件input.txt

line 1
line 2
line 3

并将复制到输出目录 属性设置为如果较新则复制

StreamReader

创建扩展方法
public static class StreamReaderExtensions
{
    public static bool TryReadNextLine(this StreamReader reader, out string line)
    {
        var isAvailable = reader != null &&
                          !reader.EndOfStream;
        line = isAvailable ? reader.ReadLine() : null;
        return isAvailable;
    }

    public static bool TryReadPrevLine(this StreamReader reader, out string line)
    {
        var stream = reader.BaseStream;
        var encoding = reader.CurrentEncoding;
        var bom = GetBOM(encoding);

        var isAvailable = reader != null &&
                          stream.Position > 0;

        if(!isAvailable)
        {
            line = null;
            return false;
        }

        var buffer = new List<byte>();
        var str = string.Empty;
        stream.Position++;
        while (!str.StartsWith(Environment.NewLine))
        {
            stream.Position -= 2;
            buffer.Insert(0, (byte)stream.ReadByte());
            var reachedBOM = buffer.Take(bom.Length).SequenceEqual(bom);
            if (reachedBOM)
                buffer = buffer.Skip(bom.Length).ToList();
            str = encoding.GetString(buffer.ToArray());
            if (reachedBOM)
                break;
        }
        stream.Position--;
        line = str.Trim(Environment.NewLine.ToArray());
        return true;
    }

    private static byte[] GetBOM(Encoding encoding)
    {
        if (encoding.Equals(Encoding.UTF7))
            return new byte[] { 0x2b, 0x2f, 0x76 };
        if (encoding.Equals(Encoding.UTF8))
            return new byte[] { 0xef, 0xbb, 0xbf };
        if (encoding.Equals(Encoding.Unicode))
            return new byte[] { 0xff, 0xfe };
        if (encoding.Equals(Encoding.BigEndianUnicode))
            return new byte[] { 0xfe, 0xff };
        if (encoding.Equals(Encoding.UTF32))
            return new byte[] { 0, 0, 0xfe, 0xff };
        return new byte[0];
    }
}

并像这样使用它:

using (var reader = new StreamReader("input.txt"))
{
    string na = "N/A";
    string line;
    for (var i = 0; i < 4; i++)
    {
        var isAvailable = reader.TryReadNextLine(out line);
        Console.WriteLine($"Next line available: {isAvailable}. Line: {(isAvailable ? line : na)}");
    }
    for (var i = 0; i < 4; i++)
    {
        var isAvailable = reader.TryReadPrevLine(out line);
        Console.WriteLine($"Prev line available: {isAvailable}. Line: {(isAvailable ? line : na)}");
    }
}

结果是:

Next line available: True. Line: line 1
Next line available: True. Line: line 2
Next line available: True. Line: line 3
Next line available: False. Line: N/A
Prev line available: True. Line: line 3
Prev line available: True. Line: line 2
Prev line available: True. Line: line 1
Prev line available: False. Line: N/A

GetBOM 基于 this.

现在,这是某种练习。此 class 使用标准 StreamReader 并进行一些修改,以实现简单的 move-forward/step-back 功能。

它还允许将 array/list 控件与从 CSV-like 文件格式读取的数据相关联。请注意,这不是 general-purpose CSV reader;它只是将字符串分成几部分,使用可以指定的分隔符调用其 AssociateControls() 方法。

class 有 3 个构造函数:

(1) public LineReader(string filePath)
(2) public LineReader(string filePath, bool hasHeader)
(3) public LineReader(string filePath, bool hasHeader, Encoding encoding)
  1. 源文件第一行没有Header,文本编码应该是auto-detected
  2. 相同,但文件的第一行包含 Header if hasHeader = true
  3. 用于指定编码,如果自动发现无法正确识别它。

文本行的位置存储在Dictionary<long, long>中,其中Key是行号,Value是行的起始位置。

这有一些优点:没有字符串存储在任何地方,文件在读取时被索引但是你可以使用后台任务来完成索引(这个功能在这里没有实现,也许以后......)。
缺点是 Dictionary 在内存中占用 space。如果文件非常大(不过只是行数很重要),它可能会成为一个问题。去测试。

关于编码的说明:
仅当编码未设置为默认编码 (UTF-8) 时,文本编码 auto-detection 才足够可靠。这里的代码,如果不指定Encoding,则设置为Encoding.ASCII。读取第一行时,自动功能会尝试确定实际编码。它通常是正确的。
在默认的 StreamReader 实现中,如果我们指定 Encoding.UTF8(或 none,两者相同)并且文本编码为 ASCII,编码器将使用默认的(Encoding.UTF8 ) 编码,因为 UTF-8 优雅地映射到 ASCII
然而,在这种情况下,[Encoding].GetPreamble() 将 return UTF-8 BOM(3 个字节),从而影响基础流中当前位置的计算。


要将控件与读取的数据相关联,您只需将 collection 个控件传递给 LineReader.AssociateControls() 方法。
这会将每个控件映射到相同位置的数据字段。
要跳过数据字段,请指定 null 而不是控件引用。

可视化示例是使用具有以下结构的 CSV 文件构建的:
(注意:此数据是使用自动 on-line tool 生成的)

seq;firstname;lastname;age;street;city;state;zip;deposit;color;date
---------------------------------------------------------------------------
1;Harriett;Gibbs;62;Segmi Center;Ebanavi;ID;57854;44.78;WHITE;05/15/1914
2;Oscar;McDaniel;49;Kulak Drive;Jetagoz;IL;57631;13.94;RED;02/11/1918
3;Winifred;Olson;29;Wahab Mill;Ucocivo;NC;46073;02.70;RED;08/11/2008

我跳过了 seqcolor 字段,传递了这个控件数组:

LineReader lineReader = null;

private void btnOpenFile_Click(object sender, EventArgs e)
{
    string filePath = Path.Combine(Application.StartupPath, @"sample.csv");
    lineReader = new LineReader(filePath, true);
    string header = lineReader.HeaderLine;
    Control[] controls = new[] { 
        null, textBox1, textBox2, textBox3, textBox4, textBox5, 
        textBox6, textBox9, textBox7, null, textBox8 };
    lineReader.AssociateControls(controls, ";");
}

空条目对应于未考虑的数据字段。

功能的视觉示例:


using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;

class LineReader : IDisposable
{
    private StreamReader reader = null;
    private Dictionary<long, long> positions;
    private string m_filePath = string.Empty;
    private Encoding m_encoding = null;
    private IEnumerable<Control> m_controls = null;
    private string m_separator = string.Empty;
    private bool m_associate = false;
    private long m_currentPosition = 0;
    private bool m_hasHeader = false;

    public LineReader(string filePath) : this(filePath, false) { }
    public LineReader(string filePath, bool hasHeader) : this(filePath, hasHeader, Encoding.ASCII) { }
    public LineReader(string filePath, bool hasHeader, Encoding encoding)
    {
        if (!File.Exists(filePath)) {
            throw new FileNotFoundException($"The file specified: {filePath} was not found");
        }
        this.m_filePath = filePath;
        m_hasHeader = hasHeader;
        CurrentLineNumber = 0;
        reader = new StreamReader(this.m_filePath, encoding, true);
        CurrentLine = reader.ReadLine();

        m_encoding = reader.CurrentEncoding;
        m_currentPosition = m_encoding.GetPreamble().Length;
        positions = new Dictionary<long, long>() { [0]= m_currentPosition };
        if (hasHeader) { this.HeaderLine = CurrentLine = this.MoveNext(); }
    }

    public string HeaderLine { get; private set; }
    public string CurrentLine { get; private set; }
    public long CurrentLineNumber { get; private set; }

    public string MoveNext()
    {
        string read = reader.ReadLine();
        if (string.IsNullOrEmpty(read)) return this.CurrentLine;
        CurrentLineNumber += 1;

        if ((positions.Count - 1) < CurrentLineNumber) {
            AdjustPositionToLineFeed();
            positions.Add(CurrentLineNumber, m_currentPosition);
        }
        else {
            m_currentPosition = positions[CurrentLineNumber];
        }
        this.CurrentLine = read;
        if (m_associate) this.Associate();
        return read;
    }

    public string MovePrevious()
    {
        if (CurrentLineNumber == 0 || (CurrentLineNumber == 1 && m_hasHeader)) return this.CurrentLine;
        CurrentLineNumber -= 1;
        m_currentPosition = positions[CurrentLineNumber];
        reader.BaseStream.Position = m_currentPosition;
        reader.DiscardBufferedData();

        this.CurrentLine = reader.ReadLine();
        if (m_associate) this.Associate();
        return this.CurrentLine;
    }

    private void AdjustPositionToLineFeed()
    {
        long linePos = m_currentPosition + m_encoding.GetByteCount(this.CurrentLine);
        long prevPos = reader.BaseStream.Position;
        reader.BaseStream.Position = linePos;

        byte[] buffer = new byte[4];
        reader.BaseStream.Read(buffer, 0, buffer.Length);
        char[] chars = m_encoding.GetChars(buffer).Where(c => c.Equals((char)10) || c.Equals((char)13)).ToArray();
        m_currentPosition = linePos + m_encoding.GetByteCount(chars);
        reader.BaseStream.Position = prevPos;
    }

    public void AssociateControls(IEnumerable<Control> controls, string separator)
    {
        m_controls = controls;
        m_separator = separator;
        m_associate = true;
        if (!string.IsNullOrEmpty(this.CurrentLine)) Associate();
    }

    private void Associate()
    {
        string[] values = this.CurrentLine.Split(new[] { m_separator }, StringSplitOptions.None);
        int associate = 0;
        m_controls.ToList().ForEach(c => {
            if (c != null) c.Text = values[associate];
            associate += 1;
        });
    }

    public override string ToString() =>
        $"File Path: {m_filePath} Encoding: {m_encoding.BodyName} CodePage: {m_encoding.CodePage}";

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (disposing) { reader?.Dispose(); }
    }
}