internal/dateprogression.go (view raw)
1package bible
2
3import (
4 "fmt"
5 "time"
6)
7
8// DateProgression handles calculating Bible position based on date
9type DateProgression struct {
10 bible *Bible
11 versesPerDay int
12 readingMode ReadingMode
13}
14
15// NewDateProgression creates a new date progression calculator
16func NewDateProgression(bible *Bible) *DateProgression {
17 return &DateProgression{bible: bible, versesPerDay: 12, readingMode: ReadingModeEvangelion}
18}
19
20// NewDateProgressionWithConfig creates a new date progression calculator with config
21func NewDateProgressionWithConfig(bible *Bible, versesPerDay int) *DateProgression {
22 if versesPerDay <= 0 {
23 versesPerDay = 12
24 }
25 return &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: ReadingModeEvangelion}
26}
27
28// NewDateProgressionWithReadingMode creates a new date progression calculator with reading mode
29func NewDateProgressionWithReadingMode(bible *Bible, versesPerDay int, readingMode ReadingMode) *DateProgression {
30 if versesPerDay <= 0 {
31 versesPerDay = 12
32 }
33 return &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: readingMode}
34}
35
36// GetPositionForDate calculates the verse range for a given date
37func (dp *DateProgression) GetPositionForDate(date time.Time) (*Bookmark, error) {
38 // Calculate day of year (1-366)
39 dayOfYear := date.YearDay()
40
41 // Each day shows configured number of verses
42 targetVerseOffset := (dayOfYear - 1) * dp.versesPerDay // Zero-indexed
43
44 // Walk through appropriate books based on reading mode
45 return dp.findPositionForOffset(targetVerseOffset)
46}
47
48// findPositionForOffset finds the bookmark for a given cumulative verse offset
49func (dp *DateProgression) findPositionForOffset(offset int) (*Bookmark, error) {
50 // Determine which books to iterate through based on reading mode
51 startBook, endBook := dp.getBookRange()
52
53 // Iterate through books in the range
54 for book := startBook; book <= endBook; book++ {
55 // Find last chapter in this book
56 maxChapter := 0
57 for chapter := 1; ; chapter++ {
58 if dp.bible.CountVersesInChapter(book, chapter) == 0 {
59 maxChapter = chapter - 1
60 break
61 }
62 }
63 if maxChapter == 0 {
64 continue // Book has no chapters? Shouldn't happen
65 }
66
67 // Iterate through chapters
68 for chapter := 1; chapter <= maxChapter; chapter++ {
69 versesInChapter := dp.bible.CountVersesInChapter(book, chapter)
70
71 // Check if offset falls within this chapter
72 if offset < versesInChapter {
73 // Offset is within this chapter
74 firstVerse := offset + 1 // Convert from 0-indexed to 1-indexed
75
76 // Create initial bookmark (like AdvanceBookmark does)
77 secondVerse := min(firstVerse+dp.versesPerDay-1, versesInChapter)
78 bookmark := &Bookmark{
79 Book: BookIndex(book),
80 Chapter: chapter,
81 FirstVerse: firstVerse,
82 SecondVerse: secondVerse,
83 }
84
85 // Apply lookahead rule (like AdjustForLookahead)
86 return dp.applyLookahead(bookmark), nil
87 }
88
89 // Move to next chapter, subtracting this chapter's verses
90 offset -= versesInChapter
91 }
92 }
93
94 // If we've gone through all books and offset is still positive, wrap
95 // around to beginning (start over) Calculate modulo offset within total verses
96 totalVerses := dp.GetTotalVersesForMode()
97 if offset >= 0 {
98 adjustedOffset := offset % totalVerses
99 // Recursively find position for adjusted offset
100 return dp.findPositionForOffset(adjustedOffset)
101 }
102
103 return nil, fmt.Errorf("could not find position for offset %d", offset)
104}
105
106// getBookRange returns the start and end book numbers for the current reading mode
107func (dp *DateProgression) getBookRange() (startBook, endBook int) {
108 switch dp.readingMode {
109 case ReadingModeEvangelion:
110 return 40, 43 // Matthew, Mark, Luke, John
111 case ReadingModeNewTestament:
112 return 40, 66 // Matthew through Revelation
113 case ReadingModeOldTestament:
114 return 1, 39 // Genesis through Malachi
115 case ReadingModeBible:
116 return 1, 66 // Entire Bible
117 default:
118 return 40, 43 // Default to Evangelion
119 }
120}
121
122// applyLookahead applies the lookahead rule (same as BookmarkManager.AdjustForLookahead)
123func (dp *DateProgression) applyLookahead(bookmark *Bookmark) *Bookmark {
124 versesInChapter := dp.bible.CountVersesInChapter(int(bookmark.Book), bookmark.Chapter)
125 versesRemaining := versesInChapter - bookmark.SecondVerse
126
127 // If we're not at the end of the chapter and less than dp.versesPerDay verses remain for NEXT snippet
128 if versesRemaining > 0 && versesRemaining < dp.versesPerDay {
129 return &Bookmark{
130 Book: bookmark.Book,
131 Chapter: bookmark.Chapter,
132 FirstVerse: bookmark.FirstVerse,
133 SecondVerse: versesInChapter,
134 }
135 }
136
137 // Otherwise return the original bookmark
138 return bookmark
139}
140
141// GetTotalVersesForMode returns the total number of verses for the current reading mode
142func (dp *DateProgression) GetTotalVersesForMode() int {
143 total := 0
144 startBook, endBook := dp.getBookRange()
145
146 for book := startBook; book <= endBook; book++ {
147 for chapter := 1; ; chapter++ {
148 verses := dp.bible.CountVersesInChapter(book, chapter)
149 if verses == 0 {
150 break
151 }
152 total += verses
153 }
154 }
155 return total
156}
157
158// GetTotalGospelVerses returns the total number of verses in all four Gospels
159// Deprecated: Use GetTotalVersesForMode instead
160func (dp *DateProgression) GetTotalGospelVerses() int {
161 // Calculate Gospels total (books 40-43)
162 total := 0
163 for book := 40; book <= 43; book++ {
164 for chapter := 1; ; chapter++ {
165 verses := dp.bible.CountVersesInChapter(book, chapter)
166 if verses == 0 {
167 break
168 }
169 total += verses
170 }
171 }
172 return total
173}