Можно ли контролировать ввод данных

Можно ли в си как-то контролировать ввод данных? Например, чтобы в строку вводились только буквы? А на ввод чисел только из определенного диапазона, и если вводились какие-то другие символы не завершалась программа?

struct database {
    char name[30];
    char gender[5];
    int age;
    char educ[11];
};

struct database inputDB (void) {
    struct database db;
    int br;
    char * nm, * edc, * gend;

    puts ("Имя:");
    fgets (db.name, 30, stdin);
    nm = strchr (db.name, '\n');
    if ( nm ) *nm = '\0';
    else while (nm);

    puts("Пол:");
    do {
        fgets(db.gender, 5, stdin);
        gend = strchr (db.gender, '\n');
    } while ((strcmp(gend, "М")!=0) || (strcmp(gend, "Ж")!=0));
    if (gend) *gend = '\0';
    else while (getchar()!='\n');

    puts("Образование:");
    do {
        fgets(db.educ, 11, stdin);
        edc = strchr (db.educ, '\n');
    } while((strcmp(edc, "школа")!=0) || (strcmp(edc, "колледж")!=0) || (strcmp(edc, "колледж")!=0));
    if (edc) *edc='\0';
    else while(getchar()!='\n');

    return db;
}


Ответы (2 шт):

Автор решения: Анонимный Паша

Смотря какая платформа, если под Windows, вам поможет _getch, если Unix, то нужна библиотека ncurses

→ Ссылка
Автор решения: avp

Занудное это занятие, программа ввода полей с терминала.

Для простоты я ограничился ascii буквами в имени. Может быть завтра запрограммирую и русские (в utf-8).

Вот такая простыня получается:

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


struct database {
    char name[30];
    char gender[5];
    int age;
    char educ[11];
};


int
trim_str_word (char *str)
{
  int n = 0;

  int i;
  for (i = 0; str[i] && isspace(str[i]); i++);
  for (; str[i] && !isspace(str[i]); i++)
    str[n++] = str[i];
  str[n] = 0;

  return n;
}

int
skip_nl ()
{
  int c;

  while ((c = getchar()) != EOF)
    if (c == '\n')
      break;

  return c;
}

int
input_str_field (const char *prompt, char *field, int f_size)
{
  int rc = EOF, n  = 0;

  printf("%s: ", prompt); fflush(stdout);
  while (fgets(field, f_size, stdin)) {
    if (field[strlen(field) - 1] != '\n')
      if ((rc = skip_nl()) == EOF)
        break;
    rc = 0;
    if ((n = trim_str_word(field)) != 0)
      break;
    puts("Empty input, try again");
    rc = EOF;
  }
  
  return rc == EOF ? rc : n;  
}

int
input_num_field (const char *prompt, int *pv)
{
  char str[32];
  char *ep;

  do {
    if (input_str_field(prompt, str, 31) == EOF)
      return EOF;
    long v = strtol(str, &ep, 10);
    *pv = v;
    if (*ep)
      puts("Invalid number, try again");
  } while (*ep);

  return 0;
}

int
inputDB (struct database *pdb)
{
  int nf = 0;
  
  do {
    if (input_str_field("name", pdb->name, sizeof(pdb->name) - 1) == EOF)
      return EOF;
    int goodname = 1;
    for (char *p = pdb->name; *p; p++)
      if (!isalpha(*p)) {
        puts("Invalid character, try again");
        goodname = 0;
        break;
      }
    if (goodname)
      break;
  } while (1);
  nf++;
  
  do {
    if (input_str_field("gender", pdb->gender, sizeof(pdb->gender) - 1) == EOF)
      return EOF;
    if (strcmp(pdb->gender, "M") == 0)
      break;
    if (strcmp(pdb->gender, "W") == 0)
      break;
    puts("invalid gender, try again");
  } while (1);
  nf++;

  do {
    if (input_num_field("birth year", &pdb->age) == EOF)
      return EOF;
    if (pdb->age < 1950 || pdb->age > 2010) {
      puts("invalid birth year, try again");
      continue;
    }
    pdb->age = 2022 - pdb->age; // конечно, тут надо вместо 2022 взять текущий год у системы
    break;
  } while (1);
  nf++;

  static const char *edu[] = {"scool", "college", "academy", 0};
  int found = 0;
  do {
    if (input_str_field("education", pdb->educ, sizeof(pdb->educ) - 1) == EOF)
      return EOF;
    for (int i = 0; edu[i]; i++)
      if (strcmp(pdb->educ, edu[i]) == 0) {
        found = 1;
        break;
      }
  } while (!found);
  nf++;

  return nf;
  
}

Я несколько изменил интерфейс функции inputDB() -- она получает адрес заполняемой структуры как параметр и возвращает число заполненных полей или EOF (по концу ввода данных пользователем).


Продолжение.

Если посмотреть на inputDB(), то можно заметить что действия по вводу каждого поля структуры весьма схожи между собой. Это вызывает желание написать подобную функцию, которая позволит осуществлять ввод сразу всех полей любой однотипной struct database структуры по некоторому описанию полей этой структуры, например, вот такому:

