Skip to content

Accessors

Enhanced Series accessor for Jalali datetime operations.

JalaliSeriesAccessor

Enhanced accessor for Jalali datetime operations on pandas Series.

Provides properties and methods for working with Jalali (Persian/Shamsi) dates in pandas Series.

Attributes:

Name Type Description
year Series

Jalali year component.

month Series

Jalali month component (1-12).

day Series

Jalali day component.

hour Series

Hour component (0-23).

minute Series

Minute component (0-59).

second Series

Second component (0-59).

microsecond Series

Microsecond component.

nanosecond Series

Nanosecond component.

quarter Series

Quarter of the year (1-4).

weekday Series

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

dayofweek Series

Alias for weekday.

dayofyear Series

Day of year (1-366).

daysinmonth Series

Number of days in the month.

week Series

Week of year.

weekofyear Series

Alias for week.

is_leap_year Series

Whether the year is a leap year.

is_month_start Series

Whether the date is the first day of the month.

is_month_end Series

Whether the date is the last day of the month.

is_quarter_start Series

Whether the date is the first day of a quarter.

is_quarter_end Series

Whether the date is the last day of a quarter.

is_year_start Series

Whether the date is the first day of the year.

is_year_end Series

Whether the date is the last day of the year.

date Series

Date part (time set to midnight).

time Series

Time part as Python time objects.

Examples:

>>> import pandas as pd
>>> import jalali_pandas
>>> s = pd.Series(pd.date_range("2023-03-21", periods=5))
>>> s.jalali.to_jalali()
>>> s.jalali.year
>>> s.jalali.month_name()
Source code in jalali_pandas/accessors/series.py
 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
