Home > Enterprise >  C using getline() to append 2 lines of an input file into an array
C using getline() to append 2 lines of an input file into an array

Time:04-30

this is my first time coming to stackoverflow I have been really struggling with this simple problem..

Here's an example of my input file...


Korn
2
Nu Metal
Alternative Metal
1993
2021
29
"Blind" 1995

Here is a snippet of my code...


//Read Artists Name From File into the Array
for (int index = 0; index < SIZE; index  )
{

    //Read Artist's Name From File into the Array
    getline(inputFile, artistArray[index].artistsName);

    //Read Number of Genre's per Artist
    inputFile >> artistArray[index].numOfGenres; 

    for (int count = 0; count < artistArray[index].numOfGenres; count  )
    {
        //Read Genre From File into the Array
        inputFile >> artistArray[repeatGenre].genre;
        repeatGenre  ;
    }

    //Read Artist's Starting Year From File into the Array
    inputFile >> artistArray[index].startingYear;

    //Read Artist's Ending Year From File into the Array
    inputFile >> artistArray[index].endingYear;

    //Read Artist's Number of Tracks from File into the Array
    inputFile >> artistArray[index].numOfTracks;

    for (int count = 0; count < artistArray[index].numOfGenres; count  )
    {
        //Read Track Title 
        inputFile >> artistArray[repeatTracks].trackTitle;

        //Read Track Year
        inputFile >> artistArray[repeatTracks].trackYear;

        //Continue to Next Track
        repeatTracks  ; 
    }
}

& Here are my results.....


Korn
2
Nu
Metal
Alternative
Metal
1993
2021 29
"Blind" 1995

HELP for the life of me I cannot figure out how to append "Nu" & "Metal" OR "Alternative" & "Metal" together like the input file has it, it's such a small issue but it is extremely detrimental to my program.

CodePudding user response:

If I'm understanding correctly, this is basically the result you want for that example:

artistArray[index].artistsName = "Korn";
artistArray[index].numOfGenres = 2;
artistArray[repeatGenre].genre = "Nu Metal";
artistArray[repeatGenre   1].genre = "Alternative Metal";
...

The problem would be in your second for loop then. Instead of using the >> operator, you should use the getline() function. The >> operator will not read the whole line. For std::strings, the >> operator reads in a string until it reaches whitespace.

I would rewrite the loop to be:

for (int count = 0; count < artistArray[index].numGenres; count  ) {
  std::string genre;
  std::getline(inputFile, genre);
  artistArray[repeatGenre].genre = genre;
  repeatGenre  ;
}

Edit: Now that I think about it, this could probably be simplified.

for (int count = 0; count < artistArray[index].numGenres; count  ) {
  std::getline(inputFile, artistArray[repeatGenre  ].genre);
}

CodePudding user response:

The problem is that you switch to using >> to a string here

for (int count = 0; count < artistArray[index].numOfGenres; count  )
{
    //Read Genre From File into the Array
    inputFile >> artistArray[repeatGenre].genre; <<<====
    repeatGenre  ;
}

>> std::string reads a word not a whole line

you need

for (int count = 0; count < artistArray[index].numOfGenres; count  )
{
    //Read Genre From File into the Array
    getline(inputFile, artistArray[repeatGenre].genre);
    repeatGenre  ;
}

I am assuming that .genre is a std::string

CodePudding user response:

Continuing from my comment, when you mix >> and getline() you must understand that >> stops reading at the first whitespace without extracting the whitespace. ('\n' is whitespace too). So after reading with >> the line ending '\n' is left in the input stream unread.

getline() reads (by default) until is reaches the '\n' character and then extracts the '\n' from the input stream but does not store it along with the line read. So if you read with >> and then call getline(), getline() will see the '\n' left in the input stream (usually as the 1st character to be read), so getline() will read nothing, extracting the '\n' and stopping at that point (without any error generated)

To handle switching between >> and getline(), the iostream library provides the .ignore() member function that will extract and discard up to a delimiter (or a fixed number of characters if a count is given). In your case, after reading with >> and before reading again with getline() you would use:

    inputFile.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

Which extracts and discards all characters in the input stream up until the first '\n' encountered. The use of std::numeric_limits<std::streamsize>::max() allows it to ignore up to the maximum number of characters in an input stream.


Reading Your Input File

