all repos — bibel @ c045b445fe953f81a929780728841400a44ce422

Unnamed repository; edit this file 'description' to name the repository.

fix: eliminate short-day bug with cumulative lookahead

- Replace stateless `(day-1)*12` offset with pre-computed cumulative
  offsets using lookahead logic — every day now reads 12+ verses
  (was 26% short days, some as low as 1 verse)
- Extract `findMaxChapter` helper and `computeConsumed` logic for
  reuse in both precomputation and position lookup
- Update test expectations for Day 110 (Mark 6 → 12) and Day 316
  (Matthew 1 → 18) to reflect corrected progression
- Resolve 11 golangci-lint issues across 6 files (`errorlint`,
  `gocritic`, `perfsprint`, `staticcheck`, `godoclint`)
- Add `.golangci.yml` linter configuration
- Update `CHANGELOG.md` with version 1.0.2 entry

💘 Generated with Crush

Assisted-by: DeepSeek-V3.2 (Thinking Mode) via Crush <crush@charm.land>
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Fri, 01 May 2026 09:41:48 +0200
commit

c045b445fe953f81a929780728841400a44ce422

parent

22bb3a6ab182daf32755b4316471ff8c8f4a4fe6

M .gitignore.gitignore

@@ -1,11 +1,11 @@

# Binaries for programs and plugins bibel -bibel*linux *.exe *.exe~ *.dll *.so *.dylib +dist/ # Test binary, built with `go test -c` *.test

@@ -27,4 +27,4 @@ # env file

.env # Honk -.goosehints +AGENTS.md
A .golangci.yml

@@ -0,0 +1,16 @@

+version: "2" + +linters: + enable: + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - usetesting # reports uses of functions with replacement inside the testing package + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative + - unused # checks for unused constants, variables, functions and types + - gocritic # provides diagnostics that check for bugs, performance and style issues + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - godoclint # checks Golang's documentation practice + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - recvcheck # checks for receiver type consistency + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library
A .goreleaser.yaml

@@ -0,0 +1,66 @@

+version: 2 + +builds: + - main: ./... + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - arm64 + - amd64 + - "386" + dir: cmd + +archives: + - formats: [tar.gz] + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + formats: [zip] + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +upx: + - # Templates: allowed. + enabled: true + + # Filter by build ID. + ids: [build1, build2] + + # Filter by GOOS. + goos: [linux, darwin] + + # Filter by GOARCH. + goarch: [arm, amd64] + + # Filter by GOARM. + goarm: [8] + + # Filter by GOAMD64. + goamd64: [v1] + + # Compress argument. + # Valid options are from '1' (faster) to '9' (better), and 'best'. + compress: best + + # Whether to try LZMA (slower). + lzma: true + + # Whether to try all methods and filters (slow). + brute: true
M CHANGELOG.mdCHANGELOG.md

@@ -8,6 +8,17 @@ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased] +## [1.0.2] - 2026-05-01 + +### Fixed + +- **Short-day bug in date progression**: 96/365 days (26%) had fewer than 12 + verses, some as low as 1. Replaced stateless `(day-1)*12` offset with + pre-computed cumulative offsets using lookahead logic — every day now reads + 12+ verses +- **Lint issues**: Resolved 11 golangci-lint issues across 6 files (`errorlint`, + `gocritic`, `perfsprint`, `staticcheck`, `godoclint`) + ## [1.0.1] - 2026-04-20 ### Fixed
M build/test/test_dates.gobuild/test/test_dates.go

@@ -39,15 +39,16 @@ expectedChapter: 1,

expectedStartVerse: 1, expectedEndVerse: 12, }, - // Day 110 (20 April, 2026) - based on current output + // Day 110 (20 April, 2026) - Mark 12:1-12 + // With cumulative lookahead, all days read 12+ verses and no short days occur. { name: "Day 110 - evangelion", date: time.Date(2026, 4, 20, 0, 0, 0, 0, time.UTC), readingMode: bible.ReadingModeEvangelion, expectedBook: 41, // Mark - expectedChapter: 6, - expectedStartVerse: 46, - expectedEndVerse: 56, + expectedChapter: 12, + expectedStartVerse: 1, + expectedEndVerse: 12, }, // Test New Testament mode - should also start at Matthew 1:1-12 {

@@ -71,16 +72,16 @@ expectedEndVerse: 12,

}, // Test wrap-around: Day that would exceed total Gospel verses // 3779 verses / 12 verses/day = ~315 days - // Day 316 should wrap back to near beginning (offset = (316-1)*12 = 3780, modulo 3779 = 1) - // So should be Matthew 1:2-13 + // With cumulative lookahead, extra verses consumed on chapter-boundary + // days push the schedule further, so day 316 lands in Matthew 18. { name: "Day 316 - evangelion (wrap test)", date: time.Date(2026, 11, 12, 0, 0, 0, 0, time.UTC), // Day 316 readingMode: bible.ReadingModeEvangelion, expectedBook: 40, - expectedChapter: 1, - expectedStartVerse: 2, - expectedEndVerse: 13, + expectedChapter: 18, + expectedStartVerse: 1, + expectedEndVerse: 12, }, }
M go.modgo.mod