struct f_descr {
  char *prompt;
  void *field;
  int f_size;
  int f_type; // 1 string, else numeric
  int (*f_check) (struct f_descr *fdscr);
  void *f_check_args;
};

Тогда мы получаем функцию

int
input_struct (struct database *pdb, struct f_descr *d)
{
  int i = 0;
  for (; d[i].field; i++) {
    int rc = d[i].f_type ?
      input_str_field(d[i].prompt ? d[i].prompt : "", (char *)d[i].field, d[i].f_size) :
      input_num_field(d[i].prompt ? d[i].prompt : "", (int *)d[i].field);
    if (rc == EOF)
      return EOF;

    switch (rc = d[i].f_check(d + i)) {
    case EOF:
      return EOF;
    case -2: // not EOF, but stop input (at least stop input this struct)
      return -2;
    case 1: // Check OK, input next field
      break;
    case 0: // Check fail, no message
      puts("Unconsistent. Try again");
    default: // Check fail, message printed, repeat input
      i--;
      //      continue;
    }    
  }

  return i;
}

И после добавления нескольких вспомогательных функций

// аналогична `trim_str_word()`, но оставляет все "слова" в строке
int
trim_str (char *str)
{
  int n = 0;

  int i, j;
  for (i = 0; str[i] && isspace(str[i]); i++);
  for (j = strlen(str) - 1; j > i && isspace(str[j]); j--);
  for (; i <= j; i++)
    str[n++] = str[i];
  str[n] = 0;

  return n;
}

static int rus_uc[] = {
  0x401, // Ё
  0x451, // ё
  0x410, // А
  0x42F, // Я
  0x430, // а
  0x44F  // я
};
               

int
rusletter_utf8_uc (char *s)
{
  if ((*s & 0xE0) != 0xC0 || (s[1] & 0xC0) != 0x80)
    return 0;
  int r = ((s[0] & 0x1f) << 6) + (s[1] & 0x3f);

  return ((r >= rus_uc[2] && r <= rus_uc[5]) || r == rus_uc[1] || r == rus_uc[0]) ?
    r : 0;
}

int
is_upper_rus (int uc)
{
  return uc == rus_uc[0] || (uc >= rus_uc[2] && uc <= rus_uc[3]);
}

int
check_name (struct f_descr *d)
{
  char *str = (char *)(d->field);

  int c;
  while (c = *str++)
    if (!isalpha(c) && !isspace(c))
      return 0;

  return 1;
}

int
check_rus_name (struct f_descr *d)
{
  char *str = (char *)(d->field);

  if (is_upper_rus(rusletter_utf8_uc(str))) {
    for (int i = 2; str[i]; i++) {
      if (isspace(str[i]))
        continue;
      if (!rusletter_utf8_uc(str + i))
        return 0;
      i++;  // because cyrillic symbols in utf-8 is 2 bytes length
    }

    return 1;
  }
  
  return check_name(d);
}


int
check_str_set (struct f_descr *d)
{
  char **a = (char **)(d->f_check_args);

  for (int i = 0; a[i]; i++) 
    if (strcmp(a[i], (char *)(d->field)) == 0)
      return 1; // OK, found

  printf("input `%s` not in any of: ", (char *)(d->field));
  for (int i = 0; a[i]; i++)
    printf("'%s' ", a[i]);
  printf(" Try again\n");
  
  return -3;  // wrong field, messagee printed, repeat the input
}

int
check_num_bounds (struct f_descr *d)
{
  int *a = (int *)(d->f_check_args);
  int v = *(int *)(d->field);
  
  return  v >= a[0] && v <= a[1];
}

Теперь мы можем писать вот такие компактные программы для ввода данных в достаточно произвольную структуру с желаемой проверкой вводимых полей

int
main()
{
  struct database dbs;
  static const char *valid_g[] = {"m", "f", "М", "Ж", 0};
  static const char *valid_edu[] = {"scool", "школа", "college", "academy", 0};
  static int birth_bnd[2] = {1950, 2010};
  
  struct f_descr dbs_dscr[] = {
    {"Name", dbs.name, sizeof(dbs.name) - 1, 1, check_rus_name, 0},
    {"Gender", dbs.gender, sizeof(dbs.gender) - 1, 1, check_str_set, valid_g},
    {"Birth year", &dbs.age, sizeof(int), 0, check_num_bounds, birth_bnd},
    {"Education", dbs.educ, sizeof(dbs.educ) - 1, 1, check_str_set, valid_edu},
    {0, 0}
  };
  
  int rc = 0;

  while ((rc = input_struct(&dbs, dbs_dscr)) != EOF)
    printf("rc = %d\ndbs: <%s> <%s> <%d> <%s>\n\n", rc,
       dbs.name, dbs.gender, dbs.age, dbs.educ);

  return puts("End") == EOF;
}

P.S.

Я сделал пример провеки вода имени для кодировки utf-8 (см. также Кириллица (блок Юникода)) в предположении, что имя должно начинаться с большой буквы.

Надеюсь, аналогичную проверку для более простого случая однобайтной кодировки русского языка (например, CP-1251) вы сможете запрограммировать сами.

→ Ссылка