You have enemies? Good. That means you’ve stood up for something, sometime in your life, Winston Churchill
We shall fight on the beaches, we shall fight on the landing grounds, we shall fight in the fields and in the streets, we shall fight in the hills; we shall never surrender, Winston Churchill
Every part of the Prayer Companion revolves around today’s date. The Verse tab needs to know which day of the year it is to pick the correct reading. The Diary tab stamps each entry with an ISO date. The Video and Online tabs simply need a “today” string for updates.
JavaScript’s Date object is famously quirky – months are zero‑based, there’s no built‑in day‑of‑year, and timezone handling can bite you. In this article we’ll wrap all those quirks into a tiny, reusable utility file that every future service can import with confidence.
No network calls, no UI changes (apart from extending our existing test screen). Just pure functions that you can verify by comparing with Python’s datetime module.
src/utils/dateUtils.ts – a small collection of date helpers:
We’ll then extend the temporary test screen on the Verse tab to display today’s date, day of year, and plan index, alongside a check against Python’s own output.

src/utils/dateUtils.tsLet’s edit the new utility file and walk through it function by function.
This is the only place in the entire app that touches raw Date objects – every other module will call these helpers and never worry about the quirks underneath.
// src/utils/dateUtils.ts
//
// DATE UTILITIES FOR PRAYER COMPANION
// ─────────────────────────────────────
// The file header: pure functions and why they matter
// A pure function is like a mathematical function – call it with the same arguments
// and you’ll always get the same result, and it doesn’t change anything outside itself.
// This makes the code:
// 1. Predictable – you don’t need to know the state of the world, just the inputs.
// No side effects, no network calls, no state changes.
// 2. Testable – you can write a test once and it will pass forever.
//
// ══════════════════════════════════════════════════════════════════════════════
// CORE DATE FUNCTIONS
// ══════════════════════════════════════════════════════════════════════════════
/**
* Return today's date as an ISO 8601 string: "YYYY-MM-DD".
*
* This is the format used everywhere in Prayer Companion:
* - SQLite date column (myprayers.date)
* - Diary file names (2026-05-14.md)
* - Display in the UI (prayer list, diary list)
*
* Equivalent Python:
* from datetime import date
* date.today().isoformat() → "2026-05-14"
*
* @returns ISO date string for today in local time, e.g. "2026-05-14"
*/
export function todayISO(): string {
// It creates a Date object for right now (date + time).
const d = new Date();
const year = d.getFullYear();
// getMonth() returns 0–11. We add 1 to get 1–12.
// .toString().padStart(2, "0") ensures two digits: "05" not "5"
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
// The template literal `${year}-${month}-${day}` stitches the parts together.
// In Python this would be: f"{year}-{month:02d}-{day:02d}"
}
/**
* Return a specific date as an ISO string: "YYYY-MM-DD".
* Useful for testing.
*
* @param date A JavaScript Date object (defaults to today)
* @returns ISO date string, e.g. "2026-05-14"
*
* @example
* dateToISO(new Date(2026, 0, 1)) // "2026-01-01" (January, month 0)
* dateToISO(new Date(2026, 11, 31)) // "2026-12-31" (December, month 11)
*/
export function dateToISO(date: Date = new Date()): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
/**
* Parse an ISO date string into its year, month, day components.
* Useful when you have a stored date string and need to create a Date object.
*
* WHY NOT just `new Date("2026-05-14")`?
* new Date("2026-05-14") parses as UTC midnight, then converts to local time.
* In UTC+2 (Spain), midnight UTC on May 14 is 02:00 local time on May 14,
* which is fine. BUT new Date("2026-05-14") in a UTC-5 timezone would give
* 19:00 on May 13 — the wrong day!
*
* Parsing the parts manually and using new Date(year, month-1, day) always
* creates a local-time date with no timezone surprises.
*
* @param iso ISO date string, e.g. "2026-05-14"
* @returns A Date object at midnight local time for that date,
* or null if the string is not a valid ISO date
*/
export function isoToDate(iso: string): Date | null {
// We first check the string format with a regex: YYYY-MM-DD
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) {
return null;
}
// Then, we parse the numbers.
const [yearStr, monthStr, dayStr] = iso.split("-");
const year = parseInt(yearStr, 10);
const month = parseInt(monthStr, 10); // 1–12
const day = parseInt(dayStr, 10);
// new Date(year, month - 1, day) creates a local-time date.
// month - 1 because JavaScript months are 0-based.
const date = new Date(year, month - 1, day);
// Validate the result. JavaScript’s new Date(year, month, day) has a surprising feature:
// if you pass February 30 ("2026-02-30"), it doesn’t throw – it silently overflows to March 2.
// To catch that, we compare the resulting date components with the original numbers.
if (date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day) {
return null; // overflow detected — invalid date
}
// The function returns Date | null, which forces callers to handle the possibility of an invalid string.
return date;
}
// ══════════════════════════════════════════════════════════════════════════════
// DAY OF YEAR
// ══════════════════════════════════════════════════════════════════════════════
/**
* Calculate the day of the year for a given date.
* January 1 = 1, December 31 = 365 (or 366 in a leap year).
*
* Equivalent Python:
* date.today().timetuple().tm_yday → 134
*
* @param date The date to calculate for (defaults to today)
* @returns Day of year, 1–366
*/
export function dayOfYear(date: Date = new Date()): number {
// 1. Create a Date for January 1 of the same year (at local midnight)
// new Date(year, 0, 1) — month 0 = January, and set the day to 1
const jan1 = new Date(date.getFullYear(), 0, 1);
// 2. Subtract the two dates to get the number of milliseconds between Jan 1 and our date
// getTime() returns milliseconds since the Unix epoch (Jan 1, 1970 UTC).
// The difference is how many milliseconds have elapsed from the start of the year to our date.
const diffMs = date.getTime() - jan1.getTime();
// 3. Divide by milliseconds per day (1000 ms/s × 60 s/min × 60 min/hr × 24 hr/day = 86,400,000)
// 4. Round down the result to get whole days (Math.floor) and add 1 because January 1st is day 1, not day 0.
return Math.floor(diffMs / 86_400_000) + 1;
}
/**
* Check if a year is a leap year.
* A year is a leap year if:
* - It is divisible by 4 AND
* - NOT divisible by 100, UNLESS it is also divisible by 400
*
* Examples: 2024 ✅ 2026 ❌ 2100 ❌ 2000 ✅
*
* Equivalent Python: calendar.isleap(year)
*
* @param year The year to check (defaults to current year)
* @returns true if leap year, false otherwise
*/
export function isLeapYear(year: number = new Date().getFullYear()): boolean {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
/**
* Return the number of days in the current year (365 or 366).
* Used to validate day-of-year values.
*
* @param year The year to check (defaults to current year)
*/
export function daysInYear(year: number = new Date().getFullYear()): number {
// A tiny wrapper: if it’s a leap year, return 366; otherwise 365.
return isLeapYear(year) ? 366 : 365;
}
// ══════════════════════════════════════════════════════════════════════════════
// PLAN INDEX
// ══════════════════════════════════════════════════════════════════════════════
/**
* Calculate which index in a reading plan corresponds to today.
* When the plan runs out, it cycles back to the beginning.
*
* This is the core algorithm that maps "today" to "today's verse".
* Plan Index: mapping today to a reading plan entry
* The question “what is today’s verse?” translates to: “which index into this array corresponds to today?”
*
* @param planLength Number of items in the reading plan
* @param date The date to calculate for (defaults to today)
* @returns 0-based index into the plan array
*
* @example
* const verses = await readLines(require("../../data/daily_plan.txt"));
* const idx = planIndex(verses.length); // e.g. 7
* const todayVerse = verses[idx]; // e.g. "Psalm 46:1"
*/
export function planIndex(planLength: number, date: Date = new Date()): number {
if (planLength <= 0) {
// Error handling. If someone passes a non‑positive planLength, we throw an error immediately.
throw new Error(`planLength must be positive, got ${planLength}`);
}
// Convert date to a 0‑based day number, dayOfYear() returns 1–366 (1-based).
// We subtract 1 to make it 0-based (0–365). This is necessary because array indices start at 0.
// The % (modulo) operator gives the remainder after division.
// If the plan has 63 items and the 0‑based day is 133 (May 14, day 134), 133 % 63 = 7. So we get index 7.
return (dayOfYear(date) - 1) % planLength;
}
// ══════════════════════════════════════════════════════════════════════════════
// DISPLAY HELPERS: FORMATTING DATES FOR HUMANS
// ══════════════════════════════════════════════════════════════════════════════
/**
* Format a date for display in the prayer list and diary list.
*
* A raw ISO string like "2026-05-14" is great for storage, but users expect to see
* something readable: “14 May 2026” or “Wednesday”.
* We provide a formatting function that can produce a few common styles.
*
* @param iso ISO date string "YYYY-MM-DD"
* @param style "long" → "Wednesday, 14 May 2026"
* "short" → "14 May 2026"
* "day" → "Wednesday"
* @returns Formatted date string
*
* @example
* formatDate("2026-05-14", "long") // "Wednesday, 14 May 2026"
* formatDate("2026-05-14", "short") // "14 May 2026"
* formatDate("2026-05-14", "day") // "Wednesday"
*/
export function formatDate(
iso: string,
style: "long" | "short" | "day" = "long"
): string {
// We use isoToDate() to parse the input string.
const date = isoToDate(iso);
// Safety first: isoToDate fallback
// If the string is invalid (e.g., "garbage"), isoToDate returns null.
// The function then returns the raw ISO string – a graceful degradation.
if (!date) return iso;
// JavaScript’s Intl.DateTimeFormat is a built‑in formatter that adapts to a locale.
// Equivalent Python: date.strftime() or locale-aware formatting.
if (style === "long") {
// We pass "en-GB" (British English) because it gives a day‑month order that matches
// how most of the world writes dates: “14 May 2026”.
return new Intl.DateTimeFormat("en-GB", {
// The options object tells the formatter which date components to include:
weekday: "long", // → “Wednesday”
day: "numeric", // → “14”
month: "long", // → “May”
year: "numeric", // → “2026”
}).format(date); // so we get “Wednesday, 14 May 2026”
}
if (style === "short") {
return new Intl.DateTimeFormat("en-GB", {
day: "numeric",
month: "long",
year: "numeric",
}).format(date);
// → so we get "14 May 2026"
}
// style === "day"
return new Intl.DateTimeFormat("en-GB", {
weekday: "long",
}).format(date);
// → "Wednesday"
}
/**
* Return true if an ISO date string represents today.
* Used to highlight today's entry in lists.
*
* @param iso ISO date string to compare with today
*/
export function isToday(iso: string): boolean {
// Simply compares the given ISO string to the one generated by todayISO().
// Because both are in YYYY-MM-DD format, a strict string equality check works perfectly
// – no need to parse or handle timezones.
return iso === todayISO();
}
/**
* Return true if an ISO date string represents yesterday.
* Useful for "yesterday's prayer" shortcuts.
*/
export function isYesterday(iso: string): boolean {
// To check if a date is yesterday, we create a Date object representing right now.
const yesterday = new Date();
// Then, we use setDate(getDate() - 1) to subtract one day.
yesterday.setDate(yesterday.getDate() - 1);
// Finally, we convert it to an ISO string and compare.
return iso === dateToISO(yesterday);
}
The goal is to add a diagnostic/test panel to the Verse tab that displays the output of several date utility functions. This panel helps developers verify that the custom date logic works correctly, and it can also serve as a quick reference for debugging date‑related issues.
The approach is to extend an existing screen (app/(tabs)/verse.tsx, VerseScreen) rather than creating a new one, which keeps the UI simple and avoids unnecessary navigation. The panel is placed inside a ScrollView below the three file cards that were implemented in a previous article.
// Add this import at the top of app/(tabs)/verse.tsx
// Imports six named exports from a local utility module.
import {
todayISO, – returns today’s date in ISO format (YYYY-MM-DD).
dayOfYear, – returns the day number within the year (1‑365/366).
planIndex, – calculates the current position in a reading plan
formatDate, – formats an ISO date into human‑readable styles ("long", "short", "day").
isLeapYear, – returns a boolean indicating whether the current year is a leap year.
daysInYear, – returns 365 or 366 accordingly.
} from "../../src/utils/dateUtils"; // it suggests a well‑organised project structure with utility modules inside src/utils.
Then add this component function outside of VerseScreen (paste it
after the styles object at the bottom of the file):
// ── DatePanel: test display for Chunk 05 ─────────────────────────────────────
// A small component that renders the date utility output.
// Defined as a separate component to keep VerseScreen clean.
// planLength – the total number of entries in the reading plan
// This value is used by planIndex to compute the current position.
function DatePanel({ planLength }: { planLength: number }) {
// Computing Values
const today = new Date(); // today is the current date/time object.
const iso = todayISO();
const doy = dayOfYear(today);
const idx = planLength > 0 ? planIndex(planLength) : 0;
const leap = isLeapYear();
const days = daysInYear();
const long = formatDate(iso, "long");
const short = formatDate(iso, "short");
const dayStr = formatDate(iso, "day");
// Data Grid Definition
const rows: Array<{ label: string; value: string }> = [
{ label: "todayISO()", value: iso },
// All values are converted to strings because they will be rendered inside Text components.
// The label for planIndex dynamically includes the planLength prop, making the output self‑documenting.
// The array is defined inside the component, so it re‑computed on every render.
{ label: "dayOfYear()", value: String(doy) },
{ label: `planIndex(${planLength})`, value: String(idx) },
{ label: "isLeapYear()", value: String(leap) },
{ label: "daysInYear()", value: String(days) },
{ label: 'formatDate("long")', value: long },
{ label: 'formatDate("short")', value: short },
{ label: 'formatDate("day")', value: dayStr },
];
// Rendering
return (
// A. Outer container. This is the card that wraps the entire panel.
<View style={dateStyles.card}>
/* B. Header section: View for layout (padding, background, border, etc.),
Text element showing the title */
<View style={dateStyles.header}>
<Text style={dateStyles.title}>📅 Date Utilities</Text>
</View>
/* C. Dynamic rows generated from rows.map */
row is an array of objects
[
{ label: "todayISO()", value: "2026-05-25" },
{ label: "dayOfYear()", value: "146" },
...
]
.map() loops over that array.
For each entry, it returns a View that contains the label (left column),
the computed value (right column)
Why key={label}? React requires a stable key for lists.
Here, label is unique, so it’s safe.
*/
{rows.map(({ label, value }) => (
<View key={label} style={dateStyles.row}>
<Text style={dateStyles.label}>{label}</Text>
<Text style={dateStyles.value}>{value}</Text>
</View>
))}
// D. Footer hint block
// Python check hint: provides a command to verify the date output independently.
<View style={dateStyles.hint}>
<Text style={dateStyles.hintText}>
Python check:{"\n"}
{`python3 -c "from datetime import date; d=date.today(); print(d.isoformat(), d.timetuple().tm_yday)"`}
</Text>
</View>
</View>
);
}
const dateStyles = StyleSheet.create({
card: {
backgroundColor: "#222840",
borderRadius: 10,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: "#2d3550",
},
header: {
marginBottom: 12,
},
title: {
fontSize: 15,
fontWeight: "bold",
color: "#c9a84c",
},
row: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
paddingVertical: 4,
borderBottomWidth: 1,
borderBottomColor: "#1a1f2e",
},
label: {
fontSize: 12,
color: "#6b7280",
fontFamily: "monospace",
flex: 1,
},
value: {
fontSize: 12,
color: "#dde4f0",
fontFamily: "monospace",
flex: 1,
textAlign: "right",
},
hint: {
marginTop: 12,
backgroundColor: "#1a1f2e",
borderRadius: 6,
padding: 10,
},
hintText: {
fontSize: 11,
color: "#4b5563",
fontFamily: "monospace",
lineHeight: 18,
},
});
The article instructs to find the rendering of file results and insert DatePanel after the map().
// In VerseScreen, find the line that renders the file results and add
// below the last card:
{results.map((result) => (
<View key={result.name} style={styles.card}>
{/* ... existing card content ... */}
</View<
))}
{/* NEW: Date utilities panel — add this after the map() */}
<DatePanel
planLength={
/* results is an array of file‑loading results. { name: string, lines: string[], total: number, error: string | null }
1. Find the result object for daily_plan.txt (.find() returns the object whose name matches "daily_plan.txt".), e.g., { name: "daily_plan.txt", total: 365, ... }
If the file failed to load or doesn’t exist, .find() returns: undenfined.
2. If it exists, take its total field (the optional chaining operator (?.) safely accesses total only if find returns a result).
3. If anything is missing, fall back to 0.
4. It evaluates to the number of lines in daily_plan.txt, if available 0, if not
results.
5. Pass that number as the planLength prop to DatePanel. */
results.find((r) => r.name === "daily_plan.txt")?.total ?? 0
}
/>
# It clears caches and starts the development server.
npx expo start --clear
Navigate to the Verse tab. Below the three file cards you should see:
📅 Date Utilities — Chunk 05
─────────────────────────────────────
todayISO() 2026-05-14
dayOfYear() 134
planIndex(63) 71
isLeapYear() false
daysInYear() 365
formatDate("long") Wednesday, 14 May 2026
formatDate("short") 14 May 2026
formatDate("day") Wednesday