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 totalVerses == 0 {
98 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)
99 }
100 if offset >= 0 {
101 adjustedOffset := offset % totalVerses
102 // Recursively find position for adjusted offset
103 return dp.findPositionForOffset(adjustedOffset)
104 }
105
106 return nil, fmt.Errorf("could not find position for offset %d", offset)
107}
108
109// getBookRange returns the start and end book numbers for the current reading mode
110func (dp *DateProgression) getBookRange() (startBook, endBook int) {
111 switch dp.readingMode {
112 case ReadingModeEvangelion:
113 return 40, 43 // Matthew, Mark, Luke, John
114 case ReadingModeNewTestament:
115 return 40, 66 // Matthew through Revelation
116 case ReadingModeOldTestament:
117 return 1, 39 // Genesis through Malachi
118 case ReadingModeBible:
119 return 1, 66 // Entire Bible
120 default:
121 return 40, 43 // Default to Evangelion
122 }
123}
124
125// applyLookahead applies the lookahead rule (same as BookmarkManager.AdjustForLookahead)
126func (dp *DateProgression) applyLookahead(bookmark *Bookmark) *Bookmark {
127 versesInChapter := dp.bible.CountVersesInChapter(int(bookmark.Book), bookmark.Chapter)
128 versesRemaining := versesInChapter - bookmark.SecondVerse
129
130 // If we're not at the end of the chapter and less than dp.versesPerDay verses remain for NEXT snippet
131 if versesRemaining > 0 && versesRemaining < dp.versesPerDay {
132 return &Bookmark{
133 Book: bookmark.Book,
134 Chapter: bookmark.Chapter,
135 FirstVerse: bookmark.FirstVerse,
136 SecondVerse: versesInChapter,
137 }
138 }
139
140 // Otherwise return the original bookmark
141 return bookmark
142}
143
144// GetTotalVersesForMode returns the total number of verses for the current reading mode
145func (dp *DateProgression) GetTotalVersesForMode() int {
146 total := 0
147 startBook, endBook := dp.getBookRange()
148
149 for book := startBook; book <= endBook; book++ {
150 for chapter := 1; ; chapter++ {
151 verses := dp.bible.CountVersesInChapter(book, chapter)
152 if verses == 0 {
153 break
154 }
155 total += verses
156 }
157 }
158 return total
159}
160
161// GetTotalGospelVerses returns the total number of verses in all four Gospels
162// Deprecated: Use GetTotalVersesForMode instead
163func (dp *DateProgression) GetTotalGospelVerses() int {
164 // Calculate Gospels total (books 40-43)
165 total := 0
166 for book := 40; book <= 43; book++ {
167 for chapter := 1; ; chapter++ {
168 verses := dp.bible.CountVersesInChapter(book, chapter)
169 if verses == 0 {
170 break
171 }
172 total += verses
173 }
174 }
175 return total
176}