Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions internal/mycli/format/width.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,6 @@ func (wc *widthCalculator) maxWidth(s string) int {
stringsiter.SplitFunc(s, 0, stringsiter.CutNewLine)))
}

func clipToMax[S interface{ ~[]E }, E cmp.Ordered](s S, maxValue E) iter.Seq[E] {
return hiter.Map(
func(in E) E {
return min(in, maxValue)
},
slices.Values(s),
)
}

func asc[T cmp.Ordered](left, right T) int {
switch {
case left < right:
Expand All @@ -106,19 +97,29 @@ func adjustToSum(limit int, vs []int) ([]int, int) {
return vs, remains
}

// Build sorted unique thresholds (descending) once.
rev := slices.SortedFunc(slices.Values(lo.Uniq(vs)), desc)

curVs := vs
for i := 1; ; i++ {
rev := slices.SortedFunc(slices.Values(lo.Uniq(vs)), desc)
v, ok := hiter.Nth(i, slices.Values(rev))
if !ok {
break
for i := 1; i < len(rev); i++ {
threshold := rev[i]
clipped := make([]int, len(vs))
total := 0
for j, v := range vs {
clipped[j] = min(v, threshold)
total += clipped[j]
}
curVs = slices.Collect(clipToMax(vs, v))
if lo.Sum(curVs) <= limit {
curVs = clipped
if total <= limit {
break
}
}
return curVs, limit - lo.Sum(curVs)

total := 0
for _, v := range curVs {
total += v
}
return curVs, limit - total
}

var invalidWidthCount = WidthCount{
Expand Down
67 changes: 40 additions & 27 deletions internal/mycli/format/width_strategy_greedy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package format
import (
"fmt"
"log/slog"
"math"
"slices"

"github.com/ngicks/go-iterator-helper/hiter"
Expand All @@ -32,8 +31,19 @@ type GreedyFrequencyStrategy struct{}
func (GreedyFrequencyStrategy) CalculateWidths(wc *widthCalculator, availableWidth int,
headers []string, rows []Row, _ []ColumnHint,
) []int {
if len(headers) == 0 {
return []int{}
}

sumWidths := func(ws []int) int {
total := 0
for _, w := range ws {
total += w
}
return total
}
formatIntermediate := func(remainsWidth int, adjustedWidths []int) string {
return fmt.Sprintf("remaining %v, adjustedWidths: %v", remainsWidth-lo.Sum(adjustedWidths), adjustedWidths)
return fmt.Sprintf("remaining %v, adjustedWidths: %v", remainsWidth-sumWidths(adjustedWidths), adjustedWidths)
}

adjustedWidths := adjustByHeader(headers, availableWidth)
Expand All @@ -45,15 +55,17 @@ func (GreedyFrequencyStrategy) CalculateWidths(wc *widthCalculator, availableWid

slog.Debug("adjustByName", "info", formatIntermediate(availableWidth, adjustedWidths))

var transposedRows [][]string
transposedRows := make([][]string, len(headers))
headerRow := StringsToRow(headers...)
for columnIdx := range len(headers) {
transposedRows = append(transposedRows, slices.Collect(
hiter.Map(
func(in Row) string {
return lo.Must(lo.Nth(in, columnIdx)).RawText()
},
hiter.Concat(hiter.Once(StringsToRow(headers...)), slices.Values(rows)),
)))
col := make([]string, 0, 1+len(rows))
col = append(col, headerRow[columnIdx].RawText())
for _, row := range rows {
if columnIdx < len(row) {
col = append(col, row[columnIdx].RawText())
}
}
transposedRows[columnIdx] = col
}

widthCounts := wc.calculateWidthCounts(adjustedWidths, transposedRows)
Expand All @@ -67,7 +79,7 @@ func (GreedyFrequencyStrategy) CalculateWidths(wc *widthCalculator, availableWid
slices.Values(widthCounts))

// find the largest count idx within available width
idx, target := wc.maxIndex(availableWidth-lo.Sum(adjustedWidths), adjustedWidths, firstCounts)
idx, target := wc.maxIndex(availableWidth-sumWidths(adjustedWidths), adjustedWidths, firstCounts)
if idx < 0 || target.Count() < 1 {
break
}
Expand All @@ -81,23 +93,24 @@ func (GreedyFrequencyStrategy) CalculateWidths(wc *widthCalculator, availableWid
slog.Debug("semi final", "info", formatIntermediate(availableWidth, adjustedWidths))

// Add rest to the longest shortage column.
// NOTE: When all columns fit within their allocated width (no remaining
// widthCounts), idx will be -1 and the remainder is not distributed.
// This matches the original calculateOptimalWidth behavior. Improving this
// to always use the full available width is left for a future refactor.
longestWidths := lo.Map(widthCounts, func(item []WidthCount, _ int) int {
return hiter.Max(hiter.Map(WidthCount.Length, slices.Values(item)))
})

idx, _ := MaxWithIdx(math.MinInt, hiter.Unify(
func(longestWidth, adjustedWidth int) int {
return longestWidth - adjustedWidth
},
hiter.Pairs(slices.Values(longestWidths), slices.Values(adjustedWidths))))

if idx != -1 {
adjustedWidths[idx] += availableWidth - lo.Sum(adjustedWidths)
// Fall back to column 0 when all columns fit (no remaining widthCounts).
longestWidths := make([]int, len(headers))
for i, wcs := range widthCounts {
for _, wc := range wcs {
longestWidths[i] = max(longestWidths[i], wc.Length())
}
}

bestIdx := 0
bestShortage := longestWidths[0] - adjustedWidths[0]
Comment thread
apstndb marked this conversation as resolved.
for i := 1; i < len(headers); i++ {
shortage := longestWidths[i] - adjustedWidths[i]
if shortage > bestShortage {
bestShortage = shortage
bestIdx = i
}
}
adjustedWidths[bestIdx] += availableWidth - sumWidths(adjustedWidths)

slog.Debug("final", "info", formatIntermediate(availableWidth, adjustedWidths))

Expand Down
26 changes: 14 additions & 12 deletions internal/mycli/format/width_strategy_proportional.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,24 @@ func (ProportionalStrategy) CalculateWidths(wc *widthCalculator, availableWidth

// Distribute remaining space proportionally to deficit.
remaining := availableWidth - lo.Sum(adjustedWidths)
if remaining > 0 && totalDeficit > 0 {
if remaining > 0 {
distributed := 0
for i := range numCols {
if deficits[i] > 0 {
share := remaining * deficits[i] / totalDeficit
// Don't exceed the natural width.
share = min(share, deficits[i])
adjustedWidths[i] += share
distributed += share
if totalDeficit > 0 {
for i := range numCols {
if deficits[i] > 0 {
share := remaining * deficits[i] / totalDeficit
// Don't exceed the natural width.
share = min(share, deficits[i])
adjustedWidths[i] += share
distributed += share
}
}
}

// Assign leftover (from integer division rounding) to the column with
// the largest remaining deficit. If all columns have reached their
// natural width, fall back to the first column to ensure the full
// available width is always used.
// Assign leftover (from integer division rounding, or all of remaining
// if totalDeficit was 0) to the column with the largest remaining
// deficit. Fall back to column 0 to ensure the full available width
// is always used.
leftover := remaining - distributed
if leftover > 0 && numCols > 0 {
remainingDeficits := make([]int, numCols)
Expand Down
72 changes: 72 additions & 0 deletions internal/mycli/format/width_strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,78 @@ func TestWrapLinesForWidth(t *testing.T) {
}
}

// TestGreedyFrequencyStrategy_ShortRows ensures GreedyFrequencyStrategy
// handles rows shorter than headers without panicking.
func TestGreedyFrequencyStrategy_ShortRows(t *testing.T) {
t.Parallel()

wc := newTestWidthCalculator()
strategy := GreedyFrequencyStrategy{}

headers := []string{"id", "name", "description"}
rows := []Row{
StringsToRow("1", "Alice"), // only 2 columns, header has 3
StringsToRow("2"), // only 1 column
}

widths := strategy.CalculateWidths(wc, 60, headers, rows, make([]ColumnHint, 3))
if len(widths) != 3 {
t.Fatalf("len(widths) = %d, want 3", len(widths))
}
for i, w := range widths {
if w < minColumnWidth {
t.Errorf("widths[%d] = %d, want >= %d", i, w, minColumnWidth)
}
}
}

// TestProportionalStrategy_ZeroDeficit ensures ProportionalStrategy uses the
// full available width even when all columns already fit (totalDeficit == 0).
func TestProportionalStrategy_ZeroDeficit(t *testing.T) {
t.Parallel()

wc := newTestWidthCalculator()
strategy := ProportionalStrategy{}

// Very short data with large available width — all columns fit easily.
headers := []string{"a", "b"}
rows := []Row{StringsToRow("1", "2")}

availableWidth := 80
widths := strategy.CalculateWidths(wc, availableWidth, headers, rows, make([]ColumnHint, 2))

totalWidth := 0
for _, w := range widths {
totalWidth += w
}
if totalWidth != availableWidth {
t.Errorf("total width = %d, want %d (full available width)", totalWidth, availableWidth)
}
}

// TestGreedyFrequencyStrategy_FullWidth ensures GreedyFrequencyStrategy uses
// the full available width even when all columns fit.
func TestGreedyFrequencyStrategy_FullWidth(t *testing.T) {
t.Parallel()

wc := newTestWidthCalculator()
strategy := GreedyFrequencyStrategy{}

headers := []string{"a", "b"}
rows := []Row{StringsToRow("1", "2")}

availableWidth := 80
widths := strategy.CalculateWidths(wc, availableWidth, headers, rows, make([]ColumnHint, 2))

totalWidth := 0
for _, w := range widths {
totalWidth += w
}
if totalWidth != availableWidth {
t.Errorf("total width = %d, want %d (full available width)", totalWidth, availableWidth)
}
}

// TestStrategyMinColumnWidth ensures all strategies respect minColumnWidth.
func TestStrategyMinColumnWidth(t *testing.T) {
t.Parallel()
Expand Down
Loading