0

I've been trying to figure this out for the past few days, but I just can't seem to get it work.

So I have a txt file which has this format:

id;könyvcím;szerző;kiadó;kiadási év;

I am using a structs and a list such as this:

public static List<Books> BooksList = new List<Books>();

public struct Books
{
    public int id;
    public string title;
    public string writer;
    public string publisher;
    public int published_year;
}  

And I'm also putting all these into a List based on the struct like this:

StreamReader booksRead = new StreamReader("konyvek.txt", Encoding.UTF8);
booksRead.ReadLine();
while (!booksRead.EndOfStream)
{
    string[] split = booksRead.ReadLine().Split(';');
    Books inRead = new Books();
    inRead.id = Convert.ToInt32(split[0]);
    inRead.title = split[1];
    inRead.writer = split[2];
    inRead.publisher = split[3];
    inRead.published_year = Convert.ToInt32(split[4]);
    BooksList.Add(inRead);
}
booksRead.Close();

All I want is, for example, to find where the line with ID 2 is, and remove that line from my textfile. I've tried to get the index of the line I want, and remove it like that from my textfile, but it even fails to get the index, I tried using IndexOf, FindIndex and trying to go on a loop. I'm pretty sure my struct is not happy with me for using it like that because I get errors such as this when I run my code:

System.InvalidCastException: 'Unable to cast object of type 'Books' to type 'System.IConvertible'.'

Here is the way I'm trying to get the index of the line I want to remove

Books item = new Books();   
for (int i = 0; i < BooksList.Count; i++)
{
    if (Convert.ToInt32(textBox_id_delete.Text) == item.id)
    {
        RemoveAt = item.id;
    }
}
int index = BooksList.FindIndex(x => Convert.ToInt32(x) == RemoveAt);
MessageBox.Show(Convert.ToString(index));

I'm pretty sure I'm approaching this extremely wrong, and I'd accept any kind of help.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Norinot
  • 11
  • 2

5 Answers5

1

You are doing it completely wrong for a number of reasons.

First, how would you do that the way you are doing:

void Main()
{
    var filename = @"c:\myFolder\mybooklist.txt";
    // read into an enumerable
    var books = File.ReadAllLines(filename)
        .Select(x => x.Split(';'))
        .Select(x => new Book {
            Id = int.TryParse(x[0], out int bookId)?bookId:0,
            Title = x[1],
            Writer = x[2],
            Publisher = x[3],
            Published_year=int.TryParse(x[4], out int year)?year:0
        });
        
        
    // remove the one with id 2
    // and save back
    var otherBooks = books.Where(b => b.Id != 2);

    File.WriteAllLines(filename, otherBooks.Select(b => $"{b.Id};{b.Title};{b.Writer};{b.Publisher};{b.Published_year}"));
}

public struct Book
{
    public int Id;
    public string Title;
    public string Writer;
    public string Publisher;
    public int Published_year;
}

And now what is wrong with this.

  1. A text file is not a database but you are trying to use a text file as a database.
  2. With a text file, you are not actually doing any control here, if the ID is unique or not (there might be N books with the ID 2).
  3. (Side matter) You are using C#, but looks like you are coming from another language and not using the naming conventions at all.

IMHO, instead you should simply use a database, an embedded one for example like LiteDb or Sqlite. If you care to see a sample with LiteDb or Sqlite, let me know.

EDIT: I am adding SQLite and LiteDb samples. In either case, you would need to add Sqlite.Data.Sqlite and LiteDB respectively from Nuget and add using statements.

In case of SQLite, please note that you could use Linq adding some drivers. I directly used the ADO.Net commands and didn't use a Book class for mapping.

LiteDB, being a NoSQL database written in C# for C#, can directly use objects and support Linq out of the box.

Samples show only the surface for both.

Cetin Basoz
  • 22,495
  • 3
  • 31
  • 39
  • I've used Sqlite before but honestly I know not much about it so an example or sample in my situation would help me a lot and also thank you for your help. – Norinot May 30 '21 at 13:59
  • @Norinot, OK I will edit this for some explanations, but will add Sqlite and LiteDb samples as different replies so you could compare them more easily. – Cetin Basoz May 30 '21 at 15:33
1

