4

Hi I am confined to stdio.h, stdlib.h and string.h and I need to ask a user for input - the input can be any number of characters between 1 and 6, however the first two characters MUST be an uppercase alphabetical letter, and the remaining four characters MUST be a number between 0 and 9.

Examples of valid input:

  • AB1
  • AB1234
  • AB
  • A

Examples of Invalid Input:

  • AB12345 (too many characters)
  • 123 (first two characters are not uppercase letters)
  • ABA (a character after the second one is not a numeric value)

Here is my attempt so far (just bear in mind I have almost no experience with C, the likelihood that this solution is "idiomatic" is next to none, and the reason I am asking this is so that I can learn):

Flightcode is a char array defined as flightcode[7] it lives inside another struct called flight. I am fgetsing it into a temp_array[7] first and then strcpying it into the flight->flightcode such that the null terminator is appended and I don't know a better way of doing that.

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

#define MAX_FLIGHTCODE_LEN 6
#define MAX_CITYCODE_LEN 3
#define MAX_NUM_FLIGHTS 50
#define DB_NAME "database"

typedef struct {
  int month;
  int day;
  int hour;
  int minute;
} date_time_t;

typedef struct {
  char flightcode[MAX_FLIGHTCODE_LEN + 1];
  date_time_t departure_dt;
  char arrival_city[MAX_CITYCODE_LEN + 1];
  date_time_t arrival_dt;
} flight_t;

date_time_t departure_dt;
date_time_t arrival_dt;

char * scanline(char *dest, int dest_len);



int main(){

char temp_string[100];
flight_t flight[MAX_NUM_FLIGHTS + 1];
int correct_code = 0;

printf("Enter flight code>\n");

scanline(temp_string, sizeof(flight->flightcode));
strcpy(flight->flightcode, temp_string);

while(correct_code == 0)
{
  for(int i = 0; flight->flightcode[i] != '\0' && correct_code == 0; i++)
  {
    while((i < 2 && (flight->flightcode[i] <= 64 || flight->flightcode[i] >= 91)) || (i > 1 && (flight->flightcode[i] < 48 || flight->flightcode[i] >= 58)))
    {
      printf("Invalid input.\n");

      scanline(temp_string, sizeof(flight->flightcode));
      strcpy(flight->flightcode, temp_string);
    }
    if((i < 2 && (flight->flightcode[i] > 64 || flight->flightcode[i] < 91)) || (i > 1 && (flight->flightcode[i] >= 48 || flight->flightcode[i] < 58)))
    {
      correct_code = 1;
    }
  }
}

}

