Need help parsing data from .csv file C

Viewed 82

I have the following .csv file containing information about the song, artist, release year (if specified) and number of listens:

Look What The Cat Dragged In,Poison,,Look What The Cat Dragged In by Poison,1,0,1,0
Nothin' But A Good Time,Poison,1988,Nothin' But A Good Time by Poison,1,1,21,21
Something To Believe In,Poison,1990,Something To Believe In by Poison,1,1,1,1
Talk Dirty To Me,Poison,1978,Talk Dirty To Me by Poison,1,1,1,1
A Salty Dog,Procol Harum,1969,A Salty Dog by Procol Harum,1,1,1,1
A Whiter Shade of Pale,Procol Harum,1967,A Whiter Shade of Pale by Procol Harum,1,1,3,3
Blurry,Puddle of Mudd,2001,Blurry by Puddle of Mudd,1,1,1,1
Amie,Pure Prairie League,,Amie by Pure Prairie League,1,0,4,0
Another One Bites the Dust,Queen,1980,Another One Bites the Dust by Queen,1,1,102,102
Bicycle Race,Queen,1978,Bicycle Race by Queen,1,1,3,3
Kiss You All Over,Kiss,1978,Kiss You All Over by Kiss,1,1,5,5

The name of the file and the desired year should be given as command line arguments, and the program should print all songs from that specific year.

e.g.: ./a.out music.csv 1978

Output:

Talk dirty to me
Bicycle Race
Kiss You All Over

Code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAX 300

typedef struct {
    char song[101], *artist, *line;
    long int year;
} music;

int checkYear(char *word)
{
    for (int i = 0; i < strlen(word); i++) {
        if (!isdigit(word[i]))
            return 0;
    }
    return 1;
}

int main(int argc, char **argv)
{
    FILE *fin = fopen(argv[1], "r");
    if (!fin)
    {
        printf("Error opening the file.\n");
        return 1;
    }
    char buf[MAX];
    //int nLines = 0; //count the number of lines
    //music *array = NULL;
    while( fgets(buf, MAX, fin))
    {
        buf[strcspn(buf, "\n")] = '\0';  // strip the trailing newline
        char *word = strtok(buf, ",");
        while (word)
        {
            //printf("Word is : %s\n", word);
            if (checkYear(word))
            {
                //printf("Year : %s\n", word);
                music *array = (music *)malloc(sizeof(music));
                char *p;
                array->year = strtol(word, &p, 10);
                if (array->year == atoi(argv[2]))
                {
                    //printf("Year : %ld\t%d\n", array->year, atoi(argv[2]));
                    if (scanf("%100[^,]", array->song) == 1)
                    {
                        printf("Song : %s\n", array->song);
                    }
                }
            }
            word = strtok(NULL, ",");
        }
    }
    //printf("I've read %d lines\n", nLines);
    fclose(fin);
    return 0;
}

So far, it's going decent, I can extract the specified year from each line, but now I just need to print the name of the song from those lines (the first token on the line). I thought about using scanf("%[^,]") to read and print everything up until the first comma but it's just stuck in an endless loop. Could you give me an idea? Thanks in advance!

2 Answers

There are multiple problems in the code:

  • you do not check that enough arguments were passed on the command line, potentially invoking undefined behavior if not.
  • you do not need to allocate a music structure: you can just parse the first 3 fields, check the year and output the name of the song directly.
  • strtok() is inappropriate to split fields from a csv file because it treats a sequence of separators as a single separator, which is incorrect and causes invalid parsing if some fields are empty.
  • sscanf("%[^,]", ...) will fail to convert an empty field.

To split the fields from the csv line, I recommend you use a utility function that behaves like strtok_r() but tailored for csv lines. A simplistic version will stop on , and \n and replace these with a null byte, returning the initial pointer and updating the pointer for the next field. A more advanced version would also handle quotes.

Here is a modified version:

#include <stdio.h>
#include <string.h>

#define MAX 300

char *get_field(char **pp) {
    char *p, *start;
    for (p = start = *pp; *p; p++) {
        if (*p == ',' || *p == '\n') {
            *p++ = '\0';
            break;
        }
    }
    *pp = p;
    return start;
}
     
int main(int argc, char *argv[]) {
    char buf[MAX];
    FILE *fin;
    char *filename;
    char *select_year;

    if (argc < 3) {
        printf("Missing arguments\n");
        return 1;
    }
    filename = argv[1];
    select_year = argv[2];
    fin = fopen(filename, "r");
    if (!fin) {
        printf("Error opening the file %s.\n", filename);
        return 1;
    }

    while (fgets(buf, sizeof buf, fin)) {
        char *p = buf;
        char *song = get_field(&p);
        char *artist = get_field(&p);
        char *year = get_field(&p);
        if (!strcmp(year, target_year)) {
            printf("%s\n", song);
        }
    }
    fclose(fin);
    return 0;
}

regarding: scanf("%[^,]") this consumes (upto but not including) the comma.

So the next instruction needs to be something like getchar() to consume the comma. Otherwise, on the next loop nothing will be read because the first character in stdin is that same comma.

Related