A simple way to read TXT config files in C++ - c++

this question seems to be already asked but I did not find any convenient solution for my case.
I have the following TXT config file to read in C++:
--CONFIGURATION 1 BEGIN--
IP address: 192.168.1.145
Total track length [m]: 1000
Output rate [1/s]: 10
Time [s]: 1
Running mode (0=OFF 1=ON): 1
Total number of attempts: 10
Mode (0=OFF, 1=BEG, 2=ADV, 3=PROF): 1
--Available only for Administrators--
Variable 1 [mV]: 2600
Gain 1 [mV]: 200
Position tracking (0=OFF 1=ON): 0
Coefficient 2 [V]: 5.2
--CONFIGURATION 1 END--
--CONFIGURATION 2 BEGIN--
Max track distance [m]: 10000
Internal track length [m]: 100
Offset distance [mV]: 1180
GAIN bias [mV]: 200
Number of track samples: 1000
Resolution (1 or 2) [profile]: 1
--CONFIGURATION 2 END--
I need to store only the value at the end of each line that could be a string (in the case of the IP address), an int, a float or a bool inside a struct. In C there is a very simple solution, I read each single line using an expression as follows:
if(!fscanf(fp, "%*s %*s %*s %*s %d\n", &(settings->trackLength))) {
printf("Invalid formatting of configuration file. Check trackLength.\n");
return -1;
}
The %*s allows to discard the label of the line and the spaces before the interested value. I use fgets to skip the empty lines or the titles. This way works also in C++. Is it good to leave my code as is or do you see a better and simple way to do this in C++?
Thank you very much.

