Skip to content

Commit

Permalink
1
Browse files Browse the repository at this point in the history
  • Loading branch information
majiidd committed Nov 8, 2024
1 parent 27a2865 commit 353a148
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 72 deletions.
2 changes: 1 addition & 1 deletion persiantools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

__title__ = "persiantools"
__url__ = "https://github.com/majiidd/persiantools"
__version__ = "4.2.0"
__version__ = "5.0.0"
__build__ = __version__
__author__ = "Majid Hajiloo"
__author_email__ = "[email protected]"
Expand Down
30 changes: 16 additions & 14 deletions persiantools/digits.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import re
from typing import Tuple

EN_TO_FA_MAP = str.maketrans("0123456789", "۰۱۲۳۴۵۶۷۸۹")
AR_TO_FA_MAP = str.maketrans("٠١٢٣٤٥٦٧٨٩", "۰۱۲۳۴۵۶۷۸۹")
Expand Down Expand Up @@ -36,12 +35,14 @@
20: lambda n, depth: RANGE[n - 10],
100: lambda n, depth: TENS[n // 10 - 2] + _to_word(n % 10, True),
1000: lambda n, depth: HUNDREDS[n // 100 - 1] + _to_word(n % 100, True),
1000000: lambda n, depth: _to_word(n // 1000, depth) + BIG_RANGE[0] + _to_word(n % 1000, True),
1000000000: lambda n, depth: _to_word(n // 1000000, depth) + BIG_RANGE[1] + _to_word(n % 1000000, True),
1000000000000: lambda n, depth: _to_word(n // 1000000000, depth) + BIG_RANGE[2] + _to_word(n % 1000000000, True),
1000000000000000: lambda n, depth: _to_word(n // 1000000000000, depth)
1_000_000: lambda n, depth: _to_word(n // 1_000, depth) + BIG_RANGE[0] + _to_word(n % 1_000, True),
1_000_000_000: lambda n, depth: _to_word(n // 1_000_000, depth) + BIG_RANGE[1] + _to_word(n % 1_000_000, True),
1_000_000_000_000: lambda n, depth: _to_word(n // 1_000_000_000, depth)
+ BIG_RANGE[2]
+ _to_word(n % 1_000_000_000, True),
1_000_000_000_000_000: lambda n, depth: _to_word(n // 1_000_000_000_000, depth)
+ BIG_RANGE[3]
+ _to_word(n % 1000000000000, True),
+ _to_word(n % 1_000_000_000_000, True),
}


Expand Down Expand Up @@ -158,7 +159,7 @@ def _to_word(number: int, depth: bool) -> str:
Parameters:
number (int): The number to be converted.
depth (bool): A flag indicating if the function is called recursively.
depth (bool): Indicates if the function is called recursively.
Returns:
str: The Persian word representation of the number.
Expand Down Expand Up @@ -213,23 +214,24 @@ def _floating_number_to_word(number: float, depth: bool) -> str:
if len(right) > 14:
raise OutOfRangeException("You are allowed to use 14 digits for a floating point")

if len(str(right).strip("0")) > 0:
if right.strip("0"):
left_word = _to_word(int(left), False)
result = "{}{} {}".format(
left_word + DELI if left_word != ZERO else "",
_to_word(int(right), False),
MANTISSA[len(str(right).rstrip("0")) - 1],
mantissa_index = len(right.rstrip("0")) - 1
if mantissa_index >= len(MANTISSA):
raise ValueError("Fractional part is too long")
result = (
f"{left_word + DELI if left_word != ZERO else ''}{_to_word(int(right), False)} {MANTISSA[mantissa_index]}"
)
if number < 0:
return NEGATIVE + result
return result
else:
if number < 0:
return NEGATIVE + (_to_word(int(left), False))
return NEGATIVE + _to_word(int(left), False)
return _to_word(int(left), False)


def to_word(number: Tuple[float, int]) -> str:
def to_word(number: int | float) -> str:
"""
Convert a number to its Persian word representation.
Expand Down
107 changes: 53 additions & 54 deletions persiantools/jdatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,18 @@
# The first entry is for indexing purposes and is not used in calculations.
_MONTH_COUNT = [
[-1, -1, -1], # for indexing purposes
[31, 31, 0], # farvardin
[31, 31, 31], # ordibehesht
[31, 31, 62], # khordad
[31, 31, 93], # tir
[31, 31, 124], # mordad
[31, 31, 155], # shahrivar
[30, 30, 186], # mehr
[30, 30, 216], # aban
[30, 30, 246], # azar
[30, 30, 276], # dey
[30, 30, 306], # bahman
[29, 30, 336], # esfand
[31, 31, 0], # Farvardin
[31, 31, 31], # Ordibehesht
[31, 31, 62], # Khordad
[31, 31, 93], # Tir
[31, 31, 124], # Mordad
[31, 31, 155], # Shahrivar
[30, 30, 186], # Mehr
[30, 30, 216], # Aban
[30, 30, 246], # Azar
[30, 30, 276], # Dey
[30, 30, 306], # Bahman
[29, 30, 336], # Esfand
]

_FRACTION_CORRECTION = [100000, 10000, 1000, 100, 10]
Expand Down Expand Up @@ -211,28 +211,30 @@ def __init__(self, year, month=None, day=None, locale="en"):
self._hashcode = -1

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

@property
def month(self):
def month(self) -> int:
return self._month

@property
def day(self):
def day(self) -> int:
return self._day

@property
def locale(self):
return self._locale

@locale.setter
def locale(self, locale):
assert locale in ("en", "fa"), "locale must be 'en' or 'fa'"
def locale(self, locale: str):
if locale not in ("en", "fa"):
raise ValueError("locale must be 'en' or 'fa'")

self._locale = locale

@classmethod
def _check_date_fields(cls, year, month, day, locale):
def _check_date_fields(cls, year: int, month: int, day: int, locale: str):
"""
Validate and normalize the date fields.
Expand All @@ -253,22 +255,22 @@ def _check_date_fields(cls, year, month, day, locale):
day = operator.index(day)

if not MINYEAR <= year <= MAXYEAR:
raise ValueError("year must be in %d..%d" % (MINYEAR, MAXYEAR), year)
raise ValueError(f"year must be in {MINYEAR}..{MAXYEAR}", year)

if not 1 <= month <= 12:
raise ValueError("month must be in 1..12", month)

dim = cls.days_in_month(month, year)
if not 1 <= day <= dim:
raise ValueError("day must be in 1..%d" % dim, day)
raise ValueError(f"day must be in 1..{dim}", day)

if locale not in ["en", "fa"]:
raise ValueError("locale must be 'en' or 'fa'")

return year, month, day, locale

@classmethod
def check_date(cls, year, month, day):
def check_date(cls, year: int, month: int, day: int) -> bool:
"""
Check if the given Jalali date fields constitute a valid date.
Expand All @@ -292,9 +294,9 @@ def check_date(cls, year, month, day):
return True

@staticmethod
def is_leap(year):
def is_leap(year: int) -> bool:
"""
This function calculates whether a given year in the Persian calendar is a leap year.
Calculate whether a given year in the Persian calendar is a leap year.
It calculates `((year + 2346) * 683) % 2820`. This expression does a few things:
- It offsets the input year by 2346. This is done to align the Persian calendar with the astronomical solar year.
Expand All @@ -318,7 +320,7 @@ def is_leap(year):
return ((year + 2346) * 683) % 2820 < 683

@classmethod
def days_in_month(cls, month, year):
def days_in_month(cls, month: int, year: int) -> int:
"""
Get the number of days in a given month for a specified year.
Expand All @@ -332,15 +334,16 @@ def days_in_month(cls, month, year):
Raises:
AssertionError: If the month is out of the valid range.
"""
assert 1 <= month <= 12, "month must be in 1..12"
if not 1 <= month <= 12:
raise ValueError("month must be in 1..12")

if month == 12 and cls.is_leap(year):
return _MONTH_COUNT[month][1]

return _MONTH_COUNT[month][0]

@staticmethod
def days_before_month(month):
def days_before_month(month: int) -> int:
"""
Get the number of days before the start of a given month.
Expand All @@ -353,7 +356,8 @@ def days_before_month(month):
Raises:
AssertionError: If the month is out of the valid range.
"""
assert 1 <= month <= 12, "month must be in 1..12"
if not 1 <= month <= 12:
raise ValueError("month must be in 1..12")

return _MONTH_COUNT[month][2]

Expand Down Expand Up @@ -425,7 +429,7 @@ def to_jalali(cls, year, month=None, day=None):

return cls(jalali_year, jalali_month, jalali_day)

def to_gregorian(self):
def to_gregorian(self) -> date:
"""
Convert a Jalali (Persian) date to a Gregorian date.
Expand Down Expand Up @@ -526,7 +530,7 @@ def timetuple(self):
"""
return self.to_gregorian().timetuple()

def isoformat(self):
def isoformat(self) -> str:
"""
Return the Jalali date as a string in ISO 8601 format.
Expand All @@ -542,7 +546,7 @@ def isoformat(self):
>>> jdate.isoformat()
'1398-03-17'
"""
iso = "%04d-%02d-%02d" % (self._year, self._month, self._day)
iso = f"{self._year:04d}-{self._month:02d}-{self._day:02d}"

if self._locale == "fa":
iso = digits.en_to_fa(iso)
Expand All @@ -551,15 +555,15 @@ def isoformat(self):

__str__ = isoformat

def toordinal(self):
def toordinal(self) -> int:
return self.to_gregorian().toordinal() - 226894

@classmethod
def fromordinal(cls, n):
def fromordinal(cls, n: int):
return cls(date.fromordinal(n + 226894))

@classmethod
def fromisoformat(cls, date_string):
def fromisoformat(cls, date_string: str):
"""Construct a date from the output of JalaliDate.isoformat()."""
if not isinstance(date_string, str):
raise TypeError("fromisoformat: argument must be str")
Expand All @@ -573,14 +577,14 @@ def fromisoformat(cls, date_string):
raise ValueError(f"Invalid isoformat string: {date_string!r}")

@classmethod
def _parse_isoformat_date(cls, dtstr):
def _parse_isoformat_date(cls, dtstr: str):
# It is assumed that this function will only be called with a
# string of length exactly 10, and (though this is not used) ASCII-only
assert len(dtstr) in (7, 8, 10)

year = int(dtstr[0:4])
if dtstr[4] != "-":
raise ValueError("Invalid date separator: %s" % dtstr[4])
raise ValueError(f"Invalid date separator: {dtstr[4]}")

month = int(dtstr[5:7])

Expand Down Expand Up @@ -612,12 +616,7 @@ def __reduce__(self):
return self.__class__, self.__getstate__()

def __repr__(self):
return "JalaliDate(%d, %d, %d, %s)" % (
self._year,
self._month,
self._day,
WEEKDAY_NAMES_EN[self.weekday()],
)
return f"JalaliDate({self._year}, {self._month}, {self._day}, {WEEKDAY_NAMES_EN[self.weekday()]})"

resolution = timedelta(1)

Expand Down Expand Up @@ -659,21 +658,21 @@ def replace(self, year=None, month=None, day=None, locale=None):
return JalaliDate(year, month, day, locale)

@classmethod
def fromtimestamp(cls, timestamp):
def fromtimestamp(cls, timestamp: float):
return cls(date.fromtimestamp(timestamp))

def weekday(self):
def weekday(self) -> int:
return (self.toordinal() + 4) % 7

def __format__(self, fmt):
def __format__(self, fmt: str):
if not isinstance(fmt, str):
raise TypeError("must be str, not %s" % type(fmt).__name__)
raise TypeError(f"must be str, not {type(fmt).__name__}")
if len(fmt) != 0:
return self.strftime(fmt)

return str(self)

def isoweekday(self):
def isoweekday(self) -> int:
return self.weekday() + 1

def week_of_year(self):
Expand All @@ -691,7 +690,7 @@ def isocalendar(self):
"""Return a 3-tuple containing ISO year, week number, and weekday."""
return self.year, self.week_of_year(), self.isoweekday()

def ctime(self):
def ctime(self) -> str:
return self.strftime("%c")

def strftime(self, fmt, locale=None):
Expand Down Expand Up @@ -730,12 +729,12 @@ def strftime(self, fmt, locale=None):
"%a": day_names_abbr[self.weekday()],
"%A": day_names[self.weekday()],
"%w": str(self.weekday()),
"%d": "%02d" % self._day,
"%d": f"{self._day:02d}",
"%b": month_names_abbr[self._month],
"%B": month_names[self._month],
"%m": "%02d" % self._month,
"%y": "%02d" % (self._year % 100),
"%Y": "%04d" % self._year,
"%m": f"{self._month:02d}",
"%y": f"{self._year % 100:02d}",
"%Y": f"{self._year:04d}",
"%H": "00",
"%I": "00",
"%p": am,
Expand All @@ -744,9 +743,9 @@ def strftime(self, fmt, locale=None):
"%f": "000000",
"%z": "",
"%Z": "",
"%j": "%03d" % (self.days_before_month(self._month) + self._day),
"%U": "%02d" % self.week_of_year(),
"%W": "%02d" % self.week_of_year(),
"%j": f"{self.days_before_month(self._month) + self._day:03d}",
"%U": f"{self.week_of_year():02d}",
"%W": f"{self.week_of_year():02d}",
"%X": "00:00:00",
"%%": "%",
}
Expand Down
5 changes: 2 additions & 3 deletions persiantools/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import re
from typing import Dict


def replace(string: str, dictionary: Dict[str, str]) -> str:
def replace(string: str, dictionary: dict[str, str]) -> str:
"""
Replace occurrences of keys in the dictionary with their corresponding values in the given string.
Expand All @@ -22,5 +21,5 @@ def replace(string: str, dictionary: Dict[str, str]) -> str:
if not dictionary:
return string

pattern = re.compile("|".join(re.escape(key) for key in dictionary.keys()))
pattern = re.compile("|".join(re.escape(key) for key in dictionary))
return pattern.sub(lambda x: dictionary[x.group()], string)
1 change: 1 addition & 0 deletions tests/test_jalalidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def test_shamsi_to_gregorian(self):
(JalaliDate(1403, 2, 23), date(2024, 5, 12)),
(JalaliDate(1403, 4, 3), date(2024, 6, 23)),
(JalaliDate(1403, 4, 8), date(2024, 6, 28)),
(JalaliDate(1403, 8, 18), date(2024, 11, 8)),
(JalaliDate(1367, 12, 29), date(1989, 3, 20)),
(JalaliDate(1392, 12, 29), date(2014, 3, 20)),
(JalaliDate(1398, 12, 29), date(2020, 3, 19)),
Expand Down

0 comments on commit 353a148

Please sign in to comment.