char * scanline(char *dest, int dest_len){
  int i, ch;
  i = 0;
  for (ch = getchar();
       ch != '\n' && ch != EOF && i < dest_len -1; ch = getchar())
      dest[i++] = ch;
  dest[i] = '\0';

  while (ch != '\n' && ch != EOF)
    ch = getchar();

  return (dest);
}
Davide Lorino
  • 875
  • 1
  • 9
  • 27
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/192757/discussion-on-question-by-davide-lorino-type-checking-arbitrary-length-array-in). –  May 03 '19 at 08:06
  • Can you use the character classification functions from ctypes.h? – Serge Ballesta May 03 '19 at 08:17
  • @SergeBallesta I cannot unfortunately, I am doing the character checking with the ascii equivalent. – Davide Lorino May 03 '19 at 08:23
  • @DavideLorino Remember to accept the answer if it is ok. – Costantino Grana May 03 '19 at 08:40
  • What exactly is your question? If it's about the quality of your solution in general than it's probably more suited for [Code Review](https://codereview.stackexchange.com/). – r3mus n0x May 03 '19 at 08:41
  • @r3musn0x the question is: how may I achieve behaviour that matches the criteria specified? I need to prompt a user for input. The input may contain anywhere between 1 and 6 characters. The first two characters (if there are two) must be alphabetical A-Z and any after that (if there are any) must be numbers between 1 and 9. If the user input does not conform to the criteria, the user is prompted again to input correct values. – Davide Lorino May 03 '19 at 08:47

4 Answers4

2

Your function scanline does not do much more than the standard function fgets. I propose to use the standard function instead. Removing the trailing newline '\n' is easy.

I have split the checks into 3 parts:

  • Check the length to be more than 0 and not more than MAX_FLIGHTCODE_LEN.
  • Check the first 2 characters to be uppercase letters A..Z
  • Check the remaining characters to be digits 0..9

Proposed code:

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

#define MAX_FLIGHTCODE_LEN 6
#define MAX_CITYCODE_LEN 3
#define MAX_NUM_FLIGHTS 50
#define DB_NAME "database"

typedef struct {
  int month;
  int day;
  int hour;
  int minute;
} date_time_t;

typedef struct {
  char flightcode[MAX_FLIGHTCODE_LEN + 1];
  date_time_t departure_dt;
  char arrival_city[MAX_CITYCODE_LEN + 1];
  date_time_t arrival_dt;
} flight_t;

date_time_t departure_dt;
date_time_t arrival_dt;


int main(void){

  char temp_string[100];
  flight_t flight[MAX_NUM_FLIGHTS + 1];
  int correct_code;
  size_t len;
  int i;

  do
  {
    /* we first assume the code is correct and set this to 0 on any error */
    correct_code = 1;
    printf("Enter flight code>\n");

    if(fgets(temp_string, sizeof(temp_string), stdin) == NULL)
    {
        if(feof(stdin)) fprintf(stderr, "no input (EOF)\n");
        else perror("fgets");
        correct_code = 0;
        temp_string[0] = '\0';
    }

    if(correct_code)
    {
      len = strlen(temp_string);

      /* cut off newline
       * Use a loop to handle CR and LF just in case Windows might leave more than one character */
      while((len > 0) &&
            ((temp_string[len - 1] == '\n') ||
             (temp_string[len - 1] == '\r')))
      {
        len--;
        temp_string[len] == '\0';
      }

      if(len > MAX_FLIGHTCODE_LEN)
      {
        correct_code = 0;
        fprintf(stderr, "Input must not be longer than %d characters.\n", MAX_FLIGHTCODE_LEN);
      }

      if(len == 0)
      {
        correct_code = 0;
        fprintf(stderr, "Empty input.\n");
      }
    }

    /* check first two letters */
    for(i = 0; (i < 2) && (i < len) && correct_code; i++)
    {
      /* you could use function isupper when you make sure the locale is set to "C" */
      if((temp_string[i] < 'A') || (temp_string[i] > 'Z'))
      {
        correct_code = 0;
        fprintf(stderr, "first two characters must be uppercase letters. Found '%c' at position %d\n", temp_string[i], i);
      }
    }

    /* check digits starting from 3rd character */
    for(i = 2; (i < MAX_FLIGHTCODE_LEN) && (i < len) && correct_code; i++)
    {
      /* you could use function isdigit here */
      if((temp_string[i] < '0') || (temp_string[i] > '9'))
      {
        correct_code = 0;
        fprintf(stderr, "Third to last characters must be digits. Found '%c' at position %d\n", temp_string[i], i);
      }
    }

    if(correct_code)
    {
      /* we already checked that length is not more than MAX_FLIGHTCODE_LEN, so we don't need strncpy to avoid buffer overflow */
      strcpy(flight->flightcode, temp_string);
      printf("Valid code: %s\n", flight->flightcode);
    }
    else
    {
      fprintf(stderr, "Invalid code.\n");
    }
  } while(!correct_code);

  return 0;

}
Bodo
  • 9,287
  • 1
  • 13
  • 29
  • Thank you I appreciate this answer, this however does not work. Have you compiled this and tested it with values such as 'AB1' and 'AB1234'? It does not work unfortunately. Also, in ANSI C, (gcc compiler with option -ansi) the newline character is fed into fgets, fgetc, scanf, any user input scanner. – Davide Lorino May 03 '19 at 09:20
  • Ok, try to supply your fgets with 'AB1' and you will see that it does not accept it – Davide Lorino May 03 '19 at 09:47
  • I do understand your implementation, however on a gcc compiler with -ansi this does not work. – Davide Lorino May 03 '19 at 10:20
  • @DavideLorino If "does not work" means that the `gcc -ansi` does not allow C++ style comments I just changed the comments to C style. According to the requirement (from a comment to the question) to repeat on invalid input I also added a loop. – Bodo May 03 '19 at 11:35
2

You have a requirement that does not fit well with what scanf can easily do, so I would stay away from it, and use fgets as a primary read utility.

But as the number of acceptable uppercase and digit characters is not fixed by only limited I would use a custom parser based on a state machine. It is probably not the most elegant nor efficient way but it is simple, robust and easy to maintain.

Just to demonstrate it, I have allowed blank characters before the first uppercase one and spaces after the last digit. So the following code accept an arbitrary long line following this regex pattern [ \t]*[A-Z]{1,maxupper}[0-9]{0,maxdigit}\s* provided it receives a buffer of size at least maxupper+maxupper+1. It returns a pointer to the buffer is successful or NULL if not.

As you have said that you could not use the ctype macros, I have defined ASCII (or any charset derived from ASCII) equivalent for the ones I have used.

#define TRUE 1
#define FALSE 0

inline int isupper(int c) {
    return c >= 'A' && c <= 'Z';  // only for ASCII and derived
}
inline int isdigit(char c) {
    return c >= '0' && c <= '9';    // guarantee per standard
}
inline int isblank(int c) {
    return c == ' ' || c == '\t';
}
inline int isspace(int c) {
    static const char spaces[] = " \t\r\n\v";
    for(const char *s=spaces; *s != '\0'; s++) {
        if (c == *s) return TRUE;
    }
    return FALSE;
}

char *get_string(char *buffer, int maxupper, int maxdigit, FILE *fd) {
    char buf[16];      // any size >=2 will fit
    char *cur = buffer;
    int state = 0, uppersize=0, digitsize=0;
    for (;;) {         // allow lines longer than buf
        if (NULL == fgets(buf, sizeof(buf), fd)) {
            *cur = '\0';           // EOF: do not forget the terminating NULL
            return state >= 1 ? buffer : NULL;   // must have at least 1 char
        }
        for (char *b=buf; *b!='\0'; b++) {
            switch(state) {
                case 0:   // spaces before first uppercase
                    if (isblank(*b)) break;
                    state++;
                case 1:   // first uppercase
                    if (! isupper(*b)) {
                        state = 5;    // must read up to \n
                        break;
                    }
                    state++;
                case 2:   // process uppercase chars
                    if (! isupper(*b)) {
                        if (uppersize > 0) state++;
                        else  {
                            state = 5;    // must read up to \n
                            break;
                        }
                    }
                    else {
                        if (uppersize >= maxupper)  {
                            state = 5;    // must read up to \n
                            break;
                        }
                        *cur++ = *b;
                        uppersize++;
                        break;
                    }
                case 3:   // process digit chars
                    if (! isdigit(*b)) {
                        state++;
                    }
                    else {
                        if (digitsize >= maxdigit)  {
                            state = 5;    // must read up to \n
                            break;
                        }
                        *cur++ = *b;
                        digitsize++;
                        break;
                    }
                case 4:    // allow spaces after last digit
                    if ('\n' == *b) {
                        *cur = '\0';
                        return buffer;
                    }
                    if (! isspace(*b)) state++
                    break;
                case 5:    // on error clean end of line
                    if ('\n' == *b) return NULL;
            }
        }
    }
}

Then in your code, you simply calls it that way:

...
printf("Enter flight code>\n");
if (NULL == get_string(flight->flightcode, 2, 4, stdin)) {
    // process the error
    ...
}
...
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • I really appreciate your answer but I am unable to use `isspace()` or `issupper()` etc as they all belong to ctype.h – Davide Lorino May 03 '19 at 09:27
  • @DavideLorino: my code does not include ctype.h but defines functions for `isspace`, `isupper` and `isblank`. They are simply compatible with the ones from ctype.h in order to not disturb a future reader. If it is a homework assignment, just use different names ;-) – Serge Ballesta May 03 '19 at 09:29
  • This solution is very sophisticated, I actually don't know how to use this. I am trying to read user input, not a file. Can you show an example of how this function would be called? – Davide Lorino May 03 '19 at 09:37
  • @DavideLorino: I have improved my code to clean up to `\n` on error – Serge Ballesta May 03 '19 at 15:40