@pd.api.extensions.register_series_accessor("jalali")
class JalaliSeriesAccessor:
    """Enhanced accessor for Jalali datetime operations on pandas Series.

    Provides properties and methods for working with Jalali (Persian/Shamsi)
    dates in pandas Series.

    Attributes:
        year: Jalali year component.
        month: Jalali month component (1-12).
        day: Jalali day component.
        hour: Hour component (0-23).
        minute: Minute component (0-59).
        second: Second component (0-59).
        microsecond: Microsecond component.
        nanosecond: Nanosecond component.
        quarter: Quarter of the year (1-4).
        weekday: Day of week (0=Saturday, 6=Friday).
        dayofweek: Alias for weekday.
        dayofyear: Day of year (1-366).
        daysinmonth: Number of days in the month.
        week: Week of year.
        weekofyear: Alias for week.
        is_leap_year: Whether the year is a leap year.
        is_month_start: Whether the date is the first day of the month.
        is_month_end: Whether the date is the last day of the month.
        is_quarter_start: Whether the date is the first day of a quarter.
        is_quarter_end: Whether the date is the last day of a quarter.
        is_year_start: Whether the date is the first day of the year.
        is_year_end: Whether the date is the last day of the year.
        date: Date part (time set to midnight).
        time: Time part as Python time objects.

    Examples:
        >>> import pandas as pd
        >>> import jalali_pandas
        >>> s = pd.Series(pd.date_range("2023-03-21", periods=5))
        >>> s.jalali.to_jalali()
        >>> s.jalali.year
        >>> s.jalali.month_name()
    """

    def __init__(self, pandas_obj: pd.Series) -> None:
        """Initialize the accessor.

        Args:
            pandas_obj: A pandas Series containing datetime data.
        """
        self._obj: pd.Series = pandas_obj

    def _validate(self) -> None:
        """Validate pandas series contains jdatetime objects.

        Raises:
            TypeError: If series doesn't contain jdatetime or string dates.
        """
        if len(self._obj) == 0:
            return
        first_valid = (
            self._obj.dropna().iloc[0] if len(self._obj.dropna()) > 0 else None
        )
        if first_valid is not None and not isinstance(
            first_valid, (str, jdatetime.date, jdatetime.datetime)
        ):
            raise TypeError("pandas series must be jdatetime or string of jdate")

    def _get_jdate_attr(self, attr: str) -> pd.Series:
        """Get an attribute from jdatetime objects in the series.

        Args:
            attr: Attribute name to get.

        Returns:
            Series with the attribute values.
        """
        self._validate()

        def get_attr(x: Any) -> Any:
            if pd.isna(x):
                return np.nan
            val = getattr(x, attr, None)
            if callable(val):
                return val()
            return val

        return cast(pd.Series, self._obj.apply(get_attr))

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

    def to_jalali(self) -> pd.Series:
        """Convert Gregorian datetime to Jalali datetime.

        Returns:
            Series of jdatetime objects.
        """
        return cast(
            pd.Series,
            self._obj.apply(
                lambda x: jdatetime.datetime.fromgregorian(date=x)
                if not pd.isna(x)
                else pd.NaT
            ),
        )

    def to_gregorian(self) -> pd.Series:
        """Convert Jalali datetime to Gregorian datetime.

        Returns:
            Series of Python datetime objects.
        """
        self._validate()
        return cast(
            pd.Series,
            self._obj.apply(lambda x: x.togregorian() if not pd.isna(x) else pd.NaT),
        )

    def parse_jalali(self, format: str = "%Y-%m-%d") -> pd.Series:
        """Parse string dates to jdatetime objects.

        Args:
            format: strftime format string. Defaults to "%Y-%m-%d".

        Returns:
            Series of jdatetime objects.
        """
        return cast(
            pd.Series,
            self._obj.apply(
                lambda x: jdatetime.datetime.strptime(x, format)
                if not pd.isna(x)
                else pd.NaT
            ),
        )

    # -------------------------------------------------------------------------
    # Basic Properties
    # -------------------------------------------------------------------------

    @property
    def year(self) -> pd.Series:
        """Get Jalali year."""
        return self._get_jdate_attr("year")

    @property
    def month(self) -> pd.Series:
        """Get Jalali month (1-12)."""
        return self._get_jdate_attr("month")

    @property
    def day(self) -> pd.Series:
        """Get Jalali day."""
        return self._get_jdate_attr("day")

    @property
    def hour(self) -> pd.Series:
        """Get hour component (0-23)."""
        return self._get_jdate_attr("hour")

    @property
    def minute(self) -> pd.Series:
        """Get minute component (0-59)."""
        return self._get_jdate_attr("minute")

    @property
    def second(self) -> pd.Series:
        """Get second component (0-59)."""
        return self._get_jdate_attr("second")

    @property
    def microsecond(self) -> pd.Series:
        """Get microsecond component."""
        return self._get_jdate_attr("microsecond")

    @property
    def nanosecond(self) -> pd.Series:
        """Get nanosecond component (always 0 for jdatetime)."""
        self._validate()
        return cast(
            pd.Series,
            self._obj.apply(lambda x: 0 if not pd.isna(x) else np.nan),
        )

    @property
    def weekday(self) -> pd.Series:
        """Get Jalali weekday (0=Saturday, 6=Friday)."""
        return self._get_jdate_attr("weekday")

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

    @property
    def weeknumber(self) -> pd.Series:
        """Get week number of the year."""
        return self._get_jdate_attr("weeknumber")

    @property
    def week(self) -> pd.Series:
        """Get week number of the year."""
        self._validate()

        def get_week(x: Any) -> Any:
            if pd.isna(x):
                return np.nan
            return week_of_year(x.year, x.month, x.day)

        return cast(pd.Series, self._obj.apply(get_week))

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

    @property
    def quarter(self) -> pd.Series:
        """Get Jalali quarter (1-4)."""
        self._validate()

        def get_quarter(x: Any) -> Any:
            if pd.isna(x):
                return np.nan
            return quarter_of_month(x.month)

        return cast(pd.Series, self._obj.apply(get_quarter))

    @property
    def dayofyear(self) -> pd.Series:
        """Get day of year (1-366)."""
        self._validate()

        def get_dayofyear(x: Any) -> Any:
            if pd.isna(x):
                return np.nan
            return day_of_year(x.year, x.month, x.day)

        return cast(pd.Series, self._obj.apply(get_dayofyear))

    @property
    def daysinmonth(self) -> pd.Series:
        """Get number of days in the month."""
        self._validate()

        def get_daysinmonth(x: Any) -> Any:
            if pd.isna(x):
                return np.nan
            return days_in_month(x.year, x.month)

        return cast(pd.Series, self._obj.apply(get_daysinmonth))

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

    # -------------------------------------------------------------------------
    # Boolean Properties
    # -------------------------------------------------------------------------

    @property
    def is_leap_year(self) -> pd.Series:
        """Check if the year is a leap year."""
        self._validate()

        def check_leap(x: Any) -> Any:
            if pd.isna(x):
                return False
            return is_leap_year(x.year)

        return cast(pd.Series, self._obj.apply(check_leap))

    @property
    def is_month_start(self) -> pd.Series:
        """Check if the date is the first day of the month."""
        self._validate()

        def check_month_start(x: Any) -> Any:
            if pd.isna(x):
                return False
            return x.day == 1

        return cast(pd.Series, self._obj.apply(check_month_start))

    @property
    def is_month_end(self) -> pd.Series:
        """Check if the date is the last day of the month."""
        self._validate()

        def check_month_end(x: Any) -> Any:
            if pd.isna(x):
                return False
            return x.day == days_in_month(x.year, x.month)

        return cast(pd.Series, self._obj.apply(check_month_end))

    @property
    def is_quarter_start(self) -> pd.Series:
        """Check if the date is the first day of a quarter."""
        self._validate()

        def check_quarter_start(x: Any) -> Any:
            if pd.isna(x):
                return False
            return x.month in (1, 4, 7, 10) and x.day == 1

        return cast(pd.Series, self._obj.apply(check_quarter_start))

    @property
    def is_quarter_end(self) -> pd.Series:
        """Check if the date is the last day of a quarter."""
        self._validate()

        def check_quarter_end(x: Any) -> Any:
            if pd.isna(x):
                return False
            if x.month not in (3, 6, 9, 12):
                return False
            return x.day == days_in_month(x.year, x.month)

        return cast(pd.Series, self._obj.apply(check_quarter_end))

    @property
    def is_year_start(self) -> pd.Series:
        """Check if the date is the first day of the year (Nowruz)."""
        self._validate()

        def check_year_start(x: Any) -> Any:
            if pd.isna(x):
                return False
            return x.month == 1 and x.day == 1

        return cast(pd.Series, self._obj.apply(check_year_start))

    @property
    def is_year_end(self) -> pd.Series:
        """Check if the date is the last day of the year."""
        self._validate()

        def check_year_end(x: Any) -> Any:
            if pd.isna(x):
                return False
            return x.month == 12 and x.day == days_in_month(x.year, 12)

        return cast(pd.Series, self._obj.apply(check_year_end))

    # -------------------------------------------------------------------------
    # Date/Time Properties
    # -------------------------------------------------------------------------

    @property
    def date(self) -> pd.Series:
        """Get date part (time set to midnight)."""
        self._validate()

        def get_date(x: Any) -> Any:
            if pd.isna(x):
                return pd.NaT
            if isinstance(x, jdatetime.datetime):
                return jdatetime.datetime(x.year, x.month, x.day)
            return x

        return cast(pd.Series, self._obj.apply(get_date))

    @property
    def time(self) -> pd.Series:
        """Get time part as Python time objects."""
        self._validate()

        def get_time(x: Any) -> Any:
            if pd.isna(x):
                return None
            if isinstance(x, jdatetime.datetime):
                return time(
                    hour=x.hour,
                    minute=x.minute,
                    second=x.second,
                    microsecond=x.microsecond,
                )
            return time(0, 0, 0)

        return cast(pd.Series, self._obj.apply(get_time))

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

    def strftime(self, date_format: str) -> pd.Series:
        """Format dates as strings.

        Args:
            date_format: strftime format string.

        Returns:
            Series of formatted strings.
        """
        self._validate()

        def format_date(x: Any) -> Any:
            if pd.isna(x):
                return None
            return x.strftime(date_format)

        return cast(pd.Series, self._obj.apply(format_date))

    def month_name(self, locale: Literal["fa", "en"] = "en") -> pd.Series:
        """Get month names.

        Args:
            locale: Language for month names. 'fa' for Persian, 'en' for English.
                Defaults to 'en'.

        Returns:
            Series of month names.
        """
        self._validate()
        names = MONTH_NAMES_FA if locale == "fa" else MONTH_NAMES_EN

        def get_month_name(x: Any) -> Any:
            if pd.isna(x):
                return None
            return names[x.month - 1]

        return cast(pd.Series, self._obj.apply(get_month_name))

    def day_name(self, locale: Literal["fa", "en"] = "en") -> pd.Series:
        """Get day names.

        Args:
            locale: Language for day names. 'fa' for Persian, 'en' for English.
                Defaults to 'en'.

        Returns:
            Series of day names.
        """
        self._validate()
        names = WEEKDAY_NAMES_FA if locale == "fa" else WEEKDAY_NAMES_EN

        def get_day_name(x: Any) -> Any:
            if pd.isna(x):
                return None
            wd = weekday_of_jalali(x.year, x.month, x.day)
            return names[wd]

        return cast(pd.Series, self._obj.apply(get_day_name))

    # -------------------------------------------------------------------------
    # Normalization Methods
    # -------------------------------------------------------------------------

    def normalize(self) -> pd.Series:
        """Normalize dates to midnight.

        Returns:
            Series with time components set to zero.
        """
        self._validate()

        def normalize_date(x: Any) -> Any:
            if pd.isna(x):
                return pd.NaT
            if isinstance(x, jdatetime.datetime):
                return jdatetime.datetime(x.year, x.month, x.day)
            return x

        return cast(pd.Series, self._obj.apply(normalize_date))

    def floor(self, freq: str) -> pd.Series:
        """Floor dates to specified frequency.

        Args:
            freq: Frequency string. Supported: 'D' (day), 'h' (hour),
                'min' (minute), 's' (second).

        Returns:
            Series with floored dates.
        """
        self._validate()

        def floor_date(x: Any) -> Any:
            if pd.isna(x):
                return pd.NaT
            if not isinstance(x, jdatetime.datetime):
                return x

            if freq in ("D", "d"):
                return jdatetime.datetime(x.year, x.month, x.day)
            elif freq in ("h", "H"):
                return jdatetime.datetime(x.year, x.month, x.day, x.hour)
            elif freq in ("min", "T"):
                return jdatetime.datetime(x.year, x.month, x.day, x.hour, x.minute)
            elif freq in ("s", "S"):
                return jdatetime.datetime(
                    x.year, x.month, x.day, x.hour, x.minute, x.second
                )
            else:
                raise ValueError(f"Unsupported frequency: {freq}")

        return cast(pd.Series, self._obj.apply(floor_date))

    def ceil(self, freq: str) -> pd.Series:
        """Ceil dates to specified frequency.

        Args:
            freq: Frequency string. Supported: 'D' (day), 'h' (hour),
                'min' (minute), 's' (second).

        Returns:
            Series with ceiled dates.
        """
        self._validate()

        def ceil_date(x: Any) -> Any:
            if pd.isna(x):
                return pd.NaT
            if not isinstance(x, jdatetime.datetime):
                return x

            if freq in ("D", "d"):
                if x.hour > 0 or x.minute > 0 or x.second > 0 or x.microsecond > 0:
                    # Add one day
                    greg = x.togregorian()
                    greg = greg + pd.Timedelta(days=1)
                    new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                    return jdatetime.datetime(new_j.year, new_j.month, new_j.day)
                return jdatetime.datetime(x.year, x.month, x.day)
            elif freq in ("h", "H"):
                if x.minute > 0 or x.second > 0 or x.microsecond > 0:
                    greg = x.togregorian()
                    greg = greg + pd.Timedelta(hours=1)
                    new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                    return jdatetime.datetime(
                        new_j.year, new_j.month, new_j.day, new_j.hour
                    )
                return jdatetime.datetime(x.year, x.month, x.day, x.hour)
            elif freq in ("min", "T"):
                if x.second > 0 or x.microsecond > 0:
                    greg = x.togregorian()
                    greg = greg + pd.Timedelta(minutes=1)
                    new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                    return jdatetime.datetime(
                        new_j.year, new_j.month, new_j.day, new_j.hour, new_j.minute
                    )
                return jdatetime.datetime(x.year, x.month, x.day, x.hour, x.minute)
            elif freq in ("s", "S"):
                if x.microsecond > 0:
                    greg = x.togregorian()
                    greg = greg + pd.Timedelta(seconds=1)
                    new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                    return jdatetime.datetime(
                        new_j.year,
                        new_j.month,
                        new_j.day,
                        new_j.hour,
                        new_j.minute,
                        new_j.second,
                    )
                return jdatetime.datetime(
                    x.year, x.month, x.day, x.hour, x.minute, x.second
                )
            else:
                raise ValueError(f"Unsupported frequency: {freq}")

        return cast(pd.Series, self._obj.apply(ceil_date))

    def round(self, freq: str) -> pd.Series:
        """Round dates to specified frequency.

        Args:
            freq: Frequency string. Supported: 'D' (day), 'h' (hour),
                'min' (minute), 's' (second).

        Returns:
            Series with rounded dates.
        """
        self._validate()

        def round_date(x: Any) -> Any:
            if pd.isna(x):
                return pd.NaT
            if not isinstance(x, jdatetime.datetime):
                return x

            if freq in ("D", "d"):
                # Round to nearest day (12:00 is the midpoint)
                if x.hour >= 12:
                    greg = x.togregorian()
                    greg = greg + pd.Timedelta(days=1)
                    new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                    return jdatetime.datetime(new_j.year, new_j.month, new_j.day)
                return jdatetime.datetime(x.year, x.month, x.day)
            elif freq in ("h", "H"):
                if x.minute >= 30:
                    greg = x.togregorian()
                    greg = greg + pd.Timedelta(hours=1)
                    new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                    return jdatetime.datetime(
                        new_j.year, new_j.month, new_j.day, new_j.hour
                    )
                return jdatetime.datetime(x.year, x.month, x.day, x.hour)
            elif freq in ("min", "T"):
                if x.second >= 30:
                    greg = x.togregorian()
                    greg = greg + pd.Timedelta(minutes=1)
                    new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                    return jdatetime.datetime(
                        new_j.year, new_j.month, new_j.day, new_j.hour, new_j.minute
                    )
                return jdatetime.datetime(x.year, x.month, x.day, x.hour, x.minute)
            elif freq in ("s", "S"):
                if x.microsecond >= 500000:
                    greg = x.togregorian()
                    greg = greg + pd.Timedelta(seconds=1)
                    new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                    return jdatetime.datetime(
                        new_j.year,
                        new_j.month,
                        new_j.day,
                        new_j.hour,
                        new_j.minute,
                        new_j.second,
                    )
                return jdatetime.datetime(
                    x.year, x.month, x.day, x.hour, x.minute, x.second
                )
            else:
                raise ValueError(f"Unsupported frequency: {freq}")

        return cast(pd.Series, self._obj.apply(round_date))

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

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

        This converts the jdatetime objects to Gregorian, localizes them,
        and returns the localized Gregorian datetimes.

        Args:
            tz: Timezone to localize to.
            ambiguous: How to handle ambiguous times. Defaults to 'raise'.
            nonexistent: How to handle nonexistent times. Defaults to 'raise'.

        Returns:
            Series of timezone-aware Gregorian datetimes.
        """
        self._validate()
        gregorian = self.to_gregorian()
        dt_index = pd.DatetimeIndex(pd.to_datetime(gregorian))
        localized = dt_index.tz_localize(
            tz, ambiguous=ambiguous, nonexistent=nonexistent
        )
        return cast(
            pd.Series,
            pd.Series(localized, index=self._obj.index, name=self._obj.name),
        )

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

        This converts the jdatetime objects to Gregorian, converts timezone,
        and returns the converted Gregorian datetimes.

        Args:
            tz: Target timezone.

        Returns:
            Series of timezone-converted Gregorian datetimes.
        """
        self._validate()
        gregorian = self.to_gregorian()
        dt_index = pd.DatetimeIndex(pd.to_datetime(gregorian))
        converted = dt_index.tz_convert(tz)
        return cast(
            pd.Series,
            pd.Series(converted, index=self._obj.index, name=self._obj.name),
        )

