There are many, many ways to handle reading a name that can have an unknown number of whitespace separated parts and a trailing number. You can trivially do it with cstdio
and reading each line with getline()
and then calling sscanf()
on the str.c_str()
with a format string of " %[^0-9] %zu"
and then trim the trailing whitespace from temporary_name
before assigning to a string.
Staying with the current era C++, you can read the line with getline
and then use the .find_first_of()
member function to locate the first digit in the string. For example, you can keep a list of digits, e.g. const char *digits = "0123456789";
and then locate the first digit with line.find_first_of(digits);
. Knowing where the first digit is, you can then use the .substr()
member function to copy the name
and then strip the trailing whitespace from the end.
The larger consideration is how to store all of the values read. If you create a simple struct
that has members std:string name;
and size_t pop;
you can then create a std::vector
of struct and just add each struct worth of data read from the file using the .push_back()
member function to add a new struct to the vector of struct.
A simple implementation of the struct could be:
struct population
{
std::string name;
size_t pop;
/* constructors */
population() { name = ""; pop = 0; }
population(const std::string& n, const size_t p) : name(n), pop(p) {}
};
To simplify the read from the file, you can create an overload of >>
that will read a line of data from the open file stream and do the separation into name
and pop
for you. A second overload of <<
will allow you to output the struct in a sane format of your choosing. Adding the overloads you would have:
/* struct to hold name population,
* and overloads of operators >> and << to facilitate splitting name/hours.
*/
struct population
{
std::string name;
size_t pop;
/* constructors */
population() { name = ""; pop = 0; }
population(const std::string& n, const size_t p) : name(n), pop(p) {}
/* overloads of >> (separates name/pop) and << (outputs name/pop) */
friend std::istream& operator >> (std::istream& is, population& p) {
const char *digits = "0123456789";
std::string line {};
if (getline (is, line)) { /* read line */
size_t popbegin = line.find_first_of(digits); /* find 1st [0-9] */
if (popbegin != std::string::npos) { /* valdiate found */
std::string tmp = line.substr(0, popbegin); /* get name */
while (isspace(tmp.back())) /* remove trailing */
tmp.pop_back(); /* .. spaces */
p.name = tmp; /* assign to name */
p.pop = stoul(line.substr(popbegin)); /* assign to pop */
}
}
return is;
}
friend std::ostream& operator << (std::ostream& os, const population& p) {
os << std::left << std::setw(32) << p.name << " " << p.pop << '\n';
return os;
}
};
Then all you need in main()
is to validate you have a filename passed as an argument, open the file and validate it is open for reading (say std::ifstream f
) and then your read and separation of values is reduced to a single trivial loop:
population p {}; /* instance of population struct to facilitate read from file */
std::vector<population> records {}; /* vector of population */
while (f >> p) { /* read population data from file */
records.push_back(p); /* add to population vector */
}
Now you have all locations and the populations for each stored in the vector of struct records
. Putting it altogether, you could do:
#include <iostream>
#include <iomanip>
#include <fstream>
#include <string>
#include <vector>
/* struct to hold name population,
* and overloads of operators >> and << to facilitate splitting name/hours.
*/
struct population
{
std::string name;
size_t pop;
/* constructors */
population() { name = ""; pop = 0; }
population(const std::string& n, const size_t p) : name(n), pop(p) {}
/* overloads of >> (separates name/pop) and << (outputs name/pop) */
friend std::istream& operator >> (std::istream& is, population& p) {
const char *digits = "0123456789";
std::string line {};
if (getline (is, line)) { /* read line */
size_t popbegin = line.find_first_of(digits); /* find 1st [0-9] */
if (popbegin != std::string::npos) { /* valdiate found */
std::string tmp = line.substr(0, popbegin); /* get name */
while (isspace(tmp.back())) /* remove trailing */
tmp.pop_back(); /* .. spaces */
p.name = tmp; /* assign to name */
p.pop = stoul(line.substr(popbegin)); /* assign to pop */
}
}
return is;
}
friend std::ostream& operator << (std::ostream& os, const population& p) {
os << std::left << std::setw(32) << p.name << " " << p.pop << '\n';
return os;
}
};
int main (int argc, char **argv) {
if (argc < 2) { /* validate 1 argument given for filename */
std::cerr << "error: filename required as 1st argument.\n";
return 1;
}
std::ifstream f (argv[1]); /* open filename provided as 1st argument */
if (!f.is_open()) { /* validate file is open for reading */
std::cerr << "file open failed: " << argv[1] << '\n';
return 1;
}
population p {}; /* instance of population struct to facilitate read from file */
std::vector<population> records {}; /* vector of population */
while (f >> p) { /* read population data from file */
records.push_back(p); /* add to population vector */
}
for (const auto& loc : records) /* output results */
std::cout << std::left << std::setw(32) << loc.name << loc.pop << '\n';
}
Example Use/Output
With your data in the file dat/population.txt
, the use and results would be:
$ ./bin/poprecords dat/population.txt
Jackson 49292
Levy 40156
Indian River 138894
Liberty 8314
Holmes 19873
Madison 19115
And since you have the data stored in a vector of struct, you can sort the vector any way you like to analyze your data.
This is just one of many ways to approach the problem. Look things over and let me know if you have further questions.