2

Scansets and the %n specifier could be used to parse the input.
The format string "%n%2[A-Z]%n%4[0-9]%n" uses the %n specifier in three places to capture the number of characters processed. The scanset %2[A-Z] will scan up to two characters if the characters are in the set of upper case letters. %4[0-9] will scan up to four characters if the characters are digits.
If two values are scanned by sscanf, the number of characters processed are subtracted to make sure there are two leading upper case characters and six or fewer total character and the trailing character is the terminating zero.

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

#define MAX_FLIGHTCODE_LEN 6
#define MAX_CITYCODE_LEN 3
#define MAX_NUM_FLIGHTS 50
#define DB_NAME "database"

typedef struct {
    int month;
    int day;
    int hour;
    int minute;
} date_time_t;

typedef struct {
    char flightcode[MAX_FLIGHTCODE_LEN + 1];
    date_time_t departure_dt;
    char arrival_city[MAX_CITYCODE_LEN + 1];
    date_time_t arrival_dt;
} flight_t;

date_time_t departure_dt;
date_time_t arrival_dt;

char * scanline(char *dest, int dest_len);

int main(){
    int head = 0, leading = 0, tail = 0;
    int correct_code = 0;
    int result = 0;
    char temp_string[100];
    char upper[3] = "";
    char digits[5] = "";
    flight_t flight[MAX_NUM_FLIGHTS + 1];
    do {
        printf("Enter flight code>\n");

        scanline(temp_string, sizeof(temp_string));
        if ( 0 < ( result = sscanf ( temp_string, "%n%2[A-Z]%n%4[0-9]%n", &head, upper, &leading, digits, &tail))) {
            if ( 1 == result && 0 == temp_string[leading]) {
                correct_code = 1;
                break;
            }
            if ( 2 == result && 2 == leading - head && 7 > tail - head && 0 == temp_string[tail]) {
                correct_code = 1;
            }
            else {
                printf ( "invalid input\n");
            }
        }
        else {
            printf ( "invalid input\n");
        }
    } while(correct_code == 0);
    printf ( "Input is: %s\n", temp_string);
    strcpy(flight->flightcode, temp_string);
    return 0;
}

