Skip to content

Core

JalaliTimestamp - Scalar Jalali datetime type.

JalaliTimestamp

A Jalali (Persian/Shamsi) calendar timestamp.

Similar to pandas.Timestamp but for the Jalali calendar system.

Attributes:

Name Type Description
year int

Jalali year.

month int

Jalali month (1-12).

day int

Jalali day (1-31).

hour int

Hour (0-23).

minute int

Minute (0-59).

second int

Second (0-59).

microsecond int

Microsecond (0-999999).

nanosecond int

Nanosecond (0-999).

tzinfo tzinfo | None

Timezone information.

Examples:

>>> ts = JalaliTimestamp(1402, 6, 15)
>>> ts.year
1402
>>> ts.month
6
>>> ts.to_gregorian()
Timestamp('2023-09-06 00:00:00')
Source code in jalali_pandas/core/timestamp.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
class JalaliTimestamp:
    """A Jalali (Persian/Shamsi) calendar timestamp.

    Similar to pandas.Timestamp but for the Jalali calendar system.

    Attributes:
        year: Jalali year.
        month: Jalali month (1-12).
        day: Jalali day (1-31).
        hour: Hour (0-23).
        minute: Minute (0-59).
        second: Second (0-59).
        microsecond: Microsecond (0-999999).
        nanosecond: Nanosecond (0-999).
        tzinfo: Timezone information.

    Examples:
        >>> ts = JalaliTimestamp(1402, 6, 15)
        >>> ts.year
        1402
        >>> ts.month
        6
        >>> ts.to_gregorian()
        Timestamp('2023-09-06 00:00:00')
    """

    __slots__ = (
        "_year",
        "_month",
        "_day",
        "_hour",
        "_minute",
        "_second",
        "_microsecond",
        "_nanosecond",
        "_tzinfo",
        "_gregorian_cache",
    )

    def __init__(
        self,
        year: int,
        month: int = 1,
        day: int = 1,
        hour: int = 0,
        minute: int = 0,
        second: int = 0,
        microsecond: int = 0,
        nanosecond: int = 0,
        tzinfo: dt_tzinfo | None = None,
    ) -> None:
        """Initialize a JalaliTimestamp.

        Args:
            year: Jalali year.
            month: Jalali month (1-12). Defaults to 1.
            day: Jalali day. Defaults to 1.
            hour: Hour (0-23). Defaults to 0.
            minute: Minute (0-59). Defaults to 0.
            second: Second (0-59). Defaults to 0.
            microsecond: Microsecond (0-999999). Defaults to 0.
            nanosecond: Nanosecond (0-999). Defaults to 0.
            tzinfo: Timezone. Defaults to None.

        Raises:
            ValueError: If any component is out of valid range.
        """
        validate_jalali_date(year, month, day)

        if not 0 <= hour <= 23:
            raise ValueError(f"Hour must be 0-23, got {hour}")
        if not 0 <= minute <= 59:
            raise ValueError(f"Minute must be 0-59, got {minute}")
        if not 0 <= second <= 59:
            raise ValueError(f"Second must be 0-59, got {second}")
        if not 0 <= microsecond <= 999999:
            raise ValueError(f"Microsecond must be 0-999999, got {microsecond}")
        if not 0 <= nanosecond <= 999:
            raise ValueError(f"Nanosecond must be 0-999, got {nanosecond}")

        self._year = year
        self._month = month
        self._day = day
        self._hour = hour
        self._minute = minute
        self._second = second
        self._microsecond = microsecond
        self._nanosecond = nanosecond
        self._tzinfo = tzinfo
        self._gregorian_cache: pd.Timestamp | None = None

    # -------------------------------------------------------------------------
    # Properties
    # -------------------------------------------------------------------------

    @property
    def year(self) -> int:
        """Jalali year."""
        return self._year

    @property
    def month(self) -> int:
        """Jalali month (1-12)."""
        return self._month

    @property
    def day(self) -> int:
        """Jalali day (1-31)."""
        return self._day

    @property
    def hour(self) -> int:
        """Hour (0-23)."""
        return self._hour

    @property
    def minute(self) -> int:
        """Minute (0-59)."""
        return self._minute

    @property
    def second(self) -> int:
        """Second (0-59)."""
        return self._second

    @property
    def microsecond(self) -> int:
        """Microsecond (0-999999)."""
        return self._microsecond

    @property
    def nanosecond(self) -> int:
        """Nanosecond (0-999)."""
        return self._nanosecond

    @property
    def tzinfo(self) -> dt_tzinfo | None:
        """Timezone information."""
        return self._tzinfo

    @property
    def tz(self) -> dt_tzinfo | None:
        """Alias for tzinfo."""
        return self._tzinfo

    # -------------------------------------------------------------------------
    # Derived Properties
    # -------------------------------------------------------------------------

    @property
    def quarter(self) -> int:
        """Quarter of the year (1-4)."""
        return quarter_of_month(self._month)

    @property
    def dayofweek(self) -> int:
        """Day of week (0=Saturday, 6=Friday)."""
        return weekday_of_jalali(self._year, self._month, self._day)

    @property
    def weekday(self) -> int:
        """Alias for dayofweek."""
        return self.dayofweek

    @property
    def dayofyear(self) -> int:
        """Day of year (1-366)."""
        from jalali_pandas.core.calendar import day_of_year

        return day_of_year(self._year, self._month, self._day)

    @property
    def week(self) -> int:
        """Week of year (1-53)."""
        return week_of_year(self._year, self._month, self._day)

    @property
    def weekofyear(self) -> int:
        """Alias for week."""
        return self.week

    @property
    def days_in_month(self) -> int:
        """Number of days in the month."""
        return days_in_month(self._year, self._month)

    @property
    def daysinmonth(self) -> int:
        """Alias for days_in_month."""
        return self.days_in_month

    @property
    def is_leap_year(self) -> bool:
        """Whether the year is a leap year."""
        return is_leap_year(self._year)

    @property
    def is_month_start(self) -> bool:
        """Whether the date is the first day of the month."""
        return self._day == 1

    @property
    def is_month_end(self) -> bool:
        """Whether the date is the last day of the month."""
        return self._day == self.days_in_month

    @property
    def is_quarter_start(self) -> bool:
        """Whether the date is the first day of a quarter."""
        return self._month in (1, 4, 7, 10) and self._day == 1

    @property
    def is_quarter_end(self) -> bool:
        """Whether the date is the last day of a quarter."""
        return self._month in (3, 6, 9, 12) and self.is_month_end

    @property
    def is_year_start(self) -> bool:
        """Whether the date is the first day of the year (Nowruz)."""
        return self._month == 1 and self._day == 1

    @property
    def is_year_end(self) -> bool:
        """Whether the date is the last day of the year."""
        return self._month == 12 and self.is_month_end

    # -------------------------------------------------------------------------
    # Conversion Methods
    # -------------------------------------------------------------------------

    def to_gregorian(self) -> pd.Timestamp:
        """Convert to pandas Timestamp (Gregorian).

        Returns:
            Equivalent pandas Timestamp in Gregorian calendar.
        """
        if self._gregorian_cache is not None:
            return self._gregorian_cache

        gregorian = jdatetime.datetime(
            self._year,
            self._month,
            self._day,
            self._hour,
            self._minute,
            self._second,
            self._microsecond,
        ).togregorian()
        ts = pd.Timestamp(
            year=gregorian.year,
            month=gregorian.month,
            day=gregorian.day,
            hour=gregorian.hour,
            minute=gregorian.minute,
            second=gregorian.second,
            microsecond=gregorian.microsecond,
            nanosecond=self._nanosecond,
            tz=self._tzinfo,
        )

        self._gregorian_cache = ts
        return ts

    def to_pydatetime(self) -> datetime:
        """Convert to Python datetime (Gregorian).

        Returns:
            Equivalent Python datetime in Gregorian calendar.
        """
        return self.to_gregorian().to_pydatetime()

    def to_datetime64(self) -> np.datetime64:
        """Convert to numpy datetime64.

        Returns:
            Equivalent numpy datetime64.
        """
        return self.to_gregorian().to_datetime64()

    @classmethod
    def from_gregorian(
        cls,
        ts: pd.Timestamp | datetime | str,
        tz: dt_tzinfo | str | None = None,
    ) -> JalaliTimestamp:
        """Create JalaliTimestamp from Gregorian datetime.

        Args:
            ts: Gregorian timestamp (pandas Timestamp, datetime, or string).
            tz: Timezone to use. Defaults to None.

        Returns:
            Equivalent JalaliTimestamp.
        """
        if (
            isinstance(ts, str)
            or isinstance(ts, datetime)
            and not isinstance(ts, pd.Timestamp)
        ):
            ts = pd.Timestamp(ts)

        if tz is not None:
            ts = ts.tz_localize(tz) if ts.tzinfo is None else ts.tz_convert(tz)

        jalali = jdatetime.datetime.fromgregorian(datetime=ts.to_pydatetime())

        return cls(
            year=jalali.year,
            month=jalali.month,
            day=jalali.day,
            hour=jalali.hour,
            minute=jalali.minute,
            second=jalali.second,
            microsecond=jalali.microsecond,
            nanosecond=ts.nanosecond,
            tzinfo=ts.tzinfo,
        )

    @classmethod
    def now(cls, tz: dt_tzinfo | str | None = None) -> JalaliTimestamp:
        """Get current JalaliTimestamp.

        Args:
            tz: Timezone. Defaults to None (local time).

        Returns:
            Current time as JalaliTimestamp.
        """
        return cls.from_gregorian(pd.Timestamp.now(tz=tz))

    @classmethod
    def today(cls) -> JalaliTimestamp:
        """Get today's date as JalaliTimestamp (midnight).

        Returns:
            Today's date as JalaliTimestamp.
        """
        now = cls.now()
        return cls(now.year, now.month, now.day)

    # -------------------------------------------------------------------------
    # String Methods
    # -------------------------------------------------------------------------

    def strftime(self, fmt: str) -> str:
        """Format timestamp as string.

        Supports standard strftime codes adapted for Jalali calendar.

        Args:
            fmt: Format string.

        Returns:
            Formatted string.
        """
        replacements = {
            "%Y": f"{self._year:04d}",
            "%y": f"{self._year % 100:02d}",
            "%m": f"{self._month:02d}",
            "%d": f"{self._day:02d}",
            "%H": f"{self._hour:02d}",
            "%M": f"{self._minute:02d}",
            "%S": f"{self._second:02d}",
            "%f": f"{self._microsecond:06d}",
            "%j": f"{self.dayofyear:03d}",
            "%W": f"{self.week:02d}",
            "%w": str(self.dayofweek),
        }

        result = fmt
        for code, value in replacements.items():
            result = result.replace(code, value)

        return result

    @classmethod
    def strptime(cls, date_string: str, fmt: str) -> JalaliTimestamp:
        """Parse string to JalaliTimestamp.

        Args:
            date_string: Date string to parse.
            fmt: Format string.

        Returns:
            Parsed JalaliTimestamp.
        """
        # Simple implementation for common formats
        import re

        # Build regex pattern from format string
        pattern = fmt
        groups: dict[str, str] = {}

        replacements = [
            ("%Y", r"(?P<year>\d{4})", "year"),
            ("%y", r"(?P<year2>\d{2})", "year2"),
            ("%m", r"(?P<month>\d{1,2})", "month"),
            ("%d", r"(?P<day>\d{1,2})", "day"),
            ("%H", r"(?P<hour>\d{1,2})", "hour"),
            ("%M", r"(?P<minute>\d{1,2})", "minute"),
            ("%S", r"(?P<second>\d{1,2})", "second"),
        ]

        for code, regex, name in replacements:
            if code in pattern:
                pattern = pattern.replace(code, regex)
                groups[name] = ""

        match = re.match(pattern, date_string)
        if not match:
            raise ValueError(f"Cannot parse '{date_string}' with format '{fmt}'")

        data = match.groupdict()

        year = int(data.get("year", 0) or data.get("year2", 0))
        if "year2" in data and data["year2"]:
            year = 1300 + year if year < 100 else year

        return cls(
            year=year,
            month=int(data.get("month", 1)),
            day=int(data.get("day", 1)),
            hour=int(data.get("hour", 0)),
            minute=int(data.get("minute", 0)),
            second=int(data.get("second", 0)),
        )

    def isoformat(self, sep: str = "T") -> str:
        """Return ISO 8601 formatted string.

        Args:
            sep: Separator between date and time. Defaults to 'T'.

        Returns:
            ISO formatted string.
        """
        date_part = f"{self._year:04d}-{self._month:02d}-{self._day:02d}"
        time_part = f"{self._hour:02d}:{self._minute:02d}:{self._second:02d}"

        if self._microsecond or self._nanosecond:
            time_part += f".{self._microsecond:06d}"

        result = f"{date_part}{sep}{time_part}"

        if self._tzinfo is not None:
            # Get timezone offset
            offset = self._tzinfo.utcoffset(None)
            if offset is not None:
                total_seconds = int(offset.total_seconds())
                hours, remainder = divmod(abs(total_seconds), 3600)
                minutes = remainder // 60
                sign = "+" if total_seconds >= 0 else "-"
                result += f"{sign}{hours:02d}:{minutes:02d}"

        return result

    # -------------------------------------------------------------------------
    # Arithmetic Operations
    # -------------------------------------------------------------------------

    def __add__(self, other: timedelta | pd.Timedelta) -> JalaliTimestamp:
        """Add timedelta to timestamp."""
        if isinstance(other, (timedelta, pd.Timedelta)):
            new_gregorian = self.to_gregorian() + other
            return JalaliTimestamp.from_gregorian(new_gregorian)
        return NotImplemented

    def __radd__(self, other: timedelta | pd.Timedelta) -> JalaliTimestamp:
        """Right add timedelta to timestamp."""
        return self.__add__(other)

    def __sub__(
        self, other: JalaliTimestamp | timedelta | pd.Timedelta
    ) -> JalaliTimestamp | pd.Timedelta:
        """Subtract timedelta or another timestamp."""
        if isinstance(other, JalaliTimestamp):
            return self.to_gregorian() - other.to_gregorian()
        if isinstance(other, (timedelta, pd.Timedelta)):
            new_gregorian = self.to_gregorian() - other
            return JalaliTimestamp.from_gregorian(new_gregorian)
        return NotImplemented

    # -------------------------------------------------------------------------
    # Comparison Operations
    # -------------------------------------------------------------------------

    def __eq__(self, other: object) -> bool:
        """Check equality."""
        if isinstance(other, JalaliTimestamp):
            return (
                self._year == other._year
                and self._month == other._month
                and self._day == other._day
                and self._hour == other._hour
                and self._minute == other._minute
                and self._second == other._second
                and self._microsecond == other._microsecond
                and self._nanosecond == other._nanosecond
            )
        return False

    def __ne__(self, other: object) -> bool:
        """Check inequality."""
        return not self.__eq__(other)

    def __lt__(self, other: JalaliTimestamp) -> bool:
        """Less than comparison."""
        if not isinstance(other, JalaliTimestamp):
            return NotImplemented
        return self.to_gregorian() < other.to_gregorian()

    def __le__(self, other: JalaliTimestamp) -> bool:
        """Less than or equal comparison."""
        if not isinstance(other, JalaliTimestamp):
            return NotImplemented
        return self.to_gregorian() <= other.to_gregorian()

    def __gt__(self, other: JalaliTimestamp) -> bool:
        """Greater than comparison."""
        if not isinstance(other, JalaliTimestamp):
            return NotImplemented
        return self.to_gregorian() > other.to_gregorian()

    def __ge__(self, other: JalaliTimestamp) -> bool:
        """Greater than or equal comparison."""
        if not isinstance(other, JalaliTimestamp):
            return NotImplemented
        return self.to_gregorian() >= other.to_gregorian()

    def __hash__(self) -> int:
        """Hash for use in sets and dicts."""
        return hash(
            (
                self._year,
                self._month,
                self._day,
                self._hour,
                self._minute,
                self._second,
                self._microsecond,
                self._nanosecond,
            )
        )

    # -------------------------------------------------------------------------
    # String Representations
    # -------------------------------------------------------------------------

    def __repr__(self) -> str:
        """Detailed string representation."""
        tz_str = f", tz='{self._tzinfo}'" if self._tzinfo else ""
        return f"JalaliTimestamp('{self.isoformat()}'{tz_str})"

    def __str__(self) -> str:
        """Human-readable string representation."""
        return self.isoformat(sep=" ")

    # -------------------------------------------------------------------------
    # Replacement Methods
    # -------------------------------------------------------------------------

    def replace(
        self,
        year: int | None = None,
        month: int | None = None,
        day: int | None = None,
        hour: int | None = None,
        minute: int | None = None,
        second: int | None = None,
        microsecond: int | None = None,
        nanosecond: int | None = None,
        tzinfo: dt_tzinfo | None | object = ...,
    ) -> JalaliTimestamp:
        """Return timestamp with replaced components.

        Args:
            year: New year (or None to keep current).
            month: New month (or None to keep current).
            day: New day (or None to keep current).
            hour: New hour (or None to keep current).
            minute: New minute (or None to keep current).
            second: New second (or None to keep current).
            microsecond: New microsecond (or None to keep current).
            nanosecond: New nanosecond (or None to keep current).
            tzinfo: New timezone (or ... to keep current).

        Returns:
            New JalaliTimestamp with replaced components.
        """
        return JalaliTimestamp(
            year=year if year is not None else self._year,
            month=month if month is not None else self._month,
            day=day if day is not None else self._day,
            hour=hour if hour is not None else self._hour,
            minute=minute if minute is not None else self._minute,
            second=second if second is not None else self._second,
            microsecond=microsecond if microsecond is not None else self._microsecond,
            nanosecond=nanosecond if nanosecond is not None else self._nanosecond,
            tzinfo=self._tzinfo if tzinfo is ... else cast(Optional[dt_tzinfo], tzinfo),
        )

    def normalize(self) -> JalaliTimestamp:
        """Return timestamp with time set to midnight.

        Returns:
            New JalaliTimestamp at midnight.
        """
        return self.replace(hour=0, minute=0, second=0, microsecond=0, nanosecond=0)

    def date(self) -> JalaliTimestamp:
        """Return date part only (time set to midnight).

        Returns:
            New JalaliTimestamp at midnight.
        """
        return self.normalize()

    def time(self) -> time:
        """Return time part as Python time object.

        Returns:
            Python time object.
        """
        return time(
            hour=self._hour,
            minute=self._minute,
            second=self._second,
            microsecond=self._microsecond,
            tzinfo=self._tzinfo,
        )

    # -------------------------------------------------------------------------
    # Timezone Methods
    # -------------------------------------------------------------------------

    def tz_localize(
        self,
        tz: dt_tzinfo | str | None,
        ambiguous: str = "raise",
        nonexistent: str = "raise",
    ) -> JalaliTimestamp:
        """Localize tz-naive timestamp to a timezone.

        Args:
            tz: Timezone to localize to. Can be a timezone object or string.
            ambiguous: How to handle ambiguous times. Defaults to 'raise'.
            nonexistent: How to handle nonexistent times. Defaults to 'raise'.

        Returns:
            New JalaliTimestamp with timezone.

        Raises:
            TypeError: If timestamp is already tz-aware.
        """
        if self._tzinfo is not None:
            raise TypeError(
                "Cannot localize tz-aware timestamp. "
                "Use tz_convert() to convert between timezones."
            )

        # Convert to Gregorian, localize, then convert back
        gregorian = self.to_gregorian()
        localized = gregorian.tz_localize(
            tz, ambiguous=ambiguous, nonexistent=nonexistent
        )

        # Create new JalaliTimestamp with the timezone
        return JalaliTimestamp(
            year=self._year,
            month=self._month,
            day=self._day,
            hour=self._hour,
            minute=self._minute,
            second=self._second,
            microsecond=self._microsecond,
            nanosecond=self._nanosecond,
            tzinfo=localized.tzinfo,
        )

    def tz_convert(self, tz: dt_tzinfo | str | None) -> JalaliTimestamp:
        """Convert tz-aware timestamp to another timezone.

        Args:
            tz: Target timezone. Can be a timezone object or string.

        Returns:
            New JalaliTimestamp in the target timezone.

        Raises:
            TypeError: If timestamp is tz-naive.
        """
        if self._tzinfo is None:
            raise TypeError(
                "Cannot convert tz-naive timestamp. "
                "Use tz_localize() first to add timezone."
            )

        # Convert to Gregorian, convert timezone, then convert back to Jalali
        gregorian = self.to_gregorian()
        converted = gregorian.tz_convert(tz)

        # Convert the new Gregorian time back to Jalali
        return JalaliTimestamp.from_gregorian(converted)

