Repository: boyter/scc

Issue:
Sorting doesn't work with csv format
**Describe the bug**
--sort <metric> flag doesn't work with --format flag set to csv

**To Reproduce**
scc compiled from master head (7befb06dbeb30afed830d2977482be16035cbc45) with `go build`
1. Get sample code base (for example: https://github.com/protocolbuffers/protobuf, commit: db7317baee15e92316ef8482d6901dc55cd3b5e8)
2. Run scc on the protobuf code base: scc --sort code --format csv .

**Expected behavior**
Returned csv lines should be sorted by "code" metric. 

**Actual behavior**
Actual output:
```
Language,Lines,Code,Comments,Blanks,Complexity,Bytes,Files,ULOC
C++,276049,225562,21120,29367,21648,10147866,483,0
C Header,190356,132019,35512,22825,9467,8172884,567,0
Java,126484,95860,17263,13361,9081,4778203,281,0
C#,183765,154984,15432,13349,23740,7573411,216,0
C,81946,63298,8409,10239,9566,2908280,127,0
Objective C,67744,54222,5725,7797,4618,2491308,80,0
Protocol Buffers,41413,28211,7257,5945,44,1487281,374,0
Python,39013,30389,5057,3567,1771,1423386,103,0
PHP,31907,18495,9927,3485,1011,1073697,155,0
Markdown,13685,10565,0,3120,0,536390,83,0
Bazel,25195,21592,1361,2242,799,810117,150,0
Rust,13675,9390,2518,1767,433,463349,84,0
Ruby,9726,7617,936,1173,376,361476,52,0
Kotlin,3537,2755,446,336,3,140217,21,0
CMake,3338,2825,223,290,235,151230,25,0
Shell,1975,1433,268,274,201,61273,35,0
YAML,3006,2551,199,256,0,119604,31,0
C++ Header,990,659,117,214,20,29978,6,0
Patch,1252,1072,0,180,0,48910,11,0
Plain Text,2882,2737,0,145,0,128183,52,0
ReStructuredText,589,451,0,138,0,20004,26,0
Lua,901,703,77,121,46,27280,2,0
Swift,645,511,59,75,9,25869,3,0
XML,1387,1311,20,56,0,63716,26,0
Autoconf,343,245,51,47,29,11319,7,0
Emacs Lisp,217,86,85,46,4,7886,1,0
Go,372,309,30,33,60,8619,4,0
MSBuild,252,204,15,33,4,12062,10,0
Makefile,139,102,7,30,10,5125,3,0
Rakefile,220,169,22,29,20,7185,2,0
Batch,106,79,3,24,15,2682,4,0
Dart,114,90,7,17,15,3170,2,0
TOML,119,88,15,16,1,3908,7,0
Vim Script,105,50,42,13,4,3805,1,0
AWK,50,34,6,10,0,684,2,0
License,54,46,0,8,0,2823,2,0
Objective C++,38,14,19,5,0,1244,1,0
Powershell,20,9,6,5,0,794,1,0
Gemfile,24,19,1,4,0,497,1,0
m4,11,8,0,3,0,454,1,0
JSON,1716,1716,0,0,0,60023,8,0
nuspec,39,39,0,0,0,2403,1,0
LD Script,6,6,0,0,0,60,1,0
```

The actual output is not sorted at all.
C# goes after java, even though C# > java in terms of "code"
And many more such errors.

**Desktop (please complete the following information):**
 - OS: ubuntu 24.04
 - go version: 1.24.5


Discussion:
@apocelipes: Hi, it looks like #536 introduced this bug.

I have sent #647, it should solve the problem, you can try it.@boyter: Merged in. @lazlo7 as mentioned if you could check it on your end that would help. Looks OK to me though.@lazlo7: Yeah, sorting seems to be fixed, thanks a lot.@boyter: All thanks to @apocelipes who implemented the fix.

Solution patch:
diff --git a/SCC-OUTPUT-REPORT.html b/SCC-OUTPUT-REPORT.html
index 3f3c0fa..82c2b0f 100644
--- a/SCC-OUTPUT-REPORT.html
+++ b/SCC-OUTPUT-REPORT.html
@@ -13,13 +13,13 @@
 	<tbody><tr>
 		<th>Go</th>
 		<th>27</th>
-		<th>22925</th>
-		<th>1439</th>
-		<th>438</th>
-		<th>21048</th>
-		<th>1362</th>
-		<th>444512</th>
-		<th>6202</th>
+		<th>23110</th>
+		<th>1442</th>
+		<th>442</th>
+		<th>21226</th>
+		<th>1369</th>
+		<th>449731</th>
+		<th>6246</th>
 	</tr><tr>
 		<td>processor/constants.go</td>
 		<td></td>
@@ -41,25 +41,25 @@
 		<td>32293</td>
 		<td>525</td>
 	</tr><tr>
-		<td>processor/formatters.go</td>
+		<td>processor/formatters_test.go</td>
 		<td></td>
-		<td>1461</td>
-		<td>201</td>
-		<td>31</td>
-		<td>1229</td>
-		<td>152</td>
-		<td>43371</td>
-		<td>768</td>
+		<td>1544</td>
+		<td>148</td>
+		<td>5</td>
+		<td>1391</td>
+		<td>124</td>
+		<td>36647</td>
+		<td>400</td>
 	</tr><tr>
-		<td>processor/formatters_test.go</td>
+		<td>processor/formatters.go</td>
 		<td></td>
-		<td>1415</td>
-		<td>146</td>
-		<td>3</td>
-		<td>1266</td>
-		<td>118</td>
-		<td>33190</td>
-		<td>360</td>
+		<td>1517</td>
+		<td>202</td>
+		<td>33</td>
+		<td>1282</td>
+		<td>153</td>
+		<td>45133</td>
+		<td>776</td>
 	</tr><tr>
 		<td>processor/workers.go</td>
 		<td></td>
@@ -200,16 +200,6 @@
 		<td>15</td>
 		<td>2771</td>
 		<td>80</td>
-	</tr><tr>
-		<td>processor/structs_test.go</td>
-		<td></td>
-		<td>96</td>
-		<td>11</td>
-		<td>1</td>
-		<td>84</td>
-		<td>10</td>
-		<td>1982</td>
-		<td>57</td>
 	</tr><tr>
 		<td>processor/trace.go</td>
 		<td></td>
@@ -220,6 +210,16 @@
 		<td>9</td>
 		<td>1957</td>
 		<td>58</td>
+	</tr><tr>
+		<td>processor/structs_test.go</td>
+		<td></td>
+		<td>96</td>
+		<td>11</td>
+		<td>1</td>
+		<td>84</td>
+		<td>10</td>
+		<td>1982</td>
+		<td>57</td>
 	</tr><tr>
 		<td>scripts/include.go</td>
 		<td></td>
@@ -294,15 +294,15 @@
 	<tfoot><tr>
 		<th>Total</th>
 		<th>27</th>
-		<th>22925</th>
-		<th>1439</th>
-		<th>438</th>
-		<th>21048</th>
-		<th>1362</th>
-		<th>444512</th>
-		<th>6202</th>
+		<th>23110</th>
+		<th>1442</th>
+		<th>442</th>
+		<th>21226</th>
+		<th>1369</th>
+		<th>449731</th>
+		<th>6246</th>
 	</tr>
 	<tr>
-		<th colspan="9">Estimated Cost to Develop (organic) $662,165<br>Estimated Schedule Effort (organic) 11.76 months<br>Estimated People Required (organic) 5.00<br></th>
+		<th colspan="9">Estimated Cost to Develop (organic) $668,046<br>Estimated Schedule Effort (organic) 11.80 months<br>Estimated People Required (organic) 5.03<br></th>
 	</tr></tfoot>
 	</table></body></html>
diff --git a/processor/formatters.go b/processor/formatters.go
index 453aa05..102b934 100644
--- a/processor/formatters.go
+++ b/processor/formatters.go
@@ -292,9 +292,65 @@ func toCSV(input chan *FileJob) string {
 	return toCSVSummary(input)
 }
 
+func getCSVSummarySortFunc(sortBy string) func(a, b []string) int {
+	// Cater for the common case of adding plural even for those options that don't make sense
+	// as it's quite common for those who English is not a first language to make a simple mistake
+	switch sortBy {
+	case "name", "names", "language", "languages", "lang", "langs":
+		return func(a, b []string) int {
+			return strings.Compare(a[0], b[0])
+		}
+	case "line", "lines":
+		return func(a, b []string) int {
+			i1, _ := strconv.ParseInt(a[1], 10, 64)
+			i2, _ := strconv.ParseInt(b[1], 10, 64)
+			return cmp.Compare(i2, i1)
+		}
+	case "blank", "blanks":
+		return func(a, b []string) int {
+			i1, _ := strconv.ParseInt(a[4], 10, 64)
+			i2, _ := strconv.ParseInt(b[4], 10, 64)
+			return cmp.Compare(i2, i1)
+		}
+	case "code", "codes":
+		return func(a, b []string) int {
+			i1, _ := strconv.ParseInt(a[2], 10, 64)
+			i2, _ := strconv.ParseInt(b[2], 10, 64)
+			return cmp.Compare(i2, i1)
+		}
+	case "comment", "comments":
+		return func(a, b []string) int {
+			i1, _ := strconv.ParseInt(a[3], 10, 64)
+			i2, _ := strconv.ParseInt(b[3], 10, 64)
+			return cmp.Compare(i2, i1)
+		}
+	case "complexity", "complexitys":
+		return func(a, b []string) int {
+			i1, _ := strconv.ParseInt(a[5], 10, 64)
+			i2, _ := strconv.ParseInt(b[5], 10, 64)
+			return cmp.Compare(i2, i1)
+		}
+	case "byte", "bytes":
+		return func(a, b []string) int {
+			i1, _ := strconv.ParseInt(a[6], 10, 64)
+			i2, _ := strconv.ParseInt(b[6], 10, 64)
+			return cmp.Compare(i2, i1)
+		}
+	case "file", "files":
+		return func(a, b []string) int {
+			i1, _ := strconv.ParseInt(a[7], 10, 64)
+			i2, _ := strconv.ParseInt(b[7], 10, 64)
+			return cmp.Compare(i2, i1)
+		}
+	default:
+		return func(a, b []string) int {
+			return strings.Compare(a[0], b[0])
+		}
+	}
+}
+
 func toCSVSummary(input chan *FileJob) string {
 	language := aggregateLanguageSummary(input)
-	language = sortLanguageSummary(language)
 
 	records := make([][]string, 0, len(language))
 
@@ -312,7 +368,7 @@ func toCSVSummary(input chan *FileJob) string {
 		})
 	}
 
