#include #include #include #include #include "lib/mlrutil.h" #include "lib/mlr_globals.h" #include "containers/sllv.h" #include "containers/slls.h" #include "containers/lhmslv.h" #include "containers/mixutil.h" #include "mapping/mappers.h" // ================================================================ // OVERVIEW // // * Suppose we are sorting records lexically ascending on field "a" and then // numerically descending on field "x". // // * CLI syntax is "mlr sort -f a -nr x". // // * We first consume all input records and for each extract the string values // of fields a and x. For each uniq combination of a-value (e.g. "red", // "green", "blue") and x-value (e.g. "1", "1.0", "2.4") -- e.g. // pairs ["red","1"], and so on -- we keep a linked list of all the records // having those sort-key values, in the order encountered. // // * For each of those unique sort-key-value combinations, we also parse the // numerical fields at this point into an array of union-string-double. // E.g. the list ["red", "1.0"] maps to the array ["red", 1.0]. // // * The pairing of parsed-value array the linked list of same-key-value records // is called a *bucket*. E.g the records // {"a":"red","b":"circle","x":"1.0","y":"3.9"} // {"a":"red","b":"square","x":"1.0","z":"5.7", "q":"even"} // would both land in the ["red","1.0"] bucket. // // * Buckets are retained in a hash map: the key is the string-list of the form // ["red","1.0"] and the value is the pairing of parsed-value array ["red",1.0] // ane linked list of records. // // * Once all the input records are ingested into this hash map, we copy the // bucket-pointers into an array and sort it: this being the pairing of // parsed-value array and linked list of records. The comparator callback for // the sort walks through the parsed-value arrays one slot at a time, // looking at the first difference, e.g. if one has "a"="red" and the other // has "a"="blue". If the first field matches then the sort moves to the // second field, and so on. // // * Recall in particular that string keys ["a":"red","x":"1"] and // ["a":"red","x":"1.0"] map to different buckets, but will sort equally. // // ================================================================ #define SORT_NUMERIC 0x80 #define SORT_DESCENDING 0x40 typedef struct _mapper_sort_state_t { // Input parameters slls_t* pkey_field_names; // Fields to sort on int* sort_params; // Lexical/numeric; ascending/descending int do_sort; // If false, just do group-by // Sort state: buckets of like records. lhmslv_t* pbuckets_by_key_field_values; sllv_t* precords_missing_sort_keys; } mapper_sort_state_t; // Each sort key is string or number; use union to save space. typedef struct _typed_sort_key_t { union { char* s; double d; } u; } typed_sort_key_t; typedef struct _sort_bucket_t { typed_sort_key_t* typed_sort_keys; sllv_t* precords; } sort_bucket_t; // ---------------------------------------------------------------- static void mapper_sort_usage(FILE* o, char* argv0, char* verb); static mapper_t* mapper_sort_parse_cli(int* pargi, int argc, char** argv); static void mapper_group_by_usage(FILE* o, char* argv0, char* verb); static mapper_t* mapper_group_by_parse_cli(int* pargi, int argc, char** argv); static mapper_t* mapper_sort_alloc(slls_t* pkey_field_names, int* sort_params, int do_sort); static void mapper_sort_free(mapper_t* pmapper); static sllv_t* mapper_sort_process(lrec_t* pinrec, context_t* pctx, void* pvstate); static typed_sort_key_t* parse_sort_keys(slls_t* pkey_field_values, int* sort_params, context_t* pctx); // qsort is non-reentrant but qsort_r isn't portable. But since Miller is // single-threaded, even if we've got one sort chained to another, only one is // active at a time. We adopt the convention that we set the sort params // right before the sort. static int* pcmp_sort_params = NULL; static int cmp_params_length = 0; static int pbucket_comparator(const void* pva, const void* pvb); // ---------------------------------------------------------------- mapper_setup_t mapper_sort_setup = { .verb = "sort", .pusage_func = mapper_sort_usage, .pparse_func = mapper_sort_parse_cli }; mapper_setup_t mapper_group_by_setup = { .verb = "group-by", .pusage_func = mapper_group_by_usage, .pparse_func = mapper_group_by_parse_cli }; // ---------------------------------------------------------------- static void mapper_sort_usage(FILE* o, char* argv0, char* verb) { fprintf(o, "Usage: %s %s {flags}\n", argv0, verb); fprintf(o, "Flags:\n"); fprintf(o, " -f {comma-separated field names} Lexical ascending\n"); fprintf(o, " -n {comma-separated field names} Numerical ascending; nulls sort last\n"); fprintf(o, " -nf {comma-separated field names} Numerical ascending; nulls sort last\n"); fprintf(o, " -r {comma-separated field names} Lexical descending\n"); fprintf(o, " -nr {comma-separated field names} Numerical descending; nulls sort first\n"); fprintf(o, "Sorts records primarily by the first specified field, secondarily by the second\n"); fprintf(o, "field, and so on. Any records not having all specified sort keys will appear\n"); fprintf(o, "at the end of the output, in the order they were encountered, regardless of the\n"); fprintf(o, "specified sort order.\n"); fprintf(o, "Example:\n"); fprintf(o, " %s %s -f a,b -nr x,y,z\n", argv0, verb); fprintf(o, "which is the same as:\n"); fprintf(o, " %s %s -f a -f b -nr x -nr y -nr z\n", argv0, verb); } static mapper_t* mapper_sort_parse_cli(int* pargi, int argc, char** argv) { if ((argc - *pargi) < 3) { mapper_sort_usage(stderr, argv[0], argv[*pargi]); return NULL; } char* verb = argv[*pargi]; *pargi += 1; slls_t* pnames = slls_alloc(); slls_t* pflags = slls_alloc(); while ((argc - *pargi) >= 1 && argv[*pargi][0] == '-') { if ((argc - *pargi) < 2) mapper_sort_usage(stderr, argv[0], verb); char* flag = argv[*pargi]; char* value = argv[*pargi+1]; *pargi += 2; if (streq(flag, "-f")) { } else if (streq(flag, "-n")) { } else if (streq(flag, "-nf")) { } else if (streq(flag, "-r")) { } else if (streq(flag, "-nr")) { } else { mapper_sort_usage(stderr, argv[0], verb); } slls_t* pnames_for_flag = slls_from_line(value, ',', FALSE); // E.g. with "-nr a,b,c", replicate the "-nr" flag three times. for (sllse_t* pe = pnames_for_flag->phead; pe != NULL; pe = pe->pnext) { slls_append_no_free(pnames, pe->value); slls_append_no_free(pflags, flag); } slls_free(pnames_for_flag); } if (pnames->length < 1) mapper_sort_usage(stderr, argv[0], verb); // Convert the list such as ["-nf","-nf","-r","-r","-r"] into an array of // bit-flags, one per sort-key field. int* opt_array = mlr_malloc_or_die(pnames->length * sizeof(int)); sllse_t* pe; int di; for (pe = pflags->phead, di = 0; pe != NULL; pe = pe->pnext, di++) { char* flag = pe->value; int opt = streq(flag, "-nf") ? SORT_NUMERIC : streq(flag, "-n") ? SORT_NUMERIC : streq(flag, "-r") ? SORT_DESCENDING : streq(flag, "-nr") ? SORT_NUMERIC|SORT_DESCENDING : 0; opt_array[di] =opt; } slls_free(pflags); return mapper_sort_alloc(pnames, opt_array, TRUE); } // ---------------------------------------------------------------- static void mapper_group_by_usage(FILE* o, char* argv0, char* verb) { fprintf(o, "Usage: %s %s {comma-separated field names}\n", argv0, verb); fprintf(o, "Outputs records in batches having identical values at specified field names.\n"); } static mapper_t* mapper_group_by_parse_cli(int* pargi, int argc, char** argv) { if ((argc - *pargi) < 2) { mapper_group_by_usage(stderr, argv[0], argv[*pargi]); return NULL; } slls_t* pnames = slls_from_line(argv[*pargi+1], ',', FALSE); int* opt_array = mlr_malloc_or_die(pnames->length * sizeof(int)); for (int i = 0; i < pnames->length; i++) opt_array[i] = 0; *pargi += 2; return mapper_sort_alloc(pnames, opt_array, FALSE); } // ---------------------------------------------------------------- static mapper_t* mapper_sort_alloc(slls_t* pkey_field_names, int* sort_params, int do_sort) { mapper_t* pmapper = mlr_malloc_or_die(sizeof(mapper_t)); mapper_sort_state_t* pstate = mlr_malloc_or_die(sizeof(mapper_sort_state_t)); pstate->pkey_field_names = pkey_field_names; pstate->sort_params = sort_params; pstate->pbuckets_by_key_field_values = lhmslv_alloc(); pstate->precords_missing_sort_keys = sllv_alloc(); pstate->do_sort = do_sort; pmapper->pvstate = pstate; pmapper->pprocess_func = mapper_sort_process; pmapper->pfree_func = mapper_sort_free; return pmapper; } // ---------------------------------------------------------------- static void mapper_sort_free(mapper_t* pmapper) { mapper_sort_state_t* pstate = pmapper->pvstate; if (pstate->pkey_field_names != NULL) slls_free(pstate->pkey_field_names); // lhmslv_free will free the hashmap keys; we need to free the void-star hashmap values. for (lhmslve_t* pa = pstate->pbuckets_by_key_field_values->phead; pa != NULL; pa = pa->pnext) { sort_bucket_t* pbucket = pa->pvvalue; free(pbucket->typed_sort_keys); free(pbucket); // precords freed in emitter } lhmslv_free(pstate->pbuckets_by_key_field_values); sllv_free(pstate->precords_missing_sort_keys); free(pstate->sort_params); free(pstate); free(pmapper); } // ---------------------------------------------------------------- static sllv_t* mapper_sort_process(lrec_t* pinrec, context_t* pctx, void* pvstate) { mapper_sort_state_t* pstate = pvstate; if (pinrec != NULL) { // Consume another input record. slls_t* pkey_field_values = mlr_reference_selected_values_from_record(pinrec, pstate->pkey_field_names); if (pkey_field_values == NULL) { sllv_append(pstate->precords_missing_sort_keys, pinrec); } else { sort_bucket_t* pbucket = lhmslv_get(pstate->pbuckets_by_key_field_values, pkey_field_values); if (pbucket == NULL) { // New key-field-value: new bucket and hash-map entry slls_t* pkey_field_values_copy = slls_copy(pkey_field_values); sort_bucket_t* pbucket = mlr_malloc_or_die(sizeof(sort_bucket_t)); pbucket->typed_sort_keys = parse_sort_keys(pkey_field_values_copy, pstate->sort_params, pctx); pbucket->precords = sllv_alloc(); sllv_append(pbucket->precords, pinrec); lhmslv_put(pstate->pbuckets_by_key_field_values, pkey_field_values_copy, pbucket, FREE_ENTRY_KEY); } else { // Previously seen key-field-value: append record to bucket sllv_append(pbucket->precords, pinrec); } slls_free(pkey_field_values); } return NULL; } else if (!pstate->do_sort) { // End of input stream: do output for group-by sllv_t* poutput = sllv_alloc(); for (lhmslve_t* pe = pstate->pbuckets_by_key_field_values->phead; pe != NULL; pe = pe->pnext) { sort_bucket_t* pbucket = pe->pvvalue; sllv_transfer(poutput, pbucket->precords); sllv_free(pbucket->precords); } sllv_transfer(poutput, pstate->precords_missing_sort_keys); sllv_append(poutput, NULL); return poutput; } else { // End of input stream: sort bucket labels int num_buckets = pstate->pbuckets_by_key_field_values->num_occupied; sort_bucket_t** pbucket_array = mlr_malloc_or_die(num_buckets * sizeof(sort_bucket_t*)); // Copy bucket-pointers to an array for qsort int i = 0; for (lhmslve_t* pe = pstate->pbuckets_by_key_field_values->phead; pe != NULL; pe = pe->pnext, i++) { pbucket_array[i] = pe->pvvalue; } pcmp_sort_params = pstate->sort_params; cmp_params_length = pstate->pkey_field_names->length; qsort(pbucket_array, num_buckets, sizeof(sort_bucket_t*), pbucket_comparator); pcmp_sort_params = NULL; cmp_params_length = 0; // Emit each bucket's record sllv_t* poutput = sllv_alloc(); for (i = 0; i < num_buckets; i++) { sllv_t* plist = pbucket_array[i]->precords; sllv_transfer(poutput, plist); sllv_free(plist); } sllv_transfer(poutput, pstate->precords_missing_sort_keys); free(pbucket_array); sllv_append(poutput, NULL); // Signal end of output-record stream. return poutput; } } static int pbucket_comparator(const void* pva, const void* pvb) { // We are sorting an array of sort_bucket_t*. const sort_bucket_t** pba = (const sort_bucket_t**)pva; const sort_bucket_t** pbb = (const sort_bucket_t**)pvb; typed_sort_key_t* akeys = (*pba)->typed_sort_keys; typed_sort_key_t* bkeys = (*pbb)->typed_sort_keys; for (int i = 0; i < cmp_params_length; i++) { int sort_param = pcmp_sort_params[i]; if (sort_param & SORT_NUMERIC) { double a = akeys[i].u.d; double b = bkeys[i].u.d; if (isnan(a)) { // null input value if (!isnan(b)) { return (sort_param & SORT_DESCENDING) ? -1 : 1; } } else if (isnan(b)) { return (sort_param & SORT_DESCENDING) ? 1 : -1; } else { double d = a - b; int s = (d < 0) ? -1 : (d > 0) ? 1 : 0; if (s != 0) return (sort_param & SORT_DESCENDING) ? -s : s; } } else { int s = strcmp(akeys[i].u.s, bkeys[i].u.s); if (s != 0) return (sort_param & SORT_DESCENDING) ? -s : s; } } return 0; } // E.g. parse the list ["red","1.0"] into the array ["red",1.0]. static typed_sort_key_t* parse_sort_keys(slls_t* pkey_field_values, int* sort_params, context_t* pctx) { typed_sort_key_t* typed_sort_keys = mlr_malloc_or_die(pkey_field_values->length * sizeof(typed_sort_key_t)); int i = 0; for (sllse_t* pe = pkey_field_values->phead; pe != NULL; pe = pe->pnext, i++) { if (sort_params[i] & SORT_NUMERIC) { if (*pe->value == 0) { // null input value typed_sort_keys[i].u.d = nan(""); } else if (!mlr_try_float_from_string(pe->value, &typed_sort_keys[i].u.d)) { fprintf(stderr, "%s: couldn't parse \"%s\" as number in file \"%s\" record %lld.\n", MLR_GLOBALS.argv0, pe->value, pctx->filename, pctx->fnr); exit(1); } } else { typed_sort_keys[i].u.s = pe->value; } } return typed_sort_keys; }