day property

day: int

Jalali day (1-31).

dayofweek property

dayofweek: int

Day of week (0=Saturday, 6=Friday).

dayofyear property

dayofyear: int

Day of year (1-366).

days_in_month property

days_in_month: int

Number of days in the month.

daysinmonth property

daysinmonth: int

Alias for days_in_month.

hour property

hour: int

Hour (0-23).

is_leap_year property

is_leap_year: bool

Whether the year is a leap year.

is_month_end property

is_month_end: bool

Whether the date is the last day of the month.

is_month_start property

is_month_start: bool

Whether the date is the first day of the month.

is_quarter_end property

is_quarter_end: bool

Whether the date is the last day of a quarter.

is_quarter_start property

is_quarter_start: bool

Whether the date is the first day of a quarter.

is_year_end property

is_year_end: bool

Whether the date is the last day of the year.

is_year_start property

is_year_start: bool

Whether the date is the first day of the year (Nowruz).

microsecond property

microsecond: int

Microsecond (0-999999).

minute property

minute: int

Minute (0-59).

month property

month: int

Jalali month (1-12).

nanosecond property

nanosecond: int

Nanosecond (0-999).

quarter property

quarter: int

Quarter of the year (1-4).

second property

second: int

Second (0-59).

tz property

tz: tzinfo | None

Alias for tzinfo.

tzinfo property

tzinfo: tzinfo | None

Timezone information.

week property

week: int

Week of year (1-53).

weekday property

weekday: int

Alias for dayofweek.

weekofyear property

weekofyear: int

Alias for week.

year property

year: int

Jalali year.

__add__

__add__(other: timedelta | Timedelta) -> JalaliTimestamp

Add timedelta to timestamp.

Source code in jalali_pandas/core/timestamp.py
486
487
488
489
490
491
def __add__(self, other: timedelta | pd.Timedelta) -> JalaliTimestamp:
    """Add timedelta to timestamp."""
    if isinstance(other, (timedelta, pd.Timedelta)):
        new_gregorian = self.to_gregorian() + other
        return JalaliTimestamp.from_gregorian(new_gregorian)
    return NotImplemented

__eq__

__eq__(other: object) -> bool

Check equality.

Source code in jalali_pandas/core/timestamp.py
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def __eq__(self, other: object) -> bool:
    """Check equality."""
    if isinstance(other, JalaliTimestamp):
        return (
            self._year == other._year
            and self._month == other._month
            and self._day == other._day
            and self._hour == other._hour
            and self._minute == other._minute
            and self._second == other._second
            and self._microsecond == other._microsecond
            and self._nanosecond == other._nanosecond
        )
    return False

__ge__

__ge__(other: JalaliTimestamp) -> bool

Greater than or equal comparison.

Source code in jalali_pandas/core/timestamp.py
549
550
551
552
553
def __ge__(self, other: JalaliTimestamp) -> bool:
    """Greater than or equal comparison."""
    if not isinstance(other, JalaliTimestamp):
        return NotImplemented
    return self.to_gregorian() >= other.to_gregorian()

__gt__

__gt__(other: JalaliTimestamp) -> bool

Greater than comparison.

Source code in jalali_pandas/core/timestamp.py
543
544
545
546
547
def __gt__(self, other: JalaliTimestamp) -> bool:
    """Greater than comparison."""
    if not isinstance(other, JalaliTimestamp):
        return NotImplemented
    return self.to_gregorian() > other.to_gregorian()

__hash__

__hash__() -> int

Hash for use in sets and dicts.

Source code in jalali_pandas/core/timestamp.py
555
556
557
558
559
560
561
562
563
564
565
566
567
568
def __hash__(self) -> int:
    """Hash for use in sets and dicts."""
    return hash(
        (
            self._year,
            self._month,
            self._day,
            self._hour,
            self._minute,
            self._second,
            self._microsecond,
            self._nanosecond,
        )
    )

__init__

__init__(year: int, month: int = 1, day: int = 1, hour: int = 0, minute: int = 0, second: int = 0, microsecond: int = 0, nanosecond: int = 0, tzinfo: tzinfo | None = None) -> None

Initialize a JalaliTimestamp.

Parameters:

Name Type Description Default
year int

Jalali year.

required
month int

Jalali month (1-12). Defaults to 1.

1
day int

Jalali day. Defaults to 1.

1
hour int

Hour (0-23). Defaults to 0.

0
minute int

Minute (0-59). Defaults to 0.

0
second int

Second (0-59). Defaults to 0.

0
microsecond int

Microsecond (0-999999). Defaults to 0.

0
nanosecond int

Nanosecond (0-999). Defaults to 0.

0
tzinfo tzinfo | None

Timezone. Defaults to None.

None

Raises:

Type Description
ValueError

If any component is out of valid range.

Source code in jalali_pandas/core/timestamp.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def __init__(
    self,
    year: int,
    month: int = 1,
    day: int = 1,
    hour: int = 0,
    minute: int = 0,
    second: int = 0,
    microsecond: int = 0,
    nanosecond: int = 0,
    tzinfo: dt_tzinfo | None = None,
) -> None:
    """Initialize a JalaliTimestamp.

    Args:
        year: Jalali year.
        month: Jalali month (1-12). Defaults to 1.
        day: Jalali day. Defaults to 1.
        hour: Hour (0-23). Defaults to 0.
        minute: Minute (0-59). Defaults to 0.
        second: Second (0-59). Defaults to 0.
        microsecond: Microsecond (0-999999). Defaults to 0.
        nanosecond: Nanosecond (0-999). Defaults to 0.
        tzinfo: Timezone. Defaults to None.

    Raises:
        ValueError: If any component is out of valid range.
    """
    validate_jalali_date(year, month, day)

    if not 0 <= hour <= 23:
        raise ValueError(f"Hour must be 0-23, got {hour}")
    if not 0 <= minute <= 59:
        raise ValueError(f"Minute must be 0-59, got {minute}")
    if not 0 <= second <= 59:
        raise ValueError(f"Second must be 0-59, got {second}")
    if not 0 <= microsecond <= 999999:
        raise ValueError(f"Microsecond must be 0-999999, got {microsecond}")
    if not 0 <= nanosecond <= 999:
        raise ValueError(f"Nanosecond must be 0-999, got {nanosecond}")

    self._year = year
    self._month = month
    self._day = day
    self._hour = hour
    self._minute = minute
    self._second = second
    self._microsecond = microsecond
    self._nanosecond = nanosecond
    self._tzinfo = tzinfo
    self._gregorian_cache: pd.Timestamp | None = None

__le__

__le__(other: JalaliTimestamp) -> bool

Less than or equal comparison.

Source code in jalali_pandas/core/timestamp.py
537
538
539
540
541
def __le__(self, other: JalaliTimestamp) -> bool:
    """Less than or equal comparison."""
    if not isinstance(other, JalaliTimestamp):
        return NotImplemented
    return self.to_gregorian() <= other.to_gregorian()

__lt__

__lt__(other: JalaliTimestamp) -> bool

Less than comparison.

Source code in jalali_pandas/core/timestamp.py
531
532
533
534
535
def __lt__(self, other: JalaliTimestamp) -> bool:
    """Less than comparison."""
    if not isinstance(other, JalaliTimestamp):
        return NotImplemented
    return self.to_gregorian() < other.to_gregorian()

__ne__

__ne__(other: object) -> bool

Check inequality.

Source code in jalali_pandas/core/timestamp.py
527
528
529
def __ne__(self, other: object) -> bool:
    """Check inequality."""
    return not self.__eq__(other)

__radd__

__radd__(other: timedelta | Timedelta) -> JalaliTimestamp

Right add timedelta to timestamp.

Source code in jalali_pandas/core/timestamp.py
493
494
495
def __radd__(self, other: timedelta | pd.Timedelta) -> JalaliTimestamp:
    """Right add timedelta to timestamp."""
    return self.__add__(other)

__repr__

__repr__() -> str

Detailed string representation.

Source code in jalali_pandas/core/timestamp.py
574
575
576
577
def __repr__(self) -> str:
    """Detailed string representation."""
    tz_str = f", tz='{self._tzinfo}'" if self._tzinfo else ""
    return f"JalaliTimestamp('{self.isoformat()}'{tz_str})"

__str__

__str__() -> str

Human-readable string representation.

Source code in jalali_pandas/core/timestamp.py
579
580
581
def __str__(self) -> str:
    """Human-readable string representation."""
    return self.isoformat(sep=" ")

__sub__

__sub__(other: JalaliTimestamp | timedelta | Timedelta) -> JalaliTimestamp | pd.Timedelta

Subtract timedelta or another timestamp.

Source code in jalali_pandas/core/timestamp.py
497
498
499
500
501
502
503
504
505
506
def __sub__(
    self, other: JalaliTimestamp | timedelta | pd.Timedelta
) -> JalaliTimestamp | pd.Timedelta:
    """Subtract timedelta or another timestamp."""
    if isinstance(other, JalaliTimestamp):
        return self.to_gregorian() - other.to_gregorian()
    if isinstance(other, (timedelta, pd.Timedelta)):
        new_gregorian = self.to_gregorian() - other
        return JalaliTimestamp.from_gregorian(new_gregorian)
    return NotImplemented

date

date() -> JalaliTimestamp

Return date part only (time set to midnight).

Returns:

Type Description
JalaliTimestamp

New JalaliTimestamp at midnight.

Source code in jalali_pandas/core/timestamp.py
635
636
637
638
639
640
641
def date(self) -> JalaliTimestamp:
    """Return date part only (time set to midnight).

    Returns:
        New JalaliTimestamp at midnight.
    """
    return self.normalize()

from_gregorian classmethod

from_gregorian(ts: Timestamp | datetime | str, tz: tzinfo | str | None = None) -> JalaliTimestamp

Create JalaliTimestamp from Gregorian datetime.

Parameters:

Name Type Description Default
ts Timestamp | datetime | str

Gregorian timestamp (pandas Timestamp, datetime, or string).

required
tz tzinfo | str | None

Timezone to use. Defaults to None.

None

Returns:

Type Description
JalaliTimestamp

Equivalent JalaliTimestamp.

Source code in jalali_pandas/core/timestamp.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
@classmethod
def from_gregorian(
    cls,
    ts: pd.Timestamp | datetime | str,
    tz: dt_tzinfo | str | None = None,
) -> JalaliTimestamp:
    """Create JalaliTimestamp from Gregorian datetime.

    Args:
        ts: Gregorian timestamp (pandas Timestamp, datetime, or string).
        tz: Timezone to use. Defaults to None.

    Returns:
        Equivalent JalaliTimestamp.
    """
    if (
        isinstance(ts, str)
        or isinstance(ts, datetime)
        and not isinstance(ts, pd.Timestamp)
    ):
        ts = pd.Timestamp(ts)

    if tz is not None:
        ts = ts.tz_localize(tz) if ts.tzinfo is None else ts.tz_convert(tz)

    jalali = jdatetime.datetime.fromgregorian(datetime=ts.to_pydatetime())

    return cls(
        year=jalali.year,
        month=jalali.month,
        day=jalali.day,
        hour=jalali.hour,
        minute=jalali.minute,
        second=jalali.second,
        microsecond=jalali.microsecond,
        nanosecond=ts.nanosecond,
        tzinfo=ts.tzinfo,
    )

isoformat

isoformat(sep: str = 'T') -> str

Return ISO 8601 formatted string.

Parameters:

Name Type Description Default
sep str

Separator between date and time. Defaults to 'T'.

'T'

Returns:

Type Description
str

ISO formatted string.

Source code in jalali_pandas/core/timestamp.py
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
def isoformat(self, sep: str = "T") -> str:
    """Return ISO 8601 formatted string.

    Args:
        sep: Separator between date and time. Defaults to 'T'.

    Returns:
        ISO formatted string.
    """
    date_part = f"{self._year:04d}-{self._month:02d}-{self._day:02d}"
    time_part = f"{self._hour:02d}:{self._minute:02d}:{self._second:02d}"

    if self._microsecond or self._nanosecond:
        time_part += f".{self._microsecond:06d}"

    result = f"{date_part}{sep}{time_part}"

    if self._tzinfo is not None:
        # Get timezone offset
        offset = self._tzinfo.utcoffset(None)
        if offset is not None:
            total_seconds = int(offset.total_seconds())
            hours, remainder = divmod(abs(total_seconds), 3600)
            minutes = remainder // 60
            sign = "+" if total_seconds >= 0 else "-"
            result += f"{sign}{hours:02d}:{minutes:02d}"

    return result

normalize

normalize() -> JalaliTimestamp

Return timestamp with time set to midnight.

Returns:

Type Description
JalaliTimestamp

New JalaliTimestamp at midnight.

