/* Contest Arbitrator v0.3.2 by Dmitry Gorokh, UR4MCK This program is FREEWARE! Changes: 11.03.2015: skiping hidden files and special dirs inside the logs directory */ #include #include #include #include #include #include #include #include #include "config.h" #include "main.h" #include "cabrillo.h" #include "contests/wakeup.h" #include "contests/general.h" #include "report.h" #define OPTION_LEN (MAX_STR) /* Max option length */ /* Configuration file parameter names */ #define CONFIG_PARAM_NAME "name" /* Contest name */ #define CONFIG_PARAM_ID "id" /* Contest id */ #define CONFIG_PARAM_START "start" /* Contest start date & time */ #define CONFIG_PARAM_END "end" /* Contest end date & time */ #define CONFIG_PARAM_ROUNDS "rounds" /* Number of rounds */ #define CONFIG_PARAM_MAX_DT "max_dt" /* Maximum allowable difference in time */ #define CONFIG_PARAM_BAND_LIST "band_list" /* Band list */ #define CONFIG_PARAM_MODE_LIST "mode_list" /* Mode list */ #define CONFIG_PARAM_SIGNATURE "signature" /* Judge's signature */ #define CONFIG_PARAM_SKIP_RST_SNT "skip_rst_snt" /* Sent RST checking */ #define CONFIG_PARAM_SKIP_RST_RCV "skip_rst_rcv" /* Received RST checking */ #define CONFIG_PARAM_SKIP_NUM_SNT "skip_num_snt" /* Sent incremental number checking */ #define CONFIG_PARAM_SKIP_NUM_RCV "skip_num_rcv" /* Received incremental number checking */ #define CONFIG_PARAM_SKIP_CHK_SNT "skip_chk_snt" /* Sent check number checking */ #define CONFIG_PARAM_SKIP_CHK_RCV "skip_chk_rcv" /* Received check number checking */ #define CONFIG_PARAM_CFM_UNRESOLVED "cfm_unresolved"/* Minimum number of unresolved QSOs to confirm them as valid */ #define CONFIG_PARAM_REPORT_DIR "report_dir" /* Log analyzis reports directory */ #define CONFIG_PARAM_UBN_DIR "ubn_dir" /* UBN files directory */ #define CONFIG_PARAM_LOG_EMAILS "log_emails" /* Log submissions emails list */ #define CONFIG_PARAM_WWW "www" /* Official results URL */ #define CONFIG_PARAM_UNIQ_MULT "uniq_mult" /* Flag to count a multiplier only once */ #define CONFIG_PARAM_STRICT_EVAL "strict_eval" /* Flag to perform strict evaluation */ #define CONFIG_PARAM_ALLOW_DUPES "allow_dupes" /* Flag to allow duplicate QSOs */ #define CONFIG_PARAM_SEARCH_TAGS "search_tags" /* Flag to search similar tags upon log errors */ #define CONFIG_PARAM_IGNORE_TAGS "ignore_tags" /* Flag to ignore unknown tags */ /* Score table entry */ typedef struct { HAM_S *ham; /* Pointer to ham record in the database */ unsigned int qso; /* Claimed QSOs */ unsigned int cfm; /* Confirmed QSOs */ } SCORE_S; CONFIG_S config; /* Configuration structure */ static BASE_S base; /* Participant's data base */ /* TODO: replace with callback funcs */ extern int parse_logs(CONFIG_S *config, BASE_S *base); extern int free_logs(BASE_S *base); extern int cross_check(BASE_S *base, int contest_id); extern int strip_leading_spaces(char *str); extern int strip_trailing_spaces(char *str); static void usage(void); static int get_config(char *file); static int parse_config(char *name, char *value, char **err_msg); static LOG_FILE_S *alloc_list(char *name); static void free_list(LOG_FILE_S *ptr); static int score_table(char *name, time_t start, time_t end, BASE_S *base); int main(int argc, char *argv[]) { /* Program entry */ int opt, n, have_config; LOG_FILE_S *cur, *prev; DIR *dir; time_t t; struct tm tm; struct dirent *dir_entry; char str[MAX_STR], *s; DEBUG("==> main(%d, %p)\n", argc, argv); printf("Contest Arbitrator version %s\n" " (c) 2009-2014 by Dmitry Gorokh, UR4MCK\n" " This program is FREEWARE\n\n", VERSION); /* Initialization */ memset(&config, 0, sizeof(config)); memset(&base, 0, sizeof(base)); cabrillo_init(); have_config = 0; /* Check for supplied options */ while ((opt = getopt(argc, argv, "hc:d:o:r:u:")) != EOF) { switch (opt) { case 'c': /* Get configuration from file */ if (strlen(optarg) < OPTION_LEN) { have_config = 1; /* Configuration file name is supplied from command line */ if (get_config(optarg) != 0) return -16; /* Error in configuration file */ } else { fprintf(stderr, "Invalid configuration file: %s\n", optarg); return -1; } break; case 'd': /* Set directory which contains log files */ if (strlen(optarg) < OPTION_LEN) config.log_dir = optarg; else { fprintf(stderr, "Invalid directory: %s\n", optarg); return -2; } break; case 'o': /* Set output file */ if (strlen(optarg) < OPTION_LEN) config.out_file = optarg; else { fprintf(stderr, "Invalid output file: %s\n", optarg); return -3; } break; case 'r': /* Set directory for log analyzis reports */ if (strlen(optarg) < OPTION_LEN) strcpy(config.report_dir, optarg); else { fprintf(stderr, "Invalid log analyzis reports directory: %s\n", optarg); return -4; } break; case 'u': /* Set directory for UBN files */ if (strlen(optarg) < OPTION_LEN) strcpy(config.ubn_dir, optarg); else { fprintf(stderr, "Invalid UBN files directory: %s\n", optarg); return -5; } break; case 'h': default: usage(); return 1; } } /* Read configuation from file (if not already done) */ if (!have_config && !config.contest.valid && get_config(DEFAULT_CONFIG_FILE) != 0) { fprintf(stderr, "Warning: Using defaults for contest configuration\n"); /* Set contest configuration defaults */ strcpy(config.contest.name, DEFAULT_CONTEST_NAME); config.contest.id = CONTEST_ID_UNDEFINED; time(&t); gmtime_r(&t, &tm); tm.tm_sec = tm.tm_min = tm.tm_hour = 0; config.contest.start = timegm(&tm); config.contest.end = config.contest.start + DEFAULT_CONTEST_DURATION; config.contest.rounds = 1; config.contest.max_dt = DEFAULT_TIME_DIFFERENCE; strcpy(config.contest.band_list, DEFAULT_BAND_LIST); strcpy(config.contest.mode_list, DEFAULT_MODE_LIST); config.contest.skip_rst_snt = 0; config.contest.skip_rst_rcv = 0; config.contest.skip_num_snt = 0; config.contest.skip_num_rcv = 0; config.contest.cfm_unresolved = DEFAULT_CFM_UNRESOLVED; config.contest.valid = 1; config.contest.uniq_mult = 0; config.contest.strict_eval = 0; config.contest.allow_dupes = 0; config.contest.search_tags = 0; config.contest.ignore_tags = 0; } /* Assign log file names supplied with command line (if any) */ prev = NULL; while (optind < argc) { DEBUG("Log file: %s\n", argv[optind]); cur = alloc_list(argv[optind++]); if (!prev) config.file_list = cur; else prev->next = cur; prev = cur; } /* Read log files from the supplied directory */ if (config.log_dir) { /* Remove trailing slashes */ n = strlen(config.log_dir); /* Directory path length */ s = config.log_dir + n - 1; while (n > 0 && (*s == '/' || *s == '\\')) { *s-- = '\0'; n--; } /* Read directory contents */ DEBUG("Reading directory: %s\n", config.log_dir); if (!(dir = opendir(config.log_dir))) { perror("opendir"); return -12; } while ((dir_entry = readdir(dir))) { /* Read each directory entry */ #ifndef __WIN32__ if (dir_entry->d_type == DT_REG) { /* Only read regular files */ #else if (dir_entry->d_name && dir_entry->d_name[0] != '.') { /* Skip hidden files and current/parent directories */ #endif /* Make full path to the log file */ if (strlen(dir_entry->d_name) + n >= MAX_STR - 1) fprintf(stderr, "WARNING: Log file path is too long: %s\\%s\n", config.log_dir, dir_entry->d_name); else { sprintf(str, "%s/%s", config.log_dir, dir_entry->d_name); DEBUG("Log file: %s\n", str); cur = alloc_list(str); if (!prev) config.file_list = cur; else prev->next = cur; prev = cur; } } } closedir(dir); } /* Assign default reports & ubn directories if they are absent */ if (config.report_dir[0] == '\0') strcpy(config.report_dir, DEFAULT_REPORT_DIR); if (config.ubn_dir[0] == '\0') strcpy(config.ubn_dir, DEFAULT_UBN_DIR); /* Open output file */ if (!config.out_file || (config.out_file && strcasecmp(config.out_file, "stdout") == 0)) config.out_fd = stdout; /* Use stdout as output file */ else if (!(config.out_fd = fopen(config.out_file, "w"))) { perror("fopen"); return -17; /* Unable to open output file */ } /* Print header to the output file */ out(NULL, "*** Contest Arbitrator v.%s results file ***\n", VERSION); if (time(&t) > 0) out(NULL, "Created: %s\n", ctime(&t)); /* Prepare functions according to contest id */ switch (config.contest.id) { case CONTEST_ID_RUQRP_WAKEUP: if (wakeup_config(config.contest.start, config.contest.end, config.contest.mode_list, config.contest.band_list, config.contest.rounds) != 0) { out(NULL, "ERROR: Unable to configure \"%s\" contest module. Aborting.\n", config.contest.name); return -32; } break; case CONTEST_ID_UNDEFINED: out(NULL, "WARNING: Contest type is not defined in configuration file\n"); case CONTEST_ID_GENERAL: default: if (general_config(config.contest.start, config.contest.end, config.contest.mode_list, config.contest.band_list, config.contest.rounds) != 0) { out(NULL, "ERROR: Unable to configure \"%s\" contest module. Aborting.\n", config.contest.name); return -33; } config.contest.id = CONTEST_ID_GENERAL; /* Forcing general contest evaluation */ } out(NULL, "\nContest: %s\n\n", config.contest.name); out(NULL, "Analyzing contest logs...\n\n", n); if ((n = parse_logs(&config, &base)) < 0) { out(NULL, "ERROR: There are an errors in the input logs. See analysis information above\n"); } else { if (n == 0) out(NULL, "WARNING: There is seems to be no correct contest logs found. Aborting\n"); else { out(NULL, "\n%u log files have been analyzed. Ready to start contest judgement\n", n); /* General cross-check */ cross_check(&base, config.contest.id); /* Contest specific cross-check */ switch (config.contest.id) { case CONTEST_ID_RUQRP_WAKEUP: wakeup_eval(&base); break; /* TODO: other contests */ case CONTEST_ID_GENERAL: general_eval(&base); break; } /* Show scoring */ score_table(config.contest.name, config.contest.start, config.contest.end, &base); /* Update UBN files */ report_ubn(&base, &config); /* Free memory */ switch (config.contest.id) { case CONTEST_ID_RUQRP_WAKEUP: wakeup_free(); break; /* TODO: other contests */ case CONTEST_ID_GENERAL: general_free(); break; } free_logs(&base); /* Release memory occupied by the database */ unresolved_free(); } } /* Close output file */ if (config.out_fd && config.out_fd != stdout) fclose(config.out_fd); /* Release log files */ free_list(config.file_list); DEBUG("<== main(%d, %p)\n", argc, argv); return 0; /* Normal exit */ } int out(FILE *report_fd, const char *fmt, ...) { /* Write to output file */ int len; va_list ap; char s[MAX_STR]; va_start(ap, fmt); vsnprintf(s, MAX_STR, fmt, ap); va_end(ap); len = strlen(s); fwrite(s, 1, len, config.out_fd); /* Write to output file */ if (report_fd) fwrite(s, 1, len, report_fd); /* Write to report file */ return len; } static void usage(void) { /* Print usage information */ DEBUG("==> usage()\n"); printf("Usage: contesta [options] [files]\n" " Available options are:\n" " -h - Show usage information\n" " -c - Specify configuration file\n" " -d - Specify directory with log files (default is current directory)\n" " -o - Specify output file with results (default is stdout)\n" " -r - Specify directory for log analyzis reports (default is ./%s)\n" " -u - Specify directory for UBN files (default is ./%s)\n" "Please report bugs to ur4mck@gmail.com\n", DEFAULT_REPORT_DIR, DEFAULT_UBN_DIR); DEBUG("<== usage()\n"); } static int get_config(char *file) { /* Read configuration from file */ FILE *fd; char s[MAX_STR], tmp[MAX_STR], *p, *name, *value; int name_ready; unsigned int line_num, errors; DEBUG("==> get_config(%s)\n", file); /* Open file */ if (!(fd = fopen(file, "r"))) { fprintf(stderr, "ERROR: Configuration file '%s' was not found\n", file); return -1; } /* Read configuration file */ line_num = errors = 0; while (!feof(fd) && fgets(s, MAX_STR, fd)) { line_num++; /* Line numbers counter */ if (s[0] == '\0' || s[0] == '\n' || s[0] == '\r') continue; /* Skip blank line */ strcpy(tmp, s); /* Preserve original string for error reporting */ strip_leading_spaces(s); /* Remove leading spaces */ /* Parse configuration file line */ p = s; name_ready = 0; name = value = NULL; while (*p != '\0') { if (*p == CONFIG_FILE_COMMENT_CHAR) { /* Skip text after the commentary char */ *p = '\0'; break; } if (!name_ready) { if (!name) name = p; /* Start of the name */ else if (*p == ' ' || *p == '\t') { *p = '\0'; /* End of the name */ name_ready = 1; value = ++p; /* Start of the value */ } } p++; } if (name_ready) { strip_leading_spaces(value); strip_trailing_spaces(value); if (parse_config(name, value, &p) != 0) { fprintf(stderr, "Error in configuration file, line %u:\n%s - %s\n\n", line_num, tmp, p); errors++; } } } fclose(fd); if (errors) { fprintf(stderr, "There was an %u error%s in configuration file %s\nAborting\n", errors, errors > 1 ? "(s)" : "", file); config.contest.valid = 0; /* Contest configuration is not valid due to errors */ return 1; } else config.contest.valid = 1; /* Contest configuration is valid now */ /* Check critical configuration options */ if (config.contest.name[0] == '\0') strcpy(config.contest.name, DEFAULT_CONTEST_NAME); if (config.contest.band_list[0] == '\0') strcpy(config.contest.band_list, DEFAULT_BAND_LIST); if (config.contest.mode_list[0] == '\0') strcpy(config.contest.mode_list, DEFAULT_MODE_LIST); if (config.contest.cfm_unresolved < DEFAULT_CFM_UNRESOLVED) config.contest.cfm_unresolved = DEFAULT_CFM_UNRESOLVED; DEBUG("<== get_config(%s)\n", file); return 0; /* Success */ } static int parse_config(char *name, char *value, char **err_msg) { /* Parse configuration file setting name and its value */ int n; struct tm tm; char tmp[MAX_STR]; DEBUG("==> parse_config('%s', '%s', %p)\n", name, value, err_msg); while (1) { /* dummy */ /* Contest name */ if (strcasecmp(name, CONFIG_PARAM_NAME) == 0) { if ((n = strlen(value)) > 0) { strncpy(config.contest.name, &value[1], n - 2); config.contest.name[n - 2] = '\0'; } else { *err_msg = "Contest name is empty"; return 1; } break; } /* Contest ID */ if (strcasecmp(name, CONFIG_PARAM_ID) == 0) { if (strlen(value) > 0) { n = atoi(value); if (n > 0) config.contest.id = n; else { *err_msg = "Contest ID is incorrect"; return 2; } } else { *err_msg = "Contest ID is not defined"; return 2; } break; } /* Contest start date */ if (strcasecmp(name, CONFIG_PARAM_START) == 0) { if ((n = strlen(value)) > 0) { strncpy(tmp, &value[1], n - 2); tmp[n - 2] = '\0'; if (strptime(tmp, "%Y-%m-%d %H%M", &tm)) { tm.tm_sec = 0; config.contest.start = timegm(&tm); } else { *err_msg = "Contest starting date/time is incorrect"; return 3; } } else { *err_msg = "Contest starting date/time is not defined"; return 3; } break; } /* Contest end date */ if (strcasecmp(name, CONFIG_PARAM_END) == 0) { if ((n = strlen(value)) > 0) { strncpy(tmp, &value[1], n - 2); tmp[n - 2] = '\0'; if (strptime(tmp, "%Y-%m-%d %H%M", &tm)) { tm.tm_sec = 0; config.contest.end = timegm(&tm); } else { *err_msg = "Contest ending date/time is incorrect"; return 4; } } else { *err_msg = "Contest ending date/time is not defined"; return 4; } break; } /* Number of rounds */ if (strcasecmp(name, CONFIG_PARAM_ROUNDS) == 0) { if (strlen(value) > 0) { n = atoi(value); if (n >= 0) config.contest.rounds = n; else { *err_msg = "Number of rounds is wrong"; return 5; } } else { *err_msg = "Number of rounds is not defined"; return 5; } break; } /* Maximum difference in time */ if (strcasecmp(name, CONFIG_PARAM_MAX_DT) == 0) { if (strlen(value) > 0) config.contest.max_dt = atoi(value); else { *err_msg = "Incorrect parameter"; return 6; } break; } /* Band list */ if (strcasecmp(name, CONFIG_PARAM_BAND_LIST) == 0) { if ((n = strlen(value)) > 0) { strncpy(config.contest.band_list, &value[1], n - 2); config.contest.band_list[n - 2] = '\0'; } else { *err_msg = "Contest allowed band list is empty"; return 7; } break; } /* Mode list */ if (strcasecmp(name, CONFIG_PARAM_MODE_LIST) == 0) { if ((n = strlen(value)) > 0) { strncpy(config.contest.mode_list, &value[1], n - 2); config.contest.mode_list[n - 2] = '\0'; } else { *err_msg = "Contest allowed mode list is empty"; return 8; } break; } /* Signature */ if (strcasecmp(name, CONFIG_PARAM_SIGNATURE) == 0) { if ((n = strlen(value)) > 0) { strncpy(config.contest.signature, &value[1], n - 2); config.contest.signature[n - 2] = '\0'; } else { *err_msg = "Signature is empty"; return 9; } break; } /* Sent RST checking */ if (strcasecmp(name, CONFIG_PARAM_SKIP_RST_SNT) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.skip_rst_snt = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 10; } break; } /* Received RST checking */ if (strcasecmp(name, CONFIG_PARAM_SKIP_RST_RCV) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.skip_rst_rcv = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 11; } break; } /* Sent incremental number checking */ if (strcasecmp(name, CONFIG_PARAM_SKIP_NUM_SNT) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.skip_num_snt = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 12; } break; } /* Received incremental number checking */ if (strcasecmp(name, CONFIG_PARAM_SKIP_NUM_RCV) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.skip_num_rcv = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 13; } break; } /* Sent check number checking */ if (strcasecmp(name, CONFIG_PARAM_SKIP_CHK_SNT) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.skip_chk_snt = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 12; } break; } /* Received check number checking */ if (strcasecmp(name, CONFIG_PARAM_SKIP_CHK_RCV) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.skip_chk_rcv = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 13; } break; } /* Minimum number of unresolved QSOs to confirm them as valid */ if (strcasecmp(name, CONFIG_PARAM_CFM_UNRESOLVED) == 0) { if (strlen(value) > 0) { n = atoi(value); if (n >= DEFAULT_CFM_UNRESOLVED) config.contest.cfm_unresolved = n; else { *err_msg = "Minimum number of unresolved QSOs is wrong"; return 14; } } else { *err_msg = "Minimum number of unresolved QSOs is empty"; return 15; } break; } /* Log analyzis reports directory */ if (strcasecmp(name, CONFIG_PARAM_REPORT_DIR) == 0) { if ((n = strlen(value)) > 0) { strncpy(config.report_dir, &value[1], n - 2); config.report_dir[n - 2] = '\0'; } else { *err_msg = "Log analyzis reports directory is empty"; return 16; } break; } /* UBN files directory */ if (strcasecmp(name, CONFIG_PARAM_UBN_DIR) == 0) { if ((n = strlen(value)) > 0) { strncpy(config.ubn_dir, &value[1], n - 2); config.ubn_dir[n - 2] = '\0'; } else { *err_msg = "UBN files directory is empty"; return 17; } break; } /* Log submission e-mails list */ if (strcasecmp(name, CONFIG_PARAM_LOG_EMAILS) == 0) { if ((n = strlen(value)) > 0) { strncpy(config.contest.email, &value[1], n - 2); config.contest.email[n - 2] = '\0'; } else { *err_msg = "Log e-mails list is empty"; return 18; } break; } /* Official results URL */ if (strcasecmp(name, CONFIG_PARAM_WWW) == 0) { if ((n = strlen(value)) > 0) { strncpy(config.contest.www, &value[1], n - 2); config.contest.www[n - 2] = '\0'; } else { *err_msg = "Official results URL is empty (see 'www' param in config)"; return 18; } break; } /* Unique mult flag */ if (strcasecmp(name, CONFIG_PARAM_UNIQ_MULT) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.uniq_mult = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 19; } break; } /* Strict eval flag */ if (strcasecmp(name, CONFIG_PARAM_STRICT_EVAL) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.strict_eval = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 19; } break; } /* Allow duplicates flag */ if (strcasecmp(name, CONFIG_PARAM_ALLOW_DUPES) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.allow_dupes = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 20; } break; } /* Search tags flag */ if (strcasecmp(name, CONFIG_PARAM_SEARCH_TAGS) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.search_tags = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 21; } break; } /* Ignore tags flag */ if (strcasecmp(name, CONFIG_PARAM_IGNORE_TAGS) == 0) { if (strlen(value) == 1 && (value[0] == '0' || value[0] == '1')) config.contest.ignore_tags = (int)(value[0] - '0'); else { *err_msg = "Parameter is wrong"; return 22; } break; } *err_msg = ""; /* No errors */ break; }; DEBUG("<== parse_config('%s', '%s', %p)\n", name, value, err_msg); return 0; /* Success */ } static LOG_FILE_S *alloc_list(char *name) { /* Allocate next leaf for the log files list */ LOG_FILE_S *ptr; DEBUG("==> alloc_list(%s)\n", name); /* Allocate memory for the list leaf */ if (!(ptr = malloc(sizeof(LOG_FILE_S)))) { perror("malloc"); exit(-33); /* Fatal error */ } if (!(ptr->name = malloc(strlen(name) + 1))) { perror("malloc"); free(ptr); exit(-34); } strcpy(ptr->name, name); ptr->next = NULL; DEBUG("<== alloc_list(%s) = %p\n", name, ptr); return ptr; } static void free_list(LOG_FILE_S *ptr) { /* Free space from log files list */ LOG_FILE_S *next; DEBUG("==> free_list(%p)\n", ptr); while (ptr) { next = ptr->next; free(ptr->name); free(ptr); ptr = next; } DEBUG("<== free_list(%p)\n", ptr); } static int score_table(char *name, time_t start, time_t end, BASE_S *base) { int i, idx, max, cfm, n; SCORE_S *score_table, *st; HAM_S *ham, *ptr; QSO_S *qso; char start_s[MAX_STR], end_s[MAX_STR]; DEBUG("==> score_table(%p)\n", base); /* Allocate score table */ if (!(score_table = calloc(base->ham_num, sizeof(SCORE_S)))) { out(NULL, "ERROR: Unable to allocate space for score table\n"); return -1; } /* Create score table (sort by scores) */ idx = 0; do { ptr = NULL; max = 0; /* Search a ham with maximum score */ for (i = 0; i < base->ham_num; i++) { ham = base->ham[i]; if (!(ham->flags & HAM_FLAG_SCORE_TAB) && ham->actual_score >= max) { max = ham->actual_score; ptr = ham; } } if (ptr) { /* Add to score table */ score_table[idx].ham = ptr; ptr->rating_place = idx + 1; /* Set ham's place in rating table */ /* Calculate total & confirmed QSO number */ max = 0; /* Now it is a QSO counter */ cfm = 0; /* Confirmed QSO counter */ qso = ptr->qso; while (qso) { if (qso->ham_id != -1 && (qso->checked == QSO_STATUS_CONTEST_XCHECK_OK || \ qso->checked == QSO_STATUS_CONTEST_XCHECK_ONE_SIDE_VALID)) cfm++; max++; qso = (QSO_S*)qso->next; } score_table[idx].qso = max; score_table[idx++].cfm = cfm; ptr->flags |= HAM_FLAG_SCORE_TAB; /* Mark this ham as appeared in score table */ } else break; } while (idx < base->ham_num); /* Convert contest start & end time to strings */ strftime(start_s, MAX_STR, "%Y-%m-%d %H%M", gmtime(&start)); strftime(end_s, MAX_STR, "%Y-%m-%d %H%M", gmtime(&end)); /* Print results */ out(NULL, "\n************************************************************************\n" "Results \"%s\" %s - %s\n\n", name, start_s, end_s); out(NULL, " CALL QSO/CFM Mults Points Total\n" "------------------------------------------------------------------------\n"); if (idx > 0) { for (i = 0; i < idx; i++) { st = &score_table[i]; n = out(NULL, "%3u. %s", i + 1, st->ham->callsign); while (n++ < 19) out(NULL, " "); n = 19 + out(NULL, "%u/%u ", st->qso, st->cfm); while (n++ < 31) out(NULL, " "); n = 31 + out(NULL, "%4u ", st->ham->total_mult); while (n++ < 40) out(NULL, " "); n = 40 + out(NULL, "%7u ", st->ham->sum_score); while (n++ < 47) out(NULL, " "); out(NULL, "%16u\n", st->ham->actual_score); } } else out(NULL, "\n Contest rating is empty\n"); out(NULL, "\n************************************************************************\n"); /* Free memory from score table */ free(score_table); DEBUG("<== score_table(%p)\n", base); return 0; }