npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

islamic-date-adjustment

v3.2.7

Published

Islamic (Hijri) calendar date adjustment library with Firebase persistence and React Native hook. Handles global offsets, per-event adjustments, month-start adjustments, smart validation, and preserves already-observed days.

Readme

🕌 Islamic Date Adjustment Library

A JavaScript library for adjusting Islamic (Hijri) calendar dates based on local moon sighting, with intelligent handling of month boundaries and already-observed days.

Works everywhere: React Native, Node.js, Web, or any JavaScript environment.

The Problem

Islamic calendar dates are determined by moon sighting, which varies by region. When an authority announces "Ramadan starts on Feb 18," your local community might sight the moon 2 days earlier (Feb 16). Later, when Eid al-Fitr is announced, you can't simply shift the entire calendar — you've already been fasting since Feb 16!

This library solves that by:

  1. Global offset — shift the entire calendar uniformly (e.g., -2 days for your local sighting)
  2. Per-event adjustment — when a specific event (Eid, Laylat al-Qadr, etc.) is confirmed on a different date, adjust only that event forward while preserving already-passed days
  3. Month-start adjustments — adjust ANY month's start date directly, not just months with special events
  4. First-sighting freedom — when no adjustments exist yet, ALL date options are available (your first moon sighting can be anything). After that, strict 29–30 day validation kicks in
  5. Smart validation — Islamic months can only be 29 or 30 days; invalid adjustments are automatically filtered out
  6. Global offset fallback — when a date can't be achieved locally (e.g., Ramadan -1 day would shrink Shaban below 29), the library tells you exactly which global offset would work
  7. Conflict prevention — global offset options are validated against existing event adjustments, showing only valid choices
  8. Reset — clear all adjustments and start fresh while keeping your global offset

✨ Key Features

  • Three adjustment types: Global offset, event-based, and month-start adjustments
  • Custom special events: Pass your own events array or use the built-in catalogue
  • Adjustment tracking: Every month and event in the calendar knows if it was directly adjusted or cascade-shifted
  • First-sighting freedom: First adjustment allows any date; subsequent ones enforce 29–30 day months
  • Smart validation: Only show valid date options that keep months within 29-30 days
  • Global offset fallback: Invalid local adjustments show the exact global offset needed to achieve the date
  • Conflict prevention: Global offset options automatically validated against existing adjustments
  • Preserve past observations: Adjustments never affect already-observed days (e.g., fasting days)
  • Reset: Clear all adjustments and start fresh while keeping global offset
  • React/React Native hook: Reactive state management with auto-save to Firebase
  • Real-time sync: Share calendar adjustments across devices
  • Interactive CLI: Test and explore adjustments from the command line
  • Fully typed: TypeScript definitions included
  • Zero dependencies: Core engine has no dependencies (Firebase adapter is optional)

Installation

npm install islamic-date-adjustment

Quick Start (React Native)

1. Basic usage (no persistence)

import {
  HijriYear,
  DEFAULT_1447_STARTS,
  SPECIAL_EVENTS,
  formatDate,
} from "islamic-date-adjustment";

const hy = new HijriYear(1447, DEFAULT_1447_STARTS);
hy.setGlobalOffset(-2); // Your local moon sighting

console.log(formatDate(hy.getMonthStart(9))); // "2026-02-15" (Ramadan)
console.log(formatDate(hy.getEventDate("eid_al_fitr"))); // "2026-03-17"

// With custom events (optional — defaults to built-in SPECIAL_EVENTS)
const customEvents = [
  ...SPECIAL_EVENTS,
  { id: "my_event", name: "My Custom Event", hijriMonth: 6, hijriDay: 15 },
];
const hyCustom = new HijriYear(1447, DEFAULT_1447_STARTS, customEvents);

2. With the React hook

import { useHijriYear, DEFAULT_1447_STARTS } from "islamic-date-adjustment";

function RamadanScreen() {
  const hijri = useHijriYear({
    year: 1447,
    monthStarts: DEFAULT_1447_STARTS,
    globalOffset: -2,
  });

  // Get valid date options for Eid (only 29–30 day month lengths)
  const eidOptions = hijri.getValidOffsets("eid_al_fitr");

  return (
    <View>
      <Text>Ramadan starts: {hijri.getMonthStart(9)}</Text>
      <Text>Eid al-Fitr: {hijri.getEventDate("eid_al_fitr")}</Text>
      <Text>Ramadan days: {hijri.getMonthDays(9)}</Text>

      {eidOptions.map((opt) => (
        <Button
          key={opt.date}
          title={`${opt.date} ${opt.label}`}
          onPress={() => hijri.adjustEvent("eid_al_fitr", opt.date)}
        />
      ))}
    </View>
  );
}