Source code in jalali_pandas/core/timestamp.py
627
628
629
630
631
632
633
def normalize(self) -> JalaliTimestamp:
    """Return timestamp with time set to midnight.

    Returns:
        New JalaliTimestamp at midnight.
    """
    return self.replace(hour=0, minute=0, second=0, microsecond=0, nanosecond=0)

now classmethod

now(tz: tzinfo | str | None = None) -> JalaliTimestamp

Get current JalaliTimestamp.

Parameters:

Name Type Description Default
tz tzinfo | str | None

Timezone. Defaults to None (local time).

None

Returns:

Type Description
JalaliTimestamp

Current time as JalaliTimestamp.

Source code in jalali_pandas/core/timestamp.py
344
345
346
347
348
349
350
351
352
353
354
@classmethod
def now(cls, tz: dt_tzinfo | str | None = None) -> JalaliTimestamp:
    """Get current JalaliTimestamp.

    Args:
        tz: Timezone. Defaults to None (local time).

    Returns:
        Current time as JalaliTimestamp.
    """
    return cls.from_gregorian(pd.Timestamp.now(tz=tz))

replace

replace(year: int | None = None, month: int | None = None, day: int | None = None, hour: int | None = None, minute: int | None = None, second: int | None = None, microsecond: int | None = None, nanosecond: int | None = None, tzinfo: tzinfo | None | object = ...) -> JalaliTimestamp

Return timestamp with replaced components.

Parameters:

Name Type Description Default
year int | None

New year (or None to keep current).

None
month int | None

New month (or None to keep current).

None
day int | None

New day (or None to keep current).

None
hour int | None

New hour (or None to keep current).

None
minute int | None

New minute (or None to keep current).

None
second int | None

New second (or None to keep current).

None
microsecond int | None

New microsecond (or None to keep current).

None
nanosecond int | None

New nanosecond (or None to keep current).

None
tzinfo tzinfo | None | object

New timezone (or ... to keep current).

...

Returns:

Type Description
JalaliTimestamp

New JalaliTimestamp with replaced components.

Source code in jalali_pandas/core/timestamp.py
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
def replace(
    self,
    year: int | None = None,
    month: int | None = None,
    day: int | None = None,
    hour: int | None = None,
    minute: int | None = None,
    second: int | None = None,
    microsecond: int | None = None,
    nanosecond: int | None = None,
    tzinfo: dt_tzinfo | None | object = ...,
) -> JalaliTimestamp:
    """Return timestamp with replaced components.

    Args:
        year: New year (or None to keep current).
        month: New month (or None to keep current).
        day: New day (or None to keep current).
        hour: New hour (or None to keep current).
        minute: New minute (or None to keep current).
        second: New second (or None to keep current).
        microsecond: New microsecond (or None to keep current).
        nanosecond: New nanosecond (or None to keep current).
        tzinfo: New timezone (or ... to keep current).

    Returns:
        New JalaliTimestamp with replaced components.
    """
    return JalaliTimestamp(
        year=year if year is not None else self._year,
        month=month if month is not None else self._month,
        day=day if day is not None else self._day,
        hour=hour if hour is not None else self._hour,
        minute=minute if minute is not None else self._minute,
        second=second if second is not None else self._second,
        microsecond=microsecond if microsecond is not None else self._microsecond,
        nanosecond=nanosecond if nanosecond is not None else self._nanosecond,
        tzinfo=self._tzinfo if tzinfo is ... else cast(Optional[dt_tzinfo], tzinfo),
    )

strftime

strftime(fmt: str) -> str

Format timestamp as string.

Supports standard strftime codes adapted for Jalali calendar.

Parameters:

Name Type Description Default
fmt str

Format string.

required

Returns:

Type Description
str

Formatted string.

Source code in jalali_pandas/core/timestamp.py
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def strftime(self, fmt: str) -> str:
    """Format timestamp as string.

    Supports standard strftime codes adapted for Jalali calendar.

    Args:
        fmt: Format string.

    Returns:
        Formatted string.
    """
    replacements = {
        "%Y": f"{self._year:04d}",
        "%y": f"{self._year % 100:02d}",
        "%m": f"{self._month:02d}",
        "%d": f"{self._day:02d}",
        "%H": f"{self._hour:02d}",
        "%M": f"{self._minute:02d}",
        "%S": f"{self._second:02d}",
        "%f": f"{self._microsecond:06d}",
        "%j": f"{self.dayofyear:03d}",
        "%W": f"{self.week:02d}",
        "%w": str(self.dayofweek),
    }

    result = fmt
    for code, value in replacements.items():
        result = result.replace(code, value)

    return result

strptime classmethod

strptime(date_string: str, fmt: str) -> JalaliTimestamp

Parse string to JalaliTimestamp.

Parameters:

Name Type Description Default
date_string str

Date string to parse.

required
fmt str

Format string.

required

Returns:

Type Description
JalaliTimestamp

Parsed JalaliTimestamp.

Source code in jalali_pandas/core/timestamp.py
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
@classmethod
def strptime(cls, date_string: str, fmt: str) -> JalaliTimestamp:
    """Parse string to JalaliTimestamp.

    Args:
        date_string: Date string to parse.
        fmt: Format string.

    Returns:
        Parsed JalaliTimestamp.
    """
    # Simple implementation for common formats
    import re

    # Build regex pattern from format string
    pattern = fmt
    groups: dict[str, str] = {}

    replacements = [
        ("%Y", r"(?P<year>\d{4})", "year"),
        ("%y", r"(?P<year2>\d{2})", "year2"),
        ("%m", r"(?P<month>\d{1,2})", "month"),
        ("%d", r"(?P<day>\d{1,2})", "day"),
        ("%H", r"(?P<hour>\d{1,2})", "hour"),
        ("%M", r"(?P<minute>\d{1,2})", "minute"),
        ("%S", r"(?P<second>\d{1,2})", "second"),
    ]

    for code, regex, name in replacements:
        if code in pattern:
            pattern = pattern.replace(code, regex)
            groups[name] = ""

    match = re.match(pattern, date_string)
    if not match:
        raise ValueError(f"Cannot parse '{date_string}' with format '{fmt}'")

    data = match.groupdict()

    year = int(data.get("year", 0) or data.get("year2", 0))
    if "year2" in data and data["year2"]:
        year = 1300 + year if year < 100 else year

    return cls(
        year=year,
        month=int(data.get("month", 1)),
        day=int(data.get("day", 1)),
        hour=int(data.get("hour", 0)),
        minute=int(data.get("minute", 0)),
        second=int(data.get("second", 0)),
    )

time

time() -> time

Return time part as Python time object.

Returns:

Type Description
time

Python time object.

Source code in jalali_pandas/core/timestamp.py
643
644
645
646
647
648
649
650
651
652
653
654
655
def time(self) -> time:
    """Return time part as Python time object.

    Returns:
        Python time object.
    """
    return time(
        hour=self._hour,
        minute=self._minute,
        second=self._second,
        microsecond=self._microsecond,
        tzinfo=self._tzinfo,
    )

to_datetime64

to_datetime64() -> np.datetime64

Convert to numpy datetime64.

Returns:

Type Description
datetime64

Equivalent numpy datetime64.

Source code in jalali_pandas/core/timestamp.py
297
298
299
300
301
302
303
def to_datetime64(self) -> np.datetime64:
    """Convert to numpy datetime64.

    Returns:
        Equivalent numpy datetime64.
    """
    return self.to_gregorian().to_datetime64()

to_gregorian

to_gregorian() -> pd.Timestamp

Convert to pandas Timestamp (Gregorian).

Returns:

Type Description
Timestamp

Equivalent pandas Timestamp in Gregorian calendar.

Source code in jalali_pandas/core/timestamp.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
def to_gregorian(self) -> pd.Timestamp:
    """Convert to pandas Timestamp (Gregorian).

    Returns:
        Equivalent pandas Timestamp in Gregorian calendar.
    """
    if self._gregorian_cache is not None:
        return self._gregorian_cache

    gregorian = jdatetime.datetime(
        self._year,
        self._month,
        self._day,
        self._hour,
        self._minute,
        self._second,
        self._microsecond,
    ).togregorian()
    ts = pd.Timestamp(
        year=gregorian.year,
        month=gregorian.month,
        day=gregorian.day,
        hour=gregorian.hour,
        minute=gregorian.minute,
        second=gregorian.second,
        microsecond=gregorian.microsecond,
        nanosecond=self._nanosecond,
        tz=self._tzinfo,
    )

    self._gregorian_cache = ts
    return ts

to_pydatetime

to_pydatetime() -> datetime

Convert to Python datetime (Gregorian).

Returns:

Type Description
datetime

Equivalent Python datetime in Gregorian calendar.

Source code in jalali_pandas/core/timestamp.py
289
290
291
292
293
294
295
def to_pydatetime(self) -> datetime:
    """Convert to Python datetime (Gregorian).

    Returns:
        Equivalent Python datetime in Gregorian calendar.
    """
    return self.to_gregorian().to_pydatetime()

today classmethod

today() -> JalaliTimestamp

Get today's date as JalaliTimestamp (midnight).

Returns:

Type Description
JalaliTimestamp

Today's date as JalaliTimestamp.

Source code in jalali_pandas/core/timestamp.py
356
357
358
359
360
361
362
363
364
@classmethod
def today(cls) -> JalaliTimestamp:
    """Get today's date as JalaliTimestamp (midnight).

    Returns:
        Today's date as JalaliTimestamp.
    """
    now = cls.now()
    return cls(now.year, now.month, now.day)

tz_convert

tz_convert(tz: tzinfo | str | None) -> JalaliTimestamp

Convert tz-aware timestamp to another timezone.

Parameters:

Name Type Description Default
tz tzinfo | str | None

Target timezone. Can be a timezone object or string.

required

Returns:

Type Description
JalaliTimestamp

New JalaliTimestamp in the target timezone.

Raises:

Type Description
TypeError

If timestamp is tz-naive.

Source code in jalali_pandas/core/timestamp.py
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
def tz_convert(self, tz: dt_tzinfo | str | None) -> JalaliTimestamp:
    """Convert tz-aware timestamp to another timezone.

    Args:
        tz: Target timezone. Can be a timezone object or string.

    Returns:
        New JalaliTimestamp in the target timezone.

    Raises:
        TypeError: If timestamp is tz-naive.
    """
    if self._tzinfo is None:
        raise TypeError(
            "Cannot convert tz-naive timestamp. "
            "Use tz_localize() first to add timezone."
        )

    # Convert to Gregorian, convert timezone, then convert back to Jalali
    gregorian = self.to_gregorian()
    converted = gregorian.tz_convert(tz)

    # Convert the new Gregorian time back to Jalali
    return JalaliTimestamp.from_gregorian(converted)

tz_localize

tz_localize(tz: tzinfo | str | None, ambiguous: str = 'raise', nonexistent: str = 'raise') -> JalaliTimestamp

Localize tz-naive timestamp to a timezone.

Parameters:

Name Type Description Default
tz tzinfo | str | None

Timezone to localize to. Can be a timezone object or string.

required
ambiguous str

How to handle ambiguous times. Defaults to 'raise'.

'raise'
nonexistent str

How to handle nonexistent times. Defaults to 'raise'.

'raise'

Returns:

Type Description
JalaliTimestamp

New JalaliTimestamp with timezone.

Raises:

Type Description
TypeError

If timestamp is already tz-aware.

Source code in jalali_pandas/core/timestamp.py
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
def tz_localize(
    self,
    tz: dt_tzinfo | str | None,
    ambiguous: str = "raise",
    nonexistent: str = "raise",
) -> JalaliTimestamp:
    """Localize tz-naive timestamp to a timezone.

    Args:
        tz: Timezone to localize to. Can be a timezone object or string.
        ambiguous: How to handle ambiguous times. Defaults to 'raise'.
        nonexistent: How to handle nonexistent times. Defaults to 'raise'.

    Returns:
        New JalaliTimestamp with timezone.

    Raises:
        TypeError: If timestamp is already tz-aware.
    """
    if self._tzinfo is not None:
        raise TypeError(
            "Cannot localize tz-aware timestamp. "
            "Use tz_convert() to convert between timezones."
        )

    # Convert to Gregorian, localize, then convert back
    gregorian = self.to_gregorian()
    localized = gregorian.tz_localize(
        tz, ambiguous=ambiguous, nonexistent=nonexistent
    )

    # Create new JalaliTimestamp with the timezone
    return JalaliTimestamp(
        year=self._year,
        month=self._month,
        day=self._day,
        hour=self._hour,
        minute=self._minute,
        second=self._second,
        microsecond=self._microsecond,
        nanosecond=self._nanosecond,
        tzinfo=localized.tzinfo,
    )

isna_jalali

isna_jalali(value: object) -> bool

Check if a value is JalaliNaT or pandas NaT.

Parameters:

Name Type Description Default
value object

Value to check.

required

Returns:

Type Description
bool

True if value is NaT.

Source code in jalali_pandas/core/timestamp.py
930
931
932
933
934
935
936
937
938
939
def isna_jalali(value: object) -> bool:
    """Check if a value is JalaliNaT or pandas NaT.

    Args:
        value: Value to check.

    Returns:
        True if value is NaT.
    """
    return isinstance(value, _JalaliNaTType) or value is pd.NaT or pd.isna(value)

JalaliDatetimeDtype - ExtensionDtype for Jalali datetime.

JalaliDatetimeDtype

Bases: ExtensionDtype

ExtensionDtype for Jalali datetime data.

This dtype represents Jalali (Persian/Shamsi) calendar datetimes and integrates with pandas' ExtensionArray system.

Attributes:

Name Type Description
name

String identifier for the dtype.

type

The scalar type for the array.

na_value

The missing value sentinel.

Examples:

>>> dtype = JalaliDatetimeDtype()
>>> dtype.name
'jalali_datetime'
>>> pd.array([...], dtype='jalali_datetime')
Source code in jalali_pandas/core/dtypes.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@register_extension_dtype
class JalaliDatetimeDtype(ExtensionDtype):
    """ExtensionDtype for Jalali datetime data.

    This dtype represents Jalali (Persian/Shamsi) calendar datetimes
    and integrates with pandas' ExtensionArray system.

    Attributes:
        name: String identifier for the dtype.
        type: The scalar type for the array.
        na_value: The missing value sentinel.

    Examples:
        >>> dtype = JalaliDatetimeDtype()
        >>> dtype.name
        'jalali_datetime'
        >>> pd.array([...], dtype='jalali_datetime')
    """

    name = "jalali_datetime"
    type = object  # Will be JalaliTimestamp
    na_value = pd.NaT
    _metadata: tuple[str, ...] = ("tz",)

    def __init__(self, tz: str | None = None) -> None:
        """Initialize JalaliDatetimeDtype.

        Args:
            tz: Timezone string (e.g., 'Asia/Tehran'). Defaults to None.
        """
        self._tz = tz

    @property
    def tz(self) -> str | None:
        """Timezone for this dtype."""
        return self._tz

    @classmethod
    def construct_array_type(cls) -> builtins.type[JalaliDatetimeArray]:
        """Return the array type associated with this dtype.

        Returns:
            JalaliDatetimeArray class.
        """
        from jalali_pandas.core.arrays import JalaliDatetimeArray

        return JalaliDatetimeArray

    @classmethod
    def construct_from_string(cls, string: str) -> JalaliDatetimeDtype:
        """Construct dtype from a string.

        Args:
            string: String representation of the dtype.

        Returns:
            JalaliDatetimeDtype instance.

        Raises:
            TypeError: If string doesn't match expected format.
        """
        if not isinstance(string, str):
            raise TypeError(f"Expected string, got {type(string)}")

        if string == cls.name:
            return cls()

        # Handle timezone specification: jalali_datetime[tz]
        if string.startswith(f"{cls.name}[") and string.endswith("]"):
            tz = string[len(cls.name) + 1 : -1]
            return cls(tz=tz if tz else None)

        raise TypeError(f"Cannot construct {cls.__name__} from '{string}'")

    def __repr__(self) -> str:
        """String representation."""
        if self._tz:
            return f"{self.name}[{self._tz}]"
        return self.name

    def __str__(self) -> str:
        """String representation."""
        return self.__repr__()

    def __hash__(self) -> int:
        """Hash for use in sets and dicts."""
        return hash((self.name, self._tz))

    def __eq__(self, other: object) -> bool:
        """Check equality with another dtype."""
        if isinstance(other, str):
            try:
                other = self.construct_from_string(other)
            except TypeError:
                return False

        if isinstance(other, JalaliDatetimeDtype):
            return self._tz == other._tz

        return False

    @property
    def _is_numeric(self) -> bool:
        """Whether this dtype is numeric."""
        return False

    @property
    def _is_boolean(self) -> bool:
        """Whether this dtype is boolean."""
        return False