SQLite sample:

private static readonly string dataFile = @"d:\temp\books.s3db";
void Main()
{
    CreateDb(dataFile);
    SeedSampleData(dataFile);
    // List the current data
    Console.WriteLine("Current Data");
    Console.WriteLine("".PadRight(100, '='));
    ListData(dataFile);
    Console.WriteLine("".PadRight(100, '='));
    DeleteSampleRow(dataFile);
    // List the current data
    Console.WriteLine("After deleting");
    Console.WriteLine("".PadRight(100, '='));
    ListData(dataFile);
    Console.WriteLine("".PadRight(100, '='));
}
void DeleteSampleRow(string dbName)
{
    string deleteById = "delete from books where id = @id";
    string deleteByTitle = "delete from books where Title = @title";
    string deleteByWriter = "delete from books where Writer = @writer";


    using (SQLiteConnection cn = new SQLiteConnection($"Data Source={dbName}"))
    using (SQLiteCommand cmdById = new SQLiteCommand(deleteById, cn))
    using (SQLiteCommand cmdByTitle = new SQLiteCommand(deleteByTitle, cn))
    using (SQLiteCommand cmdByWriter = new SQLiteCommand(deleteByWriter, cn))
    {
        cmdById.Parameters.Add("@id", DbType.Int32).Value = 2; // delete the book with id = 2
        cmdByTitle.Parameters.Add("@title", DbType.String).Value = $"Sample Title #5"; // delete all books having title "Sample Title #5"
        cmdByWriter.Parameters.Add("@writer", DbType.String).Value = $"Sample Writer #3"; // delete all books written by "Sample Writer #3"

        cn.Open();
        cmdById.ExecuteNonQuery();
        cmdByTitle.ExecuteNonQuery();
        cmdByWriter.ExecuteNonQuery();
        cn.Close();
    }
}

void ListData(string dbName)
{
    string selectCommand = "select * from books";
    using (SQLiteConnection cn = new SQLiteConnection($"Data Source={dbName}"))
    using (SQLiteCommand cmd = new SQLiteCommand(selectCommand, cn))
    {
        cn.Open();
        var r = cmd.ExecuteReader();
        while (r.Read())
        {
            Console.WriteLine($"{r["id"]},{r["title"]},{r["writer"]},{r["publisher"]},{r["published_year"]}");
        }
        cn.Close();
    }
}

private void CreateDb(string dbName)
{
    if (File.Exists(dbName)) // if it exists, delete and create afresh, just for sampling
    {
        File.Delete(dbName);
    }
    string createTable = @"Create Table books (
            id int primary key not null, 
            title varchar(500) not null,
            writer varchar(100) not null,
            publisher varchar(100) not null,
            published_year int not null
            )";
    using (SQLiteConnection cn = new SQLiteConnection($"Data Source={dbName}"))
    using (SQLiteCommand cmd = new SQLiteCommand(createTable, cn))
    {
        cn.Open();
        cmd.ExecuteNonQuery();
        cn.Close();
    }
}
private void SeedSampleData(string dbName)
{
    string insertCommand = @"insert into books 
        (id, title, writer, publisher, published_year)
        values
        (@id, @title, @writer, @publisher, @year);";
    using (SQLiteConnection cn = new SQLiteConnection($"Data Source={dbName}"))
    using (SQLiteCommand cmd = new SQLiteCommand(insertCommand, cn))
    {
        cmd.Parameters.Add("@id", DbType.Int32);
        cmd.Parameters.Add("@title", DbType.String);
        cmd.Parameters.Add("@writer", DbType.String);
        cmd.Parameters.Add("@publisher", DbType.String);
        cmd.Parameters.Add("@year", DbType.Int32);
        Random r = new Random();
        cn.Open();

        int id = 1;
        using (SQLiteTransaction transaction = cn.BeginTransaction())
        {
            cmd.Parameters["@id"].Value = id++;
            cmd.Parameters["@title"].Value = $"Around the World in Eighty Days";
            cmd.Parameters["@writer"].Value = $"Jules Verne";
            cmd.Parameters["@publisher"].Value = $"Le Temps, Pierre-Jules Hetzel";
            cmd.Parameters["@year"].Value = 1873;
            cmd.ExecuteNonQuery();
            cmd.Parameters["@id"].Value = id++;
            cmd.Parameters["@title"].Value = $"A Tale of Two Cities";
            cmd.Parameters["@writer"].Value = $"Charles Dickens";
            cmd.Parameters["@publisher"].Value = $"Chapman & Hall";
            cmd.Parameters["@year"].Value = 1859;
            cmd.ExecuteNonQuery();


            // add dummy 10 more rows
            for (int i = 0; i < 10; i++)
            {
                cmd.Parameters["@id"].Value = id++;
                cmd.Parameters["@title"].Value = $"Sample Title #{i}";
                cmd.Parameters["@writer"].Value = $"Sample Writer #{r.Next(1, 5)}";
                cmd.Parameters["@publisher"].Value = $"Sample Publisher #{i}";
                cmd.Parameters["@year"].Value = r.Next(1980, 2022);
                cmd.ExecuteNonQuery();
            }
            transaction.Commit();
        }
        // databases generally use some indexes
        new SQLiteCommand(@"Create Index if not exists ixId on books (id);", cn).ExecuteNonQuery();
        new SQLiteCommand(@"Create Index if not exists ixTitle on books (title);", cn).ExecuteNonQuery();
        new SQLiteCommand(@"Create Index if not exists ixWriter on books (writer);", cn).ExecuteNonQuery();
        new SQLiteCommand(@"Create Index if not exists ixPublisher on books (publisher);", cn).ExecuteNonQuery();

        cn.Close();
    }
}
Cetin Basoz
  • 22,495
  • 3
  • 31
  • 39
