التاريخ الهجري

ماهي المكتبة التي تستخدمها للتاريخ الهجري ك date picker ؟

بالنسبة لي bootstrap-hijri-date يادي الغرض ولكنه قديم ويعتمد على jquery ولا يتوفر ك npm package استخدمه من خلال cdn.

هل تعرف شي جديد افضل منه ؟ نورونا بتجاربكم


قبل اختيار أي مكتبة، أنصح بالانتباه إلى أمرين، لأنهما يقعان كثيراً في هذا الموضوع:

  • ليس كل ما يُسمّى «هجري» هو التقويم الهجري القمري. مكتبة date-fns-jalali المذكورة في أحد الردود خاصة بالتقويم الجلالي، أي التقويم الهجري الشمسي الإيراني، كما يقول وصفها في npm نفسه: «date utility library for jalali calendar». وتختلف أسماء شهوره وترتيبها وأطوالها عن التقويم الهجري القمري الذي يضم محرم وصفر ورمضان.
  • المكتبات الهجرية القمرية نفسها تختلف في طريقة الحساب. بعضها يتبع تقويم أم القرى المعتمد في السعودية، وبعضها يستخدم التقويم الحسابي (دورة الثلاثين سنة). جرّبت مثلاً مكتبة hijri-date (الإصدار 0.2.2): الحساب فيها حسابي، ونتيجتها اختلفت عن أم القرى في 3,594 يوماً من أصل 5,844 يوماً بين 2020 و2035. فهي تجعل 7 فبراير 2027 أول رمضان 1448، وهو في أم القرى 30 شعبان.

أبسط فحص لأي مكتبة: أول رمضان 1448 في تقويم أم القرى هو الاثنين 8 فبراير 2027، وأول جمادى الأولى 1448 هو الاثنين 12 أكتوبر 2026. إن أعطتك المكتبة غير ذلك فهي لا تتبع أم القرى، وإن طابقتهما فهذا فحص أولي فقط.

وبخصوص الخيارات المذكورة في الردود السابقة، فهناك نقطتان تستحقان التحقق: مستودع deskpro/react-datepicker-hijri صار مؤرشفاً (للقراءة فقط) على GitHub، ولم أجد حزمة باسم react-hicetnunc-datepicker في npm ولا في GitHub.

أما البديل الحديث فلا يحتاج إلى jQuery ولا إلى مكتبة: المتصفحات الحديثة تدعم تقويم أم القرى عبر Intl.DateTimeFormat (يحتاج الكود إلى متصفح حديث، ويتحقق من دعم التقويم قبل استخدامه)، فيمكن بناء منتقٍ بسيط يعرض الشهر الهجري، وتحت كل يوم تاريخه الميلادي، ويعيد إليك التاريخ الميلادي لتخزينه. الكود التالي نموذج مختصر يوضح الفكرة، وليس بديلاً كاملاً عن مكونات المنتقيات الجاهزة:

// Hijri (Umm al-Qura) month picker with Intl only: no jQuery, no library.
const hijriParts = new Intl.DateTimeFormat('en-u-ca-islamic-umalqura', {
  timeZone: 'UTC', day: 'numeric', month: 'numeric', year: 'numeric',
});
const hijriMonthName = new Intl.DateTimeFormat('ar-SA-u-ca-islamic-umalqura-nu-latn', {
  timeZone: 'UTC', month: 'long', year: 'numeric',
});

// Browsers without Umm al-Qura silently fall back to the Gregorian calendar, so check before use.
const UMALQURA_SUPPORTED = hijriParts.resolvedOptions().calendar === 'islamic-umalqura';

const DAY = 86400000;

function toHijri(date) {
  const p = Object.fromEntries(hijriParts.formatToParts(date).map((x) => [x.type, x.value]));
  return { year: parseInt(p.year, 10), month: Number(p.month), day: Number(p.day) };
}

// Gregorian date (UTC midnight) of 1/month/year in the Hijri calendar.
function hijriMonthStart(year, month) {
  // Mean-month estimate from 1 Muharram 1 AH (16 July 622, Julian), then correct with Intl.
  let t = Date.UTC(622, 6, 19) + Math.round(((year - 1) * 12 + (month - 1)) * 29.530589) * DAY;
  for (let i = 0; i < 10; i++) {
    const h = toHijri(new Date(t));
    const months = (year - h.year) * 12 + (month - h.month);
    if (months === 0 && h.day === 1) return new Date(t);
    t += (Math.round(months * 29.530589) - (h.day - 1)) * DAY;
  }
  throw new RangeError(`cannot find 1/${month}/${year}`);
}

function nextMonth(year, month) {
  return month === 12 ? [year + 1, 1] : [year, month + 1];
}

function hijriMonthLength(year, month) {
  return Math.round((hijriMonthStart(...nextMonth(year, month)) - hijriMonthStart(year, month)) / DAY);
}

