Your immediate problem was pointed out in the comment and you were directed to Why is while ( !feof (file) ) always wrong?. Always condition the read loop on the read function itself, or loop continually and check the return of the read function within the loop and break;
the loop when a successful read has occurred and all constraints are satisfied.
fp = fopen("history.txt", "r");
Don't hardcode filenames or use magic-numbers in your code. For filenames, either pass the filename to read as an argument to your program (that's what argv
is for) or prompt for its entry. (preferably pass it as an argument). If you need constants within your code, #define
one (or more) or use a global enum
to accomplish the same thing.
Now to the crux of your problem. Anytime when you have to handle differing types of values as a single record or object, you should be thinking about using a structure to coordinate the different types as a single object. Here you can declare a struct
with int
members to store the percent and test type values and char[]
(or char*
and allocate) to hold the name and date information. You can then simply declare an array of struct to hold all values read from your history.txt
file that can be easily sorted with qsort
on any of the members you choose.
First, declare a simple struct, e.g.
struct users { /* simple struct to hold values read from input */
int pct,
test;
char name[MAXNM],
date[MAXDT];
};
Then you can simply declare an array with struct users array[50];
However, you can make your life easier and avoid having to type struct users ...
over and over by creating a typedef
. In C you can even drop the struct label users
and add the typedef label at the end, e.g.
typedef struct { /* simple struct w/typedef to hold values read from input */
int pct,
test;
char name[MAXNM],
date[MAXDT];
} userhist_t;
Now you can simply create an array of struct with userhist_t array[50];
(it's up to you whether you use a typedef
or not, it isn't required, it just saves typing)
If you are sorting values in C, go ahead and do yourself a favor and learn how to write compare functions for qsort
. It is the swiss-army-knife for sorting in C. (it is also quite efficient and much more well tested than anything you will roll on your own). There is nothing difficult about writing a compare function. The prototype is:
int compare (const void *a, const void *b);
Where a
and b
are simply pointers to adjacent elements in the array you pass and you return -1
if a
sorts before b
, 0
if they are the same, or 1
if b
sorts before a
-- just like strcmp
does. To do that you simply cast a
and b
to the element type for your array and then compare the values, returning an appropriate value. For instance:
/* qsort compare function sorting array of struct by percent (descending) */
int compare_pct (const void *a, const void *b)
{
const userhist_t *as = a, /* cast the pointers to correct type */
*bs = b;
/* return descending sort (conditionals avoids potential overflow)
* for ascending sort use (as > bs) - (as < bs)
*/
return (as->pct < bs->pct) - (as->pct > bs->pct);
}
Above, the pointers a
and b
are cast as a pointer to your struct (I just tagged on the s
for struct to create different variable names, e.g. as
and bs
, you can name them anything you like).
While you could just return bs->pct - as->pct
, there is a risk of overflow if the values exceed what can be returned as an int
, (e.g. both large negative value that when subtracted would be less than INT_MIN
. Instead, the conditionals are used to normalize the return to -1, 0, 1
just using the result of the conditional expressions.
(think through it, if (as->pct < bs->pct)
the result evaluates to 1
and (as->pct > bs->pct)
evaluates to 0
yielding 1 - 0 = 1
(so b
sorts before a
in the descending case))
To sort your array, you then simply call:
qsort (array, nelements, sizeof element, compare);
Now on to reading/filling the array of struct. So let's declare our constants, you have the struct above, let's declare an array of struct and then read values from the file into the array, e.g.
/* global enum defining constants for use in code */
enum { MAXDT = 12, MAXNM = 16, MAXS = 64 };
...
userhist_t user[MAXS] = {{ .pct = 0 }}; /* array of struct */
size_t n = 0;
...
/* read/fill up to MAXS struct from file */
while (n < MAXS && fscanf (fp, "%d%% %s %d %s", &user[n].pct,
user[n].name, &user[n].test, user[n].date) == 4)
n++;
While you can use fgets
and sscanf
to read a line and then parse the values separately, fscanf
will do both for example purposes here. The key is to validate the return to insure 4-conversions succeeded and no input or matching failure occurred.
Putting it altogether, passing the filename as the 1st argument to the program (or reading from stdin
by default if no argument is provided), handling the declarations, the read, the sort with qsort
and the output, you could do something similar to the following:
#include <stdio.h>
#include <stdlib.h>
/* global enum defining constants for use in code */
enum { MAXDT = 12, MAXNM = 16, MAXS = 64 };
typedef struct { /* simple struct to hold values read from input */
int pct,
test;
char name[MAXNM],
date[MAXDT];
} userhist_t;
/* qsort compare function sorting array of struct by percent (descending) */
int compare_pct (const void *a, const void *b)
{
const userhist_t *as = a, /* cast the pointers to correct type */
*bs = b;
/* return descending sort (conditionals avoids potential overflow)
* for ascending sort use (as > bs) - (as < bs)
*/
return (as->pct < bs->pct) - (as->pct > bs->pct);
}
int main (int argc, char **argv) {
userhist_t user[MAXS] = {{ .pct = 0 }}; /* array of struct */
size_t n = 0;
/* use filename provided as 1st argument (stdin by default) */
FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
if (!fp) { /* validate file open for reading */
perror ("file open failed");
return 1;
}
/* read/fill up to MAXS struct from file */
while (n < MAXS && fscanf (fp, "%d%% %s %d %s", &user[n].pct,
user[n].name, &user[n].test, user[n].date) == 4)
n++;
if (fp != stdin) fclose (fp); /* close file if not stdin */
/* sort by calling compare_pct to sort by percent (descending order) */
qsort (user, n, sizeof *user, compare_pct);
for (size_t i = 0; i < n; i++) /* output sorted results */
printf ("%3d%% %-8s %2d %s\n",
user[i].pct, user[i].name, user[i].test, user[i].date);
return 0;
}
Example Input File
$ cat dat/history.txt
26% User1 1 01/01/2019
100% User2 3 01/01/2019
73% User3 1 01/01/2019
52% User4 1 01/01/2019
75% User5 2 01/01/2019
60% User6 1 01/01/2019
Example Use/Output
Using your input file, would result in the sorted order you describe:
$ ./bin/userhistsort dat/history.txt
100% User2 3 01/01/2019
75% User5 2 01/01/2019
73% User3 1 01/01/2019
60% User6 1 01/01/2019
52% User4 1 01/01/2019
26% User1 1 01/01/2019
If you want to save the results to a new file, simply redirect the output, e.g.
$ ./bin/userhistsort dat/history.txt > sortedfile.txt
(or you can open a new file-stream within your code and output the information there)
Look things over and let me know if you have further questions.