0

When you put books into a list from a file, you can search the book for remove from BooksList.
Delete it and save BooksList into a file.

var removeBook = BookList.FirstOrDefault(book => book.id == removeId);
if (removeBook != null)
{
   BookList.Remove(removeBook);
}

var booksAsString = BookList.Select(book => $"{book.id};{book.title};{book.writer};{book.publisher};{book.published_year}");

File.WriteAllLines("konyvek.txt", booksAsString, Encoding.UTF8);

Genusatplay
  • 761
  • 1
  • 4
  • 15
0
LiteDb sample:

private static readonly string dataFile = @"d:\temp\books.litedb";

void Main()
{
    //CreateDb(dataFile); // this step is not needed with LiteDB
    // instead we just simply delete the datafile if it exists 
    // for starting afresh
    // if it exists, delete and create afresh, just for sampling
    // so you can run this same sample over and over if you wish
    if (File.Exists(dataFile)) 
    {
        File.Delete(dataFile);
    }

    SeedSampleData(dataFile);
    // List the current data
    Console.WriteLine("Current Data");
    Console.WriteLine("".PadRight(100, '='));
    ListData(dataFile);
    Console.WriteLine("".PadRight(100, '='));
    DeleteSampleRow(dataFile);
    // List the current data
    Console.WriteLine("After deleting");
    Console.WriteLine("".PadRight(100, '='));
    ListData(dataFile);
    Console.WriteLine("".PadRight(100, '='));
}
void DeleteSampleRow(string dbName)
{
    using (var db = new LiteDatabase(dbName))
    {
        var bookCollection = db.GetCollection<Book>("Books");
        // by ID
        bookCollection.Delete(2);
        // by Title
        bookCollection.DeleteMany(c => c.Title == "Sample Title #5");
        // by Writer
        bookCollection.DeleteMany(c => c.Writer == "Sample Writer #3");
    }
}

void ListData(string dbName)
{
    using (var db = new LiteDatabase(dbName))
    {
        var bookCollection = db.GetCollection<Book>("Books");
        foreach (var book in bookCollection.FindAll())
        {
            Console.WriteLine($"{book.Id},{book.Title},{book.Writer},{book.Publisher},{book.Published_year}");
        }
    }
}