@@ -8,9 +8,7 @@ github.com/charmbracelet/bubbletea v0.24.0

github.com/charmbracelet/lipgloss v0.8.0 ) -require ( - github.com/pelletier/go-toml/v2 v2.3.0 // indirect -) +require github.com/pelletier/go-toml/v2 v2.3.0 // indirect require ( github.com/adrg/xdg v0.5.3
M go.sumgo.sum

@@ -72,8 +72,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=

github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/vjeantet/eastertime v1.0.0 h1:1jKWtfYLmSzl+aA4Oxz3arR8xy/2QDkJ1oxMtZimNZc= -github.com/vjeantet/eastertime v1.0.0/go.mod h1:oyhoGGL+4Xkm1kcuSRx/Cncj4UCur/fg7nd9MEJbAto= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
M internal/config.gointernal/config.go

@@ -1,6 +1,7 @@

package bible import ( + "errors" "fmt" "os" "path/filepath"

@@ -145,7 +146,8 @@ viper.SetDefault("date_progression.start_date", defaultCfg.DateProgression.StartDate)

// Read in config file (if it exists) if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { + var cfgErr viper.ConfigFileNotFoundError + if errors.As(err, &cfgErr) { // Config file not found; we'll use defaults if configPath != "" { logger.Info("No configuration file found at %s", configPath)
M internal/dateprogression.gointernal/dateprogression.go

@@ -7,14 +7,18 @@ )

// DateProgression handles calculating Bible position based on date type DateProgression struct { - bible *Bible + bible *Bible versesPerDay int readingMode ReadingMode + // Pre-computed cumulative offset at the START of each day of year (1-indexed) + dailyOffsets []int } // NewDateProgression creates a new date progression calculator func NewDateProgression(bible *Bible) *DateProgression { - return &DateProgression{bible: bible, versesPerDay: 12, readingMode: ReadingModeEvangelion} + dp := &DateProgression{bible: bible, versesPerDay: 12, readingMode: ReadingModeEvangelion} + dp.precomputeDailyOffsets() + return dp } // NewDateProgressionWithConfig creates a new date progression calculator with config

@@ -22,7 +26,9 @@ func NewDateProgressionWithConfig(bible *Bible, versesPerDay int) *DateProgression {

if versesPerDay <= 0 { versesPerDay = 12 } - return &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: ReadingModeEvangelion} + dp := &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: ReadingModeEvangelion} + dp.precomputeDailyOffsets() + return dp } // NewDateProgressionWithReadingMode creates a new date progression calculator with reading mode

@@ -30,7 +36,78 @@ func NewDateProgressionWithReadingMode(bible *Bible, versesPerDay int, readingMode ReadingMode) *DateProgression {

if versesPerDay <= 0 { versesPerDay = 12 } - return &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: readingMode} + dp := &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: readingMode} + dp.precomputeDailyOffsets() + return dp +} + +// precomputeDailyOffsets computes the cumulative verse offset for each day of the year +// using lookahead logic: when fewer than versesPerDay remain in a chapter, extend +// to read all remaining verses in that chapter. This eliminates short reading days. +func (dp *DateProgression) precomputeDailyOffsets() { + totalVerses := dp.GetTotalVersesForMode() + if totalVerses == 0 { + dp.dailyOffsets = make([]int, 367) + return + } + + dp.dailyOffsets = make([]int, 367) // 1-indexed by day of year + cumOffset := 0 + + for day := 1; day <= 366; day++ { + dp.dailyOffsets[day] = cumOffset % totalVerses + cumOffset += dp.computeConsumed(dp.dailyOffsets[day]) + } +} + +// computeConsumed calculates the number of verses actually read starting from a given +// cumulative offset. Uses lookahead: if fewer than versesPerDay remain in the chapter, +// extends to read all remaining verses in that chapter. +func (dp *DateProgression) computeConsumed(offset int) int { + startBook, endBook := dp.getBookRange() + + for book := startBook; book <= endBook; book++ { + maxChapter := dp.findMaxChapter(book) + if maxChapter == 0 { + continue + } + + for chapter := 1; chapter <= maxChapter; chapter++ { + versesInChapter := dp.bible.CountVersesInChapter(book, chapter) + + if offset < versesInChapter { + firstVerse := offset + 1 + lastVerse := firstVerse + dp.versesPerDay - 1 + if lastVerse > versesInChapter { + lastVerse = versesInChapter + } + // Lookahead: if fewer than versesPerDay remain, read to chapter end + remaining := versesInChapter - lastVerse + if remaining > 0 && remaining < dp.versesPerDay { + lastVerse = versesInChapter + } + return lastVerse - firstVerse + 1 + } + + offset -= versesInChapter + } + } + + // Wrap around + totalVerses := dp.GetTotalVersesForMode() + if totalVerses > 0 { + return dp.computeConsumed(offset % totalVerses) + } + return 0 +} + +// findMaxChapter finds the last chapter number in a book +func (dp *DateProgression) findMaxChapter(book int) int { + for chapter := 1; ; chapter++ { + if dp.bible.CountVersesInChapter(book, chapter) == 0 { + return chapter - 1 + } + } } // GetPositionForDate calculates the verse range for a given date

