175 lines
4.9 KiB
JavaScript
175 lines
4.9 KiB
JavaScript
/**
|
||
* Jalali (Solar Hijri / Shamsi) calendar conversion utilities (Backend)
|
||
*/
|
||
|
||
const persianMonths = [
|
||
{ value: 1, id: 1, label: 'فروردین', name: 'فروردین' },
|
||
{ value: 2, id: 2, label: 'اردیبهشت', name: 'اردیبهشت' },
|
||
{ value: 3, id: 3, label: 'خرداد', name: 'خرداد' },
|
||
{ value: 4, id: 4, label: 'تیر', name: 'تیر' },
|
||
{ value: 5, id: 5, label: 'مرداد', name: 'مرداد' },
|
||
{ value: 6, id: 6, label: 'شهریور', name: 'شهریور' },
|
||
{ value: 7, id: 7, label: 'مهر', name: 'مهر' },
|
||
{ value: 8, id: 8, label: 'آبان', name: 'آبان' },
|
||
{ value: 9, id: 9, label: 'آذر', name: 'آذر' },
|
||
{ value: 10, id: 10, label: 'دی', name: 'دی' },
|
||
{ value: 11, id: 11, label: 'بهمن', name: 'بهمن' },
|
||
{ value: 12, id: 12, label: 'اسفند', name: 'اسفند' }
|
||
]
|
||
|
||
function toEnglishDigits(str) {
|
||
if (str === null || str === undefined) return ''
|
||
return String(str)
|
||
.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d))
|
||
.replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d))
|
||
}
|
||
|
||
function jalaliToGregorian(jy, jm, jd) {
|
||
jy = Number(jy)
|
||
jm = Number(jm)
|
||
jd = Number(jd)
|
||
|
||
let gy = jy > 979 ? 1600 : 621
|
||
jy = jy > 979 ? jy - 979 : jy
|
||
|
||
let days =
|
||
365 * jy +
|
||
Math.floor(jy / 33) * 8 +
|
||
Math.floor(((jy % 33) + 3) / 4) +
|
||
78 +
|
||
jd +
|
||
(jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186)
|
||
|
||
gy += 400 * Math.floor(days / 146097)
|
||
days %= 146097
|
||
|
||
if (days > 36524) {
|
||
gy += 100 * Math.floor(--days / 36524)
|
||
days %= 36524
|
||
if (days >= 365) days++
|
||
}
|
||
|
||
gy += 4 * Math.floor(days / 1461)
|
||
days %= 1461
|
||
|
||
if (days > 365) {
|
||
gy += Math.floor((days - 1) / 365)
|
||
days = (days - 1) % 365
|
||
}
|
||
|
||
let gd = days + 1
|
||
const isLeap = (gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0
|
||
const sal_a = [0, 31, isLeap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||
|
||
let gm
|
||
for (gm = 0; gm < 13; gm++) {
|
||
const v = sal_a[gm]
|
||
if (gd <= v) break
|
||
gd -= v
|
||
}
|
||
|
||
return { gy, gm, gd }
|
||
}
|
||
|
||
function gregorianToJalali(gy, gm, gd) {
|
||
gy = Number(gy)
|
||
gm = Number(gm)
|
||
gd = Number(gd)
|
||
|
||
const g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
|
||
let jy = gy > 1600 ? 979 : 0
|
||
gy = gy > 1600 ? gy - 1600 : gy - 621
|
||
|
||
const gy2 = gm > 2 ? gy + 1 : gy
|
||
let days =
|
||
365 * gy +
|
||
Math.floor((gy2 + 3) / 4) -
|
||
Math.floor((gy2 + 99) / 100) +
|
||
Math.floor((gy2 + 399) / 400) -
|
||
80 +
|
||
gd +
|
||
g_d_m[gm - 1]
|
||
|
||
jy += 33 * Math.floor(days / 12053)
|
||
days %= 12053
|
||
|
||
jy += 4 * Math.floor(days / 1461)
|
||
days %= 1461
|
||
|
||
if (days > 365) {
|
||
jy += Math.floor((days - 1) / 365)
|
||
days = (days - 1) % 365
|
||
}
|
||
|
||
const jm = days < 186 ? 1 + Math.floor(days / 31) : 7 + Math.floor((days - 186) / 30)
|
||
const jd = 1 + (days < 186 ? days % 31 : (days - 186) % 30)
|
||
|
||
return { jy, jm, jd }
|
||
}
|
||
|
||
function jalaliToDate(jy, jm, jd) {
|
||
const y = parseInt(toEnglishDigits(jy), 10)
|
||
const m = parseInt(toEnglishDigits(jm), 10)
|
||
const d = parseInt(toEnglishDigits(jd), 10)
|
||
|
||
if (isNaN(y) || isNaN(m) || isNaN(d) || y <= 0 || m < 1 || m > 12 || d < 1 || d > 31) {
|
||
return null
|
||
}
|
||
|
||
const { gy, gm, gd } = jalaliToGregorian(y, m, d)
|
||
return new Date(Date.UTC(gy, gm - 1, gd, 12, 0, 0))
|
||
}
|
||
|
||
function parseDateToJalali(dateVal) {
|
||
if (!dateVal) return null
|
||
|
||
if (dateVal instanceof Date || (typeof dateVal === 'string' && (dateVal.includes('T') || dateVal.includes('-')))) {
|
||
const d = new Date(dateVal)
|
||
if (!isNaN(d.getTime())) {
|
||
return gregorianToJalali(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate())
|
||
}
|
||
}
|
||
|
||
if (typeof dateVal === 'string' && dateVal.includes('/')) {
|
||
const clean = toEnglishDigits(dateVal.trim())
|
||
const parts = clean.split('/')
|
||
if (parts.length === 3) {
|
||
const jy = parseInt(parts[0], 10)
|
||
const jm = parseInt(parts[1], 10)
|
||
const jd = parseInt(parts[2], 10)
|
||
if (!isNaN(jy) && !isNaN(jm) && !isNaN(jd)) {
|
||
return { jy, jm, jd }
|
||
}
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
function normalizeToDate(input) {
|
||
if (!input) return null
|
||
if (input instanceof Date && !isNaN(input.getTime())) return input
|
||
if (typeof input === 'string') {
|
||
const trimmed = input.trim()
|
||
if (trimmed.includes('/')) {
|
||
const parts = toEnglishDigits(trimmed).split('/')
|
||
if (parts.length === 3) {
|
||
return jalaliToDate(parts[0], parts[1], parts[2])
|
||
}
|
||
}
|
||
const d = new Date(trimmed)
|
||
if (!isNaN(d.getTime())) return d
|
||
}
|
||
return null
|
||
}
|
||
|
||
module.exports = {
|
||
persianMonths,
|
||
toEnglishDigits,
|
||
jalaliToGregorian,
|
||
gregorianToJalali,
|
||
jalaliToDate,
|
||
parseDateToJalali,
|
||
normalizeToDate,
|
||
}
|