2a. Handling Global Offset Fallback

When a date can't be achieved via local adjustment (e.g., Ramadan -1 day would shrink Shaban to 28 days), the library flags it with requiresGlobalOffset and tells you the exact global offset needed:

function EventDatePicker({ eventId }) {
  const hijri = useHijriYear({
    year: 1447,
    monthStarts: DEFAULT_1447_STARTS,
  });

  const options = hijri.getValidOffsets(eventId);

  const handleSelect = (option) => {
    if (option.requiresGlobalOffset) {
      // Show confirmation: "This requires changing the global offset
      // to X, which will shift ALL months. Continue?"
      Alert.alert("Global Offset Change Required", option.globalOffsetReason, [
        { text: "Cancel", style: "cancel" },
        {
          text: `Change to ${option.suggestedGlobalOffset}`,
          onPress: () => hijri.setGlobalOffset(option.suggestedGlobalOffset),
        },
      ]);
    } else if (option.valid) {
      hijri.adjustEvent(eventId, option.date);
    }
  };

  return (
    <View>
      {options
        .filter((o) => o.valid || o.requiresGlobalOffset)
        .map((opt) => (
          <TouchableOpacity key={opt.date} onPress={() => handleSelect(opt)}>
            <Text>
              {opt.date} {opt.label}
            </Text>
            {opt.requiresGlobalOffset && (
              <Text style={{ color: "orange" }}>
                ⚠️ Requires global offset → {opt.suggestedGlobalOffset}
              </Text>
            )}
          </TouchableOpacity>
        ))}
    </View>
  );
}

2b. Global Offset Picker with Validation

function GlobalOffsetSettings() {
  const hijri = useHijriYear({
    year: 1447,
    monthStarts: DEFAULT_1447_STARTS,
  });

  // Get valid global offset options (automatically filtered)
  const offsetOptions = hijri.getValidGlobalOffsets(3);

  return (
    <View>
      <Text>Current Offset: {hijri.globalOffset} days</Text>

      {offsetOptions.map((option) => (
        <TouchableOpacity
          key={option.offset}
          style={option.isCurrent ? styles.current : styles.option}
          onPress={() => hijri.setGlobalOffset(option.offset)}
        >
          <Text>
            {option.offset >= 0 ? "+" : ""}
            {option.offset} days
          </Text>
          <Text>Muharram: {option.muharramStart}</Text>
          <Text>
            Ramadan:{" "}
            {option.monthPreviews.find((m) => m.index === 9)?.adjustedStart}
          </Text>
          {option.isCurrent && <Badge>Current</Badge>}
        </TouchableOpacity>
      ))}
    </View>
  );
}

2c. Month-Start Adjustments

function MonthStartPicker({ monthIndex }) {
  const hijri = useHijriYear({
    year: 1447,
    monthStarts: DEFAULT_1447_STARTS,
  });

  // Get valid start date options for any month
  const dateOptions = hijri.getValidMonthStartDates(monthIndex, 3);

  return (
    <View>
      <Text>
        Current: {hijri.getMonthStart(monthIndex)}(
        {hijri.getMonthDays(monthIndex)} days)
      </Text>

      {dateOptions.map((option) => (
        <Button
          key={option.date}
          title={`${option.date} ${option.label}`}
          onPress={() => hijri.adjustMonthStart(monthIndex, option.date)}
        />
      ))}
    </View>
  );
}

2d. First-Sighting Behavior

When no adjustments have been made yet, the library allows any date — your first moon sighting is unconstrained. After that, strict 29–30 day validation ensures calendar integrity:

function FirstSighting({ monthIndex }) {
  const hijri = useHijriYear({
    year: 1447,
    monthStarts: DEFAULT_1447_STARTS,
  });

  // First adjustment: ALL offsets shown as valid (first sighting freedom)
  // After first adjustment: only offsets keeping months at 29-30 days
  const dateOptions = hijri.getValidMonthStartDatesConsidering(monthIndex, {
    range: 3,
  });

  const handleSelect = (option) => {
    // Uses forced mode for first adjustment, strict mode after
    hijri.adjustMonthStartConsidering(monthIndex, option.date);
  };

  return (
    <View>
      {hijri.adjustments.length === 0 && (
        <Text>🌙 First sighting — pick any date you observed</Text>
      )}
      {dateOptions
        .filter((o) => o.valid || o.requiresGlobalOffset)
        .map((opt) => (
          <Button
            key={opt.date}
            title={`${opt.date} ${opt.label}`}
            onPress={() => handleSelect(opt)}
          />
        ))}
    </View>
  );
}

3. With Firebase persistence

import firestore from "@react-native-firebase/firestore";
import {
  useHijriYear,
  DEFAULT_1447_STARTS,
  createFirebaseAdapter,
} from "islamic-date-adjustment";

const adapter = createFirebaseAdapter(firestore());

function CalendarScreen({ userId }) {
  const hijri = useHijriYear({
    year: 1447,
    monthStarts: DEFAULT_1447_STARTS,
    globalOffset: -2,
    firebaseAdapter: adapter,
    userId, // saves to Firestore doc: hijri_calendars/{userId}_1447
    realtime: true, // syncs across devices
  });

  if (hijri.loading) return <ActivityIndicator />;

  return (
    <View>
      <Text>
        Ramadan: {hijri.getMonthStart(9)} – {hijri.getMonthEnd(9)}
      </Text>
      <Text>Days fasted: {hijri.getDaysObserved(9, "2026-03-10")}</Text>

      <FlatList
        data={hijri.calendar}
        renderItem={({ item }) => (
          <Text>
            {item.name}: {item.adjustedStart} ({item.adjustedDays} days)
          </Text>
        )}
      />
    </View>
  );
}

Changes are auto-saved to Firestore 500ms after each adjustment (debounced).


Firebase Adapter API

import { createFirebaseAdapter } from 'islamic-date-adjustment';

const adapter = createFirebaseAdapter(firestoreInstance, 'collection_name');

await adapter.save(userId, hijriYear);          // Save state
const hy = await adapter.load(userId, 1447);    // Load state (returns HijriYear or null)
await adapter.remove(userId, 1447);             // Delete saved data
const years = await adapter.listYears(userId);  // List all saved years
const unsub = adapter.onSnapshot(userId, 1447, (hy) => { ... }); // Real-time listener

Firestore document structure

hijri_calendars/{userId}_{year}
├── year: 1447
├── globalOffset: -2
├── monthStarts: { 1: "2025-06-26", 2: "2025-07-26", ... }
├── adjustments: [{ eventId, targetDate, offsetDelta, ... }]
├── userId: "user_abc"
└── updatedAt: "2026-03-18T12:00:00.000Z"

Serialization helpers (for custom storage)

import { serialize, deserialize } from "islamic-date-adjustment";

const json = serialize(hijriYear); // → plain JSON-safe object
const hy = deserialize(json); // → HijriYear instance
// Use these with AsyncStorage, MMKV, or any storage backend

useHijriYear Hook Reference

const hijri = useHijriYear({
  year, // number — Hijri year (e.g. 1447)
  monthStarts, // object — { monthIndex: "YYYY-MM-DD" }
  globalOffset, // number — initial offset (default 0)
  specialEvents, // optional — custom events array (default built-in SPECIAL_EVENTS)
  firebaseAdapter, // optional — from createFirebaseAdapter()
  userId, // optional — required if firebaseAdapter is set
  realtime, // optional — subscribe to real-time updates (default false)
});

Returned object