Given the lack of a Minimal Reproducable Example in your question, it is impossible to tell what all your issues may be, but if you are using plain-old-arrays, that imposes all memory management and bounds checking on you. Instead, a better choice would be to use std::vector letting the compiler handle the details.

A quick class implementation makes things quite easy. For example you could set up your artist class, and then simply create a vector-of-artists to hold your collection. For example you could do:

class artist
{
  struct track {
    std::string title;
    uint16_t year;
  };
  std::string name;
  std::vector<std::string> genre;
  std::vector<track> tracks;
  uint16_t ngenre, yearstart, yearend, ntracks;
  
 public:
  friend std::istream& operator >> (std::istream& is, artist& a);
  friend std::ostream& operator << (std::ostream& os, const artist& a);
};

The artist class now holds all data related to one artist, including an additional vector of genre and a vector of struct that holds the title-year information for each title. The friend operator overloads for >> and << provide a convenient way to encapsulate your input of private class data and a convenient way to output all information per-artist.

(Note: your input data specifies 29 tracks for that artist, but only provides 1-track worth of data. To make the number of tracks consistent with the information provided, that will need to be changed to 1 in the sample data you provided)

To write your >> overload to handle the input of all data for one artist, you can do something similar to:

std::istream& operator >> (std::istream& is, artist& a)
{
  if (!getline (is, a.name)) {    /* read / validate name */
    return is;
  }
  if (!(is >> a.ngenre)) {        /* read / validate no. genre */
    return is;
  }
  /* extract characters to '\n' remaining in stdin */
  is.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  
  for (uint16_t n = 0; n < a.ngenre; n  ) { /* loop a.ngenre times */
    std::string line{};
    if (!getline (is, line)) {    /* read genre into line */
      return is;
    }
    a.genre.push_back (line);     /* add genre to vector */
  }
  
  if (!(is >> a.yearstart)) {     /* read start year */
    return is;
  }
  if (!(is >> a.yearend)) {       /* read end year */
    return is;
  }
  if (!(is >> a.ntracks)) {       /* read no. of tracks */
    return is;
  }
  /* extract characters to '\n' remaining in stdin */
  is.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  
  for (uint16_t n = 0; n < a.ntracks; n  ) {  /* loop a.ntracks times */
    artist::track tmp{};            /* temporary track */
    std::string line{};             /* string for track line */
    if (!getline (is, line)) {      /* read / validate entire track line */
      return is;
    }
    size_t titleend = line.rfind('"');          /* find last " in line */
    if (titleend == std::string::npos) {        /* validate found */
      return is;
    }
    tmp.title = line.substr(1, titleend - 1);   /* extract between " in line */
    /* begin after closing ", find next '1' or '2', extarct title-year */
    tmp.year = std::stoul(line.substr(line.find_first_of("12", titleend   1)));
    
    a.tracks.push_back (tmp);       /* add track to vector */
  }
  
  return is;
}

The comments make clear what is happening. The only challenge comes in separating each title and year. Above presumes that your title is contained in double-quotes where the opening double-quote is the first character in the line, and the final double-quote marks the end of the title.

Essentially what is done above is the closing double-quote is located using std::basic_string::rfind and the index is used to extract between the quotes with std::basic_string::substr.

For the year std::stoul is used to convert the string of digits (located by searching after the closing double-quote for the first character in "12" as the start of years in the range 1xxx - 2xxx). There are many other ways to split the title-line, but using the double-quotes as delimiters will automatically handle names including spaces. (for instance you could read twice using '"' as the getline() delimiter to read the title)

For your output overload, you can output the data is whatever format you like. One simple way with labels for the different information is:

std::ostream& operator << (std::ostream& os, const artist& a)
{
  /* output Artist label and name, then Genre label */
  os << "\nArtist : " << a.name
     <<  "\nGenre  :";
  
  /* loop outputting genres separated by a comma */
  for (uint16_t n = 0; n < a.genre.size(); n  ) {
    if (n) {
      os.put (',');
    }
    os << " " << a.genre[n];
  }
  
  /* output start, end years and number of tracks */
  os << "\nYears  : " << a.yearstart << ", " << a.yearend
     << "\nTracks : " << a.ntracks << '\n';
  
  /* loop outputting all tracks indented by 2-spaces */
  for (uint16_t n = 0; n < a.tracks.size(); n  ) {
    os << "  \"" << a.tracks[n].title << "\", " << a.tracks[n].year << '\n';
  }
  
  return os;
}