// Draw one Hijri month (weeks start on Saturday) with prev/next buttons.
// Each day is a button with an accessible label (Enter/Space work); clicking it calls onPick({ iso, hijri: { year, month, day }, label }).
// A minimal example, not a complete accessible date picker.
function renderHijriPicker(container, year, month, onPick) {
  const start = hijriMonthStart(year, month);
  const length = hijriMonthLength(year, month);
  const offset = (start.getUTCDay() + 1) % 7; // Saturday = 0

  const prev = document.createElement('button');
  prev.type = 'button';
  prev.textContent = '‹ السابق';
  prev.onclick = () => renderHijriPicker(container, ...(month === 1 ? [year - 1, 12] : [year, month - 1]), onPick);
  const next = document.createElement('button');
  next.type = 'button';
  next.textContent = 'التالي ›';
  next.onclick = () => renderHijriPicker(container, ...nextMonth(year, month), onPick);
  const title = document.createElement('strong');
  title.textContent = hijriMonthName.format(start);
  const header = document.createElement('div');
  header.append(prev, ' ', title, ' ', next);

  const table = document.createElement('table');
  const head = ['السبت', 'الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة'];
  table.createTHead().insertRow().append(...head.map((h) => Object.assign(document.createElement('th'), { textContent: h })));
  const body = table.createTBody();
  let row = body.insertRow();
  for (let i = 0; i < offset; i++) row.insertCell();
  for (let d = 1; d <= length; d++) {
    if (row.cells.length === 7) row = body.insertRow();
    const date = new Date(start.getTime() + (d - 1) * DAY);
    const iso = date.toISOString().slice(0, 10);
    const label = `${d} ${title.textContent}`;
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.dataset.date = iso;
    btn.setAttribute('aria-label', `${label} — ${iso}`);
    const b = document.createElement('b');
    b.textContent = d;
    const small = document.createElement('small');
    small.textContent = date.getUTCDate() + '/' + (date.getUTCMonth() + 1); // Gregorian day/month
    btn.append(b, document.createElement('br'), small);
    btn.onclick = () => onPick({ iso, hijri: { year, month, day: d }, label });
    row.insertCell().append(btn);
  }
  while (row.cells.length < 7) row.insertCell();

  const wrap = document.createElement('div');
  wrap.dir = 'rtl';
  wrap.className = 'hijri-picker';
  wrap.append(header, table);
  container.replaceChildren(wrap);
}

وطريقة الربط بحقل في النموذج:

<div id="cal"></div>
<p id="picked"></p>
<input type="hidden" name="date" id="date">

<script>
  if (!UMALQURA_SUPPORTED) {
    // إذا لم يتوفر تقويم أم القرى نعرض حقلاً لاختيار التاريخ الميلادي، بدلاً من تواريخ هجرية قد تكون خاطئة
    document.getElementById('date').type = 'date';
    document.getElementById('date').setAttribute('aria-label', 'التاريخ الميلادي');
    document.getElementById('picked').textContent = 'التقويم الهجري غير متاح في هذا المتصفح، فاختر التاريخ الميلادي:';
  } else {
    const now = new Date(); // today in the visitor's own time zone
    const today = toHijri(new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())));
    renderHijriPicker(document.getElementById('cal'), today.year, today.month, (p) => {
      document.getElementById('date').value = p.iso; // مثل 2026-10-18
      document.getElementById('picked').textContent = `${p.label} — ${p.iso}`;
    });
  }
</script>

ما الذي تحققت منه:

  • قارنت بداية كل شهر وطوله بجدول أم القرى، لكل الشهور من 1343 إلى 1500 هـ (1,895 شهراً)، فتطابقت كلها.
  • جمادى الأولى 1448 يبدأ يوم الاثنين 12 أكتوبر 2026 وطوله 30 يوماً. الضغط على اليوم السابع يضع في الحقل 2026-10-18 ويعرض «7 جمادى الأولى 1448 هـ». ورمضان 1448 طوله 29 يوماً ويبدأ في 8 فبراير 2027، والانتقال بين ذي الحجة ومحرم يعمل في الاتجاهين.

ملاحظات:

  • خزّن التاريخ الميلادي (p.iso) في قاعدة البيانات، واعرض الهجري عند الحاجة. التحويل من الميلادي إلى الهجري سطر واحد (toHijri)، أما تخزين الهجري فيجعل الفرز والمقارنة أصعب.
  • تحقق من الدعم: إذا لم يكن تقويم أم القرى مدعوماً في بيئة ما، فقد يعود Intl بصمت إلى التقويم الميلادي دون أي خطأ. لذلك يفحص الكود resolvedOptions().calendar قبل الاستخدام.
  • في تنفيذ ICU الموثّق لهذا التقويم تمتد بيانات أم القرى من 1300 إلى 1600 هـ، وخارجها يُستخدم تقويم حسابي بديل.
  • بداية الشهور مثل رمضان وشوال تُعلن برؤية الهلال، فقد يختلف التاريخ المعلن عن التاريخ المحسوب في التقويم.
  • للإفصاح: قارنت النتائج بجدول أم القرى في مكتبة مفتوحة المصدر أعمل عليها.