Also in C++ it is easy to split a line. I have already provided several answers here on SO on how to split a string. Anyway, I will explain it here in detail and for your special case. I also provide a full working example later.
We use the basic functionality of std::getline which can read a complete line or the line up to a given character. Please see here.
Let us take an example. If the text is stored in a std::string we will first put it into a std::istringstream. Then we can use std::getline to extract the data from the std::istringstream. That is always the standard approach. First, read the complete line from a file using std::getline, then, put it in a std::istringstream again, to be able extract the parts of the string again with std::getline.
If a source line looks like that:
Time [s]: 1
We can obsserve that we have several parts:
An identifier "Time [s]",
a colon, which acts as a separator,
one or more spaces and
the value "1"
So, we could write something like this:
std::string line{}; // Here we will store a complete line read from the source file
std::getline(configFileStream, line); // Read a complete line from the source file
std::istringstream iss{ line }; // Put line into a istringstream for further extraction
std::string id{}; // Here we will store the target value "id"
std::string value{}; // Here we will store the target "value"
std::getline(iss, id, ':'); // Read the ID, get read of the colon
iss >> std::ws; // Skip all white spaces
std::getline(iss, value); // Finally read the value
So, that is a lot of text. You may have heard that you can chain IO-Operations, like in std::cout << a << b << c. This works, because the << operation always returns a reference to the given stream. And the same is true for std::getline. And because it does this, we can use nested statements. Meaning, we can put the second std::getline at this parameter position (actually the first paramater) where it expects a std::istream. If we follow this approach consequently then we can write the nested statement:
std::getline(std::getline(iss, id, ':') >> std::ws, value);
Ooops, whats going on here? Let's analyze from inside out. First the operation std::getline(iss, id, ':') extracts a string from the std::istringstream and assign it to variable "id". OK, understood. Remember: std::getline, will return a reference to the given stream. So, then the above reduced statement is
std::getline(iss >> std::ws, value)
Next, iss >> std::ws will be evaluated and will result in eating up all not necessary white spaces. And guess what, it will return a refernce to the gievn stream "iss".
Statement looks now like:
std::getline(iss, value)
And this will read the value. Simple.
But, we are not finished yet. Of course std::getline will return again "iss". And in the below code, you will see something like
if (std::getline(std::getline(iss, id, ':') >> std::ws, value))
which will end up as if (iss). So, we use iss as a boolean expression? Why does this work and what does it do? It works, because the bool operator of the std::stream is overwritten and returns, if the state is OK or has a failure. Please see here for an explanation. Always check the result of any IO-operation.
And last but not least, we need to explain the if statement with initializer. You can read about it here.
I can write
if (std::string id{}, value{}; std::getline(std::getline(iss, id, ':') >> std::ws, value)) {
which is the similar to
std::string id{}, value{};
if (std::getline(std::getline(iss, id, ':') >> std::ws, value)) {
But the first example has the advantage that the defined variables will be only visible within the if-statements scope. So, we "scope" the variable as narrow as possible.
You should try to do that as often as possible. You should also always check the return state of an IO-operation by applying if to a stream-operation, as shown above.
The complete program for reading everything will then just be a few lines of code.
#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
#include <unordered_map>
#include <iomanip>
int main() {
// Open config file and check, if it coul be opened
if (std::ifstream configFileStream{ "r:\\config.txt" }; configFileStream) {
// Here we wills tore the resulting config data
std::unordered_map<std::string, std::string> configData;
// Read all lines of the source file
for (std::string line{}; std::getline(configFileStream, line); )
{
// If the line contains a colon, we treat it as valid data
if (if (line.find(':') != std::string::npos)) {
// Split data in line into an id and a value part and save it
std::istringstream iss{ line };
if (std::string id{}, value{}; std::getline(std::getline(iss, id, ':') >> std::ws, value)) {
// Add config data to our map
configData[id] = value;
}
}
}
// Some debug output
for (const auto& [id, value] : configData)
std::cout << "ID: " << std::left << std::setw(35) << id << " Value: " << value << '\n';
}
else std::cerr << "\n*** Error: Could not open config file for reading\n";
return 0;
}
For this example I store the ids and values in a map, so that they can be accessed easily.

Related

istringstream put string back on input and read again

So i read out lines out of a file and then read out the lines via stringstream.
I found the Issue that because of the format of the line rarely 2 seperate parts are written together and get read together as one string. I tryed to fix that situation by putting the wrong read value back on the stream and read again but it looks like istringstream doesnt care i put the chars back. they simply dont get read out again.
Here the Problem broken down. S1 is a good string. S2 adresses the Issue with the wrong read in the comments:
In short. Is it possible to put a string back on istringstream and read it with the next operation??
#include <sstream>
#include <string>
#include <vector>
int main()
{
std::string device_id; //126, I_VS_MainVoltageAvailabl
std::string ea_type; //E
std::string address; //0.1
std::string data_type; //BOOL
std::vector<std::string> comment; //VS - Steuerspannung vorhanden / Main voltage available"
std::string s1 = "126,I_Btn_function_stop E 1.2 BOOL Taster Stopp Funktion / Button Stop Function";
std::string s2 = "126,I_VS_MainVoltageAvailablE 0.1 BOOL VS - Steuerspannung vorhanden / Main voltage available";
std::istringstream ist{ s2 };
ist >> device_id; // Read 126, I_VS_MainVoltageAvailablE the E should be read in ea_type
ist >> ea_type; // 0.1
//my idea
if (!ea_type.empty() && isdigit(static_cast<unsigned char>(ea_type[0]))) { //first is a digit so already next was read
for (const auto& x : ea_type) //Try to put 0.1 in the stream
ist.putback(x);
ea_type = device_id[device_id.size() - 1]; // = "E"
device_id.pop_back(); // = "126, I_VS_MainVoltageAvailabl"
}
ist >> address; // Expected "0.1" instead "BOOL" why 0.1 was putback on the stream???
ist >> data_type;
for (std::string in; ist >> in;)
comment.push_back(in);
}
As usual, people are ignoring return codes. putback has a return code for a reason, and when it is false, it means putback failed.
In particular, std::istringstream is input string stream, and as such, is input-only stream. Because of that, you can't use putback on it, it will always fail.
However, you can use std::stringstream instead, and with that putback will behave the way you want it to.
I would suggest that your logic is malformed. What you actually have is a fixed field format which is not amenable to the istream extraction operators.
You would do better to read a whole line of input, then extract the "fields" by their column offsets.
Or, read one byte at a time, appending to the string variable you want to extract, until you have read enough bytes to fill it. That is, read 29 bytes into device_id, then however many (1? 8?) bytes into ea_type, etc.
I want to question your comments, though. The istream string extractor operator>>(std::istream&, std::string&) will pull one space delimited token off the input stream. In other words, your first extraction pulls off "126,". So the rest of the logic is completely wrong.

Reading In Formatted Data with Multiple Delimiters C++

I'm trying to read in data from command prompt with multiple delimiters, for example:
data 1, data2, data3.
I'd like the code to read in the data before the first comma, the data after that and before the second comma, and finally the data after that but before the period. I have been using this:
getline(cin, VAR, ',');
cin.get();
getline(cin, VAR2, ' ');
getline(cin, VAR3, ',');
cin.get();
getline(cin, VAR4, '.');
And it does what I want, provided the information is entered correctly. However, how can I check and see if the information wasn't? Because if the user doesn't enter two commas and a period, the program gets stuck as I'm assuming the cin stream gets broken after not finding the delimiter as specified in the getline() read. I've tried this:
if (cin.fail()) {
cout << "failure" << endl;
}
After a getline, but it never runs if the delimiter isn't input. What's the best way to do this, check for errors and see if data wasn't entered in the desired format?
You have to parse the input yourself.
For starters, your input does not consist of "data with multiple delimiters". Your input consists of a single line of text, that was entered, according to your description, at a command prompt of some kind.
If so, then the line of text should be read with a single std::getline() library function, since, by default, std::getline() uses \n as a default delimiter.
std::string str;
std::getline(std::cin, str);
Now, you've read your input into str. Now that this task is complete you can get down to business verifying whether str contains properly formatted input with the appropriate delimiters, or not.
To do that, you will use the rich set of methods that are available from your std::string object. For example one such method, find(), searches the string for a particular character, such as your first comma:
auto n=str.find(',');
The full documentation for find(), whose description you will find in your C++ book, will explain how to determine whether find() actually found the comma, or not. Based on that your program can either take the appropriate action (if the comma was not found) or use the substr() method, on this str, to extract the part of the string before the comma. That would be your first VAR. Then, use substr() again to extract the rest of the entered text, after the first comma.
You will also find a complete description of substr() in your C++ book.
After using substr() to extract the rest of the entered text, after the first comma, you will simply repeat the same overall process for extracting data2, and the following item of input, this time using a period.
And, at each step of the way you will have all the information your program will need to determine whether or not your input was formatted correctly.
You could read in the entire line into a std::string, then use std::istringstream to parse the string:
#include <iostream>
#include <string>
#include <sstream>
#include <cstdlib>
int main(void)
{
const std::string input_text = "data1, data2, data3\n";
std::istringstream input_stream(input_text);
std::string data1;
std::string data2;
std::string data3;
std::getline(input_stream, data1, ',');
std::getline(input_stream, data2, ',');
std::getline(input_stream, data3, ',');
std::cout << "data1: " << data1 << '\n';
std::cout << "data2: " << data2 << '\n';
std::cout << "data3: " << data3 << '\n';
return EXIT_SUCCESS;
}
If the std::getline fails, that means there is an error in the data.

C++ Read in file with only numbers (doubles)

I'm trying to read in a file that should contain only numbers in it. I can successfully read in the entire file if it meets that criteria, but if it so happened to have a letter in it, I need to return false with an error statement.
The problem is I'm finding it hard for my program to error when it finds this character. It can find it no problem, but when it does, it decides to just skip over it.
My code to read in the file and attempt to read in only numbers:
bool compute::Read (ifstream& stream)
{
double value;
string line;
int lineNumber = 1;
if (stream)
{
while (getline(stream, line))
{
lineNumber++;
istringstream strStream(line);
while (strStream >> value)
{
cout << value << endl;
}
}
}
return true;
}
The input file which I use for this is
70.5 61.2 A8 10.2
2
Notice that there is a non-number character in my input file. It should fail and return false at that point.
Currently, all it does is once it hits the "A", it simply returns to the next line, continuing the getline while loop.
Any help with this would be much appreciated.
The stringstream does catch those errors, but you're doing nothing to stop the enclosing loop from continuing when an error is found. You need to tailor your main loop so that it stops when the stringstream finds an error, which you can't do if the stringstream is being reconstructed on each iteration. You should create a for() loop instead and construct the stringstream in the declaration part. And the condition to the loop should be "as long as the stringstream and stream do not catch an error". For example:
for (std::istringstream iss; iss && std::getline(stream, line);)
{
iss.clear();
iss.str(line);
while (iss >> value)
{
std::cout << value << '\n';
}
}
Futhermore, it doesn't look like you need to use std::getline() or std::istringstream if you just want to print each value. Just do:
while (stream >> value) {
std::cout << value << '\n';
}
The above will stop when it finds an invalid character for a double.
You need the code to stop streaming but return false if it hasn't yet reached the end of the "input".
One way, possibly not the most efficient but still one way, to do that is parse a word at a time.
If you read first into a std::string and if it works (so the string is not empty) create an istringstream from that string, or reuse an existing one, and try streaming that into a double value.
If that fails, you have an invalid character.
Of course you can read a line at a time from the file, then split that into words, so that you can output a meaningful error message showing what line the bad text was found.
The issue of reading straight into doubles is that the stream will fail when it reaches end of file.
However it is possible to workaround that too because the reason for failing has an error status which you can check, i.e. you can check if it eofbit is set. Although the f in eofbit stands for "file" it applies to any stream not just files.
Although this method may sound better than reading words into a string first, I prefer that method in normal circumstances because you want to be able to report the error so you'll want to print in the error what was read.

How to insert values of set container in a map container in C++?

I have a file in the following format.
47304 - 305,463,190,444, 4, 97, 41,381,414,459,159, 75, 5,207,....
50854 - 498,214,300,274,392,390,262, 28,231,349,251, 30,254, 51,326, ..
.
.
I wish to use a map container with set as values.
Hence I want to create a map container with key as value present before '-' (ie 47304) and its value must be a set containing the values present after the '-' (ie. 305, 463, 444 etc)
Likewise I want to repeat the same with the other lines of that file.
Any help is greatly appreciated.
Thanks
There are really many many potential solutions for that.
Often the std::getline function with delimiter is used, to read up to the dash or to the comma. Also regex, in conjunction with the std::sregex_token_iterator will produce very powerfull solutions (although a little bit slow).
But in this case, we can also use a more simple solution.
First, we read a complete line. This gives us a little bit security in case of some incorrectly formatted data. This line will be put into a std::istringstream, so that we can simply extract data from there with normal >> operations.
To eat up the dash '-' and the commas ',', we will use a dummy-throw-away variable and alwys read a pair of character and number.
iss >> iss >> dummyChar >> value will always read a dash/comma and a value,
This makes then life very simple.
We can then do for example:
#include <iostream>
#include <sstream>
#include <string>
#include <map>
#include <set>
std::istringstream fileSimulationStream{R"(47304 - 305,463,190,444, 4, 97, 41,381,414,459,159, 75, 5,207
50854 - 498,214,300,274,392,390,262, 28,231,349,251, 30,254, 51,326)"};
int main() {
// Resulting data
std::map<int,std::set<int>> data;
// Read all lines from file
for (std::string line{}; std::getline(fileSimulationStream, line); ) {
// Put line in an istringstream for further extraction
std::istringstream iss{line};
// Get the key for the map
int key{}; iss>> key;
// Read the rest of the values in a temporary set
std::set<int> tempSet{}; char dummyChar{}; int value{};
while (iss >> dummyChar >> value) tempSet.insert(value);
// Now add everything to the map
data[key] = std::move(tempSet);
}
// Create some debug output
for (const auto&[key, set] : data) {
std::cout << key << "\t--> ";
for (const int i : set) std::cout << i << ' ';
std::cout << '\n';
}
}

How do I break out of a getline with a file?

I have code where I am inputting stuff from a file. My txt file looks like this:
file.txt
hello world
...
1 2
The numbers at the bottom are supposed to be read into variables. As for "hello world", it should be picked up by getline. But I don't know how many lines there will be in the txt file so I don't know how to break out of it. Here is my code:
while (getline(file, line))
{
std::cout << line << std::endl;
// ...
}
file >> a >> b; // 1 2
If I was doing this with cin I could just do Ctrl+Z to stop getline loop from running. How do I break out of the while loop at the right time before I get to 1 2?
For each line string line, you can put it into an istringstream iss. And then try to stream it into a and b using iss >> a >> b, if it can be done successfully, it means you enter the right line. Otherwise, you go on checking the next line.
int a, b;
while (getline(file, line))
{
istringstream iss(line);
if (iss >> a >> b)
{
// you are in the right line, and a,b has the values e.g. 1 2
}
}
It should also work for other strings besides "hello world", like "aaa bbb cc" etc. as long as they are not the numbers you are looking for.
P.S.: you can also take use of regex if you use C++11 to check if given line has/matches the pattern you are looking for.
Use a condition, and a break; statement.
E.g.:
while (getline(file, line))
{
std::cout << line << std::endl;
// ...
if(line == "hello world"/){
break;//Exits the loop
}
}
A break statement makes your code exit the most inner loop it's used in. In this case, it exits the while loop.
EDIT:
If you don't want to break on a specific line, then you'll better use regular expression or another mechanism (like std::stringstream) to find a match of the string you're looking for, and capture the part you're interested in. I suggest you take a look at Boost.Regex for this.
The idea is to loop on the lines, i.e. just as you do. As soon as you have a match, you can break (the same way) and capture from the string you're currently reading (which in your code would be in the line variable).
It's not too clear how you determine that you want to break out
of the loop. What is the criterion? If you want to read all
lines but the last, the simplest solution is to simply read all
of the lines into an std::vector<std::string>, and then
process that; you can iterate over a vector until the next to
the last element (which you can't do on a stream). If it's some
pattern your looking to match (say "\\d+\\s+\\d+"), then you
can add this to the condition:
std::string line;
std::regex matchNumbers( "\\d+\\s+\\d+" );
while ( std::getline( file, line ) && ! regex_match( line, matchNumbers ) ) {
// ...
}
std::istringstream numbers( line );
numbers >> a >> b;
And so on.