-	slices.SortFunc(records, getRecordsSortFunc())
+	slices.SortFunc(records, getCSVSummarySortFunc(SortBy))
 
 	recordsEnd := [][]string{{
 		"Language",
@@ -336,10 +392,10 @@ func toCSVSummary(input chan *FileJob) string {
 	return b.String()
 }
 
-func getRecordsSortFunc() func(a, b []string) int {
+func getCSVFilesSortFunc(sortBy string) func(a, b []string) int {
 	// Cater for the common case of adding plural even for those options that don't make sense
 	// as it's quite common for those who English is not a first language to make a simple mistake
-	switch SortBy {
+	switch sortBy {
 	case "name", "names":
 		return func(a, b []string) int {
 			return strings.Compare(a[2], b[2])
@@ -409,7 +465,7 @@ func toCSVFiles(input chan *FileJob) string {
 		})
 	}
 
-	slices.SortFunc(records, getRecordsSortFunc())
+	slices.SortFunc(records, getCSVFilesSortFunc(SortBy))
 
 	recordsEnd := [][]string{{
 		"Language",



Test patch:
diff --git a/processor/formatters_test.go b/processor/formatters_test.go
index d3e5150..5d4e4d4 100644
--- a/processor/formatters_test.go
+++ b/processor/formatters_test.go
@@ -3,6 +3,7 @@
 package processor
 
 import (
+	"slices"
 	"strings"
 	"testing"
 
@@ -1413,3 +1414,131 @@ func BenchmarkFileSummerize(b *testing.B) {
 		fileSummarize(fileSummaryJobQueue)
 	}
 }
+
+func TestGetCSVSummarySortFunc(t *testing.T) {
+	records := [][]string{
+		// Language,Lines,Code,Comments,Blanks,Complexity,Bytes,Files,ULOC
+		{"Go", "10", "10", "0", "1", "1", "1024", "1", "0"},
+		{"Python", "20", "20", "1", "2", "2", "2048", "2", "0"},
+		{"C#", "30", "30", "2", "3", "3", "4096", "3", "0"},
+		{"C++", "40", "40", "3", "4", "4", "8192", "4", "0"},
+	}
+	testCases := []struct {
+		sortBy   string
+		expected []string
+	}{
+		{
+			sortBy:   "names",
+			expected: []string{"C#", "C++", "Go", "Python"},
+		},
+		{
+			sortBy:   "langs",
+			expected: []string{"C#", "C++", "Go", "Python"},
+		},
+		{
+			sortBy:   "lines",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "code",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "comments",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "blanks",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "complexity",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "bytes",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "files",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "default",
+			expected: []string{"C#", "C++", "Go", "Python"},
+		},
+	}
+	for _, tc := range testCases {
+		data := slices.Clone(records) // always use an unordered records
+		slices.SortFunc(data, getCSVSummarySortFunc(tc.sortBy))
+		sortedRecords := make([]string, 0, len(data))
+		for i := range data {
+			sortedRecords = append(sortedRecords, data[i][0])
+		}
+		if !slices.Equal(sortedRecords, tc.expected) {
+			t.Errorf("sortBy: %s failed, expected: %v, got: %v", tc.sortBy, tc.expected, sortedRecords)
+		}
+	}
+}
+
+func TestGetCSVFilesSortFunc(t *testing.T) {
+	records := [][]string{
+		// Language,Provider,Filename,Lines,Code,Comments,Blanks,Complexity,Bytes,ULOC
+		{"Go", "/path/to/file", "go.go", "10", "10", "0", "1", "1", "1024", "0"},
+		{"Python", "/path/to/file", "python.py", "20", "20", "1", "2", "2", "2048", "0"},
+		{"C#", "/path/to/file", "csharp.cs", "30", "30", "2", "3", "3", "4096", "0"},
+		{"C++", "/path/to/file", "cpp.cpp", "40", "40", "3", "4", "4", "8192", "0"},
+	}
+	testCases := []struct {
+		sortBy   string
+		expected []string
+	}{
+		{
+			sortBy:   "names",
+			expected: []string{"C++", "C#", "Go", "Python"},
+		},
+		{
+			sortBy:   "langs",
+			expected: []string{"C#", "C++", "Go", "Python"},
+		},
+		{
+			sortBy:   "lines",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "code",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "comments",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "blanks",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "complexity",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "bytes",
+			expected: []string{"C++", "C#", "Python", "Go"},
+		},
+		{
+			sortBy:   "default",
+			expected: []string{"C++", "C#", "Go", "Python"},
+		},
+	}
+	for _, tc := range testCases {
+		data := slices.Clone(records) // always use an unordered records
+		slices.SortFunc(data, getCSVFilesSortFunc(tc.sortBy))
+		sortedRecords := make([]string, 0, len(data))
+		for i := range data {
+			sortedRecords = append(sortedRecords, data[i][0])
+		}
+		if !slices.Equal(sortedRecords, tc.expected) {
+			t.Errorf("sortBy: %s failed, expected: %v, got: %v", tc.sortBy, tc.expected, sortedRecords)
+		}
+	}
+}