all repos — bibel @ master

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

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	// Pre-computed cumulative offset at the START of each day of year (1-indexed)
 14	dailyOffsets []int
 15}
 16
 17// NewDateProgression creates a new date progression calculator
 18func NewDateProgression(bible *Bible) *DateProgression {
 19	dp := &DateProgression{bible: bible, versesPerDay: 12, readingMode: ReadingModeEvangelion}
 20	dp.precomputeDailyOffsets()
 21	return dp
 22}
 23
 24// NewDateProgressionWithConfig creates a new date progression calculator with config
 25func NewDateProgressionWithConfig(bible *Bible, versesPerDay int) *DateProgression {
 26	if versesPerDay <= 0 {
 27		versesPerDay = 12
 28	}
 29	dp := &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: ReadingModeEvangelion}
 30	dp.precomputeDailyOffsets()
 31	return dp
 32}
 33
 34// NewDateProgressionWithReadingMode creates a new date progression calculator with reading mode
 35func NewDateProgressionWithReadingMode(bible *Bible, versesPerDay int, readingMode ReadingMode) *DateProgression {
 36	if versesPerDay <= 0 {
 37		versesPerDay = 12
 38	}
 39	dp := &DateProgression{bible: bible, versesPerDay: versesPerDay, readingMode: readingMode}
 40	dp.precomputeDailyOffsets()
 41	return dp
 42}
 43
 44// precomputeDailyOffsets computes the cumulative verse offset for each day of the year
 45// using lookahead logic: when fewer than versesPerDay remain in a chapter, extend
 46// to read all remaining verses in that chapter. This eliminates short reading days.
 47func (dp *DateProgression) precomputeDailyOffsets() {
 48	totalVerses := dp.GetTotalVersesForMode()
 49	if totalVerses == 0 {
 50		dp.dailyOffsets = make([]int, 367)
 51		return
 52	}
 53
 54	dp.dailyOffsets = make([]int, 367) // 1-indexed by day of year
 55	cumOffset := 0
 56
 57	for day := 1; day <= 366; day++ {
 58		dp.dailyOffsets[day] = cumOffset % totalVerses
 59		cumOffset += dp.computeConsumed(dp.dailyOffsets[day])
 60	}
 61}
 62
 63// computeConsumed calculates the number of verses actually read starting from a given
 64// cumulative offset. Uses lookahead: if fewer than versesPerDay remain in the chapter,
 65// extends to read all remaining verses in that chapter.
 66func (dp *DateProgression) computeConsumed(offset int) int {
 67	startBook, endBook := dp.getBookRange()
 68
 69	for book := startBook; book <= endBook; book++ {
 70		maxChapter := dp.findMaxChapter(book)
 71		if maxChapter == 0 {
 72			continue
 73		}
 74
 75		for chapter := 1; chapter <= maxChapter; chapter++ {
 76			versesInChapter := dp.bible.CountVersesInChapter(book, chapter)
 77
 78			if offset < versesInChapter {
 79				firstVerse := offset + 1
 80				lastVerse := firstVerse + dp.versesPerDay - 1
 81				if lastVerse > versesInChapter {
 82					lastVerse = versesInChapter
 83				}
 84				// Lookahead: if fewer than versesPerDay remain, read to chapter end
 85				remaining := versesInChapter - lastVerse
 86				if remaining > 0 && remaining < dp.versesPerDay {
 87					lastVerse = versesInChapter
 88				}
 89				return lastVerse - firstVerse + 1
 90			}
 91
 92			offset -= versesInChapter
 93		}
 94	}
 95
 96	// Wrap around
 97	totalVerses := dp.GetTotalVersesForMode()
 98	if totalVerses > 0 {
 99		return dp.computeConsumed(offset % totalVerses)
100	}
101	return 0
102}
103
104// findMaxChapter finds the last chapter number in a book
105func (dp *DateProgression) findMaxChapter(book int) int {
106	for chapter := 1; ; chapter++ {
107		if dp.bible.CountVersesInChapter(book, chapter) == 0 {
108			return chapter - 1
109		}
110	}
111}
112
113// GetPositionForDate calculates the verse range for a given date
114func (dp *DateProgression) GetPositionForDate(date time.Time) (*Bookmark, error) {
115	// Calculate day of year (1-366)
116	dayOfYear := date.YearDay()
117
118	if dayOfYear < 1 || dayOfYear >= len(dp.dailyOffsets) {
119		dayOfYear = 1
120	}
121
122	// Look up pre-computed cumulative offset for this day
123	targetVerseOffset := dp.dailyOffsets[dayOfYear]
124
125	return dp.findPositionForOffset(targetVerseOffset)
126}
127
128// findPositionForOffset finds the bookmark for a given cumulative verse offset.
129// Applies lookahead: if fewer than versesPerDay remain in the chapter, extends
130// to read to the end of the chapter.
131func (dp *DateProgression) findPositionForOffset(offset int) (*Bookmark, error) {
132	startBook, endBook := dp.getBookRange()
133
134	for book := startBook; book <= endBook; book++ {
135		maxChapter := dp.findMaxChapter(book)
136		if maxChapter == 0 {
137			continue
138		}
139
140		for chapter := 1; chapter <= maxChapter; chapter++ {
141			versesInChapter := dp.bible.CountVersesInChapter(book, chapter)
142
143			if offset < versesInChapter {
144				firstVerse := offset + 1
145
146				// Default: read versesPerDay verses, clamped to chapter end
147				lastVerse := firstVerse + dp.versesPerDay - 1
148				if lastVerse > versesInChapter {
149					lastVerse = versesInChapter
150				}
151
152				// Lookahead: if fewer than versesPerDay remain, read to chapter end
153				remaining := versesInChapter - lastVerse
154				if remaining > 0 && remaining < dp.versesPerDay {
155					lastVerse = versesInChapter
156				}
157
158				return &Bookmark{
159					Book:        BookIndex(book),
160					Chapter:     chapter,
161					FirstVerse:  firstVerse,
162					SecondVerse: lastVerse,
163				}, nil
164			}
165
166			offset -= versesInChapter
167		}
168	}
169
170	// Wrap around to beginning if offset exceeds total verses
171	totalVerses := dp.GetTotalVersesForMode()
172	if totalVerses == 0 {
173		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)
174	}
175	if offset >= 0 {
176		adjustedOffset := offset % totalVerses
177		return dp.findPositionForOffset(adjustedOffset)
178	}
179
180	return nil, fmt.Errorf("could not find position for offset %d", offset)
181}
182
183// getBookRange returns the start and end book numbers for the current reading mode
184func (dp *DateProgression) getBookRange() (startBook, endBook int) {
185	switch dp.readingMode {
186	case ReadingModeEvangelion:
187		return 40, 43 // Matthew, Mark, Luke, John
188	case ReadingModeNewTestament:
189		return 40, 66 // Matthew through Revelation
190	case ReadingModeOldTestament:
191		return 1, 39 // Genesis through Malachi
192	case ReadingModeBible:
193		return 1, 66 // Entire Bible
194	default:
195		return 40, 43 // Default to Evangelion
196	}
197}
198
199// GetTotalVersesForMode returns the total number of verses for the current reading mode
200func (dp *DateProgression) GetTotalVersesForMode() int {
201	total := 0
202	startBook, endBook := dp.getBookRange()
203
204	for book := startBook; book <= endBook; book++ {
205		for chapter := 1; ; chapter++ {
206			verses := dp.bible.CountVersesInChapter(book, chapter)
207			if verses == 0 {
208				break
209			}
210			total += verses
211		}
212	}
213	return total
214}
215
216// GetTotalGospelVerses returns the total number of verses in all four Gospels.
217//
218// Deprecated: Use GetTotalVersesForMode instead.
219func (dp *DateProgression) GetTotalGospelVerses() int {
220	total := 0
221	for book := 40; book <= 43; book++ {
222		for chapter := 1; ; chapter++ {
223			verses := dp.bible.CountVersesInChapter(book, chapter)
224			if verses == 0 {
225				break
226			}
227			total += verses
228		}
229	}
230	return total
231}