| Property / Method | Type | Description | | ------------------------------------------------------- | ------------- | ---------------------------------------------------------- | | year | number | Hijri year | | globalOffset | number | Current global offset | | adjustments | Array | List of active adjustments | | calendar | Array | Full 12-month calendar with adjustment tracking (reactive) | | loading | boolean | true while loading from Firebase | | error | Error\|null | Last error | | Global Offset | | | | setGlobalOffset(days) | void | Update global offset | | getValidGlobalOffsets(range?) | Array | Valid global offset options (pre-filtered) | | Event Adjustments | | | | adjustEvent(eventId, date) | object | Adjust event, auto-saves | | simulateAdjustment(eventId, date) | object | Preview without changing state | | getValidOffsets(eventId, range?) | Array | Valid date options (strict 29-30 validation) | | getValidOffsetsConsidering(eventId, opts?) | Array | Date options (all valid if first sighting, strict after) | | Month-Start Adjustments | | | | adjustMonthStart(monthIndex, date) | object | Adjust month start (strict validation) | | adjustMonthStartConsidering(monthIndex, date) | object | Adjust month start (first sighting freedom) | | getValidMonthStartDates(monthIndex, range?) | Array | Valid start dates (strict 29-30 validation) | | getValidMonthStartDatesConsidering(monthIndex, opts?) | Array | Start dates (all valid if first sighting, strict after) | | simulateMonthStartAdjustment(monthIndex, date) | object | Preview month start adjustment | | hasMonthOffset(monthIndex) | boolean | Whether a month has any adjustment | | Queries | | | | getEventDate(eventId) | string | Adjusted date as "YYYY-MM-DD" | | getMonthStart(index) | string | Month start as "YYYY-MM-DD" | | getMonthEnd(index) | string | Month end as "YYYY-MM-DD" | | getMonthDays(index) | number | Days in month | | getUpcomingEvents(date, days?) | Array | Events within N days | | getDaysObserved(index, date) | number | Days observed in a month | | getHijriMonthForDate(date) | object | Hijri date lookup | | reset() | void | Reset all adjustments (keeps global offset) |


Core API (HijriYear class)

new HijriYear(year, monthStarts, specialEvents?)

| Param | Type | Description | | --------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------- | | year | number | Hijri year number (e.g. 1447) | | monthStarts | Object<number, string> | Map of Hijri month index (1-12) → Gregorian start date ("YYYY-MM-DD"). Missing months are auto-computed. | | specialEvents | Array (optional) | Custom array of { id, name, hijriMonth, hijriDay } objects. Defaults to the built-in SPECIAL_EVENTS. |

Methods

| Method | Returns | Description | | ------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------- | | Global Offset | | | | setGlobalOffset(days) | this | Shift all months uniformly. Range: ±15 days. | | getValidGlobalOffsets(range?) | Array | Valid global offset options (default ±3), with validation | | Event Adjustments | | | | adjustEvent(eventId, date) | { adjustment, message } | Adjust a specific event to a Gregorian date. | | getValidOffsets(eventId, range?) | Array | Date options with strict 29-30 validation + global offset info. | | getValidOffsetsConsidering(eventId, opts?) | Array | All valid if no adjustments exist; strict after first adjustment. | | simulateEventAdjustment(eventId, date) | Object | Preview impact without changing state. | | Month-Start Adjustments | | | | adjustMonthStart(monthIndex, date) | { adjustment, message } | Adjust any month's start date (strict 29-30 validation). | | adjustMonthStartConsidering(monthIndex, date) | { adjustment, message } | First adjustment is unconstrained; strict after. | | getValidMonthStartDates(monthIndex, range?) | Array | Date options with strict 29-30 validation + global offset info. | | getValidMonthStartDatesConsidering(monthIndex, opts?) | Array | All valid if no adjustments exist; strict after first adjustment. | | simulateMonthStartAdjustment(monthIndex, date) | Object | Preview month start adjustment impact. | | hasMonthOffset(monthIndex) | boolean | Whether a month has any stored adjustment. | | Queries | | | | getEventDate(eventId) | Date | Get the adjusted Gregorian date for an event. | | getMonthStart(monthIndex) | Date | Adjusted start date of a Hijri month. | | getMonthEnd(monthIndex) | Date | Adjusted last day of a Hijri month. | | getMonthDays(monthIndex) | number | Adjusted number of days in a month. | | getCalendar() | Array | Full calendar view with all adjustments. | | getHijriMonthForDate(date) | Object | Find which Hijri month a Gregorian date falls in. | | getUpcomingEvents(refDate, days?) | Array | Events within N days of a reference date. | | getDaysObserved(monthIndex, refDate) | number | Days observed in a month up to a date. | | reset() | this | Clear all adjustments (keeps global offset). |

Built-in Event IDs

These are the default events included in SPECIAL_EVENTS. You can extend or replace them by passing a custom specialEvents array to the constructor or hook.