char * scanline(char *dest, int dest_len){
    int i, ch;
    i = 0;
    for (ch = getchar(); ch != '\n' && ch != EOF && i < dest_len -1; ch = getchar()) {
        dest[i++] = ch;
    }
    dest[i] = '\0';

    while (ch != '\n' && ch != EOF) {
        ch = getchar();
    }

    return dest;
}
xing
  • 2,125
  • 2
  • 14
  • 10
  • This is amazing, this is 99.9% of what I need. Only one issue, for some reason this prints "Enter flight code>\n" twice instead of once on the first time? Do you know how to make it only print it once? – Davide Lorino May 03 '19 at 10:38
  • I will select this as the answer if we can fix the fact that it prints "Enter flight code>\n" twice on the first time – Davide Lorino May 03 '19 at 10:42
1

First thing, realize that your question text is missing a question. Moreover, your question title makes no sense.

Anyway, here it is a possible, purposely very ugly, solution. Approach: you want to do X, so you write the code to do X. Let's start with scanline():

int scanline(char *dest, int dest_len)
{
    int i = 0;
    int ch;
    while (1) {
        // Read
        ch = fgetc(stdin);
        // Check
        if (ch == EOF)
            break;
        if (ch == '\n')
            break;
        if (i >= dest_len - 1)
            break;
        // Use
        dest[i] = ch;
        ++i;
    }
    dest[i] = 0;

    // Is the string finished? Ok!
    if (ch == '\n' || ch == EOF)
        return 1;

    // Otherwise discard the rest of the line. Not ok!
    while (ch != '\n' && ch != EOF)
        ch = fgetc(stdin);
    return 0;
}

