Recipe 03 / report

Answer the small questions without SQL.

Group detail rows into a compact report, keep the order predictable, and hand the result to the next command or a human. Aggregations stay deliberately small and explicit.

Group and count

For an orders file with customer and amount, produce one row per customer with an order count and total amount:

package main

import (
    "log"

    "github.com/osvik/simplecsv"
)

func main() {
    orders, err := simplecsv.ReadCsvFileE("orders.csv")
    if err != nil {
        log.Fatal(err)
    }

    report, err := orders.GroupByE(
        []string{"customer"},
        []simplecsv.Agg{
            {Op: simplecsv.AggCount, As: "orders"},
            {Field: "amount", Op: simplecsv.AggSum, As: "total_amount"},
        },
    )
    if err != nil {
        log.Fatal(err)
    }

    report, ok := report.SortByFields(
        []string{"total_amount", "customer"},
        []bool{false, true},
    )
    if !ok {
        log.Fatal("could not sort report")
    }

    if err := report.WriteCsvFileE("customer-summary.csv"); err != nil {
        log.Fatal(err)
    }
}

Groups appear in first-seen order before the optional sort. This makes an unsorted report stable and keeps output changes easy to understand.

Available aggregations

Operation Behavior Field
AggCount Counts rows in the group. Ignored
AggSum Adds values parsed with strconv.ParseFloat. Non-numeric cells count as zero. Required
AggMin / AggMax Uses finite numeric values only. Empty when no finite value exists. Required
AggFirst / AggLast Selects the first or last cell in group order. Required
AggJoin Joins non-empty values with a comma. Required

Inspect before grouping

Use Distinct when you need first-seen category values:

statuses, ok := orders.Distinct("status")
if !ok {
    log.Fatal("status field not found")
}
fmt.Println(statuses)

Use ValueCounts when the frequency table itself is the output:

counts, ok := orders.ValueCounts("status")
if !ok {
    log.Fatal("status field not found")
}

Control report order

SortByFields is stable and accepts one direction per field. An empty direction slice means ascending for every field.

sorted, ok := report.SortByFields(
    []string{"country", "city"},
    []bool{true, true},
)

Back to all recipes ->