@@ -38,68 +115,65 @@ func (dp *DateProgression) GetPositionForDate(date time.Time) (*Bookmark, error) {

// Calculate day of year (1-366) dayOfYear := date.YearDay() - // Each day shows configured number of verses - targetVerseOffset := (dayOfYear - 1) * dp.versesPerDay // Zero-indexed + if dayOfYear < 1 || dayOfYear >= len(dp.dailyOffsets) { + dayOfYear = 1 + } - // Walk through appropriate books based on reading mode + // Look up pre-computed cumulative offset for this day + targetVerseOffset := dp.dailyOffsets[dayOfYear] + return dp.findPositionForOffset(targetVerseOffset) } -// findPositionForOffset finds the bookmark for a given cumulative verse offset +// findPositionForOffset finds the bookmark for a given cumulative verse offset. +// Applies lookahead: if fewer than versesPerDay remain in the chapter, extends +// to read to the end of the chapter. func (dp *DateProgression) findPositionForOffset(offset int) (*Bookmark, error) { - // Determine which books to iterate through based on reading mode startBook, endBook := dp.getBookRange() - - // Iterate through books in the range + for book := startBook; book <= endBook; book++ { - // Find last chapter in this book - maxChapter := 0 - for chapter := 1; ; chapter++ { - if dp.bible.CountVersesInChapter(book, chapter) == 0 { - maxChapter = chapter - 1 - break - } - } + maxChapter := dp.findMaxChapter(book) if maxChapter == 0 { - continue // Book has no chapters? Shouldn't happen + continue } - // Iterate through chapters for chapter := 1; chapter <= maxChapter; chapter++ { versesInChapter := dp.bible.CountVersesInChapter(book, chapter) - // Check if offset falls within this chapter if offset < versesInChapter { - // Offset is within this chapter - firstVerse := offset + 1 // Convert from 0-indexed to 1-indexed + firstVerse := offset + 1 - // Create initial bookmark (like AdvanceBookmark does) - secondVerse := min(firstVerse+dp.versesPerDay-1, versesInChapter) - bookmark := &Bookmark{ + // Default: read versesPerDay verses, clamped to chapter end + lastVerse := firstVerse + dp.versesPerDay - 1 + if lastVerse > versesInChapter { + lastVerse = versesInChapter + } + + // Lookahead: if fewer than versesPerDay remain, read to chapter end + remaining := versesInChapter - lastVerse + if remaining > 0 && remaining < dp.versesPerDay { + lastVerse = versesInChapter + } + + return &Bookmark{ Book: BookIndex(book), Chapter: chapter, FirstVerse: firstVerse, - SecondVerse: secondVerse, - } - - // Apply lookahead rule (like AdjustForLookahead) - return dp.applyLookahead(bookmark), nil + SecondVerse: lastVerse, + }, nil } - // Move to next chapter, subtracting this chapter's verses offset -= versesInChapter } } - // If we've gone through all books and offset is still positive, wrap - // around to beginning (start over) Calculate modulo offset within total verses + // Wrap around to beginning if offset exceeds total verses totalVerses := dp.GetTotalVersesForMode() if totalVerses == 0 { return nil, fmt.Errorf("no verses found for reading mode %s. The Bible data file may not contain books for this reading mode", dp.readingMode) } if offset >= 0 { adjustedOffset := offset % totalVerses - // Recursively find position for adjusted offset return dp.findPositionForOffset(adjustedOffset) }

@@ -122,30 +196,11 @@ return 40, 43 // Default to Evangelion

} } -// applyLookahead applies the lookahead rule (same as BookmarkManager.AdjustForLookahead) -func (dp *DateProgression) applyLookahead(bookmark *Bookmark) *Bookmark { - versesInChapter := dp.bible.CountVersesInChapter(int(bookmark.Book), bookmark.Chapter) - versesRemaining := versesInChapter - bookmark.SecondVerse - - // If we're not at the end of the chapter and less than dp.versesPerDay verses remain for NEXT snippet - if versesRemaining > 0 && versesRemaining < dp.versesPerDay { - return &Bookmark{ - Book: bookmark.Book, - Chapter: bookmark.Chapter, - FirstVerse: bookmark.FirstVerse, - SecondVerse: versesInChapter, - } - } - - // Otherwise return the original bookmark - return bookmark -} - // GetTotalVersesForMode returns the total number of verses for the current reading mode func (dp *DateProgression) GetTotalVersesForMode() int { total := 0 startBook, endBook := dp.getBookRange() - + for book := startBook; book <= endBook; book++ { for chapter := 1; ; chapter++ { verses := dp.bible.CountVersesInChapter(book, chapter)