private void SeedSampleData(string dbName)
{
    Random r = new Random();
    var books = new List<Book> {
            new Book {Title="Around the World in Eighty Days",Writer = "Jules Verne",Publisher = "Le Temps, Pierre-Jules Hetzel",Published_year= 1873},
            new Book {Title="A Tale of Two Cities",Writer = "Charles Dickens",Publisher = "Chapman & Hall",Published_year= 1859},
        };
    // add dummy 10 more rows
    books.AddRange(Enumerable.Range(0, 10).Select(i => new Book
    {
        Title = $"Sample Title #{i}",
        Writer = $"Sample Writer #{r.Next(1, 5)}",
        Publisher = $"Sample Publisher #{i}",
        Published_year = r.Next(1980, 2022)
    }));
    using (var db = new LiteDatabase(dbName))
    {
        var bookCollection = db.GetCollection<Book>("Books");
        bookCollection.InsertBulk(books);

        // databases generally use some indexes
        // create the same indexes that we created in SQLite sample
        bookCollection.EnsureIndex(c => c.Id);
        bookCollection.EnsureIndex(c => c.Title);
        bookCollection.EnsureIndex(c => c.Writer);
        bookCollection.EnsureIndex(c => c.Publisher);
    }
}

public class Book
{
    public int Id {get;set;}
    public string Title {get;set;}
    public string Writer {get;set;}
    public string Publisher {get;set;}
    public int Published_year {get;set;}
}
Cetin Basoz
  • 22,495
  • 3
  • 31
  • 39
0

welcome to SO. I'm going to assume you've got a reason for keeping the data in a text file. As several answers have suggested if you need it in a text file the easiest thing to do is to simply create a new file with the lines you want.

One way to do that is to make use of a interator function to filter the lines. This lets you easily use the .NET File class to do the rest - creating the new file and removing the old if you want to. Often keeping the old file and archiving it can be useful too but anyway, here's a way to filter the lines.

    static void Main(string[] _)
    {
        var filteredLines = FilterOnID(File.ReadAllLines("datafile.txt"), "2");

        File.WriteAllLines("updated.datafile.txt", filteredLines);

        // rename if necessary
        File.Delete("datafile.txt");
        File.Move("updated.datafile.txt", "datafile.txt");
    }

    static IEnumerable<string> FilterOnID(IEnumerable<string> lines, string id)
    {
        foreach (var line in lines)
        {
            var fields = line.Split(';');

            if (fields.Length != 0 || !string.IsNullOrEmpty(fields[0]))
            {
                if (id == fields[0])
                    continue;
            }

            yield return line;
        }
    }

To test I added simple file like so:

1;field1;field2;field3
2;field1;field2;field3
3;field1;field2;field3
4;field1;field2;field3
5;field1;field2;field3
6;field1;field2;field3

And after running you get this:

1;field1;field2;field3
3;field1;field2;field3
4;field1;field2;field3
5;field1;field2;field3
6;field1;field2;field3
MikeJ
  • 1,299
  • 7
  • 10
  • File.ReadAllLines already provides an IEnumerable which you can further add filtering attaching Where if you wanted to. See my reply for an example. – Cetin Basoz May 30 '21 at 16:26
  • @CetinBasoz guess I'm not following your point. This is using the enumerable from ReadAllLines. Are you suggesting using linq Where instead of a custom iterator method? If so then I think it would be essentially the same thing although perhaps less clear using Where. – MikeJ Jun 02 '21 at 16:01
  • @CetinBasoz yes, less clear. FilterOnID explicitly states the intent. Stuffing the same code inside a where clause would be less clear. – MikeJ Jun 03 '21 at 01:45
  • No, FilterOnID explicitly hides the intent and you need to read the content to understand that in fact it is Filtering Out On Id. I think you didn't read my reply. There Where is crystal clear compared to FilterOnId. – Cetin Basoz Jun 03 '21 at 11:21
  • "Where is crystal clear compared to FilterOnId"???? OK:) – MikeJ Jun 03 '21 at 22:16
  • you may want to review Linq Where(). books.Where(b => b.Id != 2); means all books, where Id is not 2. YMMV. – Cetin Basoz Jun 04 '21 at 01:11
  • @CetinBasoz yes if he wanted to first deserialize into book instances, then filter, and the serialize them again, Where would be a great option. But if, as he stated in his question, he just wants to remove the line that has ID 2 then just filtering the file is a simple solution. And yes, you could still use Where - write a function to extract the ID and use it inside Where. The argument about which is clearer is pointless. – MikeJ Jun 04 '21 at 20:28