fix(types): correct leap year calculation for century years

The naive year%4==0 check incorrectly treats years like 1900 and 2100
as leap years. Add types.IsLeapYear and types.YearLength helpers using
the full Gregorian calendar rule (divisible by 4, except centuries
unless divisible by 400). Replace all 8 occurrences across the
codebase.
This commit is contained in:
2026-04-08 09:41:44 +00:00
parent b02b5c3887
commit 028e9a9cac
5 changed files with 63 additions and 34 deletions

View File

@@ -54,10 +54,7 @@ func (paths RepoSet) GetRepoAuthors() ([]string, error) {
}
func (paths RepoSet) GetRepoCommits(year int, authors []string) ([][]types.Commit, error) {
yearLength := 365
if year%4 == 0 {
yearLength++
}
yearLength := types.YearLength(year)
commits := make([][]types.Commit, yearLength)
for i := 0; i < yearLength; i++ {
@@ -109,10 +106,6 @@ func (paths RepoSet) GetRepoCommits(year int, authors []string) ([][]types.Commi
}
func (paths RepoSet) FrequencyChan(year int, authors []string) (types.Freq, error) {
yearLength := 365
if year%4 == 0 {
yearLength++
}
cache, ok := GetCachedGraph(year, authors, paths)
if ok {
return cache, nil
@@ -152,10 +145,7 @@ func (paths RepoSet) FrequencyChan(year int, authors []string) (types.Freq, erro
}
func YearFreqFromChan(cc chan types.Commit, year int) types.Freq {
yearLength := 365
if year%4 == 0 {
yearLength++
}
yearLength := types.YearLength(year)
freq := make([]int, yearLength)
for commit := range cc {
freq[commit.TimeStamp.YearDay()-1]++
@@ -206,10 +196,7 @@ func (repo Repo) GetCommitChan() (chan types.Commit, error) {
}
func FreqFromChan(cc chan types.Commit, year int) types.Freq {
yearLength := 365
if year%4 == 0 {
yearLength++
}
yearLength := types.YearLength(year)
freq := make([]int, yearLength)
for commit := range cc {
if commit.TimeStamp.Year() != year {

View File

@@ -26,10 +26,7 @@ func (paths RepoSet) GetWeekFreq(authors []string) (types.Freq, error) {
return types.Freq{}, err
}
freq = append(curFreq, freq...)
today += 365
if curYear%4 == 0 {
today++
}
today += types.YearLength(curYear)
}
week := freq[today-6 : today+1]
@@ -37,10 +34,7 @@ func (paths RepoSet) GetWeekFreq(authors []string) (types.Freq, error) {
}
func (paths RepoSet) Frequency(year int, authors []string) (types.Freq, error) {
yearLength := 365
if year%4 == 0 {
yearLength++
}
yearLength := types.YearLength(year)
gfreq := make(types.Freq, yearLength)
for _, p := range paths {
repo, err := OpenRepo(p)
@@ -115,11 +109,7 @@ func (repo Repo) GetCommitSet() (CommitSet, error) {
}
func (cs CommitSet) ToYearFreq() types.Freq {
year := cs.Year
yearLength := 365
if year%4 == 0 {
yearLength++
}
yearLength := types.YearLength(cs.Year)
freq := make([]int, yearLength)
for _, v := range cs.Commits {
freq[v.TimeStamp.YearDay()-1]++

View File

@@ -48,6 +48,19 @@ type (
}
)
// IsLeapYear returns true if year is a leap year per the Gregorian calendar.
func IsLeapYear(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
// YearLength returns 366 for leap years and 365 otherwise.
func YearLength(year int) int {
if IsLeapYear(year) {
return 366
}
return 365
}
func (c Commit) String() string {
return fmt.Sprintf("%s\t%s\t%s\t%s",
c.TimeStamp.Format("0"+time.Kitchen),

View File

@@ -122,6 +122,45 @@ func TestFreqMerge(t *testing.T) {
}
}
func TestIsLeapYear(t *testing.T) {
tests := []struct {
year int
want bool
}{
{2024, true}, // divisible by 4
{2025, false}, // not divisible by 4
{1900, false}, // divisible by 100 but not 400
{2000, true}, // divisible by 400
{2100, false}, // divisible by 100 but not 400
{2400, true}, // divisible by 400
{1996, true}, // divisible by 4
{2023, false}, // not divisible by 4
}
for _, tt := range tests {
t.Run(time.Date(tt.year, 1, 1, 0, 0, 0, 0, time.UTC).Format("2006"), func(t *testing.T) {
got := IsLeapYear(tt.year)
if got != tt.want {
t.Errorf("IsLeapYear(%d) = %v, want %v", tt.year, got, tt.want)
}
})
}
}
func TestYearLength(t *testing.T) {
if YearLength(2024) != 366 {
t.Error("expected 366 for 2024")
}
if YearLength(2025) != 365 {
t.Error("expected 365 for 2025")
}
if YearLength(1900) != 365 {
t.Error("expected 365 for 1900 (century year, not leap)")
}
if YearLength(2000) != 366 {
t.Error("expected 366 for 2000 (divisible by 400)")
}
}
func TestDataSetOperations(t *testing.T) {
ds := NewDataSet()
now := time.Now().Truncate(24 * time.Hour)

View File

@@ -5,6 +5,8 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/taigrr/gico/types"
)
const (
@@ -66,12 +68,10 @@ func (m model) Init() tea.Cmd {
return nil
}
// YearLen returns the number of days in a year.
// Deprecated: Use types.YearLength instead.
func YearLen(year int) int {
yearLen := 365
if year%4 == 0 {
yearLen++
}
return yearLen
return types.YearLength(year)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {