正如其他人已经说过的,您可以检查fscanf 的返回值以找出一行是否为给定格式。然而,这不是理想的方法。首先,您的数据是按行组织的,但fscanf 将换行符视为任何其他空格。您可以先阅读带有fgets 的行,然后在该行上应用sscanf,但您仍然有一个很容易忘记的大型单一格式说明符。
我想提出另一种方法。你的数据行似乎是按字段组织的,它们之间用制表符分隔。您可以使用fgets 读取这些行,然后使用strtok 拆分它们,最后使用sscanf 扫描单独的字段。如果您将自定义包装函数写入 sscanf 语句,则可以在读取数据时对其进行完整性检查。
/*
* Return true if str has format "hh:min AM/PM"
*/
int scan_time(const char *str, int *hh, int *mm)
{
char buf[4] = {0};
int n;
char c;
n = sscanf(str, "%d:%d%4s %c", hh, mm, buf, &c);
if (n == 4) return 0; /* trailing extra chars */
if (n < 2) return 0; /* missing minutes */
if (n == 3) {
int key = (buf[0] << 16) + (buf[1] << 8) + buf[2];
#define KEY(a, b) ((a << 16) + (b << 8))
switch (key) {
case KEY('a', 'm'):
case KEY('A', 'M'):
break;
case KEY('p', 'm'):
case KEY('P', 'M'):
*hh += 12;
break;
default:
return 0; /* invalid am/pm spec */
}
}
if (*hh < 0 || *hh >= 24) return 0; /* invalid hours */
if (*mm < 0 || *mm >= 60) return 0; /* invalid minutes */
return 1;
}
/*
* Return true, if str has format "mm/dd/year"
*/
int scan_date(const char *str, int *yy, int *mm, int *dd)
{
static const int mdays[] = {
0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};
int n;
char c;
n = sscanf(str, "%d/%d/%d %c", mm, dd, yy, &c);
if (n == 4) return 0; /* trailing extra chars */
if (n < 2) return 0; /* missing day */
if (n == 2) *yy = 2014; /* set default value */
if (*yy < 100) *yy += 2000; /* allow 1/1/14 */
if (*mm < 1 || *mm > 12) return 0; /* invalid month */
if (*dd < 1 || *dd > mdays[*mm]) return 0;
if (*mm == 2 && *dd == 29 % *yy % 4) return 0;
/* invalid day */
return 1;
}
/*
* Return true if line is "date \t time \t time \t text"
*/
int scan_line(char *str, struct Event *ev)
{
char *token;
token = strtok(str, "\t");
if (token == NULL) return 0;
if (!scan_date(token, &ev->year, &ev->month, &ev->day)) return 0;
token = strtok(NULL, "\t");
if (token == NULL) return 0;
if (!scan_time(token, &ev->startH, &ev->startM)) return 0;
token = strtok(NULL, "\t");
if (token == NULL) return 0;
if (!scan_time(token, &ev->endH, &ev->endM)) return 0;
token = strtok(NULL, "\t");
if (token == NULL) return 0;
strncpy(ev->event, token, 40);
return 1;
}
/*
* Remove trailing newline
*/
void chomp(char *str)
{
int l = strlen(str);
if (l && str[l - 1] == '\n') str[l - 1] = '\0';
}
/*
* Scan file with events
*/
int scan_file(const char *fn)
{
FILE *f = fopen(fn, "r");
if (f == NULL) return -1;
for (;;) {
struct Event ev;
char line[200];
if (fgets(line, 200, f) == NULL) break;
chomp(line);
if (scan_line(line, &ev)) {
printf("%s on %d/%d/%d\n",
ev.event, ev.month, ev.day, ev.year);
}
}
return 0;
}
这里,scan_xxx 函数扫描一条数据,检查格式,分配数据并对数据进行基本检查,这样您就永远不会在 1 月 32 日或 35 日收到事件: 00小时。
这使得扫描功能比单次调用sscanf 更复杂,但也有一些好处。首先,在读取格式时进行检查。这意味着您不必检查客户端代码中的数据,因为您可以依赖合理的值。这也意味着您不必重复代码:请注意时间检查是如何只编码一次的,即在scan_time 中,尽管对于开始时间和结束时间每行应用两次。
在封装函数中按字段处理数据允许您更改格式。例如,您可以允许“1pm”作为“1:00 pm”的有效快捷方式。当第一种格式失败时,您只需要使用第二种格式字符串重新扫描您的时间字段。您也可以使用长单行格式来做到这一点,但由于您有两个时间字段,这并不容易。
还要注意上面的代码如何接受 14 作为 2014 的快捷方式,并将缺失的年份解释为 2014。所有这些对于简单的数据扫描工具来说似乎有点过于复杂,但您可以在类似的项目中重复使用您的函数。此外,编写这些整洁的函数比纠缠冗长的scanf 格式更有趣。