date property

date: Series

Get date part (time set to midnight).

day property

day: Series

Get Jalali day.

dayofweek property

dayofweek: Series

Alias for weekday.

dayofyear property

dayofyear: Series

Get day of year (1-366).

days_in_month property

days_in_month: Series

Alias for daysinmonth.

daysinmonth property

daysinmonth: Series

Get number of days in the month.

hour property

hour: Series

Get hour component (0-23).

is_leap_year property

is_leap_year: Series

Check if the year is a leap year.

is_month_end property

is_month_end: Series

Check if the date is the last day of the month.

is_month_start property

is_month_start: Series

Check if the date is the first day of the month.

is_quarter_end property

is_quarter_end: Series

Check if the date is the last day of a quarter.

is_quarter_start property

is_quarter_start: Series

Check if the date is the first day of a quarter.

is_year_end property

is_year_end: Series

Check if the date is the last day of the year.

is_year_start property

is_year_start: Series

Check if the date is the first day of the year (Nowruz).

microsecond property

microsecond: Series

Get microsecond component.

minute property

minute: Series

Get minute component (0-59).

month property

month: Series

Get Jalali month (1-12).

nanosecond property

nanosecond: Series

Get nanosecond component (always 0 for jdatetime).

quarter property

quarter: Series

Get Jalali quarter (1-4).

second property

second: Series

Get second component (0-59).

time property

time: Series

Get time part as Python time objects.

week property

week: Series

Get week number of the year.

weekday property

weekday: Series

Get Jalali weekday (0=Saturday, 6=Friday).

weeknumber property

weeknumber: Series

Get week number of the year.

weekofyear property

weekofyear: Series

Alias for week.

year property

year: Series

Get Jalali year.

__init__

__init__(pandas_obj: Series) -> None

Initialize the accessor.

Parameters:

Name Type Description Default
pandas_obj Series

A pandas Series containing datetime data.

required
Source code in jalali_pandas/accessors/series.py
69
70
71
72
73
74
75
def __init__(self, pandas_obj: pd.Series) -> None:
    """Initialize the accessor.

    Args:
        pandas_obj: A pandas Series containing datetime data.
    """
    self._obj: pd.Series = pandas_obj

ceil

ceil(freq: str) -> pd.Series

Ceil dates to specified frequency.

Parameters:

Name Type Description Default
freq str

Frequency string. Supported: 'D' (day), 'h' (hour), 'min' (minute), 's' (second).

required

Returns:

Type Description
Series

Series with ceiled dates.

Source code in jalali_pandas/accessors/series.py
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
def ceil(self, freq: str) -> pd.Series:
    """Ceil dates to specified frequency.

    Args:
        freq: Frequency string. Supported: 'D' (day), 'h' (hour),
            'min' (minute), 's' (second).

    Returns:
        Series with ceiled dates.
    """
    self._validate()

    def ceil_date(x: Any) -> Any:
        if pd.isna(x):
            return pd.NaT
        if not isinstance(x, jdatetime.datetime):
            return x

        if freq in ("D", "d"):
            if x.hour > 0 or x.minute > 0 or x.second > 0 or x.microsecond > 0:
                # Add one day
                greg = x.togregorian()
                greg = greg + pd.Timedelta(days=1)
                new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                return jdatetime.datetime(new_j.year, new_j.month, new_j.day)
            return jdatetime.datetime(x.year, x.month, x.day)
        elif freq in ("h", "H"):
            if x.minute > 0 or x.second > 0 or x.microsecond > 0:
                greg = x.togregorian()
                greg = greg + pd.Timedelta(hours=1)
                new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                return jdatetime.datetime(
                    new_j.year, new_j.month, new_j.day, new_j.hour
                )
            return jdatetime.datetime(x.year, x.month, x.day, x.hour)
        elif freq in ("min", "T"):
            if x.second > 0 or x.microsecond > 0:
                greg = x.togregorian()
                greg = greg + pd.Timedelta(minutes=1)
                new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                return jdatetime.datetime(
                    new_j.year, new_j.month, new_j.day, new_j.hour, new_j.minute
                )
            return jdatetime.datetime(x.year, x.month, x.day, x.hour, x.minute)
        elif freq in ("s", "S"):
            if x.microsecond > 0:
                greg = x.togregorian()
                greg = greg + pd.Timedelta(seconds=1)
                new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                return jdatetime.datetime(
                    new_j.year,
                    new_j.month,
                    new_j.day,
                    new_j.hour,
                    new_j.minute,
                    new_j.second,
                )
            return jdatetime.datetime(
                x.year, x.month, x.day, x.hour, x.minute, x.second
            )
        else:
            raise ValueError(f"Unsupported frequency: {freq}")

    return cast(pd.Series, self._obj.apply(ceil_date))

day_name

day_name(locale: Literal['fa', 'en'] = 'en') -> pd.Series

Get day names.

Parameters:

Name Type Description Default
locale Literal['fa', 'en']

Language for day names. 'fa' for Persian, 'en' for English. Defaults to 'en'.

'en'

Returns:

Type Description
Series

Series of day names.

Source code in jalali_pandas/accessors/series.py
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
def day_name(self, locale: Literal["fa", "en"] = "en") -> pd.Series:
    """Get day names.

    Args:
        locale: Language for day names. 'fa' for Persian, 'en' for English.
            Defaults to 'en'.

    Returns:
        Series of day names.
    """
    self._validate()
    names = WEEKDAY_NAMES_FA if locale == "fa" else WEEKDAY_NAMES_EN

    def get_day_name(x: Any) -> Any:
        if pd.isna(x):
            return None
        wd = weekday_of_jalali(x.year, x.month, x.day)
        return names[wd]

    return cast(pd.Series, self._obj.apply(get_day_name))

floor

floor(freq: str) -> pd.Series

Floor dates to specified frequency.

Parameters:

Name Type Description Default
freq str

Frequency string. Supported: 'D' (day), 'h' (hour), 'min' (minute), 's' (second).

required

Returns:

Type Description
Series

Series with floored dates.

Source code in jalali_pandas/accessors/series.py
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
def floor(self, freq: str) -> pd.Series:
    """Floor dates to specified frequency.

    Args:
        freq: Frequency string. Supported: 'D' (day), 'h' (hour),
            'min' (minute), 's' (second).

    Returns:
        Series with floored dates.
    """
    self._validate()

    def floor_date(x: Any) -> Any:
        if pd.isna(x):
            return pd.NaT
        if not isinstance(x, jdatetime.datetime):
            return x

        if freq in ("D", "d"):
            return jdatetime.datetime(x.year, x.month, x.day)
        elif freq in ("h", "H"):
            return jdatetime.datetime(x.year, x.month, x.day, x.hour)
        elif freq in ("min", "T"):
            return jdatetime.datetime(x.year, x.month, x.day, x.hour, x.minute)
        elif freq in ("s", "S"):
            return jdatetime.datetime(
                x.year, x.month, x.day, x.hour, x.minute, x.second
            )
        else:
            raise ValueError(f"Unsupported frequency: {freq}")

    return cast(pd.Series, self._obj.apply(floor_date))

month_name

month_name(locale: Literal['fa', 'en'] = 'en') -> pd.Series

Get month names.

Parameters:

Name Type Description Default
locale Literal['fa', 'en']

Language for month names. 'fa' for Persian, 'en' for English. Defaults to 'en'.

'en'

Returns:

Type Description
Series

Series of month names.

Source code in jalali_pandas/accessors/series.py
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
def month_name(self, locale: Literal["fa", "en"] = "en") -> pd.Series:
    """Get month names.

    Args:
        locale: Language for month names. 'fa' for Persian, 'en' for English.
            Defaults to 'en'.

    Returns:
        Series of month names.
    """
    self._validate()
    names = MONTH_NAMES_FA if locale == "fa" else MONTH_NAMES_EN

    def get_month_name(x: Any) -> Any:
        if pd.isna(x):
            return None
        return names[x.month - 1]

    return cast(pd.Series, self._obj.apply(get_month_name))

normalize

normalize() -> pd.Series

Normalize dates to midnight.

Returns:

Type Description
Series

Series with time components set to zero.

Source code in jalali_pandas/accessors/series.py
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
def normalize(self) -> pd.Series:
    """Normalize dates to midnight.

    Returns:
        Series with time components set to zero.
    """
    self._validate()

    def normalize_date(x: Any) -> Any:
        if pd.isna(x):
            return pd.NaT
        if isinstance(x, jdatetime.datetime):
            return jdatetime.datetime(x.year, x.month, x.day)
        return x

    return cast(pd.Series, self._obj.apply(normalize_date))

parse_jalali

parse_jalali(format: str = '%Y-%m-%d') -> pd.Series

Parse string dates to jdatetime objects.

Parameters:

Name Type Description Default
format str

strftime format string. Defaults to "%Y-%m-%d".

'%Y-%m-%d'

Returns:

Type Description
Series

Series of jdatetime objects.