| ID | Name | Hijri Date | | ------------------ | ----------------------------------- | ---------------- | | islamic_new_year | Islamic New Year | 1 Muharram | | ashura | Day of Ashura | 10 Muharram | | mawlid | Mawlid al-Nabi (Prophet's Birthday) | 12 Rabi al-Awwal | | isra_miraj | Isra and Mi'raj | 27 Rajab | | shab_e_barat | Shab-e-Barat (Mid-Shaban) | 15 Shaban | | ramadan_start | Start of Ramadan | 1 Ramadan | | laylat_al_qadr | Laylat al-Qadr (approx.) | 27 Ramadan | | eid_al_fitr | Eid al-Fitr | 1 Shawwal | | day_of_arafah | Day of Arafah | 9 Dhul Hijjah | | eid_al_adha | Eid al-Adha | 10 Dhul Hijjah |

Custom Events

You can pass your own events array to include community-specific or regional events:

import {
  SPECIAL_EVENTS,
  HijriYear,
  DEFAULT_1447_STARTS,
} from "islamic-date-adjustment";

// Extend the built-in events
const myEvents = [
  ...SPECIAL_EVENTS,
  { id: "shab_e_meraj", name: "Shab-e-Me'raj", hijriMonth: 7, hijriDay: 27 },
  { id: "shab_e_qadr", name: "Shab-e-Qadr", hijriMonth: 9, hijriDay: 21 },
];

const hy = new HijriYear(1447, DEFAULT_1447_STARTS, myEvents);

// Or completely replace with your own set
const minimalEvents = [
  { id: "ramadan", name: "Ramadan", hijriMonth: 9, hijriDay: 1 },
  { id: "eid_fitr", name: "Eid al-Fitr", hijriMonth: 10, hijriDay: 1 },
  { id: "eid_adha", name: "Eid al-Adha", hijriMonth: 12, hijriDay: 10 },
];
const hyMinimal = new HijriYear(1447, DEFAULT_1447_STARTS, minimalEvents);

The hook accepts the same option:

const hijri = useHijriYear({
  year: 1447,
  monthStarts: DEFAULT_1447_STARTS,
  specialEvents: myEvents,
});

Custom events appear in getCalendar(), getUpcomingEvents(), getEventDate(), and all adjustment methods.


Adjustment Tracking

Every month and event in the calendar output includes flags that tell you why it differs from the standard position:

| Property | Type | Description | | -------------------- | ---------------- | ------------------------------------------------------------------------ | | isAdjusted | boolean | Date/days differ from standard + global offset | | isDirectlyAdjusted | boolean | User explicitly adjusted this month or event | | isCascadeAdjusted | boolean | Shifted as a downstream result of an earlier adjustment | | adjustedBy | string \| null | The eventId of the direct adjustment (null if not directly adjusted) |

These flags appear on both months and events in the getCalendar() output:

const hy = new HijriYear(1447, DEFAULT_1447_STARTS);
hy.adjustEvent("eid_al_fitr", "2026-03-18");

const calendar = hy.getCalendar();

// Ramadan (month 9) — directly affected (its length changed)
const ramadan = calendar.find((m) => m.index === 9);
console.log(ramadan.isAdjusted); // true
console.log(ramadan.isDirectlyAdjusted); // true  — Eid (day 1 of next month) modified Ramadan's length
console.log(ramadan.isCascadeAdjusted); // false
console.log(ramadan.adjustedBy); // "eid_al_fitr"

// Shawwal (month 10) — directly adjusted (the adjustment targets it)
const shawwal = calendar.find((m) => m.index === 10);
console.log(shawwal.isDirectlyAdjusted); // true
console.log(shawwal.adjustedBy); // "eid_al_fitr"

// Dhul Qadah (month 11) — cascade shifted
const dhulQadah = calendar.find((m) => m.index === 11);
console.log(dhulQadah.isAdjusted); // true
console.log(dhulQadah.isDirectlyAdjusted); // false
console.log(dhulQadah.isCascadeAdjusted); // true  — shifted because Shawwal moved
console.log(dhulQadah.adjustedBy); // null

// Events within months carry the same flags:
const eidAdha = calendar
  .find((m) => m.index === 12)
  .events.find((e) => e.id === "eid_al_adha");
console.log(eidAdha.isAdjusted); // true
console.log(eidAdha.isCascadeAdjusted); // true  — shifted by the earlier Eid al-Fitr adjustment

Use case: Style your UI differently for directly-adjusted items (e.g., green highlight) vs. cascade-shifted items (e.g., amber indicator) vs. untouched months (no highlight).


How Adjustments Work

Day-1 Events (e.g., Eid al-Fitr = 1 Shawwal)

When you move Eid al-Fitr earlier by 1 day:

  • Ramadan (previous month) is shortened by 1 day (30 → 29)
  • Ramadan start remains unchanged (you already fasted those days)
  • Shawwal and all subsequent months shift accordingly
Before:  Ramadan (Feb 16 → Mar 17, 30 days) | Shawwal (Mar 18 →)
After:   Ramadan (Feb 16 → Mar 16, 29 days) | Shawwal (Mar 17 →)
                  ↑ unchanged                       ↑ 1 day earlier

First-Sighting Rule

When no adjustments have been made yet, the user is making their first moon sighting. The library allows any date — even ones that would create a 28-day or 31-day previous month. This is because the "standard" calendar is just an estimate, and the actual sighting may differ significantly.

Once any adjustment exists, strict 29–30 day validation applies to all subsequent adjustments. This ensures the calendar remains internally consistent after the first observation is locked in.

const hy = new HijriYear(1447, DEFAULT_1447_STARTS);

// First adjustment — Shaban is 29 days, but -1 would make it 28.
// Normally invalid, but allowed because it's the first sighting:
const options = hy.getValidMonthStartDatesConsidering(9, { range: 3 });
// ALL 7 options have valid: true

hy.adjustMonthStartConsidering(9, "2026-02-17"); // Shaban becomes 28 — allowed!

// Second adjustment — now strict validation kicks in:
const options2 = hy.getValidMonthStartDatesConsidering(7, { range: 3 });
// Only offsets that keep Jumada al-Thani at 29-30 days are valid

To start over, call reset() to clear all adjustments (the global offset is preserved).

Month-Length Validation

Islamic months can only be 29 or 30 days. Adjustments that would violate this are rejected:

const hy = new HijriYear(1447, DEFAULT_1447_STARTS);
// Ramadan is 30 days. Moving Eid 1 day later would make it 31 — REJECTED:
hy.adjustEvent("eid_al_fitr", "2026-03-20");
// throws: "Invalid adjustment: Ramadan would have 31 days, but Islamic months must be 29–30 days."

Built-in validation methods return all options with rich metadata:

  • getValidOffsets(eventId) — event adjustment options with valid and requiresGlobalOffset flags
  • getValidGlobalOffsets() — global offset options (considering existing adjustments)
  • getValidMonthStartDates(monthIndex) — month start options with valid and requiresGlobalOffset flags

Global Offset Fallback

Sometimes a date the user wants can't be achieved by a local adjustment alone. For example, with globalOffset = 0, Ramadan starts on Feb 18. The user says "Ramadan started on Feb 17 for me" — but shifting Ramadan -1 day locally would shrink Shaban from 29 to 28 days (invalid).

The library detects this and tells you exactly how to achieve it via a global offset change:

const options = hy.getValidOffsets("ramadan_start");
const feb17 = options.find((o) => o.offset === -1);

console.log(feb17);
// {
//   offset: -1,
//   date: "2026-02-16",
//   valid: false,                    // Can't do it locally
//   requiresGlobalOffset: true,      // But CAN do it via global offset
//   suggestedGlobalOffset: -1,       // Change global offset to this
//   globalOffsetReason: "Local adjustment would make Shaban 28 days...",
//   label: "(-1 day)"
// }

// Apply the suggestion:
hy.setGlobalOffset(feb17.suggestedGlobalOffset);
// Now ALL months shift by -1, Ramadan is on Feb 17, and Shaban stays 29 days ✅

Each offset option now includes:

| Field | Type | Description | | ----------------------- | ---------------- | ----------------------------------------------------------------- | | offset | number | Days from current position | | date | string | Target Gregorian date | | valid | boolean | Can be achieved via local adjustment | | requiresGlobalOffset | boolean | true if invalid locally but achievable via global offset change | | suggestedGlobalOffset | number \| null | The exact global offset value needed (null if not applicable) | | globalOffsetReason | string \| null | Human-readable explanation for UI display | | label | string | Human-readable label (e.g., "(+1 day)") |

Your UI can show all options — valid ones as normal buttons, requiresGlobalOffset ones with a warning badge — and prompt the user before applying a global shift.

Mid-Month Events (e.g., Eid al-Adha = 10 Dhul Hijjah)

When you move Eid al-Adha earlier by 1 day:

  • Dhul Qadah (previous month) absorbs the shift (shortened by 1)
  • Dhul Hijjah starts 1 day earlier (so day 10 falls on the target date)
  • The event's month length stays the same

Global Offset Validation

When you have existing event adjustments, changing the global offset can create conflicts. The library automatically validates each offset option:

const hijri = useHijriYear({ year: 1447, monthStarts: DEFAULT_1447_STARTS });

// Adjust Ramadan start by +1 day (Shaban becomes 30 days instead of 29)
hijri.adjustEvent("ramadan_start", "2026-02-19");

// Now get valid global offset options
const options = hijri.getValidGlobalOffsets(3);
// Returns only valid offsets that won't make Shaban > 30 days
// Invalid offsets (e.g., -2, -3) are automatically filtered out

UI benefit: Your users see only the offset options that actually work with their current adjustments. No error messages!

Month-Start Adjustments

You can adjust ANY month's start date directly, not just months with special events:

// Adjust Muharram to start on a specific date
const result = hijri.adjustMonthStart(1, "2025-06-25");

// Get valid options for Rajab's start date
const rajabOptions = hijri.getValidMonthStartDates(7, 3);

// First-sighting-aware version (all valid when no adjustments exist):
const options = hijri.getValidMonthStartDatesConsidering(7, { range: 3 });
hijri.adjustMonthStartConsidering(7, "2025-12-20");

// Preview impact before applying
const impact = hijri.simulateMonthStartAdjustment(7, "2025-12-20");
console.log(impact.monthLengthChanges); // See which months are affected

Use cases:

  • Communities that sight the moon for every month (not just Ramadan/Eid)
  • Correcting specific months without using a global offset
  • Advanced users who need fine-grained control

Interactive CLI

For testing and exploration, an interactive CLI is included:

npm start

Features:

  • ✅ Set global offset with visual preview of all month dates
  • ✅ Adjust specific events with impact analysis
  • ✅ Adjust month start dates
  • First-sighting mode: First adjustment allows any date; strict validation after
  • Smart filtering: Shows valid options + global offset fallback options (tagged with ⚠️)
  • Global offset prompting: When you pick an option that requires a global shift, the CLI explains the impact and asks for confirmation
  • Reset all adjustments: Clear all adjustments and start fresh
  • ✅ View full calendar with all adjustments
  • ✅ Check upcoming events
  • ✅ Hijri date lookup for any Gregorian date

Running Tests

npm test

214 tests across 3 test suites covering:

  • ✅ Calendar data & date helpers (30 tests)
  • ✅ Adjustment engine (168 tests) — offsets, events, validation, simulation, global offset fallback, first-sighting, reset, downstream clearing, edge cases
  • ✅ Firebase adapter (16 tests) — serialize/deserialize, mock Firestore CRUD, real-time

Project Structure

src/
  islamicCalendar.js    — Month definitions, event catalogue, date helpers
  adjustmentEngine.js   — HijriYear class with all adjustment logic
  defaults.js           — Default 1447 AH Gregorian start dates
  firebaseAdapter.js    — Firebase Firestore persistence (serialize/deserialize/CRUD)
  useHijriYear.js       — React/React Native hook with auto-persistence
  cli.js                — Interactive command-line interface
  index.js              — Public API entry point (re-exports everything)
tests/
  islamicCalendar.test.js    — 30 tests for data & helpers
  adjustmentEngine.test.js   — 168 tests for the engine
  firebaseAdapter.test.js    — 16 tests for Firebase adapter

Imports

// Everything from one place
import { HijriYear, useHijriYear, createFirebaseAdapter, DEFAULT_1447_STARTS, ... } from 'islamic-date-adjustment';

// Or granular imports
import { HijriYear } from 'islamic-date-adjustment/engine';
import { useHijriYear } from 'islamic-date-adjustment/hook';
import { createFirebaseAdapter, serialize, deserialize } from 'islamic-date-adjustment/firebase';
import { DEFAULT_1447_STARTS } from 'islamic-date-adjustment/defaults';
import { ISLAMIC_MONTHS, SPECIAL_EVENTS, formatDate, parseDate } from 'islamic-date-adjustment/calendar';

License

ISC