170 lines
5.4 KiB
JavaScript
170 lines
5.4 KiB
JavaScript
// date-utils.js — Date handling utilities for the calendar
|
|
const monthAbbr = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
|
|
const DAY_MS = 86400000
|
|
const WEEK_MS = 7 * DAY_MS
|
|
|
|
/**
|
|
* Get ISO week information for a given date
|
|
* @param {Date} date - The date to get week info for
|
|
* @returns {Object} Object containing week number and year
|
|
*/
|
|
const isoWeekInfo = date => {
|
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
|
const day = d.getUTCDay() || 7
|
|
d.setUTCDate(d.getUTCDate() + 4 - day)
|
|
const year = d.getUTCFullYear()
|
|
const yearStart = new Date(Date.UTC(year, 0, 1))
|
|
const diffDays = Math.floor((d - yearStart) / DAY_MS) + 1
|
|
return { week: Math.ceil(diffDays / 7), year }
|
|
}
|
|
|
|
/**
|
|
* Convert a Date object to a local date string (YYYY-MM-DD format)
|
|
* @param {Date} date - The date to convert (defaults to new Date())
|
|
* @returns {string} Date string in YYYY-MM-DD format
|
|
*/
|
|
function toLocalString(date = new Date()) {
|
|
const pad = n => String(Math.floor(Math.abs(n))).padStart(2, '0')
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
|
}
|
|
|
|
/**
|
|
* Convert a local date string (YYYY-MM-DD) to a Date object
|
|
* @param {string} dateString - Date string in YYYY-MM-DD format
|
|
* @returns {Date} Date object
|
|
*/
|
|
function fromLocalString(dateString) {
|
|
const [year, month, day] = dateString.split('-').map(Number)
|
|
return new Date(year, month - 1, day)
|
|
}
|
|
|
|
/**
|
|
* Get the index of Monday for a given date (0-6, where Monday = 0)
|
|
* @param {Date} d - The date
|
|
* @returns {number} Monday index (0-6)
|
|
*/
|
|
const mondayIndex = d => (d.getDay() + 6) % 7
|
|
|
|
/**
|
|
* Pad a number with leading zeros to make it 2 digits
|
|
* @param {number} n - Number to pad
|
|
* @returns {string} Padded string
|
|
*/
|
|
const pad = n => String(n).padStart(2, '0')
|
|
|
|
/**
|
|
* Calculate number of days between two date strings (inclusive)
|
|
* @param {string} aStr - First date string (YYYY-MM-DD)
|
|
* @param {string} bStr - Second date string (YYYY-MM-DD)
|
|
* @returns {number} Number of days inclusive
|
|
*/
|
|
function daysInclusive(aStr, bStr) {
|
|
const a = fromLocalString(aStr)
|
|
const b = fromLocalString(bStr)
|
|
const A = new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime()
|
|
const B = new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime()
|
|
return Math.floor(Math.abs(B - A) / DAY_MS) + 1
|
|
}
|
|
|
|
/**
|
|
* Add days to a date string
|
|
* @param {string} str - Date string in YYYY-MM-DD format
|
|
* @param {number} n - Number of days to add (can be negative)
|
|
* @returns {string} New date string
|
|
*/
|
|
function addDaysStr(str, n) {
|
|
const d = fromLocalString(str)
|
|
d.setDate(d.getDate() + n)
|
|
return toLocalString(d)
|
|
}
|
|
|
|
/**
|
|
* Get localized weekday names starting from Monday
|
|
* @returns {Array<string>} Array of localized weekday names
|
|
*/
|
|
function getLocalizedWeekdayNames() {
|
|
const res = []
|
|
const base = new Date(2025, 0, 6) // A Monday
|
|
for (let i = 0; i < 7; i++) {
|
|
const d = new Date(base)
|
|
d.setDate(base.getDate() + i)
|
|
res.push(d.toLocaleDateString(undefined, { weekday: 'short' }))
|
|
}
|
|
return res
|
|
}
|
|
|
|
/**
|
|
* Get localized month name
|
|
* @param {number} idx - Month index (0-11)
|
|
* @param {boolean} short - Whether to return short name
|
|
* @returns {string} Localized month name
|
|
*/
|
|
function getLocalizedMonthName(idx, short = false) {
|
|
const d = new Date(2025, idx, 1)
|
|
return d.toLocaleDateString(undefined, { month: short ? 'short' : 'long' })
|
|
}
|
|
|
|
/**
|
|
* Format a date range for display
|
|
* @param {Date} startDate - Start date
|
|
* @param {Date} endDate - End date
|
|
* @returns {string} Formatted date range string
|
|
*/
|
|
function formatDateRange(startDate, endDate) {
|
|
if (toLocalString(startDate) === toLocalString(endDate)) return toLocalString(startDate)
|
|
const startISO = toLocalString(startDate)
|
|
const endISO = toLocalString(endDate)
|
|
const [sy, sm] = startISO.split('-')
|
|
const [ey, em, ed] = endISO.split('-')
|
|
if (sy === ey && sm === em) return `${startISO}/${ed}`
|
|
if (sy === ey) return `${startISO}/${em}-${ed}`
|
|
return `${startISO}/${endISO}`
|
|
}
|
|
|
|
/**
|
|
* Compute lunar phase symbol for the four main phases on a given date.
|
|
* Returns one of: 🌑 (new), 🌓 (first quarter), 🌕 (full), 🌗 (last quarter), or '' otherwise.
|
|
* Uses an approximate algorithm with a fixed epoch.
|
|
*/
|
|
function lunarPhaseSymbol(date) {
|
|
// Reference new moon: 2000-01-06 18:14 UTC (J2000 era), often used in approximations
|
|
const ref = Date.UTC(2000, 0, 6, 18, 14, 0)
|
|
const synodic = 29.530588853 // days
|
|
// Use UTC noon of given date to reduce timezone edge effects
|
|
const dUTC = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0)
|
|
const daysSince = (dUTC - ref) / DAY_MS
|
|
const phase = ((daysSince / synodic) % 1 + 1) % 1
|
|
const phases = [
|
|
{ t: 0.0, s: '🌑' }, // New Moon
|
|
{ t: 0.25, s: '🌓' }, // First Quarter
|
|
{ t: 0.5, s: '🌕' }, // Full Moon
|
|
{ t: 0.75, s: '🌗' } // Last Quarter
|
|
]
|
|
// threshold in days from exact phase to still count for this date
|
|
const thresholdDays = 0.5 // ±12 hours
|
|
for (const p of phases) {
|
|
let delta = Math.abs(phase - p.t)
|
|
if (delta > 0.5) delta = 1 - delta
|
|
if (delta * synodic <= thresholdDays) return p.s
|
|
}
|
|
return ''
|
|
}
|
|
|
|
// Export all functions and constants
|
|
export {
|
|
monthAbbr,
|
|
DAY_MS,
|
|
WEEK_MS,
|
|
isoWeekInfo,
|
|
toLocalString,
|
|
fromLocalString,
|
|
mondayIndex,
|
|
pad,
|
|
daysInclusive,
|
|
addDaysStr,
|
|
getLocalizedWeekdayNames,
|
|
getLocalizedMonthName,
|
|
formatDateRange
|
|
,lunarPhaseSymbol
|
|
}
|