Source code in jalali_pandas/accessors/series.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def parse_jalali(self, format: str = "%Y-%m-%d") -> pd.Series:
    """Parse string dates to jdatetime objects.

    Args:
        format: strftime format string. Defaults to "%Y-%m-%d".

    Returns:
        Series of jdatetime objects.
    """
    return cast(
        pd.Series,
        self._obj.apply(
            lambda x: jdatetime.datetime.strptime(x, format)
            if not pd.isna(x)
            else pd.NaT
        ),
    )

round

round(freq: str) -> pd.Series

Round dates to specified frequency.

Parameters:

Name Type Description Default
freq str

Frequency string. Supported: 'D' (day), 'h' (hour), 'min' (minute), 's' (second).

required

Returns:

Type Description
Series

Series with rounded dates.

Source code in jalali_pandas/accessors/series.py
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
def round(self, freq: str) -> pd.Series:
    """Round dates to specified frequency.

    Args:
        freq: Frequency string. Supported: 'D' (day), 'h' (hour),
            'min' (minute), 's' (second).

    Returns:
        Series with rounded dates.
    """
    self._validate()

    def round_date(x: Any) -> Any:
        if pd.isna(x):
            return pd.NaT
        if not isinstance(x, jdatetime.datetime):
            return x

        if freq in ("D", "d"):
            # Round to nearest day (12:00 is the midpoint)
            if x.hour >= 12:
                greg = x.togregorian()
                greg = greg + pd.Timedelta(days=1)
                new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                return jdatetime.datetime(new_j.year, new_j.month, new_j.day)
            return jdatetime.datetime(x.year, x.month, x.day)
        elif freq in ("h", "H"):
            if x.minute >= 30:
                greg = x.togregorian()
                greg = greg + pd.Timedelta(hours=1)
                new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                return jdatetime.datetime(
                    new_j.year, new_j.month, new_j.day, new_j.hour
                )
            return jdatetime.datetime(x.year, x.month, x.day, x.hour)
        elif freq in ("min", "T"):
            if x.second >= 30:
                greg = x.togregorian()
                greg = greg + pd.Timedelta(minutes=1)
                new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                return jdatetime.datetime(
                    new_j.year, new_j.month, new_j.day, new_j.hour, new_j.minute
                )
            return jdatetime.datetime(x.year, x.month, x.day, x.hour, x.minute)
        elif freq in ("s", "S"):
            if x.microsecond >= 500000:
                greg = x.togregorian()
                greg = greg + pd.Timedelta(seconds=1)
                new_j = jdatetime.datetime.fromgregorian(datetime=greg)
                return jdatetime.datetime(
                    new_j.year,
                    new_j.month,
                    new_j.day,
                    new_j.hour,
                    new_j.minute,
                    new_j.second,
                )
            return jdatetime.datetime(
                x.year, x.month, x.day, x.hour, x.minute, x.second
            )
        else:
            raise ValueError(f"Unsupported frequency: {freq}")

    return cast(pd.Series, self._obj.apply(round_date))

strftime

strftime(date_format: str) -> pd.Series

Format dates as strings.

Parameters:

Name Type Description Default
date_format str

strftime format string.

required

Returns:

Type Description
Series

Series of formatted strings.

Source code in jalali_pandas/accessors/series.py
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
def strftime(self, date_format: str) -> pd.Series:
    """Format dates as strings.

    Args:
        date_format: strftime format string.

    Returns:
        Series of formatted strings.
    """
    self._validate()

    def format_date(x: Any) -> Any:
        if pd.isna(x):
            return None
        return x.strftime(date_format)

    return cast(pd.Series, self._obj.apply(format_date))

to_gregorian

to_gregorian() -> pd.Series

Convert Jalali datetime to Gregorian datetime.

Returns:

Type Description
Series

Series of Python datetime objects.

Source code in jalali_pandas/accessors/series.py
133
134
135
136
137
138
139
140
141
142
143
def to_gregorian(self) -> pd.Series:
    """Convert Jalali datetime to Gregorian datetime.

    Returns:
        Series of Python datetime objects.
    """
    self._validate()
    return cast(
        pd.Series,
        self._obj.apply(lambda x: x.togregorian() if not pd.isna(x) else pd.NaT),
    )

to_jalali

to_jalali() -> pd.Series

Convert Gregorian datetime to Jalali datetime.

Returns:

Type Description
Series

Series of jdatetime objects.

Source code in jalali_pandas/accessors/series.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def to_jalali(self) -> pd.Series:
    """Convert Gregorian datetime to Jalali datetime.

    Returns:
        Series of jdatetime objects.
    """
    return cast(
        pd.Series,
        self._obj.apply(
            lambda x: jdatetime.datetime.fromgregorian(date=x)
            if not pd.isna(x)
            else pd.NaT
        ),
    )

tz_convert

tz_convert(tz: tzinfo | str | None) -> pd.Series

Convert tz-aware dates to another timezone.

This converts the jdatetime objects to Gregorian, converts timezone, and returns the converted Gregorian datetimes.

Parameters:

Name Type Description Default
tz tzinfo | str | None

Target timezone.

required

Returns:

Type Description
Series

Series of timezone-converted Gregorian datetimes.

Source code in jalali_pandas/accessors/series.py
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
def tz_convert(self, tz: dt_tzinfo | str | None) -> pd.Series:
    """Convert tz-aware dates to another timezone.

    This converts the jdatetime objects to Gregorian, converts timezone,
    and returns the converted Gregorian datetimes.

    Args:
        tz: Target timezone.

    Returns:
        Series of timezone-converted Gregorian datetimes.
    """
    self._validate()
    gregorian = self.to_gregorian()
    dt_index = pd.DatetimeIndex(pd.to_datetime(gregorian))
    converted = dt_index.tz_convert(tz)
    return cast(
        pd.Series,
        pd.Series(converted, index=self._obj.index, name=self._obj.name),
    )

tz_localize

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

Localize tz-naive dates to a timezone.

This converts the jdatetime objects to Gregorian, localizes them, and returns the localized Gregorian datetimes.

Parameters:

Name Type Description Default
tz tzinfo | str | None

Timezone to localize to.

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
Series

Series of timezone-aware Gregorian datetimes.

Source code in jalali_pandas/accessors/series.py
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
def tz_localize(
    self,
    tz: dt_tzinfo | str | None,
    ambiguous: str = "raise",
    nonexistent: str = "raise",
) -> pd.Series:
    """Localize tz-naive dates to a timezone.

    This converts the jdatetime objects to Gregorian, localizes them,
    and returns the localized Gregorian datetimes.

    Args:
        tz: Timezone to localize to.
        ambiguous: How to handle ambiguous times. Defaults to 'raise'.
        nonexistent: How to handle nonexistent times. Defaults to 'raise'.

    Returns:
        Series of timezone-aware Gregorian datetimes.
    """
    self._validate()
    gregorian = self.to_gregorian()
    dt_index = pd.DatetimeIndex(pd.to_datetime(gregorian))
    localized = dt_index.tz_localize(
        tz, ambiguous=ambiguous, nonexistent=nonexistent
    )
    return cast(
        pd.Series,
        pd.Series(localized, index=self._obj.index, name=self._obj.name),
    )

Enhanced DataFrame accessor for Jalali datetime operations.

JalaliDataFrameAccessor

Enhanced accessor for Jalali datetime operations on pandas DataFrames.

Provides methods for working with Jalali (Persian/Shamsi) dates in pandas DataFrames, including groupby, resample, and column conversion.

Attributes:

Name Type Description
jdate str

Name of the detected Jalali date column.

columns Index[Any]

DataFrame columns.

Examples:

>>> import pandas as pd
>>> import jalali_pandas
>>> df = pd.DataFrame({
...     'date': pd.date_range('2023-03-21', periods=5),
...     'value': [1, 2, 3, 4, 5]
... })
>>> df['jdate'] = df['date'].jalali.to_jalali()
>>> df.jalali.groupby('month').sum()
Source code in jalali_pandas/accessors/dataframe.py
 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
