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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 0.8.3 - Unreleased

- Round compact stat badges (issue, PR, star, and release download counts) to the nearest thousand and million instead of truncating, so a count like 19,999 shows as "20K" rather than "19K" and matches GitHub's own compact numbers (thanks @LeoLin990405). (#81)

## 0.8.2 - 2026-06-12

- Streamline the GitHub API submenu by showing sample age once, while moving shared-budget guidance into a dedicated API settings tab.
Expand Down
18 changes: 10 additions & 8 deletions Sources/RepoBar/Support/StatValueFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ enum StatValueFormatter {
return "\(short)K"
}
if value < 1_000_000 {
return "\(value / 1000)K"
let thousands = self.rounded(value, divisor: 1000)
return thousands >= 1000 ? "1M" : "\(thousands)K"
}
if value < 10_000_000 {
let short = self.oneDecimal(value, divisor: 1_000_000)
return "\(short)M"
}
if value >= 1_000_000_000 {
return "999M"
}
return "\(value / 1_000_000)M"
let millions = self.rounded(value, divisor: 1_000_000)
return millions >= 1000 ? "999M" : "\(millions)M"
}

private static func rounded(_ value: Int, divisor: Int) -> Int {
// Round half up without an intermediate `value + divisor/2` that could
// overflow near Int.max: compare the remainder against half the divisor.
value / divisor + (value % divisor * 2 >= divisor ? 1 : 0)
}

private static func oneDecimal(_ value: Int, divisor: Double) -> String {
Expand All @@ -26,9 +31,6 @@ enum StatValueFormatter {
if formatted.hasSuffix(".0") {
return String(formatted.dropLast(2))
}
if formatted.hasPrefix("10") {
return "10"
}
return formatted
}
}
55 changes: 55 additions & 0 deletions Tests/RepoBarTests/StatValueFormatterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@testable import RepoBar
import Testing

struct StatValueFormatterTests {
@Test
func `passes through values below one thousand`() {
#expect(StatValueFormatter.compact(0) == "0")
#expect(StatValueFormatter.compact(42) == "42")
#expect(StatValueFormatter.compact(999) == "999")
}

@Test
func `keeps one decimal below ten thousand`() {
#expect(StatValueFormatter.compact(1000) == "1K")
#expect(StatValueFormatter.compact(1500) == "1.5K")
#expect(StatValueFormatter.compact(1999) == "2K")
#expect(StatValueFormatter.compact(9999) == "10K")
}

@Test
func `rounds thousands to nearest instead of truncating`() {
#expect(StatValueFormatter.compact(10000) == "10K")
#expect(StatValueFormatter.compact(10500) == "11K")
#expect(StatValueFormatter.compact(19999) == "20K")
#expect(StatValueFormatter.compact(99500) == "100K")
#expect(StatValueFormatter.compact(123_456) == "123K")
}

@Test
func `rolls rounded thousands up into millions`() {
#expect(StatValueFormatter.compact(999_499) == "999K")
#expect(StatValueFormatter.compact(999_500) == "1M")
#expect(StatValueFormatter.compact(999_999) == "1M")
}

@Test
func `keeps one decimal below ten million`() {
#expect(StatValueFormatter.compact(1_000_000) == "1M")
#expect(StatValueFormatter.compact(1_500_000) == "1.5M")
#expect(StatValueFormatter.compact(9_999_999) == "10M")
}

@Test
func `rounds millions and caps oversized counts`() {
#expect(StatValueFormatter.compact(10_500_000) == "11M")
#expect(StatValueFormatter.compact(99_999_999) == "100M")
#expect(StatValueFormatter.compact(999_000_000) == "999M")
#expect(StatValueFormatter.compact(1_000_000_000) == "999M")
}

@Test
func `caps Int.max without integer overflow`() {
#expect(StatValueFormatter.compact(Int.max) == "999M")
}
}
Loading