Skip to content

Commit bae4a0c

Browse files
authored
feat(comment): order changed projects by absolute cost change (#24)
Sort the changed projects table by descending absolute total cost change, falling back to alphabetical order for ties. Hide zero-change projects when at least one project changed cost.
1 parent 3ef779b commit bae4a0c

3 files changed

Lines changed: 118 additions & 3 deletions

File tree

pkg/vcs/comment/costs.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,37 @@ func (data *Data) buildCostTableEntries() []CostTableEntry {
4949
project.PastTotalMonthlyCost, project.TotalMonthlyCost, data.Currency,
5050
costChangeOpts{},
5151
),
52-
NewTotalCost: formatCost(project.TotalMonthlyCost, data.Currency),
52+
NewTotalCost: formatCost(project.TotalMonthlyCost, data.Currency),
53+
absTotalCostChange: safeSub(project.TotalMonthlyCost, project.PastTotalMonthlyCost).Abs(),
5354
})
5455
}
5556

57+
// If at least one project changed cost, hide the projects whose cost did not
58+
// change. When no project changed cost, keep them all so the table is not
59+
// empty.
60+
hasCostChange := false
61+
for _, entry := range entries {
62+
if !entry.absTotalCostChange.IsZero() {
63+
hasCostChange = true
64+
break
65+
}
66+
}
67+
if hasCostChange {
68+
changed := make([]CostTableEntry, 0, len(entries))
69+
for _, entry := range entries {
70+
if !entry.absTotalCostChange.IsZero() {
71+
changed = append(changed, entry)
72+
}
73+
}
74+
entries = changed
75+
}
76+
77+
// Biggest changes first, falling back to alphabetical ordering for entries
78+
// with an equal absolute cost change.
5679
sort.Slice(entries, func(i, j int) bool {
80+
if !entries[i].absTotalCostChange.Equals(entries[j].absTotalCostChange) {
81+
return entries[i].absTotalCostChange.GreaterThan(entries[j].absTotalCostChange)
82+
}
5783
if entries[i].ProjectName != entries[j].ProjectName {
5884
return entries[i].ProjectName < entries[j].ProjectName
5985
}
@@ -130,8 +156,8 @@ func projectHasDiff(project ProjectResult) bool {
130156

131157
// costChangeOpts controls formatting of cost change strings.
132158
type costChangeOpts struct {
133-
skipPercent bool
134-
skipIfZero bool
159+
skipPercent bool
160+
skipIfZero bool
135161
skipPlusMinus bool
136162
}
137163

pkg/vcs/comment/costs_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,91 @@ package comment
33
import (
44
"testing"
55
"unicode/utf8"
6+
7+
"github.com/infracost/go-proto/pkg/rat"
68
)
79

10+
func TestBuildCostTableEntriesOrdering(t *testing.T) {
11+
project := func(name string, past, current int64) ProjectResult {
12+
return ProjectResult{
13+
Name: name,
14+
PastTotalMonthlyCost: rat.New(past),
15+
TotalMonthlyCost: rat.New(current),
16+
DiffBreakdown: &CostBreakdown{
17+
Resources: []BreakdownResource{{Name: "aws_instance.web"}},
18+
},
19+
}
20+
}
21+
22+
data := &Data{
23+
Projects: []ProjectResult{
24+
project("b-same-change", 100, 110),
25+
project("small-increase", 100, 150),
26+
project("big-decrease", 1000, 100),
27+
project("a-same-change", 100, 110),
28+
project("big-increase", 100, 700),
29+
},
30+
}
31+
32+
entries := data.buildCostTableEntries()
33+
34+
want := []string{
35+
"big-decrease", // -$900
36+
"big-increase", // +$600
37+
"small-increase", // +$50
38+
"a-same-change", // +$10, alphabetical tie-break
39+
"b-same-change", // +$10
40+
}
41+
42+
if len(entries) != len(want) {
43+
t.Fatalf("got %d entries, want %d", len(entries), len(want))
44+
}
45+
for i, name := range want {
46+
if entries[i].ProjectName != name {
47+
t.Errorf("entry %d = %q, want %q", i, entries[i].ProjectName, name)
48+
}
49+
}
50+
}
51+
52+
func TestBuildCostTableEntriesHidesUnchangedProjects(t *testing.T) {
53+
project := func(name string, past, current int64) ProjectResult {
54+
return ProjectResult{
55+
Name: name,
56+
PastTotalMonthlyCost: rat.New(past),
57+
TotalMonthlyCost: rat.New(current),
58+
DiffBreakdown: &CostBreakdown{
59+
Resources: []BreakdownResource{{Name: "aws_instance.web"}},
60+
},
61+
}
62+
}
63+
64+
t.Run("some projects changed", func(t *testing.T) {
65+
data := &Data{Projects: []ProjectResult{
66+
project("unchanged", 100, 100),
67+
project("changed", 100, 150),
68+
}}
69+
70+
entries := data.buildCostTableEntries()
71+
72+
if len(entries) != 1 || entries[0].ProjectName != "changed" {
73+
t.Errorf("got %+v, want only the changed project", entries)
74+
}
75+
})
76+
77+
t.Run("no projects changed", func(t *testing.T) {
78+
data := &Data{Projects: []ProjectResult{
79+
project("a", 100, 100),
80+
project("b", 50, 50),
81+
}}
82+
83+
entries := data.buildCostTableEntries()
84+
85+
if len(entries) != 2 {
86+
t.Errorf("got %d entries, want both projects kept", len(entries))
87+
}
88+
})
89+
}
90+
891
func TestTruncateMiddle(t *testing.T) {
992
tests := []struct {
1093
name string

pkg/vcs/comment/template.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"strings"
88
"text/template"
99
"unicode/utf8"
10+
11+
"github.com/infracost/go-proto/pkg/rat"
1012
)
1113

1214
const (
@@ -541,6 +543,10 @@ type CostTableEntry struct {
541543

542544
// NewTotalCost is the formatted new monthly cost, e.g. "$560.00".
543545
NewTotalCost string
546+
547+
// absTotalCostChange is the absolute total cost change, used to sort rows so
548+
// the biggest changes show first. It is not rendered in the template.
549+
absTotalCostChange *rat.Rat
544550
}
545551

546552
type ProjectError struct {

0 commit comments

Comments
 (0)