diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-20 21:14:11 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-20 21:14:11 +0200 |
| commit | 0d3e15caf1d09e94e66946bdbc98001465d7f9f7 (patch) | |
| tree | 6c72d1e20fc87e1d3e08644c02f1233b3033d6d7 /internal | |
| parent | 59e86c9fd39308bc6b632e02ecf4d37265dabc91 (diff) | |
refactor: apply Go best practices and remove obsolete files
- Reorganize types.go: place types and constructors before methods
- Simplify main.go: improve flag handling and reduce code complexity
- Extract processRecordsFile() in aggregate.go: reduce function size, ensure immediate defer
- Change Reporter receivers to value semantics for read-only operations
- Fix wordWrap() function: properly account for line length with leading space
- Remove guprecords.raku: Go implementation is the primary version now
- Remove compare-with-raku.sh: no longer needed
- Update README.md and .gitignore accordingly
All tests pass: go test ./... and ./goprecords test
Amp-Thread-ID: https://ampcode.com/threads/T-019c7c73-58f9-7516-958d-f30eb17a3bff
Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/goprecords/aggregate.go | 94 | ||||
| -rw-r--r-- | internal/goprecords/report.go | 26 | ||||
| -rw-r--r-- | internal/goprecords/types.go | 134 |
3 files changed, 132 insertions, 122 deletions
diff --git a/internal/goprecords/aggregate.go b/internal/goprecords/aggregate.go index 3fb9144..ba446cb 100644 --- a/internal/goprecords/aggregate.go +++ b/internal/goprecords/aggregate.go @@ -61,53 +61,61 @@ func (ag *Aggregator) Aggregate(ctx context.Context) (*Aggregates, error) { return nil, fmt.Errorf("last kernel %s: %w", path, err) } out.Host[host] = NewHostAggregate(host, lastKernel) - f, err := os.Open(path) - if err != nil { - return nil, fmt.Errorf("open %s: %w", path, err) + if err := processRecordsFile(ctx, path, host, out); err != nil { + return nil, err } - defer f.Close() - sc := bufio.NewScanner(f) - for sc.Scan() { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - line := strings.TrimSpace(sc.Text()) - if line == "" { - continue - } - parts := strings.SplitN(line, ":", 3) - if len(parts) != 3 { - continue - } - uptime, _ := strconv.ParseUint(parts[0], 10, 64) - bootTime, _ := strconv.ParseUint(parts[1], 10, 64) - osStr := parts[2] - uname := osStr - if i := strings.Index(osStr, " "); i > 0 { - uname = osStr[:i] - } - osMajor := uname + " " - rest := osStr - if i := strings.Index(osStr, " "); i >= 0 { - rest = osStr[i+1:] - } - if j := strings.Index(rest, "."); j >= 0 { - osMajor += rest[:j] + "..." - } else { - osMajor += rest + "..." - } - out.Host[host].AddRecord(uptime, bootTime) - getOrNewAggregate(out.Kernel, osStr).AddRecord(uptime, bootTime) - getOrNewAggregate(out.KernelName, uname).AddRecord(uptime, bootTime) - getOrNewAggregate(out.KernelMajor, osMajor).AddRecord(uptime, bootTime) + } + return out, nil +} + +func processRecordsFile(ctx context.Context, path, host string, out *Aggregates) error { + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("open %s: %w", path, err) + } + defer f.Close() + + sc := bufio.NewScanner(f) + for sc.Scan() { + select { + case <-ctx.Done(): + return ctx.Err() + default: } - if err := sc.Err(); err != nil { - return nil, fmt.Errorf("scan %s: %w", path, err) + line := strings.TrimSpace(sc.Text()) + if line == "" { + continue + } + parts := strings.SplitN(line, ":", 3) + if len(parts) != 3 { + continue } + uptime, _ := strconv.ParseUint(parts[0], 10, 64) + bootTime, _ := strconv.ParseUint(parts[1], 10, 64) + osStr := parts[2] + uname := osStr + if i := strings.Index(osStr, " "); i > 0 { + uname = osStr[:i] + } + osMajor := uname + " " + rest := osStr + if i := strings.Index(osStr, " "); i >= 0 { + rest = osStr[i+1:] + } + if j := strings.Index(rest, "."); j >= 0 { + osMajor += rest[:j] + "..." + } else { + osMajor += rest + "..." + } + out.Host[host].AddRecord(uptime, bootTime) + getOrNewAggregate(out.Kernel, osStr).AddRecord(uptime, bootTime) + getOrNewAggregate(out.KernelName, uname).AddRecord(uptime, bootTime) + getOrNewAggregate(out.KernelMajor, osMajor).AddRecord(uptime, bootTime) } - return out, nil + if err := sc.Err(); err != nil { + return fmt.Errorf("scan %s: %w", path, err) + } + return nil } func getOrNewAggregate(m map[string]*Aggregate, name string) *Aggregate { diff --git a/internal/goprecords/report.go b/internal/goprecords/report.go index af61b29..01ca570 100644 --- a/internal/goprecords/report.go +++ b/internal/goprecords/report.go @@ -34,7 +34,7 @@ func NewHostReporter(aggregates *Aggregates, limit uint, metric Metric, outputFo } // Report returns the formatted report string. -func (r *Reporter) Report() string { +func (r Reporter) Report() string { var rows []tableRow var hasLastKernel bool if r.category == CategoryHost { @@ -48,7 +48,7 @@ func (r *Reporter) Report() string { return r.formatReport(rows, hasLastKernel) } -func (r *Reporter) buildHostTable() ([]tableRow, bool) { +func (r Reporter) buildHostTable() ([]tableRow, bool) { type keyVal struct { agg *HostAggregate key uint64 @@ -93,7 +93,7 @@ func (r *Reporter) buildHostTable() ([]tableRow, bool) { return rows, true } -func (r *Reporter) buildCategoryTable() ([]tableRow, bool) { +func (r Reporter) buildCategoryTable() ([]tableRow, bool) { m := r.aggregates.Kernel switch r.category { case CategoryKernelMajor: @@ -140,7 +140,7 @@ func (r *Reporter) buildCategoryTable() ([]tableRow, bool) { return rows, false } -func (r *Reporter) humanStrHost(h *HostAggregate) string { +func (r Reporter) humanStrHost(h *HostAggregate) string { switch r.metric { case MetricUptime: return formatDuration(h.Uptime) @@ -157,7 +157,7 @@ func (r *Reporter) humanStrHost(h *HostAggregate) string { } } -func (r *Reporter) humanStrAgg(a *Aggregate) string { +func (r Reporter) humanStrAgg(a *Aggregate) string { switch r.metric { case MetricUptime: return formatDuration(a.Uptime) @@ -170,7 +170,7 @@ func (r *Reporter) humanStrAgg(a *Aggregate) string { } } -func (r *Reporter) formatReport(rows []tableRow, hasLastKernel bool) string { +func (r Reporter) formatReport(rows []tableRow, hasLastKernel bool) string { cW, nW, vW, lkW := r.reportWidths(rows, hasLastKernel) border := r.buildBorder(cW, nW, vW, lkW, hasLastKernel) header := r.buildReportHeader(cW, nW, vW, lkW, hasLastKernel, border) @@ -183,7 +183,7 @@ func (r *Reporter) formatReport(rows []tableRow, hasLastKernel bool) string { return out } -func (r *Reporter) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nameW, valueW, lastKernelW int) { +func (r Reporter) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nameW, valueW, lastKernelW int) { countW = 3 nameW = len(r.category.String()) valueW = len(r.metric.String()) @@ -207,7 +207,7 @@ func (r *Reporter) reportWidths(rows []tableRow, hasLastKernel bool) (countW, na return countW, nameW, valueW, lastKernelW } -func (r *Reporter) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string { +func (r Reporter) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string { parts := []string{ "+" + strings.Repeat("-", 2+countW), "+" + strings.Repeat("-", 2+nameW), @@ -219,7 +219,7 @@ func (r *Reporter) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKe return strings.Join(parts, "") + "+\n" } -func (r *Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasLastKernel bool, border string) string { +func (r Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasLastKernel bool, border string) string { var h string if r.outputFormat == FormatMarkdown || r.outputFormat == FormatGemtext { h = strings.Repeat("#", int(r.headerIndent)) + " " @@ -227,8 +227,8 @@ func (r *Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, has h += fmt.Sprintf("Top %d %s's by %s\n\n", r.limit, r.metric, r.category) desc := MetricDescription(r.metric) lineLimit := len(border) - if r.outputFormat == FormatPlaintext && lineLimit > 0 && len(desc) > lineLimit { - desc = " " + wordWrap(desc, lineLimit) + if r.outputFormat == FormatPlaintext && lineLimit > 0 && len(desc) > lineLimit-1 { + desc = " " + wordWrap(desc, lineLimit-1) } h += desc + "\n\n" if r.outputFormat == FormatMarkdown || r.outputFormat == FormatGemtext { @@ -245,14 +245,14 @@ func (r *Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, has return h } -func (r *Reporter) buildFormatStr(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string { +func (r Reporter) buildFormatStr(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string { if hasLastKernel { return fmt.Sprintf("| %%%ds | %%%ds | %%%ds | %%%ds |", countW, nameW, valueW, lastKernelW) } return fmt.Sprintf("| %%%ds | %%%ds | %%%ds |", countW, nameW, valueW) } -func (r *Reporter) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel bool) string { +func (r Reporter) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel bool) string { var b strings.Builder for _, row := range rows { if hasLastKernel { diff --git a/internal/goprecords/types.go b/internal/goprecords/types.go index ce44df8..05a68f8 100644 --- a/internal/goprecords/types.go +++ b/internal/goprecords/types.go @@ -24,6 +24,65 @@ const ( CategoryKernelName ) +// Metric is the value to rank by (Boots, Uptime, etc.). +type Metric int + +const ( + MetricBoots Metric = iota + MetricUptime + MetricScore + MetricDowntime + MetricLifespan +) + +// OutputFormat is the report output format. +type OutputFormat int + +const ( + FormatPlaintext OutputFormat = iota + FormatMarkdown + FormatGemtext +) + +// Epoch is a Unix timestamp for duration/date formatting. +type Epoch uint64 + +// Aggregate holds per-entity stats (Host, Kernel, etc.). +type Aggregate struct { + Name string + Uptime uint64 + FirstBoot uint64 + LastSeen uint64 + Boots uint64 +} + +// NewAggregate constructs an Aggregate with the given name. +func NewAggregate(name string) *Aggregate { + return &Aggregate{Name: name} +} + +// HostAggregate adds last-kernel and lifespan/downtime for host reports. +type HostAggregate struct { + Aggregate + LastKernel string +} + +// NewHostAggregate constructs a HostAggregate. +func NewHostAggregate(name, lastKernel string) *HostAggregate { + return &HostAggregate{ + Aggregate: Aggregate{Name: name}, + LastKernel: lastKernel, + } +} + +// tableRow is one row in the report table. +type tableRow struct { + Pos string + Name string + Value string + LastKernel string +} + // String returns the category name. func (c Category) String() string { switch c { @@ -40,17 +99,6 @@ func (c Category) String() string { } } -// Metric is the value to rank by (Boots, Uptime, etc.). -type Metric int - -const ( - MetricBoots Metric = iota - MetricUptime - MetricScore - MetricDowntime - MetricLifespan -) - // String returns the metric name. func (m Metric) String() string { switch m { @@ -69,15 +117,6 @@ func (m Metric) String() string { } } -// OutputFormat is the report output format. -type OutputFormat int - -const ( - FormatPlaintext OutputFormat = iota - FormatMarkdown - FormatGemtext -) - // String returns the format name. func (f OutputFormat) String() string { switch f { @@ -92,9 +131,6 @@ func (f OutputFormat) String() string { } } -// Epoch is a Unix timestamp for duration/date formatting. -type Epoch uint64 - // HumanDuration returns a human-readable duration from epoch (e.g. "1 years, 2 months, 3 days"). func (e Epoch) HumanDuration() string { t := time.Unix(int64(e), 0).UTC() @@ -110,20 +146,6 @@ func (e Epoch) NewerThan(limitDays uint) bool { return time.Since(then) < time.Duration(limitDays)*24*time.Hour } -// Aggregate holds per-entity stats (Host, Kernel, etc.). -type Aggregate struct { - Name string - Uptime uint64 - FirstBoot uint64 - LastSeen uint64 - Boots uint64 -} - -// NewAggregate constructs an Aggregate with the given name. -func NewAggregate(name string) *Aggregate { - return &Aggregate{Name: name} -} - // AddRecord adds one uptime record. func (a *Aggregate) AddRecord(uptimeSec, bootTime uint64) { lastSeen := uptimeSec + bootTime @@ -151,20 +173,6 @@ func (a *Aggregate) MetaScore() uint64 { return ((a.Uptime*2 + a.Boots*uint64(Day) + activeBonus) / 1000000) } -// HostAggregate adds last-kernel and lifespan/downtime for host reports. -type HostAggregate struct { - Aggregate - LastKernel string -} - -// NewHostAggregate constructs a HostAggregate. -func NewHostAggregate(name, lastKernel string) *HostAggregate { - return &HostAggregate{ - Aggregate: Aggregate{Name: name}, - LastKernel: lastKernel, - } -} - // Lifespan returns last-seen minus first-boot. func (h *HostAggregate) Lifespan() uint64 { return h.LastSeen - h.FirstBoot } @@ -176,14 +184,6 @@ func (h *HostAggregate) MetaScore() uint64 { return uint64(h.Downtime()/2000000) + h.Aggregate.MetaScore() } -// tableRow is one row in the report table. -type tableRow struct { - Pos string - Name string - Value string - LastKernel string -} - // MetricDescription returns the description text for a metric. func MetricDescription(m Metric) string { switch m { @@ -257,22 +257,24 @@ func wordWrap(s string, lineLimit int) string { var b strings.Builder chars := 0 for _, word := range strings.Fields(s) { - wlen := len(word) - if chars > 0 { - wlen++ + wordLen := len(word) + needsSpace := chars > 0 + totalLen := wordLen + if needsSpace { + totalLen++ } - if chars+wlen > lineLimit { + if chars+totalLen > lineLimit { if chars > 0 { b.WriteByte('\n') } b.WriteString(word) - chars = len(word) + chars = wordLen } else { - if chars > 0 { + if needsSpace { b.WriteByte(' ') } b.WriteString(word) - chars += wlen + chars += totalLen } } return b.String() |