tz property

tz: str | None

Timezone for this dtype.

__eq__

__eq__(other: object) -> bool

Check equality with another dtype.

Source code in jalali_pandas/core/dtypes.py
103
104
105
106
107
108
109
110
111
112
113
114
def __eq__(self, other: object) -> bool:
    """Check equality with another dtype."""
    if isinstance(other, str):
        try:
            other = self.construct_from_string(other)
        except TypeError:
            return False

    if isinstance(other, JalaliDatetimeDtype):
        return self._tz == other._tz

    return False

__hash__

__hash__() -> int

Hash for use in sets and dicts.

Source code in jalali_pandas/core/dtypes.py
 99
100
101
def __hash__(self) -> int:
    """Hash for use in sets and dicts."""
    return hash((self.name, self._tz))

__init__

__init__(tz: str | None = None) -> None

Initialize JalaliDatetimeDtype.

Parameters:

Name Type Description Default
tz str | None

Timezone string (e.g., 'Asia/Tehran'). Defaults to None.

None
Source code in jalali_pandas/core/dtypes.py
39
40
41
42
43
44
45
def __init__(self, tz: str | None = None) -> None:
    """Initialize JalaliDatetimeDtype.

    Args:
        tz: Timezone string (e.g., 'Asia/Tehran'). Defaults to None.
    """
    self._tz = tz

__repr__

__repr__() -> str

String representation.

Source code in jalali_pandas/core/dtypes.py
89
90
91
92
93
def __repr__(self) -> str:
    """String representation."""
    if self._tz:
        return f"{self.name}[{self._tz}]"
    return self.name

__str__

__str__() -> str

String representation.

Source code in jalali_pandas/core/dtypes.py
95
96
97
def __str__(self) -> str:
    """String representation."""
    return self.__repr__()

construct_array_type classmethod

construct_array_type() -> builtins.type[JalaliDatetimeArray]

Return the array type associated with this dtype.

Returns:

Type Description
type[JalaliDatetimeArray]

JalaliDatetimeArray class.

Source code in jalali_pandas/core/dtypes.py
52
53
54
55
56
57
58
59
60
61
@classmethod
def construct_array_type(cls) -> builtins.type[JalaliDatetimeArray]:
    """Return the array type associated with this dtype.

    Returns:
        JalaliDatetimeArray class.
    """
    from jalali_pandas.core.arrays import JalaliDatetimeArray

    return JalaliDatetimeArray

construct_from_string classmethod

construct_from_string(string: str) -> JalaliDatetimeDtype

Construct dtype from a string.

Parameters:

Name Type Description Default
string str

String representation of the dtype.

required

Returns:

Type Description
JalaliDatetimeDtype

JalaliDatetimeDtype instance.

Raises:

Type Description
TypeError

If string doesn't match expected format.

Source code in jalali_pandas/core/dtypes.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@classmethod
def construct_from_string(cls, string: str) -> JalaliDatetimeDtype:
    """Construct dtype from a string.

    Args:
        string: String representation of the dtype.

    Returns:
        JalaliDatetimeDtype instance.

    Raises:
        TypeError: If string doesn't match expected format.
    """
    if not isinstance(string, str):
        raise TypeError(f"Expected string, got {type(string)}")

    if string == cls.name:
        return cls()

    # Handle timezone specification: jalali_datetime[tz]
    if string.startswith(f"{cls.name}[") and string.endswith("]"):
        tz = string[len(cls.name) + 1 : -1]
        return cls(tz=tz if tz else None)

    raise TypeError(f"Cannot construct {cls.__name__} from '{string}'")

JalaliDatetimeArray - ExtensionArray for Jalali datetime.

JalaliDatetimeArray

Bases: ExtensionArray

ExtensionArray for Jalali datetime data.

This array stores Jalali timestamps and integrates with pandas' ExtensionArray system for seamless DataFrame/Series operations.

Attributes:

Name Type Description
dtype JalaliDatetimeDtype

The JalaliDatetimeDtype for this array.

Examples:

>>> arr = JalaliDatetimeArray._from_sequence([
...     JalaliTimestamp(1402, 1, 1),
...     JalaliTimestamp(1402, 1, 2),
... ])
>>> arr[0]
JalaliTimestamp('1402-01-01T00:00:00')
Source code in jalali_pandas/core/arrays.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
class JalaliDatetimeArray(ExtensionArray):
    """ExtensionArray for Jalali datetime data.

    This array stores Jalali timestamps and integrates with pandas'
    ExtensionArray system for seamless DataFrame/Series operations.

    Attributes:
        dtype: The JalaliDatetimeDtype for this array.

    Examples:
        >>> arr = JalaliDatetimeArray._from_sequence([
        ...     JalaliTimestamp(1402, 1, 1),
        ...     JalaliTimestamp(1402, 1, 2),
        ... ])
        >>> arr[0]
        JalaliTimestamp('1402-01-01T00:00:00')
    """

    _dtype: JalaliDatetimeDtype
    _data: npt.NDArray[np.object_]  # Object array of JalaliTimestamp or NaT

    def __init__(
        self,
        data: np.ndarray,
        dtype: JalaliDatetimeDtype | None = None,
        copy: bool = False,
    ) -> None:
        """Initialize JalaliDatetimeArray.

        Args:
            data: Array of JalaliTimestamp objects.
            dtype: JalaliDatetimeDtype instance. Defaults to None.
            copy: Whether to copy the data. Defaults to False.
        """
        if copy:
            data = data.copy()

        self._data = data
        self._dtype = dtype if dtype is not None else JalaliDatetimeDtype()

    @property
    def dtype(self) -> JalaliDatetimeDtype:
        """Return the dtype for this array."""
        return self._dtype

    @property
    def nbytes(self) -> int:
        """Return the number of bytes in the array."""
        return int(self._data.nbytes)

    def __len__(self) -> int:
        """Return the length of the array."""
        return len(self._data)

    @overload
    def __getitem__(self, key: int) -> JalaliTimestamp | NaTType: ...

    @overload
    def __getitem__(
        self, key: slice | Sequence[int] | npt.NDArray[np.bool_]
    ) -> JalaliDatetimeArray: ...

    def __getitem__(self, key: Any) -> Any:
        """Get item(s) from the array."""
        result = self._data[key]

        if isinstance(result, np.ndarray):
            return type(self)(cast(npt.NDArray[np.object_], result), dtype=self._dtype)

        return result

    def __setitem__(self, key: Any, value: Any) -> None:
        """Set item(s) in the array."""
        if isinstance(value, JalaliTimestamp):
            self._data[key] = value
        elif isinstance(value, JalaliDatetimeArray):
            self._data[key] = value._data
        elif isinstance(value, (list, np.ndarray)):
            values = self._from_sequence(cast(Sequence[Any], value), dtype=self._dtype)
            self._data[key] = values._data
        elif pd.isna(value):
            self._data[key] = pd.NaT
        else:
            raise TypeError(f"Cannot set {type(value)} in JalaliDatetimeArray")

    def __iter__(self) -> Iterator[Any]:
        """Iterate over the array."""
        return iter(self._data)

    def __eq__(self, other: Any) -> Any:
        """Element-wise equality comparison."""
        if isinstance(other, JalaliDatetimeArray):
            return cast(npt.NDArray[np.bool_], self._data == other._data)
        if isinstance(other, JalaliTimestamp):
            return cast(
                npt.NDArray[np.bool_],
                np.array([x == other for x in self._data], dtype=bool),
            )
        return NotImplemented

    def __ne__(self, other: Any) -> Any:
        """Element-wise inequality comparison."""
        result = self.__eq__(other)
        if result is NotImplemented:
            return result
        return ~result

    @classmethod
    def _from_sequence(
        cls,
        scalars: Sequence[Any],
        *,
        dtype: JalaliDatetimeDtype | None = None,
        copy: bool = False,
    ) -> JalaliDatetimeArray:
        """Create array from sequence of scalars.

        Args:
            scalars: Sequence of JalaliTimestamp, strings, or NaT values.
            dtype: JalaliDatetimeDtype instance.
            copy: Whether to copy the data.

        Returns:
            JalaliDatetimeArray instance.
        """
        result: list[JalaliTimestamp | NaTType] = []
        for scalar in scalars:
            if isinstance(scalar, JalaliTimestamp):
                result.append(scalar)
            elif pd.isna(scalar):
                result.append(pd.NaT)
            elif isinstance(scalar, str):
                # Try to parse string
                try:
                    result.append(JalaliTimestamp.strptime(scalar, "%Y-%m-%d"))
                except ValueError:
                    try:
                        result.append(
                            JalaliTimestamp.strptime(scalar, "%Y-%m-%d %H:%M:%S")
                        )
                    except ValueError:
                        result.append(pd.NaT)
            elif isinstance(scalar, pd.Timestamp):
                result.append(JalaliTimestamp.from_gregorian(scalar))
            else:
                result.append(pd.NaT)

        data = cast(npt.NDArray[np.object_], np.array(result, dtype=object))
        return cls(data, dtype=dtype, copy=copy)

    @classmethod
    def _from_sequence_of_strings(
        cls,
        strings: Sequence[str],
        *,
        dtype: JalaliDatetimeDtype | None = None,
        copy: bool = False,
    ) -> JalaliDatetimeArray:
        """Create array from sequence of strings.

        Args:
            strings: Sequence of date strings.
            dtype: JalaliDatetimeDtype instance.
            copy: Whether to copy the data.

        Returns:
            JalaliDatetimeArray instance.
        """
        return cls._from_sequence(strings, dtype=dtype, copy=copy)

    @classmethod
    def _from_factorized(
        cls, values: npt.NDArray[np.object_], original: JalaliDatetimeArray
    ) -> JalaliDatetimeArray:
        """Reconstruct array from factorized values.

        Args:
            values: Unique values array.
            original: Original array for dtype.

        Returns:
            JalaliDatetimeArray instance.
        """
        return cls(values, dtype=original.dtype)

    def _values_for_factorize(self) -> tuple[npt.NDArray[np.object_], NaTType]:
        """Return values and NA value for factorization."""
        return self._data, pd.NaT

    def isna(self) -> npt.NDArray[np.bool_]:
        """Return boolean array indicating NA values."""
        return cast(
            npt.NDArray[np.bool_],
            np.array([pd.isna(x) for x in self._data], dtype=bool),
        )

    def take(
        self,
        indices: Sequence[int],
        *,
        allow_fill: bool = False,
        fill_value: Any = None,
    ) -> JalaliDatetimeArray:
        """Take elements from the array.

        Args:
            indices: Indices to take.
            allow_fill: Whether to allow fill values for -1 indices.
            fill_value: Value to use for -1 indices.

        Returns:
            JalaliDatetimeArray with taken elements.
        """
        if allow_fill:
            if fill_value is None:
                fill_value = pd.NaT

            result: list[object] = []
            for i in indices:
                if i == -1:
                    result.append(fill_value)
                else:
                    result.append(self._data[i])
            data = cast(npt.NDArray[np.object_], np.array(result, dtype=object))
        else:
            data = cast(npt.NDArray[np.object_], self._data[list(indices)])

        return type(self)(data, dtype=self._dtype)

    def copy(self) -> JalaliDatetimeArray:
        """Return a copy of the array."""
        return type(self)(self._data.copy(), dtype=self._dtype)

    @classmethod
    def _concat_same_type(
        cls, to_concat: Sequence[JalaliDatetimeArray]
    ) -> JalaliDatetimeArray:
        """Concatenate arrays of the same type.

        Args:
            to_concat: Sequence of arrays to concatenate.

        Returns:
            Concatenated JalaliDatetimeArray.
        """
        data = cast(
            npt.NDArray[np.object_], np.concatenate([arr._data for arr in to_concat])
        )
        return cls(data, dtype=to_concat[0].dtype)

    def __repr__(self) -> str:
        """String representation."""
        data_repr = ", ".join(repr(x) for x in self._data[:5])
        if len(self._data) > 5:
            data_repr += ", ..."
        return f"JalaliDatetimeArray([{data_repr}], dtype={self._dtype})"

    # -------------------------------------------------------------------------
    # Jalali-specific methods
    # -------------------------------------------------------------------------

    @property
    def year(self) -> npt.NDArray[np.float64]:
        """Return array of years."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.year if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    @property
    def month(self) -> npt.NDArray[np.float64]:
        """Return array of months."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.month if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    @property
    def day(self) -> npt.NDArray[np.float64]:
        """Return array of days."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.day if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    @property
    def hour(self) -> npt.NDArray[np.float64]:
        """Return array of hours."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.hour if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    @property
    def minute(self) -> npt.NDArray[np.float64]:
        """Return array of minutes."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.minute if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    @property
    def second(self) -> npt.NDArray[np.float64]:
        """Return array of seconds."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.second if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    @property
    def quarter(self) -> npt.NDArray[np.float64]:
        """Return array of quarters."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.quarter if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    @property
    def dayofweek(self) -> npt.NDArray[np.float64]:
        """Return array of day of week values."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.dayofweek if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    @property
    def dayofyear(self) -> npt.NDArray[np.float64]:
        """Return array of day of year values."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.dayofyear if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    @property
    def week(self) -> npt.NDArray[np.float64]:
        """Return array of week numbers."""
        return cast(
            npt.NDArray[np.float64],
            np.array(
                [x.week if not pd.isna(x) else np.nan for x in self._data],
                dtype=float,
            ),
        )

    def to_gregorian(self) -> pd.DatetimeIndex:
        """Convert to pandas DatetimeIndex (Gregorian).

        Returns:
            DatetimeIndex with Gregorian timestamps.
        """
        timestamps = [
            x.to_gregorian() if not pd.isna(x) else pd.NaT for x in self._data
        ]
        return pd.DatetimeIndex(timestamps)

    def strftime(self, fmt: str) -> npt.NDArray[np.object_]:
        """Format timestamps as strings.

        Args:
            fmt: Format string.

        Returns:
            Array of formatted strings.
        """
        return cast(
            npt.NDArray[np.object_],
            np.array(
                [x.strftime(fmt) if not pd.isna(x) else None for x in self._data],
                dtype=object,
            ),
        )

day property

day: NDArray[float64]

Return array of days.

dayofweek property

dayofweek: NDArray[float64]

Return array of day of week values.

dayofyear property

dayofyear: NDArray[float64]

Return array of day of year values.

dtype property

dtype: JalaliDatetimeDtype

Return the dtype for this array.

hour property

hour: NDArray[float64]

Return array of hours.

minute property

minute: NDArray[float64]

Return array of minutes.

month property

month: NDArray[float64]

Return array of months.

nbytes property

nbytes: int

Return the number of bytes in the array.

quarter property

quarter: NDArray[float64]

Return array of quarters.

second property

second: NDArray[float64]

Return array of seconds.

week property

week: NDArray[float64]

Return array of week numbers.

year property

year: NDArray[float64]

Return array of years.

__eq__

__eq__(other: Any) -> Any

Element-wise equality comparison.

Source code in jalali_pandas/core/arrays.py
107
108
109
110
111
112
113
114
115
116
def __eq__(self, other: Any) -> Any:
    """Element-wise equality comparison."""
    if isinstance(other, JalaliDatetimeArray):
        return cast(npt.NDArray[np.bool_], self._data == other._data)
    if isinstance(other, JalaliTimestamp):
        return cast(
            npt.NDArray[np.bool_],
            np.array([x == other for x in self._data], dtype=bool),
        )
    return NotImplemented

__getitem__

__getitem__(key: int) -> JalaliTimestamp | NaTType
__getitem__(key: slice | Sequence[int] | NDArray[bool_]) -> JalaliDatetimeArray
__getitem__(key: Any) -> Any

Get item(s) from the array.

Source code in jalali_pandas/core/arrays.py
80
81
82
83
84
85
86
87
def __getitem__(self, key: Any) -> Any:
    """Get item(s) from the array."""
    result = self._data[key]

    if isinstance(result, np.ndarray):
        return type(self)(cast(npt.NDArray[np.object_], result), dtype=self._dtype)

    return result

__init__

__init__(data: ndarray, dtype: JalaliDatetimeDtype | None = None, copy: bool = False) -> None

Initialize JalaliDatetimeArray.

Parameters:

Name Type Description Default
data ndarray

Array of JalaliTimestamp objects.

required
dtype JalaliDatetimeDtype | None

JalaliDatetimeDtype instance. Defaults to None.

None
copy bool

Whether to copy the data. Defaults to False.

False
Source code in jalali_pandas/core/arrays.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def __init__(
    self,
    data: np.ndarray,
    dtype: JalaliDatetimeDtype | None = None,
    copy: bool = False,
) -> None:
    """Initialize JalaliDatetimeArray.

    Args:
        data: Array of JalaliTimestamp objects.
        dtype: JalaliDatetimeDtype instance. Defaults to None.
        copy: Whether to copy the data. Defaults to False.
    """
    if copy:
        data = data.copy()

    self._data = data
    self._dtype = dtype if dtype is not None else JalaliDatetimeDtype()

__iter__

__iter__() -> Iterator[Any]

Iterate over the array.

Source code in jalali_pandas/core/arrays.py
103
104
105
def __iter__(self) -> Iterator[Any]:
    """Iterate over the array."""
    return iter(self._data)

__len__

__len__() -> int

Return the length of the array.

Source code in jalali_pandas/core/arrays.py
68
69
70
def __len__(self) -> int:
    """Return the length of the array."""
    return len(self._data)

__ne__

__ne__(other: Any) -> Any

Element-wise inequality comparison.

Source code in jalali_pandas/core/arrays.py
118
119
120
121
122
123
def __ne__(self, other: Any) -> Any:
    """Element-wise inequality comparison."""
    result = self.__eq__(other)
    if result is NotImplemented:
        return result
    return ~result

__repr__

__repr__() -> str

String representation.

Source code in jalali_pandas/core/arrays.py
268
269
270
271
272
273
def __repr__(self) -> str:
    """String representation."""
    data_repr = ", ".join(repr(x) for x in self._data[:5])
    if len(self._data) > 5:
        data_repr += ", ..."
    return f"JalaliDatetimeArray([{data_repr}], dtype={self._dtype})"

__setitem__

__setitem__(key: Any, value: Any) -> None

Set item(s) in the array.

Source code in jalali_pandas/core/arrays.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def __setitem__(self, key: Any, value: Any) -> None:
    """Set item(s) in the array."""
    if isinstance(value, JalaliTimestamp):
        self._data[key] = value
    elif isinstance(value, JalaliDatetimeArray):
        self._data[key] = value._data
    elif isinstance(value, (list, np.ndarray)):
        values = self._from_sequence(cast(Sequence[Any], value), dtype=self._dtype)
        self._data[key] = values._data
    elif pd.isna(value):
        self._data[key] = pd.NaT
    else:
        raise TypeError(f"Cannot set {type(value)} in JalaliDatetimeArray")

copy

copy() -> JalaliDatetimeArray

Return a copy of the array.

Source code in jalali_pandas/core/arrays.py
247
248
249
def copy(self) -> JalaliDatetimeArray:
    """Return a copy of the array."""
    return type(self)(self._data.copy(), dtype=self._dtype)

isna

isna() -> npt.NDArray[np.bool_]

Return boolean array indicating NA values.

Source code in jalali_pandas/core/arrays.py
207
208
209
210
211
212
def isna(self) -> npt.NDArray[np.bool_]:
    """Return boolean array indicating NA values."""
    return cast(
        npt.NDArray[np.bool_],
        np.array([pd.isna(x) for x in self._data], dtype=bool),
    )

strftime

strftime(fmt: str) -> npt.NDArray[np.object_]

Format timestamps as strings.

Parameters:

Name Type Description Default
fmt str

Format string.

required

Returns:

Type Description
NDArray[object_]

Array of formatted strings.

Source code in jalali_pandas/core/arrays.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def strftime(self, fmt: str) -> npt.NDArray[np.object_]:
    """Format timestamps as strings.

    Args:
        fmt: Format string.

    Returns:
        Array of formatted strings.
    """
    return cast(
        npt.NDArray[np.object_],
        np.array(
            [x.strftime(fmt) if not pd.isna(x) else None for x in self._data],
            dtype=object,
        ),
    )

take

take(indices: Sequence[int], *, allow_fill: bool = False, fill_value: Any = None) -> JalaliDatetimeArray

Take elements from the array.

Parameters:

Name Type Description Default
indices Sequence[int]

Indices to take.

required
allow_fill bool

Whether to allow fill values for -1 indices.

False
fill_value Any

Value to use for -1 indices.

None

Returns:

Type Description
JalaliDatetimeArray

JalaliDatetimeArray with taken elements.

Source code in jalali_pandas/core/arrays.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def take(
    self,
    indices: Sequence[int],
    *,
    allow_fill: bool = False,
    fill_value: Any = None,
) -> JalaliDatetimeArray:
    """Take elements from the array.

    Args:
        indices: Indices to take.
        allow_fill: Whether to allow fill values for -1 indices.
        fill_value: Value to use for -1 indices.

    Returns:
        JalaliDatetimeArray with taken elements.
    """
    if allow_fill:
        if fill_value is None:
            fill_value = pd.NaT

        result: list[object] = []
        for i in indices:
            if i == -1:
                result.append(fill_value)
            else:
                result.append(self._data[i])
        data = cast(npt.NDArray[np.object_], np.array(result, dtype=object))
    else:
        data = cast(npt.NDArray[np.object_], self._data[list(indices)])

    return type(self)(data, dtype=self._dtype)

to_gregorian

to_gregorian() -> pd.DatetimeIndex

Convert to pandas DatetimeIndex (Gregorian).

Returns:

Type Description
DatetimeIndex

DatetimeIndex with Gregorian timestamps.

Source code in jalali_pandas/core/arrays.py
389
390
391
392
393
394
395
396
397
398
def to_gregorian(self) -> pd.DatetimeIndex:
    """Convert to pandas DatetimeIndex (Gregorian).

    Returns:
        DatetimeIndex with Gregorian timestamps.
    """
    timestamps = [
        x.to_gregorian() if not pd.isna(x) else pd.NaT for x in self._data
    ]
    return pd.DatetimeIndex(timestamps)

JalaliDatetimeIndex - Index for Jalali datetime data.

JalaliDatetimeIndex

Bases: Index

Index for Jalali datetime data.

A JalaliDatetimeIndex is an immutable array of Jalali timestamps, suitable for use as an index in pandas DataFrames and Series.

Examples:

>>> idx = JalaliDatetimeIndex(["1402-01-01", "1402-01-02", "1402-01-03"])
>>> idx
JalaliDatetimeIndex(['1402-01-01', '1402-01-02', '1402-01-03'],
                   dtype='jalali_datetime64[ns]', freq=None)
>>> idx.year
Index([1402, 1402, 1402], dtype='float64')
Source code in jalali_pandas/core/indexes.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
class JalaliDatetimeIndex(Index):
    """Index for Jalali datetime data.

    A JalaliDatetimeIndex is an immutable array of Jalali timestamps,
    suitable for use as an index in pandas DataFrames and Series.

    Examples:
        >>> idx = JalaliDatetimeIndex(["1402-01-01", "1402-01-02", "1402-01-03"])
        >>> idx
        JalaliDatetimeIndex(['1402-01-01', '1402-01-02', '1402-01-03'],
                           dtype='jalali_datetime64[ns]', freq=None)

        >>> idx.year
        Index([1402, 1402, 1402], dtype='float64')
    """

    _typ = "jalalidatetimeindex"
    _data: JalaliDatetimeArray
    _freq: JalaliOffset | str | None
    _name: Hashable
    _cache: dict[str, Any]

    # -------------------------------------------------------------------------
    # Construction
    # -------------------------------------------------------------------------

    def __new__(
        cls,
        data: Sequence[Any] | JalaliDatetimeArray | JalaliDatetimeIndex | None = None,
        freq: str | JalaliOffset | None = None,
        tz: tzinfo | str | None = None,
        dtype: JalaliDatetimeDtype | None = None,
        copy: bool = False,
        name: Hashable = None,
    ) -> JalaliDatetimeIndex:
        """Create a new JalaliDatetimeIndex."""
        if dtype is None:
            # Convert tzinfo to string if needed
            tz_str = str(tz) if tz is not None and not isinstance(tz, str) else tz
            dtype = JalaliDatetimeDtype(tz=tz_str)

        if data is None:
            data = []

        if isinstance(data, JalaliDatetimeIndex):
            data = data._data.copy() if copy else data._data
        elif isinstance(data, JalaliDatetimeArray):
            if copy:
                data = data.copy()
        else:
            data = JalaliDatetimeArray._from_sequence(
                list(data), dtype=dtype, copy=copy
            )

        # Create the index
        result = object.__new__(cls)
        result._data = data
        result._name = name
        result._freq = freq
        result._cache = {}

        return result

    @classmethod
    def _simple_new(
        cls,
        values: JalaliDatetimeArray,
        name: Hashable = None,
        freq: str | JalaliOffset | None = None,
        refs: Any = None,  # noqa: ARG003 - pandas compatibility
    ) -> JalaliDatetimeIndex:
        """Create a new JalaliDatetimeIndex from values without validation."""
        result = object.__new__(cls)
        result._data = values
        result._name = name
        result._freq = freq
        result._cache = {}
        return result

    # -------------------------------------------------------------------------
    # Index Properties
    # -------------------------------------------------------------------------

    @property
    def dtype(self) -> JalaliDatetimeDtype:
        """Return the dtype of the index."""
        return self._data.dtype

    @property
    def freq(self) -> JalaliOffset | str | None:
        """Return the frequency of the index."""
        return self._freq

    @freq.setter
    def freq(self, value: JalaliOffset | str | None) -> None:
        """Set the frequency of the index."""
        self._freq = value

    @property
    def freqstr(self) -> str | None:
        """Return the frequency as a string."""
        if self._freq is None:
            return None
        if isinstance(self._freq, str):
            return self._freq
        return str(self._freq)

    @property
    def inferred_freq(self) -> str | None:
        """Try to infer the frequency from the data."""
        if len(self) < 3:
            return None

        # Get differences between consecutive timestamps
        diffs = []
        for i in range(1, min(len(self), 10)):
            diff = self[i].to_gregorian() - self[i - 1].to_gregorian()
            diffs.append(diff)

        # Check if all differences are the same
        if len(set(diffs)) == 1:
            diff = diffs[0]
            if diff == pd.Timedelta(days=1):
                return "D"
            elif diff == pd.Timedelta(hours=1):
                return "h"
            elif diff == pd.Timedelta(minutes=1):
                return "min"
            elif diff == pd.Timedelta(seconds=1):
                return "s"

        return None

    def __len__(self) -> int:
        """Return the length of the index."""
        return len(self._data)

    def __getitem__(self, key: Any) -> Any:
        """Get item(s) from the index."""
        result = self._data[key]

        if isinstance(result, JalaliDatetimeArray):
            return type(self)._simple_new(result, name=self._name, freq=None)

        return result

    def __iter__(self) -> Any:
        """Iterate over the index."""
        return iter(self._data)

    def __contains__(self, key: Any) -> bool:
        """Check if key is in the index."""
        try:
            self.get_loc(key)
            return True
        except KeyError:
            return False

    def __repr__(self) -> str:
        """String representation."""
        data_repr = ", ".join(f"'{x.strftime('%Y-%m-%d')}'" for x in self._data[:5])
        if len(self._data) > 5:
            data_repr += ", ..."
        freq_str = f", freq='{self.freqstr}'" if self.freqstr else ", freq=None"
        return (
            f"JalaliDatetimeIndex([{data_repr}], dtype='{self.dtype.name}'{freq_str})"
        )

    # -------------------------------------------------------------------------
    # Jalali Properties (vectorized)
    # -------------------------------------------------------------------------

    @property
    def year(self) -> Any:
        """Return array of years."""
        return pd.Index(self._data.year, name=self._name)

    @property
    def month(self) -> Any:
        """Return array of months."""
        return pd.Index(self._data.month, name=self._name)

    @property
    def day(self) -> Any:
        """Return array of days."""
        return pd.Index(self._data.day, name=self._name)

    @property
    def hour(self) -> Any:
        """Return array of hours."""
        return pd.Index(self._data.hour, name=self._name)

    @property
    def minute(self) -> Any:
        """Return array of minutes."""
        return pd.Index(self._data.minute, name=self._name)

    @property
    def second(self) -> Any:
        """Return array of seconds."""
        return pd.Index(self._data.second, name=self._name)

    @property
    def quarter(self) -> Any:
        """Return array of quarters."""
        return pd.Index(self._data.quarter, name=self._name)

    @property
    def dayofweek(self) -> Any:
        """Return array of day of week values."""
        return pd.Index(self._data.dayofweek, name=self._name)

    @property
    def weekday(self) -> Any:
        """Alias for dayofweek."""
        return self.dayofweek

    @property
    def dayofyear(self) -> Any:
        """Return array of day of year values."""
        return pd.Index(self._data.dayofyear, name=self._name)

    @property
    def week(self) -> Any:
        """Return array of week numbers."""
        return pd.Index(self._data.week, name=self._name)

    @property
    def weekofyear(self) -> Any:
        """Alias for week."""
        return self.week

    # -------------------------------------------------------------------------
    # Conversion Methods
    # -------------------------------------------------------------------------

    def to_gregorian(self) -> pd.DatetimeIndex:
        """Convert to pandas DatetimeIndex (Gregorian).

        Returns:
            DatetimeIndex with Gregorian timestamps.
        """
        return self._data.to_gregorian()

    def strftime(self, fmt: str) -> Any:
        """Format timestamps as strings.

        Args:
            fmt: Format string.

        Returns:
            Index of formatted strings.
        """
        return pd.Index(self._data.strftime(fmt), name=self._name)

    # -------------------------------------------------------------------------
    # Indexing Methods
    # -------------------------------------------------------------------------

    def get_loc(self, key: Any) -> int | slice | npt.NDArray[np.bool_]:
        """Get integer location for requested label.

        Supports:
        - JalaliTimestamp: exact match
        - String: "1402-06-15" for exact date
        - Partial string: "1402-06" for month, "1402" for year

        Args:
            key: Label to look up.

        Returns:
            Integer location, slice, or boolean mask.

        Raises:
            KeyError: If key is not found.
        """
        if isinstance(key, JalaliTimestamp):
            # Exact match
            for i, val in enumerate(self._data):
                if val == key:
                    return i
            raise KeyError(key)

        if isinstance(key, str):
            return self._get_string_loc(key)

        raise KeyError(key)

    def _get_string_loc(self, key: str) -> int | slice | npt.NDArray[np.bool_]:
        """Get location for string key with partial string indexing support."""
        # Try exact date match first
        try:
            ts = JalaliTimestamp.strptime(key, "%Y-%m-%d")
            for i, val in enumerate(self._data):
                if not pd.isna(val) and val == ts:
                    return i
        except ValueError:
            pass

        # Try partial string indexing
        # Year only: "1402"
        year_match = re.match(r"^(\d{4})$", key)
        if year_match:
            year = int(year_match.group(1))
            mask = cast(
                npt.NDArray[np.bool_],
                np.array(
                    [not pd.isna(x) and x.year == year for x in self._data],
                    dtype=bool,
                ),
            )
            if mask.any():
                return mask
            raise KeyError(key)

        # Year-month: "1402-06"
        ym_match = re.match(r"^(\d{4})-(\d{1,2})$", key)
        if ym_match:
            year = int(ym_match.group(1))
            month = int(ym_match.group(2))
            mask = cast(
                npt.NDArray[np.bool_],
                np.array(
                    [
                        not pd.isna(x) and x.year == year and x.month == month
                        for x in self._data
                    ],
                    dtype=bool,
                ),
            )
            if mask.any():
                return mask
            raise KeyError(key)

        raise KeyError(key)

    def slice_locs(
        self,
        start: str | JalaliTimestamp | None = None,
        end: str | JalaliTimestamp | None = None,
        step: int | None = None,  # noqa: ARG002
    ) -> tuple[int, int]:
        """Compute slice locations for input labels.

        Args:
            start: Start label.
            end: End label.
            step: Step size.

        Returns:
            Tuple of (start, end) integer locations.
        """
        start_loc = 0
        end_loc = len(self)

        if start is not None:
            start_ts = self._parse_to_timestamp(start)
            for i, val in enumerate(self._data):
                if not pd.isna(val) and val >= start_ts:
                    start_loc = i
                    break

        if end is not None:
            end_ts = self._parse_to_timestamp(end)
            for i in range(len(self._data) - 1, -1, -1):
                val = self._data[i]
                if not pd.isna(val) and val <= end_ts:
                    end_loc = i + 1
                    break

        return start_loc, end_loc

    def _parse_to_timestamp(self, key: str | JalaliTimestamp) -> JalaliTimestamp:
        """Parse a key to JalaliTimestamp."""
        if isinstance(key, JalaliTimestamp):
            return key
        if isinstance(key, str):
            try:
                return JalaliTimestamp.strptime(key, "%Y-%m-%d")
            except ValueError:
                try:
                    return JalaliTimestamp.strptime(key, "%Y-%m-%d %H:%M:%S")
                except ValueError:
                    pass
            # Try partial string - use start of period
            year_match = re.match(r"^(\d{4})$", key)
            if year_match:
                return JalaliTimestamp(int(year_match.group(1)), 1, 1)
            ym_match = re.match(r"^(\d{4})-(\d{1,2})$", key)
            if ym_match:
                return JalaliTimestamp(
                    int(ym_match.group(1)), int(ym_match.group(2)), 1
                )
        raise ValueError(f"Cannot parse '{key}' to JalaliTimestamp")

    # -------------------------------------------------------------------------
    # Shift and Snap Methods
    # -------------------------------------------------------------------------

    def shift(
        self,
        periods: int = 1,
        freq: str | JalaliOffset | pd.Timedelta | None = None,
    ) -> JalaliDatetimeIndex:
        """Shift index by desired number of time frequency increments.

        Args:
            periods: Number of periods to shift.
            freq: Frequency to shift by. If None, uses the index's freq.

        Returns:
            Shifted JalaliDatetimeIndex.
        """
        if freq is None:
            freq = self._freq

        if freq is None:
            raise ValueError("freq must be specified if index has no frequency")

        # Parse string frequency
        if isinstance(freq, str):
            # Check if it's a Jalali frequency
            from jalali_pandas.offsets.aliases import get_jalali_offset

            offset_class = get_jalali_offset(freq.upper())
            if offset_class is not None:
                freq = offset_class(n=periods)
                periods = 1
            else:
                # Use pandas Timedelta for standard frequencies
                freq = pd.Timedelta(freq)

        # Apply the shift
        new_data: list[Any] = []
        for val in self._data:
            if pd.isna(val):
                new_data.append(pd.NaT)
            else:
                if isinstance(freq, pd.Timedelta):
                    new_val = val + freq * periods
                else:
                    # JalaliOffset
                    new_val = val
                    for _ in range(abs(periods)):
                        new_val = freq + new_val if periods > 0 else new_val - freq
                new_data.append(new_val)

        new_array = JalaliDatetimeArray._from_sequence(new_data, dtype=self.dtype)
        return type(self)._simple_new(new_array, name=self._name, freq=self._freq)

    def snap(self, freq: str = "s") -> JalaliDatetimeIndex:
        """Snap time stamps to nearest occurring frequency.

        Args:
            freq: Frequency to snap to (e.g., 's' for second, 'min' for minute).

        Returns:
            Snapped JalaliDatetimeIndex.
        """
        # Convert to Gregorian, snap, convert back
        gregorian = self.to_gregorian()
        snapped = gregorian.snap(freq)

        new_data = [
            JalaliTimestamp.from_gregorian(ts) if not pd.isna(ts) else pd.NaT
            for ts in snapped
        ]
        new_array = JalaliDatetimeArray._from_sequence(new_data, dtype=self.dtype)
        return type(self)._simple_new(new_array, name=self._name, freq=self._freq)

    # -------------------------------------------------------------------------
    # Set Operations
    # -------------------------------------------------------------------------

    def union(  # type: ignore[override]
        self, other: JalaliDatetimeIndex, sort: bool | None = None
    ) -> JalaliDatetimeIndex:
        """Form the union of two JalaliDatetimeIndex objects.

        Args:
            other: Another JalaliDatetimeIndex.
            sort: Whether to sort the result.

        Returns:
            Union of the two indexes.
        """
        if not isinstance(other, JalaliDatetimeIndex):
            raise TypeError("other must be a JalaliDatetimeIndex")

        # Combine and deduplicate
        combined = list(self._data) + list(other._data)
        seen: set[JalaliTimestamp] = set()
        unique: list[Any] = []
        for val in combined:
            if pd.isna(val):
                if pd.NaT not in seen:
                    unique.append(pd.NaT)
                    seen.add(pd.NaT)  # type: ignore
            elif val not in seen:
                unique.append(val)
                seen.add(val)

        if sort is True or (sort is None and len(unique) > 0):
            # Sort by Gregorian equivalent
            unique = sorted(
                unique,
                key=lambda x: x.to_gregorian() if not pd.isna(x) else pd.Timestamp.min,
            )

        new_array = JalaliDatetimeArray._from_sequence(unique, dtype=self.dtype)
        return type(self)._simple_new(new_array, name=self._name)

    def intersection(  # type: ignore[override]
        self, other: JalaliDatetimeIndex, sort: bool = False
    ) -> JalaliDatetimeIndex:
        """Form the intersection of two JalaliDatetimeIndex objects.

        Args:
            other: Another JalaliDatetimeIndex.
            sort: Whether to sort the result.

        Returns:
            Intersection of the two indexes.
        """
        if not isinstance(other, JalaliDatetimeIndex):
            raise TypeError("other must be a JalaliDatetimeIndex")

        other_set = set(other._data)
        common: list[Any] = []
        for val in self._data:
            if pd.isna(val):
                if any(pd.isna(x) for x in other._data):
                    common.append(pd.NaT)
            elif val in other_set:
                common.append(val)

        if sort:
            common = sorted(
                common,
                key=lambda x: x.to_gregorian() if not pd.isna(x) else pd.Timestamp.min,
            )

        new_array = JalaliDatetimeArray._from_sequence(common, dtype=self.dtype)
        return type(self)._simple_new(new_array, name=self._name)

    def difference(  # type: ignore[override]
        self, other: JalaliDatetimeIndex, sort: bool | None = True
    ) -> JalaliDatetimeIndex:
        """Return a new JalaliDatetimeIndex with elements not in other.

        Args:
            other: Another JalaliDatetimeIndex.
            sort: Whether to sort the result.

        Returns:
            Difference of the two indexes.
        """
        if not isinstance(other, JalaliDatetimeIndex):
            raise TypeError("other must be a JalaliDatetimeIndex")

        other_set = set(other._data)
        diff: list[Any] = []
        for val in self._data:
            if pd.isna(val):
                if not any(pd.isna(x) for x in other._data):
                    diff.append(pd.NaT)
            elif val not in other_set:
                diff.append(val)

        if sort:
            diff = sorted(
                diff,
                key=lambda x: x.to_gregorian() if not pd.isna(x) else pd.Timestamp.min,
            )

        new_array = JalaliDatetimeArray._from_sequence(diff, dtype=self.dtype)
        return type(self)._simple_new(new_array, name=self._name)

    # -------------------------------------------------------------------------
    # Required Index Methods
    # -------------------------------------------------------------------------

    def copy(self, name: Hashable = None, deep: bool = True) -> JalaliDatetimeIndex:
        """Make a copy of this object.

        Args:
            name: Name for the new index.
            deep: Whether to make a deep copy.

        Returns:
            Copy of the index.
        """
        new_data = self._data.copy() if deep else self._data
        return type(self)._simple_new(
            new_data,
            name=name if name is not None else self._name,
            freq=self._freq,
        )

    def _shallow_copy(
        self, values: JalaliDatetimeArray | None = None
    ) -> JalaliDatetimeIndex:
        """Create a shallow copy with optional new values."""
        if values is None:
            values = self._data
        return type(self)._simple_new(values, name=self._name, freq=self._freq)

    @property
    def _constructor(self) -> type[JalaliDatetimeIndex]:
        """Return the constructor for this type."""
        return type(self)

    def equals(self, other: object) -> bool:
        """Determine if two Index objects are equal."""
        if not isinstance(other, JalaliDatetimeIndex):
            return False
        if len(self) != len(other):
            return False
        return all(
            (pd.isna(a) and pd.isna(b)) or a == b
            for a, b in zip(self._data, other._data)
        )

    def __eq__(self, other: Any) -> Any:
        """Element-wise equality comparison."""
        if isinstance(other, JalaliDatetimeIndex):
            return cast(npt.NDArray[np.bool_], self._data == other._data)
        if isinstance(other, JalaliTimestamp):
            return cast(
                npt.NDArray[np.bool_],
                np.array([x == other for x in self._data], dtype=bool),
            )
        return NotImplemented

    def __ne__(self, other: Any) -> Any:
        """Element-wise inequality comparison."""
        result = self.__eq__(other)
        if result is NotImplemented:
            return result
        return ~result

    def _isna(self) -> npt.NDArray[np.bool_]:
        """Return boolean array indicating NA values."""
        return self._data.isna()

    def _notna(self) -> npt.NDArray[np.bool_]:
        """Return boolean array indicating non-NA values."""
        return ~self._isna()

    @property
    def values(self) -> JalaliDatetimeArray:
        """Return the underlying data as a JalaliDatetimeArray."""
        return self._data

    def to_numpy(  # type: ignore[override]
        self,
        dtype: Any = None,
        copy: bool = False,
        na_value: Any = None,  # noqa: ARG002
    ) -> npt.NDArray[Any]:
        """Convert to numpy array."""
        if dtype is None:
            return self._data._data.copy() if copy else self._data._data
        return np.array(self._data._data, dtype=dtype, copy=copy)

    def to_list(self) -> list[JalaliTimestamp]:
        """Return a list of the values."""
        return list(self._data)

    def tolist(self) -> list[JalaliTimestamp]:
        """Return a list of the values."""
        return self.to_list()

day property

day: Any

Return array of days.

dayofweek property

dayofweek: Any

Return array of day of week values.

dayofyear property

dayofyear: Any

Return array of day of year values.

dtype property

dtype: JalaliDatetimeDtype

Return the dtype of the index.

freq property writable

freq: JalaliOffset | str | None

Return the frequency of the index.

freqstr property

freqstr: str | None

Return the frequency as a string.

hour property

hour: Any

Return array of hours.

inferred_freq property

inferred_freq: str | None

Try to infer the frequency from the data.

minute property

minute: Any

Return array of minutes.

month property

month: Any

Return array of months.

quarter property

quarter: Any

Return array of quarters.

second property

second: Any

Return array of seconds.

values property

values: JalaliDatetimeArray

Return the underlying data as a JalaliDatetimeArray.

week property

week: Any

Return array of week numbers.

weekday property

weekday: Any

Alias for dayofweek.

weekofyear property

weekofyear: Any

Alias for week.

year property

year: Any

Return array of years.

__contains__

__contains__(key: Any) -> bool

Check if key is in the index.

Source code in jalali_pandas/core/indexes.py
174
175
176
177
178
179
180
def __contains__(self, key: Any) -> bool:
    """Check if key is in the index."""
    try:
        self.get_loc(key)
        return True
    except KeyError:
        return False

__eq__

__eq__(other: Any) -> Any

Element-wise equality comparison.

Source code in jalali_pandas/core/indexes.py
647
648
649
650
651
652
653
654
655
656
def __eq__(self, other: Any) -> Any:
    """Element-wise equality comparison."""
    if isinstance(other, JalaliDatetimeIndex):
        return cast(npt.NDArray[np.bool_], self._data == other._data)
    if isinstance(other, JalaliTimestamp):
        return cast(
            npt.NDArray[np.bool_],
            np.array([x == other for x in self._data], dtype=bool),
        )
    return NotImplemented

__getitem__

__getitem__(key: Any) -> Any

Get item(s) from the index.

Source code in jalali_pandas/core/indexes.py
161
162
163
164
165
166
167
168
def __getitem__(self, key: Any) -> Any:
    """Get item(s) from the index."""
    result = self._data[key]

    if isinstance(result, JalaliDatetimeArray):
        return type(self)._simple_new(result, name=self._name, freq=None)

    return result

__iter__

__iter__() -> Any

Iterate over the index.

Source code in jalali_pandas/core/indexes.py
170
171
172
def __iter__(self) -> Any:
    """Iterate over the index."""
    return iter(self._data)

__len__

__len__() -> int

Return the length of the index.

Source code in jalali_pandas/core/indexes.py
157
158
159
def __len__(self) -> int:
    """Return the length of the index."""
    return len(self._data)

__ne__

__ne__(other: Any) -> Any

Element-wise inequality comparison.

Source code in jalali_pandas/core/indexes.py
658
659
660
661
662
663
def __ne__(self, other: Any) -> Any:
    """Element-wise inequality comparison."""
    result = self.__eq__(other)
    if result is NotImplemented:
        return result
    return ~result

__new__

__new__(data: Sequence[Any] | JalaliDatetimeArray | JalaliDatetimeIndex | None = None, freq: str | JalaliOffset | None = None, tz: tzinfo | str | None = None, dtype: JalaliDatetimeDtype | None = None, copy: bool = False, name: Hashable = None) -> JalaliDatetimeIndex

Create a new JalaliDatetimeIndex.

Source code in jalali_pandas/core/indexes.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def __new__(
    cls,
    data: Sequence[Any] | JalaliDatetimeArray | JalaliDatetimeIndex | None = None,
    freq: str | JalaliOffset | None = None,
    tz: tzinfo | str | None = None,
    dtype: JalaliDatetimeDtype | None = None,
    copy: bool = False,
    name: Hashable = None,
) -> JalaliDatetimeIndex:
    """Create a new JalaliDatetimeIndex."""
    if dtype is None:
        # Convert tzinfo to string if needed
        tz_str = str(tz) if tz is not None and not isinstance(tz, str) else tz
        dtype = JalaliDatetimeDtype(tz=tz_str)

    if data is None:
        data = []

    if isinstance(data, JalaliDatetimeIndex):
        data = data._data.copy() if copy else data._data
    elif isinstance(data, JalaliDatetimeArray):
        if copy:
            data = data.copy()
    else:
        data = JalaliDatetimeArray._from_sequence(
            list(data), dtype=dtype, copy=copy
        )

    # Create the index
    result = object.__new__(cls)
    result._data = data
    result._name = name
    result._freq = freq
    result._cache = {}

    return result

__repr__

__repr__() -> str

String representation.

Source code in jalali_pandas/core/indexes.py
182
183
184
185
186
187
188
189
190
def __repr__(self) -> str:
    """String representation."""
    data_repr = ", ".join(f"'{x.strftime('%Y-%m-%d')}'" for x in self._data[:5])
    if len(self._data) > 5:
        data_repr += ", ..."
    freq_str = f", freq='{self.freqstr}'" if self.freqstr else ", freq=None"
    return (
        f"JalaliDatetimeIndex([{data_repr}], dtype='{self.dtype.name}'{freq_str})"
    )

copy

copy(name: Hashable = None, deep: bool = True) -> JalaliDatetimeIndex

Make a copy of this object.

Parameters:

Name Type Description Default
name Hashable

Name for the new index.

None
deep bool

Whether to make a deep copy.

True

Returns:

Type Description
JalaliDatetimeIndex

Copy of the index.

Source code in jalali_pandas/core/indexes.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
def copy(self, name: Hashable = None, deep: bool = True) -> JalaliDatetimeIndex:
    """Make a copy of this object.

    Args:
        name: Name for the new index.
        deep: Whether to make a deep copy.

    Returns:
        Copy of the index.
    """
    new_data = self._data.copy() if deep else self._data
    return type(self)._simple_new(
        new_data,
        name=name if name is not None else self._name,
        freq=self._freq,
    )

difference

difference(other: JalaliDatetimeIndex, sort: bool | None = True) -> JalaliDatetimeIndex

Return a new JalaliDatetimeIndex with elements not in other.

Parameters:

Name Type Description Default
other JalaliDatetimeIndex

Another JalaliDatetimeIndex.

required
sort bool | None

Whether to sort the result.

True

Returns:

Type Description
JalaliDatetimeIndex

Difference of the two indexes.

Source code in jalali_pandas/core/indexes.py
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
def difference(  # type: ignore[override]
    self, other: JalaliDatetimeIndex, sort: bool | None = True
) -> JalaliDatetimeIndex:
    """Return a new JalaliDatetimeIndex with elements not in other.

    Args:
        other: Another JalaliDatetimeIndex.
        sort: Whether to sort the result.

    Returns:
        Difference of the two indexes.
    """
    if not isinstance(other, JalaliDatetimeIndex):
        raise TypeError("other must be a JalaliDatetimeIndex")

    other_set = set(other._data)
    diff: list[Any] = []
    for val in self._data:
        if pd.isna(val):
            if not any(pd.isna(x) for x in other._data):
                diff.append(pd.NaT)
        elif val not in other_set:
            diff.append(val)

    if sort:
        diff = sorted(
            diff,
            key=lambda x: x.to_gregorian() if not pd.isna(x) else pd.Timestamp.min,
        )

    new_array = JalaliDatetimeArray._from_sequence(diff, dtype=self.dtype)
    return type(self)._simple_new(new_array, name=self._name)

equals

equals(other: object) -> bool

Determine if two Index objects are equal.

Source code in jalali_pandas/core/indexes.py
636
637
638
639
640
641
642
643
644
645
def equals(self, other: object) -> bool:
    """Determine if two Index objects are equal."""
    if not isinstance(other, JalaliDatetimeIndex):
        return False
    if len(self) != len(other):
        return False
    return all(
        (pd.isna(a) and pd.isna(b)) or a == b
        for a, b in zip(self._data, other._data)
    )

get_loc

get_loc(key: Any) -> int | slice | npt.NDArray[np.bool_]

Get integer location for requested label.

Supports: - JalaliTimestamp: exact match - String: "1402-06-15" for exact date - Partial string: "1402-06" for month, "1402" for year

Parameters:

Name Type Description Default
key Any

Label to look up.

required

Returns:

Type Description
int | slice | NDArray[bool_]

Integer location, slice, or boolean mask.

Raises:

Type Description
KeyError

If key is not found.

Source code in jalali_pandas/core/indexes.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def get_loc(self, key: Any) -> int | slice | npt.NDArray[np.bool_]:
    """Get integer location for requested label.

    Supports:
    - JalaliTimestamp: exact match
    - String: "1402-06-15" for exact date
    - Partial string: "1402-06" for month, "1402" for year

    Args:
        key: Label to look up.

    Returns:
        Integer location, slice, or boolean mask.

    Raises:
        KeyError: If key is not found.
    """
    if isinstance(key, JalaliTimestamp):
        # Exact match
        for i, val in enumerate(self._data):
            if val == key:
                return i
        raise KeyError(key)

    if isinstance(key, str):
        return self._get_string_loc(key)

    raise KeyError(key)

intersection

intersection(other: JalaliDatetimeIndex, sort: bool = False) -> JalaliDatetimeIndex

Form the intersection of two JalaliDatetimeIndex objects.

Parameters:

Name Type Description Default
other JalaliDatetimeIndex

Another JalaliDatetimeIndex.

required
sort bool

Whether to sort the result.

False

Returns:

Type Description
JalaliDatetimeIndex

Intersection of the two indexes.

Source code in jalali_pandas/core/indexes.py
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
def intersection(  # type: ignore[override]
    self, other: JalaliDatetimeIndex, sort: bool = False
) -> JalaliDatetimeIndex:
    """Form the intersection of two JalaliDatetimeIndex objects.

    Args:
        other: Another JalaliDatetimeIndex.
        sort: Whether to sort the result.

    Returns:
        Intersection of the two indexes.
    """
    if not isinstance(other, JalaliDatetimeIndex):
        raise TypeError("other must be a JalaliDatetimeIndex")

    other_set = set(other._data)
    common: list[Any] = []
    for val in self._data:
        if pd.isna(val):
            if any(pd.isna(x) for x in other._data):
                common.append(pd.NaT)
        elif val in other_set:
            common.append(val)

    if sort:
        common = sorted(
            common,
            key=lambda x: x.to_gregorian() if not pd.isna(x) else pd.Timestamp.min,
        )

    new_array = JalaliDatetimeArray._from_sequence(common, dtype=self.dtype)
    return type(self)._simple_new(new_array, name=self._name)

shift

shift(periods: int = 1, freq: str | JalaliOffset | Timedelta | None = None) -> JalaliDatetimeIndex

Shift index by desired number of time frequency increments.

Parameters:

Name Type Description Default
periods int

Number of periods to shift.

1
freq str | JalaliOffset | Timedelta | None

Frequency to shift by. If None, uses the index's freq.

None

Returns:

Type Description
JalaliDatetimeIndex

Shifted JalaliDatetimeIndex.

Source code in jalali_pandas/core/indexes.py
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
def shift(
    self,
    periods: int = 1,
    freq: str | JalaliOffset | pd.Timedelta | None = None,
) -> JalaliDatetimeIndex:
    """Shift index by desired number of time frequency increments.

    Args:
        periods: Number of periods to shift.
        freq: Frequency to shift by. If None, uses the index's freq.

    Returns:
        Shifted JalaliDatetimeIndex.
    """
    if freq is None:
        freq = self._freq

    if freq is None:
        raise ValueError("freq must be specified if index has no frequency")

    # Parse string frequency
    if isinstance(freq, str):
        # Check if it's a Jalali frequency
        from jalali_pandas.offsets.aliases import get_jalali_offset

        offset_class = get_jalali_offset(freq.upper())
        if offset_class is not None:
            freq = offset_class(n=periods)
            periods = 1
        else:
            # Use pandas Timedelta for standard frequencies
            freq = pd.Timedelta(freq)

    # Apply the shift
    new_data: list[Any] = []
    for val in self._data:
        if pd.isna(val):
            new_data.append(pd.NaT)
        else:
            if isinstance(freq, pd.Timedelta):
                new_val = val + freq * periods
            else:
                # JalaliOffset
                new_val = val
                for _ in range(abs(periods)):
                    new_val = freq + new_val if periods > 0 else new_val - freq
            new_data.append(new_val)

    new_array = JalaliDatetimeArray._from_sequence(new_data, dtype=self.dtype)
    return type(self)._simple_new(new_array, name=self._name, freq=self._freq)

slice_locs

slice_locs(start: str | JalaliTimestamp | None = None, end: str | JalaliTimestamp | None = None, step: int | None = None) -> tuple[int, int]

Compute slice locations for input labels.

Parameters:

Name Type Description Default
start str | JalaliTimestamp | None

Start label.

None
end str | JalaliTimestamp | None

End label.

None
step int | None

Step size.

None

Returns:

Type Description
tuple[int, int]

Tuple of (start, end) integer locations.

Source code in jalali_pandas/core/indexes.py
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def slice_locs(
    self,
    start: str | JalaliTimestamp | None = None,
    end: str | JalaliTimestamp | None = None,
    step: int | None = None,  # noqa: ARG002
) -> tuple[int, int]:
    """Compute slice locations for input labels.

    Args:
        start: Start label.
        end: End label.
        step: Step size.

    Returns:
        Tuple of (start, end) integer locations.
    """
    start_loc = 0
    end_loc = len(self)

    if start is not None:
        start_ts = self._parse_to_timestamp(start)
        for i, val in enumerate(self._data):
            if not pd.isna(val) and val >= start_ts:
                start_loc = i
                break

    if end is not None:
        end_ts = self._parse_to_timestamp(end)
        for i in range(len(self._data) - 1, -1, -1):
            val = self._data[i]
            if not pd.isna(val) and val <= end_ts:
                end_loc = i + 1
                break

    return start_loc, end_loc

snap

snap(freq: str = 's') -> JalaliDatetimeIndex

Snap time stamps to nearest occurring frequency.

Parameters:

Name Type Description Default
freq str

Frequency to snap to (e.g., 's' for second, 'min' for minute).

's'

Returns:

Type Description
JalaliDatetimeIndex

Snapped JalaliDatetimeIndex.

Source code in jalali_pandas/core/indexes.py
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
def snap(self, freq: str = "s") -> JalaliDatetimeIndex:
    """Snap time stamps to nearest occurring frequency.

    Args:
        freq: Frequency to snap to (e.g., 's' for second, 'min' for minute).

    Returns:
        Snapped JalaliDatetimeIndex.
    """
    # Convert to Gregorian, snap, convert back
    gregorian = self.to_gregorian()
    snapped = gregorian.snap(freq)

    new_data = [
        JalaliTimestamp.from_gregorian(ts) if not pd.isna(ts) else pd.NaT
        for ts in snapped
    ]
    new_array = JalaliDatetimeArray._from_sequence(new_data, dtype=self.dtype)
    return type(self)._simple_new(new_array, name=self._name, freq=self._freq)

strftime

strftime(fmt: str) -> Any

Format timestamps as strings.

Parameters:

Name Type Description Default
fmt str

Format string.

required

Returns:

Type Description
Any

Index of formatted strings.

Source code in jalali_pandas/core/indexes.py
268
269
270
271
272
273
274
275
276
277
def strftime(self, fmt: str) -> Any:
    """Format timestamps as strings.

    Args:
        fmt: Format string.

    Returns:
        Index of formatted strings.
    """
    return pd.Index(self._data.strftime(fmt), name=self._name)

to_gregorian

to_gregorian() -> pd.DatetimeIndex

Convert to pandas DatetimeIndex (Gregorian).

Returns:

Type Description
DatetimeIndex

DatetimeIndex with Gregorian timestamps.

Source code in jalali_pandas/core/indexes.py
260
261
262
263
264
265
266
def to_gregorian(self) -> pd.DatetimeIndex:
    """Convert to pandas DatetimeIndex (Gregorian).

    Returns:
        DatetimeIndex with Gregorian timestamps.
    """
    return self._data.to_gregorian()

to_list

to_list() -> list[JalaliTimestamp]

Return a list of the values.

Source code in jalali_pandas/core/indexes.py
689
690
691
def to_list(self) -> list[JalaliTimestamp]:
    """Return a list of the values."""
    return list(self._data)

to_numpy

to_numpy(dtype: Any = None, copy: bool = False, na_value: Any = None) -> npt.NDArray[Any]

Convert to numpy array.

Source code in jalali_pandas/core/indexes.py
678
679
680
681
682
683
684
685
686
687
def to_numpy(  # type: ignore[override]
    self,
    dtype: Any = None,
    copy: bool = False,
    na_value: Any = None,  # noqa: ARG002
) -> npt.NDArray[Any]:
    """Convert to numpy array."""
    if dtype is None:
        return self._data._data.copy() if copy else self._data._data
    return np.array(self._data._data, dtype=dtype, copy=copy)

tolist

tolist() -> list[JalaliTimestamp]

Return a list of the values.

Source code in jalali_pandas/core/indexes.py
693
694
695
def tolist(self) -> list[JalaliTimestamp]:
    """Return a list of the values."""
    return self.to_list()

union

union(other: JalaliDatetimeIndex, sort: bool | None = None) -> JalaliDatetimeIndex

Form the union of two JalaliDatetimeIndex objects.

Parameters:

Name Type Description Default
other JalaliDatetimeIndex

Another JalaliDatetimeIndex.

required
sort bool | None

Whether to sort the result.

None

Returns:

Type Description
JalaliDatetimeIndex

Union of the two indexes.

Source code in jalali_pandas/core/indexes.py
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
def union(  # type: ignore[override]
    self, other: JalaliDatetimeIndex, sort: bool | None = None
) -> JalaliDatetimeIndex:
    """Form the union of two JalaliDatetimeIndex objects.

    Args:
        other: Another JalaliDatetimeIndex.
        sort: Whether to sort the result.

    Returns:
        Union of the two indexes.
    """
    if not isinstance(other, JalaliDatetimeIndex):
        raise TypeError("other must be a JalaliDatetimeIndex")

    # Combine and deduplicate
    combined = list(self._data) + list(other._data)
    seen: set[JalaliTimestamp] = set()
    unique: list[Any] = []
    for val in combined:
        if pd.isna(val):
            if pd.NaT not in seen:
                unique.append(pd.NaT)
                seen.add(pd.NaT)  # type: ignore
        elif val not in seen:
            unique.append(val)
            seen.add(val)

    if sort is True or (sort is None and len(unique) > 0):
        # Sort by Gregorian equivalent
        unique = sorted(
            unique,
            key=lambda x: x.to_gregorian() if not pd.isna(x) else pd.Timestamp.min,
        )

    new_array = JalaliDatetimeArray._from_sequence(unique, dtype=self.dtype)
    return type(self)._simple_new(new_array, name=self._name)

Jalali calendar rules and utilities.

day_of_year

day_of_year(year: int, month: int, day: int) -> int

Get the day of year (1-366).

Parameters:

Name Type Description Default
year int

Jalali year.

required
month int

Jalali month (1-12).

required
day int

Jalali day.

required

Returns:

Type Description
int

Day of year (1-366).

Source code in jalali_pandas/core/calendar.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def day_of_year(year: int, month: int, day: int) -> int:
    """Get the day of year (1-366).

    Args:
        year: Jalali year.
        month: Jalali month (1-12).
        day: Jalali day.

    Returns:
        Day of year (1-366).
    """
    max_day = days_in_month(year, month)
    if not 1 <= day <= max_day:
        raise ValueError(f"Day must be 1-{max_day} for month {month}, got {day}")
    return MONTH_STARTS[month - 1] + day

days_in_month

days_in_month(year: int, month: int) -> int

Get the number of days in a Jalali month.

Parameters:

Name Type Description Default
year int

Jalali year.

required
month int

Jalali month (1-12).

required

Returns:

Type Description
int

Number of days in the month.

Raises:

Type Description
ValueError

If month is not in range 1-12.

Source code in jalali_pandas/core/calendar.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def days_in_month(year: int, month: int) -> int:
    """Get the number of days in a Jalali month.

    Args:
        year: Jalali year.
        month: Jalali month (1-12).

    Returns:
        Number of days in the month.

    Raises:
        ValueError: If month is not in range 1-12.
    """
    if not 1 <= month <= 12:
        raise ValueError(f"Month must be 1-12, got {month}")

    if month == 12:
        return 30 if is_leap_year(year) else 29
    return MONTH_LENGTHS[month - 1]

days_in_month_vectorized

days_in_month_vectorized(years: NDArray[int64], months: NDArray[int64]) -> npt.NDArray[np.int64]

Vectorized calculation of days in month.

Parameters:

Name Type Description Default
years NDArray[int64]

Array of Jalali years.

required
months NDArray[int64]

Array of Jalali months (1-12).

required

Returns:

Type Description
NDArray[int64]

Array of days in each month.

Source code in jalali_pandas/core/calendar.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def days_in_month_vectorized(
    years: npt.NDArray[np.int64], months: npt.NDArray[np.int64]
) -> npt.NDArray[np.int64]:
    """Vectorized calculation of days in month.

    Args:
        years: Array of Jalali years.
        months: Array of Jalali months (1-12).

    Returns:
        Array of days in each month.
    """
    # Base month lengths (non-leap)
    month_lengths = np.array(MONTH_LENGTHS, dtype=np.int64)
    result = month_lengths[months - 1]

    # Adjust Esfand for leap years
    is_esfand = months == 12
    is_leap = is_leap_year_vectorized(years)
    result = np.where(is_esfand & is_leap, 30, result)

    return result

days_in_year

days_in_year(year: int) -> int

Get the number of days in a Jalali year.

Parameters:

Name Type Description Default
year int

Jalali year.

required

Returns:

Type Description
int

365 for normal years, 366 for leap years.

Source code in jalali_pandas/core/calendar.py
106
107
108
109
110
111
112
113
114
115
def days_in_year(year: int) -> int:
    """Get the number of days in a Jalali year.

    Args:
        year: Jalali year.

    Returns:
        365 for normal years, 366 for leap years.
    """
    return 366 if is_leap_year(year) else 365

is_leap_year

is_leap_year(year: int) -> bool

Check if a Jalali year is a leap year.

Uses the 33-year cycle algorithm used by jdatetime.

Parameters:

Name Type Description Default
year int

Jalali year.

required

Returns:

Type Description
bool

True if the year is a leap year, False otherwise.

Source code in jalali_pandas/core/calendar.py
79
80
81
82
83
84
85
86
87
88
89
90
def is_leap_year(year: int) -> bool:
    """Check if a Jalali year is a leap year.

    Uses the 33-year cycle algorithm used by jdatetime.

    Args:
        year: Jalali year.

    Returns:
        True if the year is a leap year, False otherwise.
    """
    return year % 33 in LEAP_YEAR_REMAINDERS

is_leap_year_vectorized

is_leap_year_vectorized(years: NDArray[int64]) -> npt.NDArray[np.bool_]

Vectorized check for leap years.

Parameters:

Name Type Description Default
years NDArray[int64]

Array of Jalali years.

required

Returns:

Type Description
NDArray[bool_]

Boolean array indicating leap years.

Source code in jalali_pandas/core/calendar.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def is_leap_year_vectorized(years: npt.NDArray[np.int64]) -> npt.NDArray[np.bool_]:
    """Vectorized check for leap years.

    Args:
        years: Array of Jalali years.

    Returns:
        Boolean array indicating leap years.
    """
    remainders = years % 33
    return np.isin(remainders, LEAP_YEAR_REMAINDERS)

jalali_to_jdn

jalali_to_jdn(year: int, month: int, day: int) -> int

Convert Jalali date to Julian Day Number.

Parameters:

Name Type Description Default
year int

Jalali year.

required
month int

Jalali month (1-12).

required
day int

Jalali day.

required

Returns:

Type Description
int

Julian Day Number.

Source code in jalali_pandas/core/calendar.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def jalali_to_jdn(year: int, month: int, day: int) -> int:
    """Convert Jalali date to Julian Day Number.

    Args:
        year: Jalali year.
        month: Jalali month (1-12).
        day: Jalali day.

    Returns:
        Julian Day Number.
    """
    # Algorithm based on the 2820-year cycle
    a = year - 474 if year > 0 else year - 473
    b = a % 2820

    jdn = (
        day
        + (month - 1) * 30
        + min(6, month - 1)
        + ((b * 682 - 110) // 2816)
        + (b - 1) * 365
        + (a // 2820) * 1029983
        + 2121445
    )
    return jdn

jdn_to_jalali

jdn_to_jalali(jdn: int) -> tuple[int, int, int]

Convert Julian Day Number to Jalali date.

Parameters:

Name Type Description Default
jdn int

Julian Day Number.

required

Returns:

Type Description
tuple[int, int, int]

Tuple of (year, month, day).

Source code in jalali_pandas/core/calendar.py
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def jdn_to_jalali(jdn: int) -> tuple[int, int, int]:
    """Convert Julian Day Number to Jalali date.

    Args:
        jdn: Julian Day Number.

    Returns:
        Tuple of (year, month, day).
    """
    # Use the inverse of jalali_to_jdn algorithm
    # Based on the 2820-year cycle
    jdn_offset = jdn - 2121445  # Epoch matching jalali_to_jdn

    cycle_2820 = jdn_offset // 1029983
    day_in_cycle = jdn_offset % 1029983

    if day_in_cycle < 0:
        cycle_2820 -= 1
        day_in_cycle += 1029983

    # Find year within cycle using binary search approach
    # For year b within cycle, start of year (day=1, month=1) has offset:
    # 1 + ((b*682-110)//2816) + (b-1)*365
    def days_to_year_start(b: int) -> int:
        return 1 + ((b * 682 - 110) // 2816) + (b - 1) * 365

    # Binary search for the year
    lo, hi = 0, 2820
    while lo < hi:
        mid = (lo + hi + 1) // 2
        if days_to_year_start(mid) <= day_in_cycle:
            lo = mid
        else:
            hi = mid - 1

    year_in_cycle = lo
    remaining_days = day_in_cycle - days_to_year_start(year_in_cycle)

    year = cycle_2820 * 2820 + year_in_cycle + 474

    # Find month and day from remaining_days (0-indexed day within year)
    if remaining_days < 186:  # First 6 months (31 days each)
        month = remaining_days // 31 + 1
        day = remaining_days % 31 + 1
    else:
        remaining_days -= 186
        if remaining_days < 150:  # Months 7-11 (30 days each)
            month = remaining_days // 30 + 7
            day = remaining_days % 30 + 1
        else:
            remaining_days -= 150
            month = 12
            day = remaining_days + 1

    return (year, month, day)

quarter_of_month

quarter_of_month(month: int) -> int

Get the quarter for a given month.

Parameters:

Name Type Description Default
month int

Jalali month (1-12).

required

Returns:

Type Description
int

Quarter number (1-4).

Source code in jalali_pandas/core/calendar.py
163
164
165
166
167
168
169
170
171
172
def quarter_of_month(month: int) -> int:
    """Get the quarter for a given month.

    Args:
        month: Jalali month (1-12).

    Returns:
        Quarter number (1-4).
    """
    return (month - 1) // 3 + 1

validate_jalali_date

validate_jalali_date(year: int, month: int, day: int) -> bool

Validate a Jalali date.

Parameters:

Name Type Description Default
year int

Jalali year.

required
month int

Jalali month (1-12).

required
day int

Jalali day.

required

Returns:

Type Description
bool

True if the date is valid.

Raises:

Type Description
ValueError

If the date is invalid.

Source code in jalali_pandas/core/calendar.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def validate_jalali_date(year: int, month: int, day: int) -> bool:
    """Validate a Jalali date.

    Args:
        year: Jalali year.
        month: Jalali month (1-12).
        day: Jalali day.

    Returns:
        True if the date is valid.

    Raises:
        ValueError: If the date is invalid.
    """
    if not 1 <= month <= 12:
        raise ValueError(f"Month must be 1-12, got {month}")

    max_day = days_in_month(year, month)
    if not 1 <= day <= max_day:
        raise ValueError(f"Day must be 1-{max_day} for month {month}, got {day}")

    return True

week_of_year

week_of_year(year: int, month: int, day: int) -> int

Get the week number of the year.

Week 1 is the first week containing at least 4 days. Weeks start on Saturday (Jalali calendar convention).

Parameters:

Name Type Description Default
year int

Jalali year.

required
month int

Jalali month (1-12).

required
day int

Jalali day.

required

Returns:

Type Description
int

Week number (1-53).

Source code in jalali_pandas/core/calendar.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def week_of_year(year: int, month: int, day: int) -> int:
    """Get the week number of the year.

    Week 1 is the first week containing at least 4 days.
    Weeks start on Saturday (Jalali calendar convention).

    Args:
        year: Jalali year.
        month: Jalali month (1-12).
        day: Jalali day.

    Returns:
        Week number (1-53).
    """
    # Calculate day of year
    doy = day_of_year(year, month, day)

    # Calculate weekday of first day of year (0=Saturday)
    first_day_weekday = weekday_of_jalali(year, 1, 1)

    # ISO-like week calculation adapted for Saturday start
    # Week 1 is the week containing the first Thursday (day 5 in 0-indexed Sat start)
    week = (doy + first_day_weekday - 1) // 7 + 1

    return week

weekday_of_jalali

weekday_of_jalali(year: int, month: int, day: int) -> int

Get the weekday of a Jalali date.

Parameters:

Name Type Description Default
year int

Jalali year.

required
month int

Jalali month (1-12).

required
day int

Jalali day.

required

Returns:

Type Description
int

Weekday (0=Saturday, 6=Friday).

Source code in jalali_pandas/core/calendar.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def weekday_of_jalali(year: int, month: int, day: int) -> int:
    """Get the weekday of a Jalali date.

    Args:
        year: Jalali year.
        month: Jalali month (1-12).
        day: Jalali day.

    Returns:
        Weekday (0=Saturday, 6=Friday).
    """
    # Convert to Julian Day Number and calculate weekday
    jdn = jalali_to_jdn(year, month, day)
    # Adjust for Jalali weekday convention (0=Saturday, 6=Friday)
    return (jdn + 2) % 7

Vectorized conversion utilities for Jalali-Gregorian date conversions.

This module provides optimized conversion functions using NumPy for efficient batch processing of date conversions.

datetime64_to_jalali

datetime64_to_jalali(dt: NDArray[datetime64]) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]

Convert numpy datetime64 array to Jalali date components.

Parameters:

Name Type Description Default
dt NDArray[datetime64]

Array of numpy datetime64 values.

required

Returns:

Type Description
tuple[NDArray[int64], NDArray[int64], NDArray[int64]]

Tuple of (jalali_year, jalali_month, jalali_day) arrays.

Source code in jalali_pandas/core/conversion.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def datetime64_to_jalali(
    dt: NDArray[np.datetime64],
) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]:
    """Convert numpy datetime64 array to Jalali date components.

    Args:
        dt: Array of numpy datetime64 values.

    Returns:
        Tuple of (jalali_year, jalali_month, jalali_day) arrays.
    """
    # Convert to pandas DatetimeIndex for easy component extraction
    dti = pd.DatetimeIndex(dt)
    year = dti.year.to_numpy().astype(np.int64)
    month = dti.month.to_numpy().astype(np.int64)
    day = dti.day.to_numpy().astype(np.int64)

    return gregorian_to_jalali_vectorized(year, month, day, use_lookup=True)

gregorian_to_jalali_scalar cached

gregorian_to_jalali_scalar(year: int, month: int, day: int) -> tuple[int, int, int]

Convert a single Gregorian date to Jalali.

Parameters:

Name Type Description Default
year int

Gregorian year.

required
month int

Gregorian month (1-12).

required
day int

Gregorian day.

required

Returns:

Type Description
tuple[int, int, int]

Tuple of (jalali_year, jalali_month, jalali_day).

Source code in jalali_pandas/core/conversion.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@lru_cache(maxsize=16384)
def gregorian_to_jalali_scalar(year: int, month: int, day: int) -> tuple[int, int, int]:
    """Convert a single Gregorian date to Jalali.

    Args:
        year: Gregorian year.
        month: Gregorian month (1-12).
        day: Gregorian day.

    Returns:
        Tuple of (jalali_year, jalali_month, jalali_day).
    """
    if _LOOKUP_READY:
        cached = _GREGORIAN_TO_JALALI_LOOKUP.get((year, month, day))
        if cached is not None:
            return cached

    gdate = date(year, month, day)
    jdate = jdatetime.date.fromgregorian(date=gdate)
    return jdate.year, jdate.month, jdate.day

gregorian_to_jalali_vectorized

gregorian_to_jalali_vectorized(year: NDArray[int64], month: NDArray[int64], day: NDArray[int64], *, use_lookup: bool = True) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]

Convert arrays of Gregorian dates to Jalali.

Parameters:

Name Type Description Default
year NDArray[int64]

Array of Gregorian years.

required
month NDArray[int64]

Array of Gregorian months (1-12).

required
day NDArray[int64]

Array of Gregorian days.

required

Returns:

Type Description
tuple[NDArray[int64], NDArray[int64], NDArray[int64]]

Tuple of (jalali_year, jalali_month, jalali_day) arrays.

Source code in jalali_pandas/core/conversion.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def gregorian_to_jalali_vectorized(
    year: NDArray[np.int64],
    month: NDArray[np.int64],
    day: NDArray[np.int64],
    *,
    use_lookup: bool = True,
) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]:
    """Convert arrays of Gregorian dates to Jalali.

    Args:
        year: Array of Gregorian years.
        month: Array of Gregorian months (1-12).
        day: Array of Gregorian days.

    Returns:
        Tuple of (jalali_year, jalali_month, jalali_day) arrays.
    """
    n = len(year)
    if use_lookup and n >= _LOOKUP_MIN_SIZE:
        _ensure_lookup_tables()

    lookup = _GREGORIAN_TO_JALALI_LOOKUP if _LOOKUP_READY else None
    j_year: NDArray[np.int64] = np.empty(n, dtype=np.int64)
    j_month: NDArray[np.int64] = np.empty(n, dtype=np.int64)
    j_day: NDArray[np.int64] = np.empty(n, dtype=np.int64)

    for i in range(n):
        key = (int(year[i]), int(month[i]), int(day[i]))
        if lookup is not None:
            cached = lookup.get(key)
            if cached is not None:
                jy, jm, jd = cached
            else:
                jy, jm, jd = gregorian_to_jalali_scalar(*key)
        else:
            jy, jm, jd = gregorian_to_jalali_scalar(*key)
        j_year[i] = jy
        j_month[i] = jm
        j_day[i] = jd

    return j_year, j_month, j_day

jalali_to_datetime64

jalali_to_datetime64(year: NDArray[int64], month: NDArray[int64], day: NDArray[int64], hour: NDArray[int64] | None = None, minute: NDArray[int64] | None = None, second: NDArray[int64] | None = None, microsecond: NDArray[int64] | None = None, nanosecond: NDArray[int64] | None = None) -> NDArray[np.datetime64]

Convert Jalali date components to numpy datetime64 array.

Parameters:

Name Type Description Default
year NDArray[int64]

Array of Jalali years.

required
month NDArray[int64]

Array of Jalali months (1-12).

required
day NDArray[int64]

Array of Jalali days.

required
hour NDArray[int64] | None

Optional array of hours.

None
minute NDArray[int64] | None

Optional array of minutes.

None
second NDArray[int64] | None

Optional array of seconds.

None
microsecond NDArray[int64] | None

Optional array of microseconds.

None
nanosecond NDArray[int64] | None

Optional array of nanoseconds.

None

Returns:

Type Description
NDArray[datetime64]

Array of numpy datetime64 values.

Source code in jalali_pandas/core/conversion.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def jalali_to_datetime64(
    year: NDArray[np.int64],
    month: NDArray[np.int64],
    day: NDArray[np.int64],
    hour: NDArray[np.int64] | None = None,
    minute: NDArray[np.int64] | None = None,
    second: NDArray[np.int64] | None = None,
    microsecond: NDArray[np.int64] | None = None,
    nanosecond: NDArray[np.int64] | None = None,
) -> NDArray[np.datetime64]:
    """Convert Jalali date components to numpy datetime64 array.

    Args:
        year: Array of Jalali years.
        month: Array of Jalali months (1-12).
        day: Array of Jalali days.
        hour: Optional array of hours.
        minute: Optional array of minutes.
        second: Optional array of seconds.
        microsecond: Optional array of microseconds.
        nanosecond: Optional array of nanoseconds.

    Returns:
        Array of numpy datetime64 values.
    """
    g_year, g_month, g_day = jalali_to_gregorian_vectorized(
        year, month, day, use_lookup=True
    )

    # Build datetime strings
    n = len(year)
    if hour is None:
        hour = np.zeros(n, dtype=np.int64)
    if minute is None:
        minute = np.zeros(n, dtype=np.int64)
    if second is None:
        second = np.zeros(n, dtype=np.int64)
    if microsecond is None:
        microsecond = np.zeros(n, dtype=np.int64)
    if nanosecond is None:
        nanosecond = np.zeros(n, dtype=np.int64)

    # Create datetime64 array
    # Convert to nanoseconds since epoch
    dates = pd.to_datetime(
        {
            "year": g_year,
            "month": g_month,
            "day": g_day,
            "hour": hour,
            "minute": minute,
            "second": second,
        }
    )

    # Add microseconds and nanoseconds
    result = (
        dates
        + pd.to_timedelta(microsecond, unit="us")
        + pd.to_timedelta(nanosecond, unit="ns")
    )

    return result.to_numpy()

jalali_to_gregorian_scalar cached

jalali_to_gregorian_scalar(year: int, month: int, day: int) -> tuple[int, int, int]

Convert a single Jalali date to Gregorian.

Parameters:

Name Type Description Default
year int

Jalali year.

required
month int

Jalali month (1-12).

required
day int

Jalali day.

required

Returns:

Type Description
tuple[int, int, int]

Tuple of (gregorian_year, gregorian_month, gregorian_day).

Source code in jalali_pandas/core/conversion.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@lru_cache(maxsize=16384)
def jalali_to_gregorian_scalar(year: int, month: int, day: int) -> tuple[int, int, int]:
    """Convert a single Jalali date to Gregorian.

    Args:
        year: Jalali year.
        month: Jalali month (1-12).
        day: Jalali day.

    Returns:
        Tuple of (gregorian_year, gregorian_month, gregorian_day).
    """
    if _LOOKUP_READY:
        cached = _JALALI_TO_GREGORIAN_LOOKUP.get((year, month, day))
        if cached is not None:
            return cached

    jdate = jdatetime.date(year, month, day)
    gdate = jdate.togregorian()
    return gdate.year, gdate.month, gdate.day

jalali_to_gregorian_vectorized

jalali_to_gregorian_vectorized(year: NDArray[int64], month: NDArray[int64], day: NDArray[int64], *, use_lookup: bool = True) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]

Convert arrays of Jalali dates to Gregorian.

Parameters:

Name Type Description Default
year NDArray[int64]

Array of Jalali years.

required
month NDArray[int64]

Array of Jalali months (1-12).

required
day NDArray[int64]

Array of Jalali days.

required

Returns:

Type Description
tuple[NDArray[int64], NDArray[int64], NDArray[int64]]

Tuple of (gregorian_year, gregorian_month, gregorian_day) arrays.

Source code in jalali_pandas/core/conversion.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def jalali_to_gregorian_vectorized(
    year: NDArray[np.int64],
    month: NDArray[np.int64],
    day: NDArray[np.int64],
    *,
    use_lookup: bool = True,
) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]:
    """Convert arrays of Jalali dates to Gregorian.

    Args:
        year: Array of Jalali years.
        month: Array of Jalali months (1-12).
        day: Array of Jalali days.

    Returns:
        Tuple of (gregorian_year, gregorian_month, gregorian_day) arrays.
    """
    n = len(year)
    if use_lookup and n >= _LOOKUP_MIN_SIZE:
        _ensure_lookup_tables()

    lookup = _JALALI_TO_GREGORIAN_LOOKUP if _LOOKUP_READY else None
    g_year: NDArray[np.int64] = np.empty(n, dtype=np.int64)
    g_month: NDArray[np.int64] = np.empty(n, dtype=np.int64)
    g_day: NDArray[np.int64] = np.empty(n, dtype=np.int64)

    for i in range(n):
        key = (int(year[i]), int(month[i]), int(day[i]))
        if lookup is not None:
            cached = lookup.get(key)
            if cached is not None:
                gy, gm, gd = cached
            else:
                gy, gm, gd = jalali_to_gregorian_scalar(*key)
        else:
            gy, gm, gd = jalali_to_gregorian_scalar(*key)
        g_year[i] = gy
        g_month[i] = gm
        g_day[i] = gd

    return g_year, g_month, g_day