@@ -158,10 +213,10 @@ }

return total } -// GetTotalGospelVerses returns the total number of verses in all four Gospels -// Deprecated: Use GetTotalVersesForMode instead +// GetTotalGospelVerses returns the total number of verses in all four Gospels. +// +// Deprecated: Use GetTotalVersesForMode instead. func (dp *DateProgression) GetTotalGospelVerses() int { - // Calculate Gospels total (books 40-43) total := 0 for book := 40; book <= 43; book++ { for chapter := 1; ; chapter++ {

@@ -173,4 +228,4 @@ total += verses

} } return total -}+}
M internal/easterprogression.gointernal/easterprogression.go

@@ -88,11 +88,12 @@ } else {

label = "Easter" } - if days > 0 { + switch { + case days > 0: return fmt.Sprintf("%s in %d days, %d hours, %d minutes", label, days, hours, minutes), nil - } else if hours > 0 { + case hours > 0: return fmt.Sprintf("%s in %d hours, %d minutes", label, hours, minutes), nil - } else { + default: return fmt.Sprintf("%s in %d minutes", label, minutes), nil } }
M internal/formatter.gointernal/formatter.go

@@ -2,6 +2,7 @@ package bible

import ( "fmt" + "strconv" "strings" "github.com/charmbracelet/lipgloss"

@@ -57,9 +58,9 @@

// Replace template variables result := template result = strings.ReplaceAll(result, "{book}", bookName) - result = strings.ReplaceAll(result, "{chapter}", fmt.Sprintf("%d", bookmark.Chapter)) - result = strings.ReplaceAll(result, "{first_verse}", fmt.Sprintf("%d", bookmark.FirstVerse)) - result = strings.ReplaceAll(result, "{second_verse}", fmt.Sprintf("%d", bookmark.SecondVerse)) + result = strings.ReplaceAll(result, "{chapter}", strconv.Itoa(bookmark.Chapter)) + result = strings.ReplaceAll(result, "{first_verse}", strconv.Itoa(bookmark.FirstVerse)) + result = strings.ReplaceAll(result, "{second_verse}", strconv.Itoa(bookmark.SecondVerse)) if !f.useColours { return result

@@ -147,7 +148,7 @@ }

// Handle numbered output if f.numbered { - if i > 0 && !(f.paragraphs && hasPilcrow) { + if i > 0 && (!f.paragraphs || !hasPilcrow) { // Don't add newline if we already added one for paragraph sb.WriteString("\n") }

@@ -155,7 +156,7 @@ fmt.Fprintf(&sb, "%d. %s", verse.VerseNum, text)

} else { // Regular mode - join with spaces // Don't add space if we just added paragraph break - if i > 0 && !(f.paragraphs && hasPilcrow) { + if i > 0 && (!f.paragraphs || !hasPilcrow) { sb.WriteString(" ") } sb.WriteString(text)
M internal/verse.gointernal/verse.go

@@ -24,7 +24,7 @@

// BookIndex represents Bible book numbers type BookIndex int -// BookIndex.String returns the string representation of the book index. +// String returns the string representation of the book index. // Note: The actual book name should be retrieved from Bible data using the Formatter. // This is kept for backward compatibility but returns a simple numeric representation. func (b BookIndex) String() string {
M pkg/eastertime.gopkg/eastertime.go

@@ -34,7 +34,6 @@ package eastertime

import ( "errors" - "math" "time" )

@@ -91,7 +90,7 @@ }

var a, b, c, d, e int var month time.Month - var day float64 + var day int a = year % 4 b = year % 7

@@ -99,8 +98,8 @@ c = year % 19

d = (19*c + 15) % 30 e = (2*a + 4*b - d + 34) % 7 month = time.Month((d + e + 114) / 31) - day = math.Floor(float64((d+e+114)%31 + 1)) - day = day + 13 + day = (d+e+114)%31 + 1 + day += 13 - return time.Date(year, month, int(day), 0, 0, 0, 0, time.Local), nil + return time.Date(year, month, day, 0, 0, 0, 0, time.Local), nil }