@pd.api.extensions.register_dataframe_accessor("jalali")
class JalaliDataFrameAccessor:
    """Enhanced accessor for Jalali datetime operations on pandas DataFrames.

    Provides methods for working with Jalali (Persian/Shamsi) dates in
    pandas DataFrames, including groupby, resample, and column conversion.

    Attributes:
        jdate: Name of the detected Jalali date column.
        columns: DataFrame columns.

    Examples:
        >>> import pandas as pd
        >>> import jalali_pandas
        >>> df = pd.DataFrame({
        ...     'date': pd.date_range('2023-03-21', periods=5),
        ...     'value': [1, 2, 3, 4, 5]
        ... })
        >>> df['jdate'] = df['date'].jalali.to_jalali()
        >>> df.jalali.groupby('month').sum()
    """

    TEMP_COLUMNS: list[str] = [
        "__year",
        "__month",
        "__quarter",
        "__weekday",
        "__day",
        "__week",
        "__dayofyear",
    ]

    def __init__(self, pandas_obj: pd.DataFrame) -> None:
        """Initialize the accessor.

        Args:
            pandas_obj: A pandas DataFrame containing Jalali datetime data.
        """
        self._obj: pd.DataFrame = pandas_obj
        self.columns: pd.Index[Any] = self._obj.columns
        self.jdate: str = "jdate"
        self._validate()

    def _validate(self) -> None:
        """Check if the DataFrame has a jdatetime column.

        Raises:
            ValueError: If no jdatetime column is found.
        """
        for col in self.columns:
            if len(self._obj) > 0:
                first_val = self._obj[col].iloc[0]
                if isinstance(first_val, (jdatetime.date, jdatetime.datetime)):
                    self.jdate = str(col)
                    return
        raise ValueError("No jdatetime column found in the dataframe.")

    def set_date_column(self, column: str) -> JalaliDataFrameAccessor:
        """Set the Jalali date column to use for operations.

        Args:
            column: Name of the column containing Jalali dates.

        Returns:
            Self for method chaining.

        Raises:
            ValueError: If column doesn't exist or doesn't contain jdatetime.
        """
        if column not in self.columns:
            raise ValueError(f"Column '{column}' not found in DataFrame.")

        if len(self._obj) > 0:
            first_val = self._obj[column].iloc[0]
            if not isinstance(first_val, (jdatetime.date, jdatetime.datetime)):
                raise ValueError(
                    f"Column '{column}' does not contain jdatetime objects."
                )

        self.jdate = column
        return self

    @property
    def _df(self) -> pd.DataFrame:
        """Generate temp DataFrame with extracted date components.

        Returns:
            DataFrame with year, month, day, quarter, weekday columns.
        """
        df = self._obj.copy()
        df["__year"] = df[self.jdate].jalali.year
        df["__month"] = df[self.jdate].jalali.month
        df["__day"] = df[self.jdate].jalali.day
        df["__quarter"] = df[self.jdate].jalali.quarter
        df["__weekday"] = df[self.jdate].jalali.weekday
        df["__week"] = df[self.jdate].jalali.week
        df["__dayofyear"] = df[self.jdate].jalali.dayofyear
        return df

    def _clean_groupby(self, group: DataFrameGroupByT) -> DataFrameGroupByT:
        """Clean the groupby object by removing temp columns.

        Args:
            group: The groupby object to clean.

        Returns:
            Cleaned groupby object with only original columns.
        """
        numeric_columns: pd.Index[Any] = cast(
            pd.DataFrame, self._obj.select_dtypes(include="number")
        ).columns
        remaining_columns = [
            col
            for col in numeric_columns
            if col in set(self.columns).difference(self.TEMP_COLUMNS)
        ]
        if not remaining_columns:
            return group
        return cast(DataFrameGroupByT, group[remaining_columns])

    def groupby(self, grouper: str = "md") -> DataFrameGroupByT:
        """Group by Jalali date components.

        Args:
            grouper: Grouping key. Options:
                - 'year': Group by year
                - 'month': Group by month
                - 'day': Group by day
                - 'week': Group by week number
                - 'dayofweek': Group by day of week
                - 'dayofmonth': Group by day of month (alias for 'day')
                - 'quarter': Group by quarter
                - 'dayofyear': Group by day of year
                - 'ym': Group by year and month
                - 'yq': Group by year and quarter
                - 'ymd': Group by year, month, and day
                - 'md': Group by month and day (default)

        Returns:
            DataFrameGroupBy object.

        Raises:
            ValueError: If grouper is not a valid option.
        """
        possible_keys = [
            "year",
            "month",
            "day",
            "week",
            "dayofweek",
            "dayofmonth",
            "quarter",
            "dayofyear",
            "ym",
            "yq",
            "ymd",
            "md",
        ]
        df = self._df
        if grouper not in possible_keys:
            raise ValueError(
                f"{grouper} is not a valid groupby type. Choose from {possible_keys}"
            )

        keys: dict[str, list[str]] = {
            "md": ["month", "day"],
            "ym": ["year", "month"],
            "yq": ["year", "quarter"],
            "ymd": ["year", "month", "day"],
        }
        grouper_cols: list[str]
        if grouper in keys:
            grouper_cols = [f"__{g}" for g in keys[grouper]]
        elif grouper == "dayofmonth":
            grouper_cols = ["__day"]
        else:
            grouper_cols = [f"__{grouper}"]

        group = cast(DataFrameGroupByT, df.groupby(grouper_cols))
        return self._clean_groupby(group)

    def resample(self, resample_type: str) -> pd.DataFrame:
        """Resample by Jalali frequency.

        Groups the DataFrame by Jalali calendar periods and aggregates.

        Args:
            resample_type: The resample frequency. Options:
                - 'month': Group by Jalali month
                - 'quarter': Group by Jalali quarter
                - 'year': Group by Jalali year
                - 'week': Group by Jalali week

        Returns:
            DataFrame with aggregated values grouped by the specified period.

        Raises:
            ValueError: If resample_type is not valid.

        Examples:
            >>> df.jalali.resample('month')
            >>> df.jalali.resample('quarter')
        """
        valid_types = ["month", "quarter", "year", "week"]
        if resample_type not in valid_types:
            raise ValueError(
                f"{resample_type} is not a valid resample type. "
                f"Choose from {valid_types}"
            )

        type_to_groupby: dict[str, str] = {
            "month": "ym",
            "quarter": "yq",
            "year": "year",
            "week": "week",
        }

        groupby_key = type_to_groupby[resample_type]
        return self.groupby(groupby_key).sum(numeric_only=True).reset_index(drop=True)

    def convert_columns(
        self,
        columns: list[str] | str,
        to_jalali: bool = True,
        format: str = "%Y-%m-%d",
    ) -> pd.DataFrame:
        """Convert date columns between Jalali and Gregorian.

        Args:
            columns: Column name(s) to convert.
            to_jalali: If True, convert Gregorian to Jalali.
                If False, convert Jalali to Gregorian. Defaults to True.
            format: Format string for parsing string dates. Defaults to "%Y-%m-%d".

        Returns:
            DataFrame with converted columns.

        Examples:
            >>> df.jalali.convert_columns('date', to_jalali=True)
            >>> df.jalali.convert_columns(['date1', 'date2'], to_jalali=False)
        """
        df = self._obj.copy()

        if isinstance(columns, str):
            columns = [columns]

        for col in columns:
            if col not in df.columns:
                raise ValueError(f"Column '{col}' not found in DataFrame.")

            if to_jalali:
                df[col] = df[col].jalali.to_jalali()
            else:
                if df[col].dtype == object:
                    first_val = (
                        df[col].dropna().iloc[0] if len(df[col].dropna()) > 0 else None
                    )
                    if isinstance(first_val, str):
                        df[col] = df[col].jalali.parse_jalali(format)
                df[col] = df[col].jalali.to_gregorian()

        return df

    def to_period(self, freq: str = "M") -> pd.DataFrame:
        """Convert Jalali dates to period representation.

        Args:
            freq: Frequency for period. Options:
                - 'Y': Year
                - 'Q': Quarter
                - 'M': Month (default)
                - 'W': Week
                - 'D': Day

        Returns:
            DataFrame with period column added.
        """
        df = self._obj.copy()

        def get_period(x: Any) -> str | None:
            if pd.isna(x):
                return None
            if freq == "Y":
                return f"{x.year}"
            elif freq == "Q":
                q = (x.month - 1) // 3 + 1
                return f"{x.year}Q{q}"
            elif freq == "M":
                return f"{x.year}-{x.month:02d}"
            elif freq == "W":
                from jalali_pandas.core.calendar import week_of_year

                w = week_of_year(x.year, x.month, x.day)
                return f"{x.year}W{w:02d}"
            elif freq == "D":
                return f"{x.year}-{x.month:02d}-{x.day:02d}"
            else:
                raise ValueError(f"Unsupported frequency: {freq}")

        df[f"{self.jdate}_period"] = df[self.jdate].apply(get_period)
        return df

    def filter_by_year(self, year: int | list[int]) -> pd.DataFrame:
        """Filter DataFrame by Jalali year(s).

        Args:
            year: Year or list of years to filter by.

        Returns:
            Filtered DataFrame.
        """
        years = [year] if isinstance(year, int) else year
        mask = self._obj[self.jdate].jalali.year.isin(years)
        return cast(pd.DataFrame, self._obj[mask].copy())

    def filter_by_month(self, month: int | list[int]) -> pd.DataFrame:
        """Filter DataFrame by Jalali month(s).

        Args:
            month: Month or list of months to filter by (1-12).

        Returns:
            Filtered DataFrame.
        """
        months = [month] if isinstance(month, int) else month
        mask = self._obj[self.jdate].jalali.month.isin(months)
        return cast(pd.DataFrame, self._obj[mask].copy())

    def filter_by_quarter(self, quarter: int | list[int]) -> pd.DataFrame:
        """Filter DataFrame by Jalali quarter(s).

        Args:
            quarter: Quarter or list of quarters to filter by (1-4).

        Returns:
            Filtered DataFrame.
        """
        quarters = [quarter] if isinstance(quarter, int) else quarter
        mask = self._obj[self.jdate].jalali.quarter.isin(quarters)
        return cast(pd.DataFrame, self._obj[mask].copy())

    def filter_by_date_range(
        self,
        start: str | jdatetime.date | None = None,
        end: str | jdatetime.date | None = None,
    ) -> pd.DataFrame:
        """Filter DataFrame by Jalali date range.

        Args:
            start: Start date (inclusive). Can be string 'YYYY-MM-DD' or jdatetime.
            end: End date (inclusive). Can be string 'YYYY-MM-DD' or jdatetime.

        Returns:
            Filtered DataFrame.
        """
        df = self._obj.copy()
        mask = pd.Series([True] * len(df), index=df.index)

        if start is not None:
            if isinstance(start, str):
                start = jdatetime.datetime.strptime(start, "%Y-%m-%d")
            mask = mask & (df[self.jdate] >= start)

        if end is not None:
            if isinstance(end, str):
                end = jdatetime.datetime.strptime(end, "%Y-%m-%d")
            mask = mask & (df[self.jdate] <= end)

        return cast(pd.DataFrame, df[mask].copy())

__init__

__init__(pandas_obj: DataFrame) -> None

Initialize the accessor.

Parameters:

Name Type Description Default
pandas_obj DataFrame

A pandas DataFrame containing Jalali datetime data.

required
Source code in jalali_pandas/accessors/dataframe.py
51
52
53
54
55
56
57
58
59
60
def __init__(self, pandas_obj: pd.DataFrame) -> None:
    """Initialize the accessor.

    Args:
        pandas_obj: A pandas DataFrame containing Jalali datetime data.
    """
    self._obj: pd.DataFrame = pandas_obj
    self.columns: pd.Index[Any] = self._obj.columns
    self.jdate: str = "jdate"
    self._validate()

