I would like to propose an additional solution, a “more” modern C++ and object oriented solution. And, a full working solution.
In C++ we usually put data, and functions operating on that data, into an object, a class. The class and only the class should know how to handle its data.
So in your example the “id,” "name" and “price” are attributes for an object, and extracting them from a std::istream
or inserting them into a std::ostream
should be only known by that object and handled by it.
As a consequence, we create a class/struct, define 3 data members, namely the "id", “name” and "price" as a unsigned long",
std::stringand the “price” as a
double````. Then, we overwrite the extractor operator (>>) and the inserter operator. The extractor operator is a little bit tricky, since we first read a complete line and then split it into tokens.
In C++ your file structure is called CSV (Comma Separated Value). And, reading this is a standard task. First, read the complete line, then, using a given delimiter, extract the tokens of that string. The “splitting” is also called “tokenizing” and C++ has a dedicated functionality for that purpose: std::sregex_token_iterator
.
This thing is an iterator. For iterating over a string, hence “sregex”. The begin part defines, on what range of input we shall operate, then there is a std::regex for what should be matched / or what should not be matched in the input string. The type of matching strategy is given with last parameter.
1 --> give me the stuff that I defined in the regex and
-1 --> give me that what is NOT matched based on the regex.
We can use this iterator for storing the tokens in a std::vector. The std::vector has a range constructor, which takes 2 iterators as parameter, and copies the data between the first iterator and 2nd iterator to the std::vector. The statement
std::vector tokens(std::sregex_token_iterator(line.begin(), line.end(), re, -1), {});
defines a variable “tokens” as a std::vector
and uses the so called range-constructor of the std::vector
. Please note: I am using C++17 and can define the std::vector
without template argument. The compiler can deduce the argument from the given function parameters. This feature is called CTAD ("class template argument deduction").
Additionally, you can see that I do not use the "end()"-iterator explicitly.
This iterator will be constructed from the empty brace-enclosed initializer list with the correct type, because it will be deduced to be the same as the type of the first argument due to the std::vector
constructor requiring that.
Then, after reading the line and splitting it into tokens, we check, if we have exactly 3 tokens and then store the result as "id", “name” and “price”. So, overall, a very simple operation.
in main
I put some example driver code. For example, you can see that I store a new product into the file with a simple
productFile << product;
statement. And reading the complete CSV file and parsint it, is done with a simple one-liner:
std::vector productList(std::istream_iterator<Product>(productFile), {});
Finding an element is just using std::find_if
.
Please see below a complete example:
(I put many comments and empty lines to make the code more readble.)
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>
#include <fstream>
#include <regex>
const std::string delimiter(",");
const std::regex re(delimiter.c_str());
struct Product {
// Data
unsigned long id{};
std::string name{};
double price{};
// Member Functions
// Simple Inserter.
friend std::ostream& operator << (std::ostream& os, const Product& product) {
// Unfortunately the ostream_joiner does not work on my system . . .
return os << product.id << delimiter << product.name << delimiter << product.price << "\n";
}
// simple Extractor
friend std::istream& operator >> (std::istream& is, Product& product) {
// Read a complete line
if (std::string line{}; std::getline(is,line)) {
// Convert line into tokens. Split any number of csv columns. One-liner
std::vector tokens(std::sregex_token_iterator(line.begin(), line.end(), re, -1), {});
// if we have read 3 tokens as expected
if (3 == tokens.size()) {
// Assign values to data member
product.id = std::strtoul(static_cast<std::string>(tokens[0]).c_str(), nullptr, 10);
product.name = tokens[1];
product.price = std::strtod(static_cast<std::string>(tokens[2]).c_str(), nullptr);
}
}
return is;
}
};
int main() {
const std::string filename{ "r:\\product.csv" };
// First, we ask the user to enter product data
std::cout << "\nEnter product ID, product name and product price in one line separated by comma:\n";
// Get input
if (Product product; std::cin >> product) {
// Save the user data in a file
if (std::ofstream productFile(filename,std::ios::app); productFile) {
// Write to output file stream
productFile << product;
}
else {
std::cerr << "\n\n***Could not write to file\n\n";
}
}
// Now test the search function
// Get the name to look for
std::cout << "\n\nEnter a name to look for:\n ";
if (std::string name{}; std::getline(std::cin, name)) {
// Open the file for reading
if (std::ifstream productFile(filename); productFile) {
// Read the complete file. One-liner
std::vector productList(std::istream_iterator<Product>(productFile), {});
// Search for the name
auto result = std::find_if(productList.begin(), productList.end(), [&name](const Product& p) { return p.name == name; });
// If found, print result
if (result != productList.end())
std::cout << "Found: " << *result <<"\n\n";
else
std::cout << "Name '" << name <<"' not found\n\n";
// For the fun of it: Show all data
std::copy(productList.begin(), productList.end(), std::ostream_iterator<Product>(std::cout));
}
else {
std::cerr << "\n\n***Could not read from file\n\n";
}
}
return 0;
}
I hope that gives you an idea, how such a problem can be solved. Of course there are tons of other solutions. . .