// What has been read in the Reader: { chapters: { "JHN.3": { n: times read, last: "2026-10-06" } }, days: ["2026-10-06", ...] }. // Pure helpers for recording it, merging two copies, and turning it into the numbers and pictures on the progress map. import { bookOptions, CHAPTER_COUNTS } from './bibleUtils.jsx'; export const emptyHistory = () => ({ chapters: {}, days: [] }); const DATE = /^\d{4}-\d{2}-\d{2}$/; export const chapterKey = (book, chapter) => `${book}.${chapter}`; export function normalizeHistory(raw) { const chapters = {}; if (raw?.chapters && typeof raw.chapters === 'object' && !Array.isArray(raw.chapters)) { for (const [k, v] of Object.entries(raw.chapters)) { if (/^[1-3A-Z]{3}\.\d{1,3}$/.test(k) && v && typeof v === 'object') chapters[k] = { n: Number.isInteger(v.n) && v.n > 0 ? v.n : 1, last: DATE.test(v.last ?? '') ? v.last : '' }; } } const days = Array.isArray(raw?.days) ? [...new Set(raw.days.filter((d) => typeof d === 'string' && DATE.test(d)))].sort() : []; return { chapters, days }; } export function mergeHistory(a, b) { const x = normalizeHistory(a); const y = normalizeHistory(b); const chapters = { ...x.chapters }; for (const [k, v] of Object.entries(y.chapters)) { const mine = chapters[k]; chapters[k] = mine ? { n: Math.max(mine.n, v.n), last: mine.last > v.last ? mine.last : v.last } : v; } return { chapters, days: [...new Set([...x.days, ...y.days])].sort() }; } export const historyEqual = (a, b) => JSON.stringify(normalizeHistory(a)) === JSON.stringify(normalizeHistory(b)); // A chapter read on `date`: counted once per day however long you stay, and the day is marked as a reading day. export function recordRead(history, book, chapter, date) { const key = chapterKey(book, chapter); const was = history.chapters[key]; const alreadyToday = was?.last === date; const chapters = alreadyToday ? history.chapters : { ...history.chapters, [key]: { n: (was?.n ?? 0) + 1, last: date } }; const days = history.days.includes(date) ? history.days : [...history.days, date].sort(); return chapters === history.chapters && days === history.days ? history : { chapters, days }; } // Chapters finished in reading plans count as read too: -> Set of "JHN.3" keys export function planChapterKeys(planProgress, getSchedule) { const keys = new Set(); for (const p of planProgress ?? []) { for (const list of Object.values(p.chaptersRead ?? {})) for (const k of list ?? []) keys.add(String(k).toUpperCase()); let schedule; try { schedule = getSchedule(p.planId) ?? []; } catch { schedule = []; } for (const day of p.completedDays ?? []) for (const r of schedule[day] ?? []) if (r?.book && r?.chapter) keys.add(chapterKey(r.book, r.chapter)); } return keys; } // -> { books: [{ abbrev, name, read, total, pct, testament }], read, total, pct, ot: {read,total}, nt: {read,total} } export function progressSummary(history, extraKeys = new Set()) { const have = new Set([...Object.keys(history.chapters), ...extraKeys]); const books = bookOptions.map((b, i) => { const total = CHAPTER_COUNTS[b.abbrev] ?? 0; let read = 0; for (let c = 1; c <= total; c += 1) if (have.has(chapterKey(b.abbrev, c))) read += 1; return { abbrev: b.abbrev, name: b.name, read, total, pct: total ? read / total : 0, testament: i < 39 ? 'ot' : 'nt' }; }); const sum = (list) => list.reduce((acc, b) => ({ read: acc.read + b.read, total: acc.total + b.total }), { read: 0, total: 0 }); const all = sum(books); return { books, read: all.read, total: all.total, pct: all.total ? all.read / all.total : 0, ot: sum(books.filter((b) => b.testament === 'ot')), nt: sum(books.filter((b) => b.testament === 'nt')) }; } const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; // The last `weeks` weeks as columns of 7 days (Sunday first), ending with the week of `today`. // -> [[{ date, on, future }]] export function heatmapWeeks(daySet, today = new Date(), weeks = 26) { const end = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const startOfThisWeek = new Date(end.getFullYear(), end.getMonth(), end.getDate() - end.getDay()); return Array.from({ length: weeks }, (_, w) => Array.from({ length: 7 }, (__, d) => { const date = new Date(startOfThisWeek.getFullYear(), startOfThisWeek.getMonth(), startOfThisWeek.getDate() - (weeks - 1 - w) * 7 + d); return { date: iso(date), on: daySet.has(iso(date)), future: date > end }; })); }