convert_columns

convert_columns(columns: list[str] | str, to_jalali: bool = True, format: str = '%Y-%m-%d') -> pd.DataFrame

Convert date columns between Jalali and Gregorian.

Parameters:

Name Type Description Default
columns list[str] | str

Column name(s) to convert.

required
to_jalali bool

If True, convert Gregorian to Jalali. If False, convert Jalali to Gregorian. Defaults to True.

True
format str

Format string for parsing string dates. Defaults to "%Y-%m-%d".

'%Y-%m-%d'

Returns:

Type Description
DataFrame

DataFrame with converted columns.

Examples:

>>> df.jalali.convert_columns('date', to_jalali=True)
>>> df.jalali.convert_columns(['date1', 'date2'], to_jalali=False)
Source code in jalali_pandas/accessors/dataframe.py
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
def convert_columns(
    self,
    columns: list[str] | str,
    to_jalali: bool = True,
    format: str = "%Y-%m-%d",
) -> pd.DataFrame:
    """Convert date columns between Jalali and Gregorian.

    Args:
        columns: Column name(s) to convert.
        to_jalali: If True, convert Gregorian to Jalali.
            If False, convert Jalali to Gregorian. Defaults to True.
        format: Format string for parsing string dates. Defaults to "%Y-%m-%d".

    Returns:
        DataFrame with converted columns.

    Examples:
        >>> df.jalali.convert_columns('date', to_jalali=True)
        >>> df.jalali.convert_columns(['date1', 'date2'], to_jalali=False)
    """
    df = self._obj.copy()

    if isinstance(columns, str):
        columns = [columns]

    for col in columns:
        if col not in df.columns:
            raise ValueError(f"Column '{col}' not found in DataFrame.")

        if to_jalali:
            df[col] = df[col].jalali.to_jalali()
        else:
            if df[col].dtype == object:
                first_val = (
                    df[col].dropna().iloc[0] if len(df[col].dropna()) > 0 else None
                )
                if isinstance(first_val, str):
                    df[col] = df[col].jalali.parse_jalali(format)
            df[col] = df[col].jalali.to_gregorian()

    return df

filter_by_date_range

filter_by_date_range(start: str | date | None = None, end: str | date | None = None) -> pd.DataFrame

Filter DataFrame by Jalali date range.

Parameters:

Name Type Description Default
start str | date | None

Start date (inclusive). Can be string 'YYYY-MM-DD' or jdatetime.

None
end str | date | None

End date (inclusive). Can be string 'YYYY-MM-DD' or jdatetime.

None

Returns:

Type Description
DataFrame

Filtered DataFrame.

Source code in jalali_pandas/accessors/dataframe.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
def filter_by_date_range(
    self,
    start: str | jdatetime.date | None = None,
    end: str | jdatetime.date | None = None,
) -> pd.DataFrame:
    """Filter DataFrame by Jalali date range.

    Args:
        start: Start date (inclusive). Can be string 'YYYY-MM-DD' or jdatetime.
        end: End date (inclusive). Can be string 'YYYY-MM-DD' or jdatetime.

    Returns:
        Filtered DataFrame.
    """
    df = self._obj.copy()
    mask = pd.Series([True] * len(df), index=df.index)

    if start is not None:
        if isinstance(start, str):
            start = jdatetime.datetime.strptime(start, "%Y-%m-%d")
        mask = mask & (df[self.jdate] >= start)

    if end is not None:
        if isinstance(end, str):
            end = jdatetime.datetime.strptime(end, "%Y-%m-%d")
        mask = mask & (df[self.jdate] <= end)

    return cast(pd.DataFrame, df[mask].copy())

filter_by_month

filter_by_month(month: int | list[int]) -> pd.DataFrame

Filter DataFrame by Jalali month(s).

Parameters:

Name Type Description Default
month int | list[int]

Month or list of months to filter by (1-12).

required

Returns:

Type Description
DataFrame

Filtered DataFrame.

Source code in jalali_pandas/accessors/dataframe.py
334
335
336
337
338
339
340
341
342
343
344
345
def filter_by_month(self, month: int | list[int]) -> pd.DataFrame:
    """Filter DataFrame by Jalali month(s).

    Args:
        month: Month or list of months to filter by (1-12).

    Returns:
        Filtered DataFrame.
    """
    months = [month] if isinstance(month, int) else month
    mask = self._obj[self.jdate].jalali.month.isin(months)
    return cast(pd.DataFrame, self._obj[mask].copy())

filter_by_quarter

filter_by_quarter(quarter: int | list[int]) -> pd.DataFrame

Filter DataFrame by Jalali quarter(s).

Parameters:

Name Type Description Default
quarter int | list[int]

Quarter or list of quarters to filter by (1-4).

required

Returns:

Type Description
DataFrame

Filtered DataFrame.

Source code in jalali_pandas/accessors/dataframe.py
347
348
349
350
351
352
353
354
355
356
357
358
def filter_by_quarter(self, quarter: int | list[int]) -> pd.DataFrame:
    """Filter DataFrame by Jalali quarter(s).

    Args:
        quarter: Quarter or list of quarters to filter by (1-4).

    Returns:
        Filtered DataFrame.
    """
    quarters = [quarter] if isinstance(quarter, int) else quarter
    mask = self._obj[self.jdate].jalali.quarter.isin(quarters)
    return cast(pd.DataFrame, self._obj[mask].copy())

filter_by_year

filter_by_year(year: int | list[int]) -> pd.DataFrame

Filter DataFrame by Jalali year(s).

Parameters:

Name Type Description Default
year int | list[int]

Year or list of years to filter by.

required

Returns:

Type Description
DataFrame

Filtered DataFrame.

Source code in jalali_pandas/accessors/dataframe.py
321
322
323
324
325
326
327
328
329
330
331
332
def filter_by_year(self, year: int | list[int]) -> pd.DataFrame:
    """Filter DataFrame by Jalali year(s).

    Args:
        year: Year or list of years to filter by.

    Returns:
        Filtered DataFrame.
    """
    years = [year] if isinstance(year, int) else year
    mask = self._obj[self.jdate].jalali.year.isin(years)
    return cast(pd.DataFrame, self._obj[mask].copy())

groupby

groupby(grouper: str = 'md') -> DataFrameGroupByT

Group by Jalali date components.

Parameters:

Name Type Description Default
grouper str

Grouping key. Options: - 'year': Group by year - 'month': Group by month - 'day': Group by day - 'week': Group by week number - 'dayofweek': Group by day of week - 'dayofmonth': Group by day of month (alias for 'day') - 'quarter': Group by quarter - 'dayofyear': Group by day of year - 'ym': Group by year and month - 'yq': Group by year and quarter - 'ymd': Group by year, month, and day - 'md': Group by month and day (default)

'md'

Returns:

Type Description
DataFrameGroupByT

DataFrameGroupBy object.

Raises:

Type Description
ValueError

If grouper is not a valid option.

Source code in jalali_pandas/accessors/dataframe.py
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
def groupby(self, grouper: str = "md") -> DataFrameGroupByT:
    """Group by Jalali date components.

    Args:
        grouper: Grouping key. Options:
            - 'year': Group by year
            - 'month': Group by month
            - 'day': Group by day
            - 'week': Group by week number
            - 'dayofweek': Group by day of week
            - 'dayofmonth': Group by day of month (alias for 'day')
            - 'quarter': Group by quarter
            - 'dayofyear': Group by day of year
            - 'ym': Group by year and month
            - 'yq': Group by year and quarter
            - 'ymd': Group by year, month, and day
            - 'md': Group by month and day (default)

    Returns:
        DataFrameGroupBy object.

    Raises:
        ValueError: If grouper is not a valid option.
    """
    possible_keys = [
        "year",
        "month",
        "day",
        "week",
        "dayofweek",
        "dayofmonth",
        "quarter",
        "dayofyear",
        "ym",
        "yq",
        "ymd",
        "md",
    ]
    df = self._df
    if grouper not in possible_keys:
        raise ValueError(
            f"{grouper} is not a valid groupby type. Choose from {possible_keys}"
        )

    keys: dict[str, list[str]] = {
        "md": ["month", "day"],
        "ym": ["year", "month"],
        "yq": ["year", "quarter"],
        "ymd": ["year", "month", "day"],
    }
    grouper_cols: list[str]
    if grouper in keys:
        grouper_cols = [f"__{g}" for g in keys[grouper]]
    elif grouper == "dayofmonth":
        grouper_cols = ["__day"]
    else:
        grouper_cols = [f"__{grouper}"]

    group = cast(DataFrameGroupByT, df.groupby(grouper_cols))
    return self._clean_groupby(group)

resample

resample(resample_type: str) -> pd.DataFrame

Resample by Jalali frequency.

Groups the DataFrame by Jalali calendar periods and aggregates.

Parameters:

Name Type Description Default
resample_type str

The resample frequency. Options: - 'month': Group by Jalali month - 'quarter': Group by Jalali quarter - 'year': Group by Jalali year - 'week': Group by Jalali week

required

Returns:

Type Description
DataFrame

DataFrame with aggregated values grouped by the specified period.

Raises:

Type Description
ValueError

If resample_type is not valid.

Examples:

>>> df.jalali.resample('month')
>>> df.jalali.resample('quarter')
Source code in jalali_pandas/accessors/dataframe.py
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
def resample(self, resample_type: str) -> pd.DataFrame:
    """Resample by Jalali frequency.

    Groups the DataFrame by Jalali calendar periods and aggregates.

    Args:
        resample_type: The resample frequency. Options:
            - 'month': Group by Jalali month
            - 'quarter': Group by Jalali quarter
            - 'year': Group by Jalali year
            - 'week': Group by Jalali week

    Returns:
        DataFrame with aggregated values grouped by the specified period.

    Raises:
        ValueError: If resample_type is not valid.

    Examples:
        >>> df.jalali.resample('month')
        >>> df.jalali.resample('quarter')
    """
    valid_types = ["month", "quarter", "year", "week"]
    if resample_type not in valid_types:
        raise ValueError(
            f"{resample_type} is not a valid resample type. "
            f"Choose from {valid_types}"
        )

    type_to_groupby: dict[str, str] = {
        "month": "ym",
        "quarter": "yq",
        "year": "year",
        "week": "week",
    }

    groupby_key = type_to_groupby[resample_type]
    return self.groupby(groupby_key).sum(numeric_only=True).reset_index(drop=True)

set_date_column

set_date_column(column: str) -> JalaliDataFrameAccessor

Set the Jalali date column to use for operations.

Parameters:

Name Type Description Default
column str

Name of the column containing Jalali dates.

required

Returns:

Type Description
JalaliDataFrameAccessor

Self for method chaining.

Raises:

Type Description
ValueError

If column doesn't exist or doesn't contain jdatetime.

Source code in jalali_pandas/accessors/dataframe.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def set_date_column(self, column: str) -> JalaliDataFrameAccessor:
    """Set the Jalali date column to use for operations.

    Args:
        column: Name of the column containing Jalali dates.

    Returns:
        Self for method chaining.

    Raises:
        ValueError: If column doesn't exist or doesn't contain jdatetime.
    """
    if column not in self.columns:
        raise ValueError(f"Column '{column}' not found in DataFrame.")

    if len(self._obj) > 0:
        first_val = self._obj[column].iloc[0]
        if not isinstance(first_val, (jdatetime.date, jdatetime.datetime)):
            raise ValueError(
                f"Column '{column}' does not contain jdatetime objects."
            )

    self.jdate = column
    return self

to_period

to_period(freq: str = 'M') -> pd.DataFrame

Convert Jalali dates to period representation.

Parameters:

Name Type Description Default
freq str

Frequency for period. Options: - 'Y': Year - 'Q': Quarter - 'M': Month (default) - 'W': Week - 'D': Day

'M'

Returns:

Type Description
DataFrame

DataFrame with period column added.

Source code in jalali_pandas/accessors/dataframe.py
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
def to_period(self, freq: str = "M") -> pd.DataFrame:
    """Convert Jalali dates to period representation.

    Args:
        freq: Frequency for period. Options:
            - 'Y': Year
            - 'Q': Quarter
            - 'M': Month (default)
            - 'W': Week
            - 'D': Day

    Returns:
        DataFrame with period column added.
    """
    df = self._obj.copy()

    def get_period(x: Any) -> str | None:
        if pd.isna(x):
            return None
        if freq == "Y":
            return f"{x.year}"
        elif freq == "Q":
            q = (x.month - 1) // 3 + 1
            return f"{x.year}Q{q}"
        elif freq == "M":
            return f"{x.year}-{x.month:02d}"
        elif freq == "W":
            from jalali_pandas.core.calendar import week_of_year

            w = week_of_year(x.year, x.month, x.day)
            return f"{x.year}W{w:02d}"
        elif freq == "D":
            return f"{x.year}-{x.month:02d}-{x.day:02d}"
        else:
            raise ValueError(f"Unsupported frequency: {freq}")

    df[f"{self.jdate}_period"] = df[self.jdate].apply(get_period)
    return df

Legacy accessors

Handle Jalali dates in pandas DataFrames.

JalaliDataframeAccessor

Accessor methods on pandas DataFrames to handle Jalali dates.

Source code in jalali_pandas/df_handler.py
 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
class JalaliDataframeAccessor:
    """Accessor methods on pandas DataFrames to handle Jalali dates."""

    TEMP_COLUMNS = ["__year", "__month", "__quarter", "__weekday", "__day"]

    def __init__(self, pandas_obj: pd.DataFrame) -> None:
        """Initialize the accessor.

        Args:
            pandas_obj: A pandas DataFrame containing Jalali datetime data.
        """
        self._obj: pd.DataFrame = pandas_obj
        self.columns: pd.Index[Any] = self._obj.columns
        self.jdate: str = "jdate"
        self._validate()

    def _validate(self) -> None:
        """Check if the DataFrame has a jdatetime column.

        Raises:
            ValueError: If no jdatetime column is found.
        """
        for col in self.columns:
            if isinstance(self._obj[col].iloc[0], jdatetime.date):
                self.jdate = str(col)
                return
        raise ValueError("No jdatetime column found in the dataframe.")

    @property
    def _df(self) -> pd.DataFrame:
        """Generate temp DataFrame for groupby.

        Returns:
            pd.DataFrame: DataFrame with year, month, day, quarter, weekday columns.
        """
        df = self._obj.copy()
        df["__year"] = df[self.jdate].jalali.year
        df["__month"] = df[self.jdate].jalali.month
        df["__day"] = df[self.jdate].jalali.day
        df["__quarter"] = df[self.jdate].jalali.quarter
        df["__weekday"] = df[self.jdate].jalali.weekday
        return df

    def _clean_groupby(self, group: DataFrameGroupByT) -> DataFrameGroupByT:
        """Clean the groupby object by removing temp columns.

        Args:
            group: The groupby object to clean.

        Returns:
            Cleaned groupby object with only original columns.
        """
        numeric_columns: pd.Index[Any] = cast(
            pd.DataFrame, self._obj.select_dtypes(include="number")
        ).columns
        remaining_columns = [
            col
            for col in numeric_columns
            if col in set(self.columns).difference(self.TEMP_COLUMNS)
        ]
        if not remaining_columns:
            return group
        return cast(DataFrameGroupByT, group[remaining_columns])

    def groupby(self, grouper: str = "md") -> DataFrameGroupByT:
        """Group by Jalali date components.

        Args:
            grouper: Grouping key. Options: year, month, day, week, dayofweek,
                dayofmonth, ym, yq, ymd, md. Defaults to 'md'.

        Returns:
            DataFrameGroupBy object.

        Raises:
            ValueError: If grouper is not a valid option.
        """
        possible_keys = [
            "year",
            "month",
            "day",
            "week",
            "dayofweek",
            "dayofmonth",
            "ym",
            "yq",
            "ymd",
            "md",
        ]
        df = self._df
        if grouper not in possible_keys:
            raise ValueError(
                f"{grouper} is not a valid groupby type. Choose from {possible_keys}"
            )

        keys: dict[str, list[str]] = {
            "md": ["month", "day"],
            "ym": ["year", "month"],
            "yq": ["year", "quarter"],
            "ymd": ["year", "month", "day"],
        }
        grouper_cols: list[str]
        if grouper in keys:
            grouper_cols = [f"__{g}" for g in keys[grouper]]
        else:
            grouper_cols = [f"__{grouper}"]

        group = cast(DataFrameGroupByT, df.groupby(grouper_cols))
        return self._clean_groupby(group)

    def resample(self, resample_type: str) -> pd.DataFrame:
        """Resample by Jalali frequency.

        Groups the DataFrame by Jalali calendar periods and aggregates.

        Args:
            resample_type: The resample frequency. Options:
                - 'month': Group by Jalali month
                - 'quarter': Group by Jalali quarter
                - 'year': Group by Jalali year

        Returns:
            DataFrame with aggregated values grouped by the specified period.

        Examples:
            >>> df.jalali.resample('month')
            >>> df.jalali.resample('quarter')
        """
        valid_types = ["month", "quarter", "year"]
        if resample_type not in valid_types:
            raise ValueError(
                f"{resample_type} is not a valid resample type. "
                f"Choose from {valid_types}"
            )

        # Map resample type to groupby key
        type_to_groupby: dict[str, str] = {
            "month": "ym",
            "quarter": "yq",
            "year": "year",
        }

        groupby_key = type_to_groupby[resample_type]
        return self.groupby(groupby_key).sum(numeric_only=True).reset_index(drop=True)

__init__

__init__(pandas_obj: DataFrame) -> None

Initialize the accessor.

Parameters:

Name Type Description Default
pandas_obj DataFrame

A pandas DataFrame containing Jalali datetime data.

required
Source code in jalali_pandas/df_handler.py
25
26
27
28
29
30
31
32
33
34
def __init__(self, pandas_obj: pd.DataFrame) -> None:
    """Initialize the accessor.

    Args:
        pandas_obj: A pandas DataFrame containing Jalali datetime data.
    """
    self._obj: pd.DataFrame = pandas_obj
    self.columns: pd.Index[Any] = self._obj.columns
    self.jdate: str = "jdate"
    self._validate()

groupby

groupby(grouper: str = 'md') -> DataFrameGroupByT

Group by Jalali date components.

Parameters:

Name Type Description Default
grouper str

Grouping key. Options: year, month, day, week, dayofweek, dayofmonth, ym, yq, ymd, md. Defaults to 'md'.

'md'

Returns:

Type Description
DataFrameGroupByT

DataFrameGroupBy object.

Raises:

Type Description
ValueError

If grouper is not a valid option.

Source code in jalali_pandas/df_handler.py
 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
def groupby(self, grouper: str = "md") -> DataFrameGroupByT:
    """Group by Jalali date components.

    Args:
        grouper: Grouping key. Options: year, month, day, week, dayofweek,
            dayofmonth, ym, yq, ymd, md. Defaults to 'md'.

    Returns:
        DataFrameGroupBy object.

    Raises:
        ValueError: If grouper is not a valid option.
    """
    possible_keys = [
        "year",
        "month",
        "day",
        "week",
        "dayofweek",
        "dayofmonth",
        "ym",
        "yq",
        "ymd",
        "md",
    ]
    df = self._df
    if grouper not in possible_keys:
        raise ValueError(
            f"{grouper} is not a valid groupby type. Choose from {possible_keys}"
        )

    keys: dict[str, list[str]] = {
        "md": ["month", "day"],
        "ym": ["year", "month"],
        "yq": ["year", "quarter"],
        "ymd": ["year", "month", "day"],
    }
    grouper_cols: list[str]
    if grouper in keys:
        grouper_cols = [f"__{g}" for g in keys[grouper]]
    else:
        grouper_cols = [f"__{grouper}"]

    group = cast(DataFrameGroupByT, df.groupby(grouper_cols))
    return self._clean_groupby(group)