Reading from the sample data provided in the file dat/artistsinfo.txt where 29 was replaced with 1 to make the title information consistent, running the code above you would get output from the << overload of:

$ ./bin/artistgenre dat/artistsinfo.txt

Artist : Korn
Genre  : Nu Metal, Alternative Metal
Years  : 1993, 2021
Tracks : 1
  "Blind", 1995

Adding a main() that reads from the filename provided as the first argument to your program, a short complete example could look like:

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <limits>

class artist
{
  struct track {
    std::string title;
    uint16_t year;
  };
  std::string name;
  std::vector<std::string> genre;
  std::vector<track> tracks;
  uint16_t ngenre, yearstart, yearend, ntracks;
  
 public:
  friend std::istream& operator >> (std::istream& is, artist& a);
  friend std::ostream& operator << (std::ostream& os, const artist& a);
};

int main (int argc, char **argv)
{
  if (argc < 2) {  /* validate 1 argument for filename */
    std::cerr << "usage: ./" << argv[0] << " dict_file\n";
    return 1;
  }
  
  artist tmp{};
  std::vector<artist> artists{};
  std::ifstream f (argv[1]);              /* open artists file */
  
  if (!f.is_open()) {   /* validate file open for reading */
    std::cerr << "error: file open failed - " << argv[1] << '\n';
    return 1;
  }
  
  while (f >> tmp) {                /* read all artists from file */
    artists.push_back (tmp);
  }
  
  for (const auto& a : artists) {   /* output all artists to stdout */
    std::cout << a;
  }
}

std::istream& operator >> (std::istream& is, artist& a)
{
  if (!getline (is, a.name)) {    /* read / validate name */
    return is;
  }
  if (!(is >> a.ngenre)) {        /* read / validate no. genre */
    return is;
  }
  /* extract characters to '\n' remaining in stdin */
  is.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  
  for (uint16_t n = 0; n < a.ngenre; n  ) { /* loop a.ngenre times */
    std::string line{};
    if (!getline (is, line)) {    /* read genre into line */
      return is;
    }
    a.genre.push_back (line);     /* add genre to vector */
  }
  
  if (!(is >> a.yearstart)) {     /* read start year */
    return is;
  }
  if (!(is >> a.yearend)) {       /* read end year */
    return is;
  }
  if (!(is >> a.ntracks)) {       /* read no. of tracks */
    return is;
  }
  /* extract characters to '\n' remaining in stdin */
  is.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  
  for (uint16_t n = 0; n < a.ntracks; n  ) {  /* loop a.ntracks times */
    artist::track tmp{};            /* temporary track */
    std::string line{};             /* string for track line */
    if (!getline (is, line)) {      /* read / validate entire track line */
      return is;
    }
    size_t titleend = line.rfind('"');          /* find last " in line */
    if (titleend == std::string::npos) {        /* validate found */
      return is;
    }
    tmp.title = line.substr(1, titleend - 1);   /* extract between " in line */
    /* begin after closing ", find next '1' or '2', extarct title-year */
    tmp.year = std::stoul(line.substr(line.find_first_of("12", titleend   1)));
    
    a.tracks.push_back (tmp);       /* add track to vector */
  }
  
  return is;
}

std::ostream& operator << (std::ostream& os, const artist& a)
{
  /* output Artist label and name, then Genre label */
  os << "\nArtist : " << a.name
     <<  "\nGenre  :";
  
  /* loop outputting genres separated by a comma */
  for (uint16_t n = 0; n < a.genre.size(); n  ) {
    if (n) {
      os.put (',');
    }
    os << " " << a.genre[n];
  }
  
  /* output start, end years and number of tracks */
  os << "\nYears  : " << a.yearstart << ", " << a.yearend
     << "\nTracks : " << a.ntracks << '\n';
  
  /* loop outputting all tracks indented by 2-spaces */
  for (uint16_t n = 0; n < a.tracks.size(); n  ) {
    os << "  \"" << a.tracks[n].title << "\", " << a.tracks[n].year << '\n';
  }
  
  return os;
}

Look things over and let me know if you have questions above. If you would like to edit your question and include a full MRE, I'm happy to check your implementation as well.

  •  Tags:  
  • c
  • Related