I know this is ugly, but I believe that it is helpful to clarify the three steps involved in file input: read, check, use. Note that it returns true if the line was up to the required number of characters (one less than the buffer size to accomodate for the terminator.

Then you want to check if:

  1. scanline() is successful
  2. there is at least one character.
  3. character 0 is between 'A' and 'Z'
  4. character 1 is between 'A' and 'Z'
  5. character 2 is between '0' and '1'
  6. character 3 is between '0' and '1'
  7. character 4 is between '0' and '1'
  8. character 5 is between '0' and '1'

Lets write the code for that:

int main(void) 
{
    flight_t flight;

    while (1) {
        printf("Enter flight code>\n");
        if (!scanline(flight.flightcode, sizeof(flight.flightcode))) {
            printf("Too many characters.\n");
            continue;
        }
        int i = 0;
        if (flight.flightcode[i] == 0) {
            printf("Empty input.\n");
            continue;
        }
        if (flight.flightcode[i] < 'A' || flight.flightcode[i] > 'Z') {
            printf("Character %d is not upper case.\n", i);
            continue;
        }
        i++;
        if (flight.flightcode[i] == 0)
            break;
        if (flight.flightcode[i] < 'A' || flight.flightcode[i] > 'Z') {
            printf("Character %d is not upper case.\n", i);
            continue;
        }
        i++;
        if (flight.flightcode[i] == 0)
            break;
        if (flight.flightcode[i] < '0' || flight.flightcode[i] > '9') {
            printf("Character %d is not a digit.\n", i);
            continue;
        }
        i++;
        if (flight.flightcode[i] == 0)
            break;
        if (flight.flightcode[i] < '0' || flight.flightcode[i] > '9') {
            printf("Character %d is not a digit.\n", i);
            continue;
        }
        i++;
        if (flight.flightcode[i] == 0)
            break;
        if (flight.flightcode[i] < '0' || flight.flightcode[i] > '9') {
            printf("Character %d is not a digit.\n", i);
            continue;
        }
        i++;
        if (flight.flightcode[i] == 0)
            break;
        if (flight.flightcode[i] < '0' || flight.flightcode[i] > '9') {
            printf("Character %d is not a digit.\n", i);
            continue;
        }
        i++;
        if (flight.flightcode[i] == 0)
            break;
    }
}

Some remarks:

  1. in your code you set correct_code to 1 as soon as the first character was ok. If you want to loop through the characters you must check if there is an error and exit the loop.
  2. don't use ASCII codes when you have the specific character literals available.
  3. I suggest that you take my solution and, as an exercise fix it to be able to work with arbitrary MAX_FLIGHTCODE_LEN, and possibly with arbitrary number of letters and numbers. Of course MAX_FLIGHTCODE_LEN shall be equal to their sum!
  4. Drop the useless requirement for not using <ctype.h>, and use also <stdbool.h>, which makes the programmer intention clearer.
Costantino Grana
  • 3,132
  • 1
  • 15
  • 35
  • Thank you I really appreciate this. I have compiled this and it does not accept an input such as 'AB1' - it says "character 1 is not upper case". I would love to drop that requirement as I would have been finished long ago, but unfortunately I must stick to these header files. The user needs to be able to input any number of characters between 1 and 6, however the first two characters (if there are two) must be A-Z, and if there are any characters after the first two, they must be between 0 and 9. – Davide Lorino May 03 '19 at 08:45