Well, I got your comment, but let's try an array of struct to make your data handling manageable. This is no trivial addition to your last question. Trying to coordinate 5 separate arrays (3 of which would be 2D character arrays) poses more problems than it is worth. A struct
is the proper way to coordinate the different types of information as one unit. Also, unless you have an undying need to use bool
simply use int
instead. The compiler can handle the native type int
just as efficiently.
Let's start with your struct. A struct
is nothing more than a convenient wrapper allowing you to collect different types and handle them as one object. In your case your character strings name, plate, date
, your int type
and your double value
(your amount
) can be the members of a struct
. You can use a typedef
to a struct to make its use more convenient, so instead of writing struct nameofstruct
everywhere, you can simply use the typedeffed nameofstruct
as you would an ordinary type, like int
. For for example:
#define MAXS 16u /* max number of structs */
#define MAXNM 32u /* max characters in name and other arrays in struct */
#define MAXC 1024u /* max characters in read buffer */
#define SECPY 31536000u /* seconds per-year */
typedef struct mydata {
char name[MAXNM],
plate[MAXNM],
date[MAXNM];
int type;
double value;
} mydata_t;
Create a struct mydata
with the typedef
of mydata_t
that is a reference (an alias) to struct mydata
(you can actually omit the mydata
and just typedef the anonymous struct).
Now you can create an array of struct mydata
or simply of mydata_t
and each element of the array is a struct that can hold each of those values.
Now let's start on your problem. As mentioned above, just declare an array of mydata_t
instead of 5 separate arrays, e.g.
int main (int argc, char **argv) {
...
int n = 0, ndx = 0; /* NOTE when dealing with array, start at ZERO */
...
mydata_t data[MAXS] = {{ .name = "" }};
Now you have an array of 16
struct to work with (adjust your constant sizing the array as needed) and you have 2 counters n
to keep track of which member in your struct your are currently reading, and ndx
the index of which struct in the array you are filling.
Using them you can read each line like in your last question and use n
to determine what to do with the line (you can use isspace()
to check if the first character is whitespae (which includes a '\n'
) to skip empty-lines. You then just pass n
to switch(n)
to take the appropriate action for that line (or use a bunch of if ... else if ... else if ...
) For example:
while (fgets (buf, MAXC, fp)) { /* read each line in file */
if (isspace(*buf)) /* skip blank lines (or start with space) */
continue;
buf[strcspn (buf, "\r\n")] = 0; /* trim '\n' from end of buf */
/* if line isn't a date line, just output line as non-date line */
switch (n) {
case 0: /* fill the name in struct */
case 1: /* fill the plate in struct */
case 2: /* set the type in struct */
...
(note: the buf[strcspn (buf, "\r\n")] = 0;
is simply a handy way to trim the '\n'
(or \r\n
) from the end of buf before you copy it to name, plate, date
, etc..)
In each of your cases, you simply VALIDATE that the string read with fgets
will fit in the string member of your struct before you copy it or you convert the string to the numeric value you need, example:
case 0:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].name, buf);
else {
fputs ("error: name exceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
n++; /* advance n counter to act on next member */
break;
or
case 2:
if (sscanf (buf, "%d", &data[ndx].type) != 1) {
fputs ("error: type not an integer.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
(You use the '.'
(dot) operator to access each member of a struct when dealing with the struct itself, or you use the '->'
(arrow) operator if what you have is a pointer to struct. When you have an array of stuct the array index ([..]
) acts as a dereference. Exactly the same as an array of anything else)
Since you don't store year, month, day
, you simply need a function that checks the time from now so you can test whether it is more than a year. That way you simply pass your date to the function and get a time in seconds back as a double
, e.g.
double check_time_from_now (const char *str)
{
int y, m, d;
if (sscanf (str, "%4d%2d%2d", &y, &m, &d) != 3) {
fprintf (stderr, "error non-date string: '%s'.\n", str);
exit (EXIT_FAILURE);
}
time_t now = time(NULL),
then = fill_broken_down_time (y, m, d);
double secs = difftime (now, then); /* get seconds between dates */
return secs;
}
Another simply function allows you to print you array of struct, e.g.
void prn_data_t_array (mydata_t *data, int n)
{
for (int i = 0; i < n; i++)
printf ("%-12s %-8s %d %9.2f %s\n", data[i].name, data[i].plate,
data[i].type, data[i].value, data[i].date);
}
With that we can look at the remainder of your switch(n)
that shows how to handle each case:
while (fgets (buf, MAXC, fp)) { /* read each line in file */
if (isspace(*buf)) /* skip blank lines (or start with space) */
continue;
buf[strcspn (buf, "\r\n")] = 0; /* trim '\n' from end of buf */
/* if line isn't a date line, just output line as non-date line */
switch (n) {
case 0:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].name, buf);
else {
fputs ("error: name exceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 1:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].plate, buf);
else {
fputs ("error: plate exceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 2:
if (sscanf (buf, "%d", &data[ndx].type) != 1) {
fputs ("error: type not an integer.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 3:
if (sscanf (buf, "%lf", &data[ndx].value) != 1) {
fputs ("error: value not a double.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 4:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].date, buf);
else {
fputs ("error: date exceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
if (check_time_from_now (data[ndx].date) > SECPY) {
if (data[ndx].type)
data[ndx].value *= 1.5;
else
data[ndx].value *= 2.5;
}
n = 0;
ndx++;
if (ndx == MAXS)
goto arrayfull;
break;
default:
fputs ("error: you shouldn't get here!\n", stderr);
break;
}
}
(note: look closely at case 4:
and how the line counter n
is reset to zero and how your array index ndx
is increment so you then fill the next struct in your array. Also note you protect your array bounds by checking if (ndx == MAXS)
to make sure you don't write more structs than you have in the array.)
Putting it altogether you would have:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#define MAXS 16u /* max number of structs */
#define MAXNM 32u /* max characters in name and other arrays in struct */
#define MAXC 1024u /* max characters in read buffer */
#define SECPY 31536000u /* seconds per-year */
typedef struct {
char name[MAXNM],
plate[MAXNM],
date[MAXNM];
int type;
double value;
} mydata_t;
time_t fill_broken_down_time (int y, int m, int d)
{ /* initialize struct members */
struct tm bdt = { .tm_sec=0, .tm_min=0, .tm_hour=0, .tm_mday=d,
.tm_mon=m>0?m-1:0, .tm_year=y-1900, .tm_isdst=-1 };
return mktime(&bdt); /* return mktime conversion to time_t */
}
double check_time_from_now (const char *str)
{
int y, m, d;
if (sscanf (str, "%4d%2d%2d", &y, &m, &d) != 3) {
fprintf (stderr, "error non-date string: '%s'.\n", str);
exit (EXIT_FAILURE);
}
time_t now = time(NULL),
then = fill_broken_down_time (y, m, d);
double secs = difftime (now, then); /* get seconds between dates */
return secs;
}
void prn_data_t_array (mydata_t *data, int n)
{
for (int i = 0; i < n; i++)
printf ("%-12s %-8s %d %9.2f %s\n", data[i].name, data[i].plate,
data[i].type, data[i].value, data[i].date);
}
int main (int argc, char **argv) {
/* use filename provided as 1st argument (stdin by default) */
FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
int n = 0, ndx = 0; /* NOTE when dealing with array, start at ZERO */
char buf[MAXC]; /* buffer to hold each line read from file */
mydata_t data[MAXS] = {{ .name = "" }};
if (!fp) { /* validate file open for reading */
perror ("file open failed");
return 1;
}
while (fgets (buf, MAXC, fp)) { /* read each line in file */
if (isspace(*buf)) /* skip blank lines (or start with space) */
continue;
buf[strcspn (buf, "\r\n")] = 0; /* trim '\n' from end of buf */
/* if line isn't a date line, just output line as non-date line */
switch (n) {
case 0:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].name, buf);
else {
fputs ("error: name exceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 1:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].plate, buf);
else {
fputs ("error: plate exceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 2:
if (sscanf (buf, "%d", &data[ndx].type) != 1) {
fputs ("error: type not an integer.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 3:
if (sscanf (buf, "%lf", &data[ndx].value) != 1) {
fputs ("error: value not a double.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 4:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].date, buf);
else {
fputs ("error: date exceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
if (check_time_from_now (data[ndx].date) > SECPY) {
if (data[ndx].type)
data[ndx].value *= 1.5;
else
data[ndx].value *= 2.5;
}
n = 0;
ndx++;
if (ndx == MAXS)
goto arrayfull;
break;
default:
fputs ("error: you shouldn't get here!\n", stderr);
break;
}
}
arrayfull:;
if (fp != stdin) fclose (fp); /* close file if not stdin */
puts ("\ncomputed information\n");
prn_data_t_array (data, ndx); /* print the computed values */
return 0;
}
Now using your data file:
Example Input File
$ cat dat/namelbltypevaldate.txt
Hanif Hefaz
BA123HB
0
100.50
20180101
Jacki Shroff
UP673MK
1
3000.99
20170512
Example Use/Output
The code would produce:
$ ./bin/time_from_now3 dat/namelbltypevaldate.txt
computed information
Hanif Hefaz BA123HB 0 251.25 20180101
Jacki Shroff UP673MK 1 4501.48 20170512
If you check, the appropriate multiplier has been applied to each.
I really wouldn't want to do this with 5 separate arrays. A struct is the proper tool for the job. You can use 5 arrays, in the exact same way I use the array of struct above -- you just have a much longer variable list and a lot more names to keep track of in your code. Look this over and let me know if you have more questions.
Dynamically Allocating Array of Struct
Functionally, the program doesn't care where the storage for your data is. However, allocating with automatic storage does have the drawback of not being able to dynamically grow as you continue to add records. The alternative to the dynamically allocate your data using malloc, calloc, realloc
. This adds a slight amount of additional complexity as it is now up to you to track the allocated storage available, the storage used, and to reallocate when your storage used equals your storage available.
You do not want to realloc
every line -- that is inefficient. Instead you will allocate some reasonable starting number of structs, fill them until you reach the limit and then reallocate. How much you reallocate is up to you, but common reallocation schemes are to double the currently allocated size (or you can add some other multiple like 3/2
, etc... depending on how quickly you want your allocation to grow). The example uses the good old double method.
First the changes (to only print lines greater than 1 years from now) simply involves moving your output to within:
if (check_time_from_now (data[ndx].date) > SECPY) {
Adding a function to output record ndx
is something you can do to keep the body of your code tidy, e.g.
void prn_data_t_rec (mydata_t *data, int n)
{
printf ("%-12s %-8s %d %9.2f %s\n", data[n].name, data[n].plate,
data[n].type, data[n].value, data[n].date);
}
Which makes calling from the body of your code:
if (check_time_from_now (data[ndx].date) > SECPY) {
if (data[ndx].type)
data[ndx].value *= 1.5;
else
data[ndx].value *= 2.5;
prn_data_t_rec (data, ndx); /* output > 1 year */
}
Now to the dynamic allocation. Instead of declaring:
mydata_t data[MAXS] = {{ .name = "" }};
You simply change the declaration and initial allocation to:
#define MAXS 16u /* initial number of structs */
...
int maxs = MAXS; /* variable to track allocated number of struct */
mydata_t *data = calloc (maxs, sizeof *data); /* allocate storage */
(note: calloc
was chosen over malloc
because I want to initialize all memory zero and avoid having to write an explicit initializer to set every type
or value
member to zero otherwise. The overhead in allowing calloc
to do it is negligible)
Now *Validate EVERY Allocation
if (!data) { /* validate every allocation */
perror ("calloc-data");
return 1;
}
The same applies to every reallocation with a caveat. You Always realloc
with a Temporary Pointer. If you reallocate with the pointer itself, e..g ptr = realloc (ptr, newsize);
and realloc
fails returning NULL
- you overwrite your original address with NULL
creating a memory leak and lose all access to your existing data!
Everything else in the code is identical until you get to the realloc
in case 4:
of the switch()
. There when (ndx == maxs)
you must realloc
additional storage, e.g.
case 4:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].date, buf);
else {
fputs ("error: date excceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
if (check_time_from_now (data[ndx].date) > SECPY) {
if (data[ndx].type)
data[ndx].value *= 1.5;
else
data[ndx].value *= 2.5;
prn_data_t_rec (data, ndx);
}
n = 0;
ndx++;
if (ndx == maxs) { /* check if realloc required */
/* realloc w/temp pointer to 2X current size */
void *tmp = realloc (data, 2 * maxs * sizeof *data);
if (!tmp) { /* validate every allocation */
perror ("realloc-data");
goto outofmem; /* original data still good */
}
data = tmp; /* set data to newly reallocated block */
/* zero the new memory allocated (optional) */
memset (data + maxs, 0, maxs * sizeof *data);
maxs *= 2; /* update the currently allocated max */
}
break;
(note: the continued use of goto
-- it is imperative here to preserve your original data
if realloc
fails. In the even of failure ALL of your original data
is still good - and since you used a temporary pointer to test the reallocation, you haven't lost access to it. So simply jump out of the switch and while
loop and you can continue to make use of it and free
it when no longer needed.
Putting the entire example together you would have:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#define MAXS 16u /* initial number of structs */
#define MAXNM 32u /* max characters in name and other arrays in struct */
#define MAXC 1024u /* max characters in read buffer */
#define SECPY 31536000u /* seconds per-year */
typedef struct {
char name[MAXNM],
plate[MAXNM],
date[MAXNM];
int type;
double value;
} mydata_t;
time_t fill_broken_down_time (int y, int m, int d)
{ /* initialize struct members */
struct tm bdt = { .tm_sec=0, .tm_min=0, .tm_hour=0, .tm_mday=d,
.tm_mon=m>0?m-1:0, .tm_year=y-1900, .tm_isdst=-1 };
return mktime(&bdt); /* return mktime conversion to time_t */
}
double check_time_from_now (const char *str)
{
int y, m, d;
if (sscanf (str, "%4d%2d%2d", &y, &m, &d) != 3) {
fprintf (stderr, "error non-date string: '%s'.\n", str);
exit (EXIT_FAILURE);
}
time_t now = time(NULL),
then = fill_broken_down_time (y, m, d);
double secs = difftime (now, then); /* get seconds between dates */
return secs;
}
void prn_data_t_array (mydata_t *data, int n)
{
for (int i = 0; i < n; i++)
printf ("%-12s %-8s %d %9.2f %s\n", data[i].name, data[i].plate,
data[i].type, data[i].value, data[i].date);
}
void prn_data_t_rec (mydata_t *data, int n)
{
printf ("%-12s %-8s %d %9.2f %s\n", data[n].name, data[n].plate,
data[n].type, data[n].value, data[n].date);
}
int main (int argc, char **argv) {
/* use filename provided as 1st argument (stdin by default) */
FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
int n = 0, ndx = 0, /* NOTE when dealing with array, start at ZERO */
maxs = MAXS; /* variable to track allocated number of struct */
char buf[MAXC]; /* buffer to hold each line read from file */
mydata_t *data = calloc (maxs, sizeof *data); /* allocate storage */
if (!data) { /* validate every allocation */
perror ("calloc-data");
return 1;
}
if (!fp) { /* validate file open for reading */
perror ("file open failed");
return 1;
}
puts ("\ncomputed information for records > 1 year from now\n");
while (fgets (buf, MAXC, fp)) { /* read each line in file */
if (isspace(*buf)) /* skip blank lines (or start with space) */
continue;
buf[strcspn (buf, "\r\n")] = 0; /* trim '\n' from end of buf */
/* if line isn't a date line, just output line as non-date line */
switch (n) {
case 0:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].name, buf);
else {
fputs ("error: name excceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 1:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].plate, buf);
else {
fputs ("error: plate excceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 2:
if (sscanf (buf, "%d", &data[ndx].type) != 1) {
fputs ("error: type not an integer.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 3:
if (sscanf (buf, "%lf", &data[ndx].value) != 1) {
fputs ("error: value not a double.\n", stderr);
exit (EXIT_FAILURE);
}
n++;
break;
case 4:
if (strlen (buf) < MAXNM)
strcpy (data[ndx].date, buf);
else {
fputs ("error: date excceeds storage.\n", stderr);
exit (EXIT_FAILURE);
}
if (check_time_from_now (data[ndx].date) > SECPY) {
if (data[ndx].type)
data[ndx].value *= 1.5;
else
data[ndx].value *= 2.5;
prn_data_t_rec (data, ndx);
}
n = 0;
ndx++;
if (ndx == maxs) { /* check if realloc required */
/* realloc w/temp pointer to 2X current size */
void *tmp = realloc (data, 2 * maxs * sizeof *data);
if (!tmp) { /* validate every allocation */
perror ("realloc-data");
goto outofmem; /* original data still good */
}
data = tmp; /* set data to newly reallocated block */
/* zero the new memory allocated (optional) */
memset (data + maxs, 0, maxs * sizeof *data);
maxs *= 2; /* update the currently allocated max */
}
break;
default:
fputs ("error: you shouldn't get here!\n", stderr);
break;
}
}
outofmem:;
if (fp != stdin) fclose (fp); /* close file if not stdin */
puts ("\nall stored information\n");
prn_data_t_array (data, ndx); /* print the computed values */
free (data); /* don't forget to free what you allocate */
return 0;
}
(note: it print > 1 year
during the loop and output the total records at the end -- adjust as needed)
Example Use/Output
Using your same data file:
$ ./bin/time_from_now4 dat/namelbltypevaldate.txt
computed information for records > 1 year from now
Hanif Hefaz BA123HB 0 251.25 20180101
Jacki Shroff UP673MK 1 4501.48 20170512
all stored information
Hanif Hefaz BA123HB 0 251.25 20180101
Jacki Shroff UP673MK 1 4501.48 20170512
Memory Use/Error Check
In any code you write that dynamically allocates memory, you have 2 responsibilities regarding any block of memory allocated: (1) always preserve a pointer to the starting address for the block of memory so, (2) it can be freed when it is no longer needed.
It is imperative that you use a memory error checking program to insure you do not attempt to access memory or write beyond/outside the bounds of your allocated block, attempt to read or base a conditional jump on an uninitialized value, and finally, to confirm that you free all the memory you have allocated.
For Linux valgrind
is the normal choice. There are similar memory checkers for every platform. They are all simple to use, just run your program through it.
$ ./bin/time_from_now4 dat/namelbltypevaldate.txt
==4331== Memcheck, a memory error detector
==4331== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==4331== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==4331== Command: ./bin/time_from_now4 dat/namelbltypevaldate.txt
==4331==
computed information for records > 1 year from now
Hanif Hefaz BA123HB 0 251.25 20180101
Jacki Shroff UP673MK 1 4501.48 20170512
all stored information
Hanif Hefaz BA123HB 0 251.25 20180101
Jacki Shroff UP673MK 1 4501.48 20170512
==4331==
==4331== HEAP SUMMARY:
==4331== in use at exit: 0 bytes in 0 blocks
==4331== total heap usage: 12 allocs, 12 frees, 5,333 bytes allocated
==4331==
==4331== All heap blocks were freed -- no leaks are possible
==4331==
==4331== For counts of detected and suppressed errors, rerun with: -v
==4331== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Always confirm that you have freed all memory you have allocated and that there are no memory errors.
Let me know if you have further questions (which will probably warrant a new question at this point)