mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
* Tier-2 structured verb options: OptionSpec, initial migration (#2098) PR 3 of the AI-friendly roadmap (plans/plan-2098-llm.md). Infrastructure: - Add OptionSpec{Flag,Arg,Type,Desc,Repeatable,Values} to pkg/transformers/aaa_record_transformer.go alongside TransformerSetup. Type is one of: bool, string, int, float, csv-list, regex, filename, format, enum. For type=="enum", Values lists the valid choices. - Add Options []OptionSpec to TransformerSetup (nil = not yet migrated). - Emit Options in VerbInfoForJSON (omitempty so unmigrated verbs stay backward-compatible; agents check key presence for Tier-2 availability). UsageText is always present as the Tier-1 prose fallback. - Add VerbOptionsNilCheck() in aaa_verb_options_check.go: progress report of migrated vs. unmigrated verbs, analogous to FLAG_TABLE.NilCheck(). - Wire verb-options-nil-check into mlr help (internal/docgen section). Initial migration (5/70 verbs): - nothing: empty Options (no verb-specific options, explicitly migrated) - cat: -n (bool), -N (string), -g (csv-list), --filename, --filenum (bool) - head: -g (csv-list), -n (int) - tail: -g (csv-list), -n (int) - tee: -a, -p (bool) Tests: - 5 new unit tests in aaa_transformer_json_test.go covering migrated/ unmigrated paths, field population, JSON round-trip, and key-presence. - Regression test case 0003: mlr help verb-options-nil-check golden output. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Migrate all 70 verbs to structured OptionSpec; bump catalog schema to v2 Completes the Tier-2 migration started in the previous commit. Every verb in TRANSFORMER_LOOKUP_TABLE now has a non-nil Options field. - Workflow-migrated all 65 remaining verbs. Each Setup var now carries Options: []OptionSpec{...} with Flag/Arg/Type/Desc fields. Verbs with no verb-specific options (altkv, check, group-like, nothing, etc.) use an empty slice to signal "migrated but no options." - Drop `omitempty` from VerbInfoForJSON.Options: empty slices were silently dropped, making migrated-no-option verbs indistinguishable from unmigrated ones in JSON. Without omitempty: null=unmigrated, []=migrated-no-options, [...]= migrated-with-options. Bump catalogSchemaVersion 1→2 for this shape change. - Replace the two "unmigrated-verb" unit tests (which used stats1 as an example) with TestAllVerbsFullyMigrated (asserts every verb has non-nil Options) and TestAllVerbsHaveOptionsKeyInJSON (asserts every migrated verb emits the "options" key in JSON). - Regenerate test/cases/cli-help/0003/expout: now reads "Verb options migration: 70/70 migrated." Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * remove a transitional helper * git rms * Render verb usage Options blocks from structured OptionSpec Each verb's usage message and its Tier-2 OptionSpec list previously duplicated the option text. New WriteVerbOptions (aaa_verb_usage.go) renders the "Options:" block from the specs: aligned flag column, descriptions word-wrapped at 80, uniform trailing -h|--help line. - OptionSpec gains Aliases (JSON "aliases") so long-form spellings like join's --lk|--left-keep-field-names survive in both outputs - All 70 verbs migrated; options literals hoisted to package-level vars (usage funcs can't reference their Setup var without a Go init cycle) - Hand-written per-option details the specs had condensed away are merged into Desc, enriching the JSON catalog - Non-option prose (examples, cross-references, dynamic accumulator listings) kept verbatim - Regenerated the six usage-embedding regression expectations and the two affected doc pages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix pre-existing usage-text bugs surfaced by the OptionSpec migration - gap: usage said "One of -f or -g is required" but the parser takes -n or -g - seqgen: drop description line copy-pasted from cat ("Passes input records directly to output...") which contradicted "Discards the input record stream" - utf8-to-latin1: description read inverted ("from Latin-1 to UTF-8") - sec2gmtdate: usage said "../c/mlr" instead of "mlr" - top: document the accepted-but-undocumented --max flag - stats2: add linreg-pca to the -a enum values, matching the runtime accumulator table Regression expectations and docs regenerated accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix check usage sentence order; stats1 usage blank line to usage stream - check: the description's second and third lines were swapped, reading "Consumes records without printing any output, / Useful for doing a well-formatted check on input data. / with the exception that warnings are printed to stderr." - stats1: a bare fmt.Println() in the usage func wrote its blank line to process stdout instead of the usage output stream Regression expectation and docs regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
486 lines
16 KiB
Go
486 lines
16 KiB
Go
// 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* or a *group*. 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"] group.
|
|
//
|
|
// * Groups 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]
|
|
// and linked list of records.
|
|
//
|
|
// * Once all the input records are ingested into this hash map, we copy the
|
|
// group-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.
|
|
//
|
|
// * Note in particular that string keys ["a":"red","x":"1"] and
|
|
// ["a":"red","x":"1.0"] map to different groups, but will sort equally.
|
|
//
|
|
|
|
package transformers
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/johnkerl/miller/v6/pkg/cli"
|
|
"github.com/johnkerl/miller/v6/pkg/lib"
|
|
"github.com/johnkerl/miller/v6/pkg/mlrval"
|
|
"github.com/johnkerl/miller/v6/pkg/types"
|
|
)
|
|
|
|
const verbNameSort = "sort"
|
|
|
|
var sortOptions = []OptionSpec{
|
|
{Flag: "-f", Arg: "{a,b,c}", Type: "csv-list", Desc: "Lexical ascending sort on the specified field names.", Repeatable: true},
|
|
{Flag: "-r", Arg: "{a,b,c}", Type: "csv-list", Desc: "Lexical descending sort on the specified field names.", Repeatable: true},
|
|
{Flag: "-c", Arg: "{a,b,c}", Type: "csv-list", Desc: "Case-folded lexical ascending sort on the specified field names.", Repeatable: true},
|
|
{Flag: "-cr", Arg: "{a,b,c}", Type: "csv-list", Desc: "Case-folded lexical descending sort on the specified field names.", Repeatable: true},
|
|
{Flag: "-n", Arg: "{a,b,c}", Type: "csv-list", Desc: "Numerical ascending sort on the specified field names; nulls sort last.", Repeatable: true},
|
|
{Flag: "-nf", Arg: "{a,b,c}", Type: "csv-list", Desc: "Same as -n.", Repeatable: true},
|
|
{Flag: "-nr", Arg: "{a,b,c}", Type: "csv-list", Desc: "Numerical descending sort on the specified field names; nulls sort first.", Repeatable: true},
|
|
{Flag: "-t", Arg: "{a,b,c}", Type: "csv-list", Desc: "Natural ascending sort on the specified field names.", Repeatable: true},
|
|
{Flag: "-b", Type: "bool", Desc: "Move sort fields to start of record, as in reorder -b."},
|
|
{Flag: "-tr", Aliases: []string{"-rt"}, Arg: "{a,b,c}", Type: "csv-list", Desc: "Natural descending sort on the specified field names.", Repeatable: true},
|
|
}
|
|
|
|
var SortSetup = TransformerSetup{
|
|
Verb: verbNameSort,
|
|
UsageFunc: transformerSortUsage,
|
|
ParseCLIFunc: transformerSortParseCLI,
|
|
IgnoresInput: false,
|
|
Options: sortOptions,
|
|
}
|
|
|
|
func transformerSortUsage(
|
|
o *os.File,
|
|
) {
|
|
fmt.Fprintf(o, "Usage: %s %s {flags}\n", "mlr", verbNameSort)
|
|
fmt.Fprintf(o, "Sorts records primarily by the first specified field, secondarily by the second\n")
|
|
fmt.Fprintf(o, "field, and so on. (Any records not having all specified sort keys will appear\n")
|
|
fmt.Fprintf(o, "at the end of the output, in the order they were encountered, regardless of the\n")
|
|
fmt.Fprintf(o, "specified sort order.) The sort is stable: records that compare equal will sort\n")
|
|
fmt.Fprintf(o, "in the order they were encountered in the input record stream.\n")
|
|
fmt.Fprintf(o, "\n")
|
|
WriteVerbOptions(o, sortOptions)
|
|
fmt.Fprintf(o, "\n")
|
|
fmt.Fprintf(o, "Example:\n")
|
|
fmt.Fprintf(o, " %s %s -f a,b -nr x,y,z\n", "mlr", verbNameSort)
|
|
fmt.Fprintf(o, "which is the same as:\n")
|
|
fmt.Fprintf(o, " %s %s -f a -f b -nr x -nr y -nr z\n", "mlr", verbNameSort)
|
|
}
|
|
|
|
func transformerSortParseCLI(
|
|
pargi *int,
|
|
argc int,
|
|
args []string,
|
|
_ *cli.TOptions,
|
|
doConstruct bool, // false for first pass of CLI-parse, true for second pass
|
|
) (RecordTransformer, error) {
|
|
|
|
// Skip the verb name from the current spot in the mlr command line
|
|
argi := *pargi
|
|
verb := args[argi]
|
|
argi++
|
|
|
|
groupByFieldNames := []string{}
|
|
comparatorFuncs := []mlrval.CmpFuncInt{}
|
|
doMoveToHead := false
|
|
|
|
for argi < argc /* variable increment: 1 or 2 depending on flag */ {
|
|
opt := args[argi]
|
|
if !strings.HasPrefix(opt, "-") {
|
|
break // No more flag options to process
|
|
}
|
|
if args[argi] == "--" {
|
|
break // All transformers must do this so main-flags can follow verb-flags
|
|
}
|
|
argi++
|
|
|
|
switch opt {
|
|
case "-h", "--help":
|
|
transformerSortUsage(os.Stdout)
|
|
return nil, cli.ErrHelpRequested
|
|
|
|
case "-f":
|
|
subList, err := cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.LexicalAscendingComparator)
|
|
}
|
|
|
|
case "-c":
|
|
// See comments over "-n" -- similar hack.
|
|
if args[argi] == "-r" {
|
|
// Treat like "-cr"
|
|
if err := cli.VerbCheckArgCount(verb, args[argi], args, argi, argc, 1); err != nil {
|
|
return nil, err
|
|
}
|
|
argi++
|
|
subList, err := cli.VerbGetStringArrayArg(verb, "-nr", args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.CaseFoldDescendingComparator)
|
|
}
|
|
} else {
|
|
|
|
subList, err := cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.CaseFoldAscendingComparator)
|
|
}
|
|
}
|
|
|
|
case "-t":
|
|
// See comments over "-n" -- similar hack.
|
|
if err := cli.VerbCheckArgCount(verb, opt, args, argi, argc, 1); err != nil {
|
|
return nil, err
|
|
}
|
|
if args[argi] == "-r" {
|
|
// Treat like "-tr"
|
|
if err := cli.VerbCheckArgCount(verb, args[argi], args, argi, argc, 1); err != nil {
|
|
return nil, err
|
|
}
|
|
argi++
|
|
subList, err := cli.VerbGetStringArrayArg(verb, "-tr", args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.NaturalAscendingComparator)
|
|
}
|
|
} else {
|
|
subList, err := cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.NaturalDescendingComparator)
|
|
}
|
|
}
|
|
|
|
case "-r":
|
|
// See comments over "-n" -- similar hack.
|
|
if err := cli.VerbCheckArgCount(verb, opt, args, argi, argc, 1); err != nil {
|
|
return nil, err
|
|
}
|
|
if args[argi] == "-t" {
|
|
// Treat like "-rt" which is same as "-tr"
|
|
if err := cli.VerbCheckArgCount(verb, args[argi], args, argi, argc, 1); err != nil {
|
|
return nil, err
|
|
}
|
|
argi++
|
|
subList, err := cli.VerbGetStringArrayArg(verb, "-tr", args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.NaturalAscendingComparator)
|
|
}
|
|
} else {
|
|
subList, err := cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.LexicalDescendingComparator)
|
|
}
|
|
}
|
|
|
|
case "-n":
|
|
// This is a bit of a hack.
|
|
//
|
|
// As of Miller 6 we have a getoptish feature wherein "-xyz" is
|
|
// expanded to "-x -y -z" while "--xyz" is left intact. This is OK
|
|
// to do globally (before any verb such as this one sees the
|
|
// command line) since Miller is quite consistent (in main, verbs,
|
|
// auxents, and terminals) that multi-character options start with
|
|
// two dashes, e.g. "--csv" ...
|
|
//
|
|
// ... with the sole exception being -nf/-nr, right here. This goes
|
|
// back to the very start of Miller, and we don't want to break the
|
|
// command-line interface to sort.
|
|
//
|
|
// Before Miller 6, opt and next arg would have been "-nf x,y,z" or
|
|
// "-nr x,y,z". Now they're split into "-n -f x,y,z" or "-n -r
|
|
// x,y,z", respectively. Note that "-n x,y,z" and "-f x,y,z" and
|
|
// "-r x,y,z" are also valid. This means -n needs a field-name list
|
|
// after it unless it's followed immediately by -r or -f.
|
|
//
|
|
// So here we special-case this: if "-n" is followed immediately by
|
|
// "-f", we treat it the same as "-nf". Likewise, "-n" followed by
|
|
// "-r" is treated like "-nr".
|
|
if err := cli.VerbCheckArgCount(verb, opt, args, argi, argc, 1); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch args[argi] {
|
|
case "-f":
|
|
// Treat like "-nf"
|
|
argi++
|
|
subList, err := cli.VerbGetStringArrayArg(verb, "-nf", args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.NumericAscendingComparator)
|
|
}
|
|
|
|
case "-r":
|
|
// Treat like "-nr"
|
|
argi++
|
|
subList, err := cli.VerbGetStringArrayArg(verb, "-nr", args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.NumericDescendingComparator)
|
|
}
|
|
|
|
default:
|
|
// Treat like "-n"
|
|
subList, err := cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.NumericAscendingComparator)
|
|
}
|
|
}
|
|
|
|
case "-nf":
|
|
subList, err := cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.NumericAscendingComparator)
|
|
}
|
|
|
|
case "-nr":
|
|
subList, err := cli.VerbGetStringArrayArg(verb, opt, args, &argi, argc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range subList {
|
|
groupByFieldNames = append(groupByFieldNames, item)
|
|
comparatorFuncs = append(comparatorFuncs, mlrval.NumericDescendingComparator)
|
|
}
|
|
|
|
case "-b":
|
|
doMoveToHead = true
|
|
|
|
default:
|
|
return nil, cli.VerbErrorf(verb, "option \"%s\" not recognized", opt)
|
|
}
|
|
}
|
|
|
|
if len(groupByFieldNames) == 0 {
|
|
return nil, cli.VerbErrorf(verb, "-f sort fields required")
|
|
}
|
|
|
|
*pargi = argi
|
|
if !doConstruct { // All transformers must do this for main command-line parsing
|
|
return nil, nil
|
|
}
|
|
|
|
transformer, err := NewTransformerSort(
|
|
groupByFieldNames,
|
|
comparatorFuncs,
|
|
doMoveToHead,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transformer, nil
|
|
}
|
|
|
|
// Example:
|
|
// * mlr sort -f a -n i
|
|
// * group-by field-name list is "a,i"
|
|
// * input record 'a=pan,b=pan,i=1,x=0.3467,y=0.7268'
|
|
// o values at a,i are "pan",1
|
|
// o grouping key for the ordered map from string to record-group is the string "pan,1"
|
|
// o we also need a map from "pan,1" to the array of mlrvals ["pan", 1].
|
|
// * next input record 'a=eks,b=pan,i=2,x=0.7586,y=0.5221'
|
|
// o values at a,i are "eks",2
|
|
// o grouping key for the ordered map from string to record-group is the string "eks,2"
|
|
// o we also need a map from "eks,2" to the array of mlrvals ["eks", 2].
|
|
// * what gets sorted are the group-heading arrays of mlrvals:
|
|
// o make an array [ ("pan,1", ["pan", 1]), ("eks,2", ["eks", 2])
|
|
// o sort that
|
|
// * output is simply for each slot in the array, emit each record in the group
|
|
|
|
type TransformerSort struct {
|
|
// -- Input
|
|
groupByFieldNames []string
|
|
comparatorFuncs []mlrval.CmpFuncInt
|
|
doMoveToHead bool
|
|
|
|
// -- State
|
|
// Map from string to record slices:
|
|
recordListsByGroup *lib.OrderedMap[*[]*types.RecordAndContext]
|
|
// Map from string to []*lib.Mlrval:
|
|
groupHeads *lib.OrderedMap[[]*mlrval.Mlrval]
|
|
spillGroup []*types.RecordAndContext // e.g. sort by field "a" -- this is for records lacking a field named "a"
|
|
}
|
|
|
|
func NewTransformerSort(
|
|
groupByFieldNames []string,
|
|
comparatorFuncs []mlrval.CmpFuncInt,
|
|
doMoveToHead bool,
|
|
) (*TransformerSort, error) {
|
|
|
|
tr := &TransformerSort{
|
|
groupByFieldNames: groupByFieldNames,
|
|
comparatorFuncs: comparatorFuncs,
|
|
doMoveToHead: doMoveToHead,
|
|
|
|
recordListsByGroup: lib.NewOrderedMap[*[]*types.RecordAndContext](),
|
|
groupHeads: lib.NewOrderedMap[[]*mlrval.Mlrval](),
|
|
spillGroup: []*types.RecordAndContext{},
|
|
}
|
|
|
|
return tr, nil
|
|
}
|
|
|
|
type GroupingKeysAndMlrvals struct {
|
|
groupingKey string
|
|
mlrvals []*mlrval.Mlrval
|
|
}
|
|
|
|
func (tr *TransformerSort) Transform(
|
|
inrecAndContext *types.RecordAndContext,
|
|
outputRecordsAndContexts *[]*types.RecordAndContext, // list of *types.RecordAndContext
|
|
inputDownstreamDoneChannel <-chan bool,
|
|
outputDownstreamDoneChannel chan<- bool,
|
|
) {
|
|
HandleDefaultDownstreamDone(inputDownstreamDoneChannel, outputDownstreamDoneChannel)
|
|
if !inrecAndContext.EndOfStream {
|
|
inrec := inrecAndContext.Record
|
|
|
|
if tr.doMoveToHead {
|
|
n := len(tr.groupByFieldNames)
|
|
for i := n - 1; i >= 0; i-- {
|
|
inrec.MoveToHead(tr.groupByFieldNames[i])
|
|
}
|
|
}
|
|
|
|
groupingKey, selectedValues, ok := inrec.GetSelectedValuesAndJoined(
|
|
tr.groupByFieldNames,
|
|
)
|
|
if !ok {
|
|
tr.spillGroup = append(tr.spillGroup, inrecAndContext)
|
|
return
|
|
}
|
|
|
|
recordListForGroup := tr.recordListsByGroup.Get(groupingKey)
|
|
if recordListForGroup == nil {
|
|
records := []*types.RecordAndContext{}
|
|
recordListForGroup = &records
|
|
tr.recordListsByGroup.Put(groupingKey, recordListForGroup)
|
|
tr.groupHeads.Put(groupingKey, selectedValues)
|
|
}
|
|
|
|
*recordListForGroup = append(*recordListForGroup, inrecAndContext)
|
|
|
|
} else { // End of record stream
|
|
|
|
// At this point, in the above example, groupHeads is:
|
|
//
|
|
// {
|
|
// "pan,1" : ["pan", 1],
|
|
// "eks,2" : ["eks", 2]
|
|
// }
|
|
//
|
|
// We need to make an array like
|
|
//
|
|
// [
|
|
// [ "pan,1", ["pan', 1],
|
|
// [ "eks,2", ["eks', 2]
|
|
// ]
|
|
|
|
groupingKeysAndMlrvals := groupHeadsToArray(tr.groupHeads)
|
|
|
|
// Go sort API: for ascending sort, return true if element i < element j.
|
|
sort.Slice(groupingKeysAndMlrvals, func(i, j int) bool {
|
|
for k, comparator := range tr.comparatorFuncs {
|
|
result := comparator(
|
|
groupingKeysAndMlrvals[i].mlrvals[k],
|
|
groupingKeysAndMlrvals[j].mlrvals[k],
|
|
)
|
|
if result < 0 {
|
|
return true
|
|
} else if result > 0 {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
|
|
// Now output the groups
|
|
for _, groupingKeyAndMlrvals := range groupingKeysAndMlrvals {
|
|
recordsInGroup := tr.recordListsByGroup.Get(groupingKeyAndMlrvals.groupingKey)
|
|
*outputRecordsAndContexts = append(*outputRecordsAndContexts, *recordsInGroup...)
|
|
}
|
|
|
|
*outputRecordsAndContexts = append(*outputRecordsAndContexts, tr.spillGroup...)
|
|
|
|
*outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) // end-of-stream marker
|
|
}
|
|
}
|
|
|
|
func groupHeadsToArray(groupHeads *lib.OrderedMap[[]*mlrval.Mlrval]) []GroupingKeysAndMlrvals {
|
|
retval := make([]GroupingKeysAndMlrvals, groupHeads.FieldCount)
|
|
|
|
i := 0
|
|
for entry := groupHeads.Head; entry != nil; entry = entry.Next {
|
|
retval[i] = GroupingKeysAndMlrvals{
|
|
groupingKey: entry.Key,
|
|
mlrvals: entry.Value,
|
|
}
|
|
i++
|
|
}
|
|
|
|
return retval
|
|
}
|