resample

resample(resample_type: str) -> pd.DataFrame

Resample by Jalali frequency.

Groups the DataFrame by Jalali calendar periods and aggregates.

Parameters:

Name Type Description Default
resample_type str

The resample frequency. Options: - 'month': Group by Jalali month - 'quarter': Group by Jalali quarter - 'year': Group by Jalali year

required

Returns:

Type Description
DataFrame

DataFrame with aggregated values grouped by the specified period.

Examples:

>>> df.jalali.resample('month')
>>> df.jalali.resample('quarter')
Source code in jalali_pandas/df_handler.py
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
def resample(self, resample_type: str) -> pd.DataFrame:
    """Resample by Jalali frequency.

    Groups the DataFrame by Jalali calendar periods and aggregates.

    Args:
        resample_type: The resample frequency. Options:
            - 'month': Group by Jalali month
            - 'quarter': Group by Jalali quarter
            - 'year': Group by Jalali year

    Returns:
        DataFrame with aggregated values grouped by the specified period.

    Examples:
        >>> df.jalali.resample('month')
        >>> df.jalali.resample('quarter')
    """
    valid_types = ["month", "quarter", "year"]
    if resample_type not in valid_types:
        raise ValueError(
            f"{resample_type} is not a valid resample type. "
            f"Choose from {valid_types}"
        )

    # Map resample type to groupby key
    type_to_groupby: dict[str, str] = {
        "month": "ym",
        "quarter": "yq",
        "year": "year",
    }

    groupby_key = type_to_groupby[resample_type]
    return self.groupby(groupby_key).sum(numeric_only=True).reset_index(drop=True)

Handle Jalali dates in pandas series.

JalaliSerieAccessor

Accessor methods on pandas series to handle Jalali dates.

Source code in jalali_pandas/serie_handler.py
 11
 12
 13
 14
 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
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
class JalaliSerieAccessor:
    """Accessor methods on pandas series to handle Jalali dates."""

    def __init__(self, pandas_obj: pd.Series[Any]) -> None:
        """Initialize the accessor.

        Args:
            pandas_obj: A pandas Series containing datetime data.
        """
        self._obj = pandas_obj

    def _validate(self) -> None:
        """Validate pandas series contains jdatetime objects.

        Raises:
            TypeError: If series doesn't contain jdatetime or string dates.
        """
        if not all(isinstance(x, (str, jdatetime.date)) for x in self._obj):
            raise TypeError("pandas series must be jdatetime or string of jdate")

    def to_jalali(self) -> pd.Series[Any]:
        """convert python datetime to jalali datetime.

        Returns:
            pd.Series:  pd.Series of jalali datetime.
        """
        return cast(
            pd.Series,
            self._obj.apply(lambda x: jdatetime.datetime.fromgregorian(date=x)),
        )

    def to_gregorian(self) -> pd.Series[Any]:
        """convert jalali datetime to python default datetime.

        Returns:
            pd.Series: pd.Series of python datetime.
        """

        return cast(pd.Series, self._obj.apply(jdatetime.datetime.togregorian))

    #  pylint: disable=redefined-builtin
    def parse_jalali(self, format: str = "%Y-%m-%d") -> pd.Series[Any]:
        """[summary]

        Args:
            format (str, optional): like gregorian datetime format. Defaults to "%Y-%m-%d".

        Returns:
            pd.Series: pd.Series of jalali datetime.
        """
        return cast(
            pd.Series, self._obj.apply(lambda x: jdatetime.datetime.strptime(x, format))
        )

    @property
    def year(self) -> pd.Series[Any]:
        """get Jalali year

        Returns:
            pd.Series: Jalali year
        """
        self._validate()
        return cast(pd.Series, self._obj.apply(lambda x: x.year))

    @property
    def month(self) -> pd.Series[Any]:
        """get Jalali


        Returns:
            pd.Series: Jalali month
        """
        self._validate()
        return cast(pd.Series, self._obj.apply(lambda x: x.month))

    @property
    def day(self) -> pd.Series[Any]:
        """get Jalali day

        Returns:
            pd.Series: Jalali day
        """
        self._validate()
        return cast(pd.Series, self._obj.apply(lambda x: x.day))

    @property
    def hour(self) -> pd.Series[Any]:
        """get Jalali hour

        Returns:
            pd.Series: Jalali hour
        """
        self._validate()
        return cast(pd.Series, self._obj.apply(lambda x: x.hour))

    @property
    def minute(self) -> pd.Series[Any]:
        """get Jalali minute

        Returns:
            pd.Series: Jalali minute
        """
        self._validate()
        return cast(pd.Series, self._obj.apply(lambda x: x.minute))

    @property
    def second(self) -> pd.Series[Any]:
        """get Jalali second

        Returns:
            pd.Series: Jalali second
        """
        self._validate()
        return cast(pd.Series, self._obj.apply(lambda x: x.second))

    @property
    def weekday(self) -> pd.Series[Any]:
        """get Jalali weekday

        Returns:
            pd.Series: Jalali weekday
        """
        self._validate()
        return cast(pd.Series, self._obj.apply(lambda x: x.weekday()))

    @property
    def weeknumber(self) -> pd.Series[Any]:
        """get Jalali day of year

        Returns:
            pd.Series: Jalali day of year
        """
        self._validate()
        return cast(pd.Series, self._obj.apply(lambda x: x.weeknumber()))

    @property
    def quarter(self) -> pd.Series[Any]:
        """Get Jalali quarter.

        Returns:
            pd.Series: Jalali quarter (1-4).
        """
        self._validate()
        month = self.month
        return cast(pd.Series, month.apply(lambda x: (x - 1) // 3 + 1))

day property

day: Series[Any]

get Jalali day

Returns:

Type Description
Series[Any]

pd.Series: Jalali day

hour property

hour: Series[Any]

get Jalali hour

Returns:

Type Description
Series[Any]

pd.Series: Jalali hour

minute property

minute: Series[Any]

get Jalali minute

Returns:

Type Description
Series[Any]

pd.Series: Jalali minute

month property

month: Series[Any]

get Jalali

Returns:

Type Description
Series[Any]

pd.Series: Jalali month

quarter property

quarter: Series[Any]

Get Jalali quarter.

Returns:

Type Description
Series[Any]

pd.Series: Jalali quarter (1-4).

second property

second: Series[Any]

get Jalali second

Returns:

Type Description
Series[Any]

pd.Series: Jalali second

weekday property

weekday: Series[Any]

get Jalali weekday

Returns:

Type Description
Series[Any]

pd.Series: Jalali weekday

weeknumber property

weeknumber: Series[Any]

get Jalali day of year

Returns:

Type Description
Series[Any]

pd.Series: Jalali day of year

year property

year: Series[Any]

get Jalali year

Returns:

Type Description
Series[Any]

pd.Series: Jalali year

__init__

__init__(pandas_obj: Series[Any]) -> None

Initialize the accessor.

Parameters:

Name Type Description Default
pandas_obj Series[Any]

A pandas Series containing datetime data.

required
Source code in jalali_pandas/serie_handler.py
14
15
16
17
18
19
20
def __init__(self, pandas_obj: pd.Series[Any]) -> None:
    """Initialize the accessor.

    Args:
        pandas_obj: A pandas Series containing datetime data.
    """
    self._obj = pandas_obj

parse_jalali

parse_jalali(format: str = '%Y-%m-%d') -> pd.Series[Any]

[summary]

Parameters:

Name Type Description Default
format str

like gregorian datetime format. Defaults to "%Y-%m-%d".

'%Y-%m-%d'

Returns:

Type Description
Series[Any]

pd.Series: pd.Series of jalali datetime.

Source code in jalali_pandas/serie_handler.py
52
53
54
55
56
57
58
59
60
61
62
63
def parse_jalali(self, format: str = "%Y-%m-%d") -> pd.Series[Any]:
    """[summary]

    Args:
        format (str, optional): like gregorian datetime format. Defaults to "%Y-%m-%d".

    Returns:
        pd.Series: pd.Series of jalali datetime.
    """
    return cast(
        pd.Series, self._obj.apply(lambda x: jdatetime.datetime.strptime(x, format))
    )

to_gregorian

to_gregorian() -> pd.Series[Any]

convert jalali datetime to python default datetime.

Returns:

Type Description
Series[Any]

pd.Series: pd.Series of python datetime.

Source code in jalali_pandas/serie_handler.py
42
43
44
45
46
47
48
49
def to_gregorian(self) -> pd.Series[Any]:
    """convert jalali datetime to python default datetime.

    Returns:
        pd.Series: pd.Series of python datetime.
    """

    return cast(pd.Series, self._obj.apply(jdatetime.datetime.togregorian))

to_jalali

to_jalali() -> pd.Series[Any]

convert python datetime to jalali datetime.

Returns:

Type Description
Series[Any]

pd.Series: pd.Series of jalali datetime.

Source code in jalali_pandas/serie_handler.py
31
32
33
34
35
36
37
38
39
40
def to_jalali(self) -> pd.Series[Any]:
    """convert python datetime to jalali datetime.

    Returns:
        pd.Series:  pd.Series of jalali datetime.
    """
    return cast(
        pd.Series,
        self._obj.apply(lambda x: jdatetime.datetime.fromgregorian(date=x)),
    )