File size: 4,085 Bytes
ed76d6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
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
import re
from datetime import datetime
from typing import Union

from dateutil import parser as gregorian_parser
from num2fawords import words
from num2words import num2words
from persiantools import digits
from persiantools.jdatetime import JalaliDate

from rules.base import BaseProcessor
import random


Date = Union[datetime, JalaliDate]

GREGORIAN_MONTHS = (
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
)

DATE_PATTERN = re.compile(
    r"""
    (?:
        (?P<year>\d{4})[^\d]+(?P<month>\d{1,2})[^\d]+(?P<day>\d{1,2})
        |
        (?P<day_first>\d{1,2})[^\d]+(?P<month_first>\d{1,2})[^\d]+(?P<year_first>\d{4})
    )
    """,
    re.VERBOSE,
)


class DateProcessor(BaseProcessor):
    def __init__(self, epsilon:float=1.0):
        super(DateProcessor, self).__init__(epsilon=epsilon)

    def process(self, date_string: str, language: str, *args, **kwargs) -> str:
        if random.random() > self.epsilon:
            return date_string
        if not self.is_date(date_string):
            return date_string

        try:
            date = self._parse_date(date_string)
        except ValueError:
            return date_string

        if date is None:
            raise date_string

        return self._date_to_text(date, language)

    @staticmethod
    def is_date(date_string: str) -> bool:
        date_string = digits.fa_to_en(date_string)
        return DATE_PATTERN.fullmatch(date_string) is not None

    def _parse_date(self, date_string: str) -> Date | None:
        clean_string = digits.fa_to_en(date_string)

        match = DATE_PATTERN.search(clean_string)

        if match:
            year, month, day = self._extract_date_parts(match)

            # Years less than 1600 is considered as Jalali
            if year < 1600:
                try:
                    return JalaliDate(year, month, day)
                except ValueError:
                    pass

        # Try Gregorian parsing.
        try:
            date = gregorian_parser.parse(clean_string)

            if date.year >= 1600:
                return date

        except (ValueError, OverflowError):
            pass

        # If regex matched, give Jalali parsing one final attempt so that
        # invalid Jalali dates produce a useful error.
        if match:
            year, month, day = self._extract_date_parts(match)

            try:
                return JalaliDate(year, month, day)
            except ValueError as exc:
                raise ValueError(
                    f"Invalid Jalali date: {date_string!r}"
                ) from exc

        return None

    @staticmethod
    def _extract_date_parts(
        match: re.Match[str],
    ) -> tuple[int, int, int]:
        if match.group("year"):
            return (
                int(match.group("year")),
                int(match.group("month")),
                int(match.group("day")),
            )

        return (
            int(match.group("year_first")),
            int(match.group("month_first")),
            int(match.group("day_first")),
        )

    def _date_to_text(self, date: Date, language: str) -> str:
        if language == "fa":
            return self._date_to_persian(date)

        if language == "en":
            return self._date_to_english(date)

        raise ValueError(f"Unsupported language: {language!r}")

    @staticmethod
    def _date_to_persian(date: Date) -> str:
        return f"{words(date.day)} {words(date.month)} {words(date.year)}"

    @staticmethod
    def _date_to_english(date: Date) -> str:
        year = date.year

        if "00" in str(year) or len(str(year)) < 4:
            year_text = num2words(year)
        else:
            year_text = (
                f"{num2words(year // 100)} "
                f"{num2words(year % 100)}"
            )

        return (
            f"{num2words(date.day, ordinal=True)} "
            f"of {GREGORIAN_MONTHS[date.month - 1]}, "
            f"{year_text}"
        )