dateparser.languages.locale — DateParser 1.4.2 documentation

# Source code for dateparser.languages.locale

    from itertools import chain

    import regex as re
    from dateutil import parser

    from dateparser.timezone_parser import pop_tz_offset_from_string, word_is_tz
    from dateparser.utils import combine_dicts, normalize_unicode

    from .dictionary import ALWAYS_KEEP_TOKENS, Dictionary, NormalizedDictionary

    NUMERAL_PATTERN = re.compile(r"(\d+)", re.U)

    def _parse_bool(value):
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            return value.strip().lower() in ("true", "1", "yes")
        return False

    class Locale:
        """
        Class that deals with applicability and translation from a locale.

        :param shortname:
            A locale code, e.g. 'fr-PF', 'qu-EC', 'af-NA'.
        :type shortname: str

        :param language_info:
            Language info (translation data) of the language the locale belongs to.
        :type language_info: dict

        :return: A Locale instance
        """

        _dictionary = None
        _normalized_dictionary = None
        _simplifications = None
        _normalized_simplifications = None
        _splitters = None
        _wordchars = None
        _relative_translations = None
        _normalized_relative_translations = None
        _abbreviations = None
        _split_dictionary = None
        _wordchars_for_detection = None

        def __init__(self, shortname, language_info):
            self.shortname = shortname
            locale_specific_info = language_info.get("locale_specific", {}).get(
                shortname, {}
            )
            self.info = combine_dicts(language_info, locale_specific_info)
            self.info.pop("locale_specific", None)

        def is_applicable(
            self,
            date_string,
            strip_timezone=False,
            settings=None,
            ignore_surrounding_text=False,
        ):
            """
            Check if the locale is applicable to translate date string.

            :param date_string:
                A string representing date and/or time in a recognizably valid format.
            :type date_string: str

            :param strip_timezone:
                If True, timezone is stripped from date string.
            :type strip_timezone: bool

            :param ignore_surrounding_text:
                If True, tokens that the locale does not recognise are ignored at
                the edges of the date string (``IGNORE_SURROUNDING_TEXT`` setting).
            :type ignore_surrounding_text: bool

            :return: boolean value representing if the locale is applicable for the date string or not.
            """
            if strip_timezone:
                date_string, _ = pop_tz_offset_from_string(date_string, as_offset=False)

            date_string = self._translate_numerals(date_string)
            if settings.NORMALIZE:
                date_string = normalize_unicode(date_string)
            date_string = self._simplify(date_string, settings=settings)
            dictionary = self._get_dictionary(settings)
            date_tokens = dictionary.split(date_string)
            if ignore_surrounding_text:
                date_tokens = dictionary._strip_unknown_edge_tokens(date_tokens)
            return dictionary.are_tokens_valid(date_tokens)

        def count_applicability(self, text, strip_timezone=False, settings=None):
            if strip_timezone:
                text, _ = pop_tz_offset_from_string(text, as_offset=False)

            text = self._simplify(text, settings=settings)
            sentences = self._sentence_split(text, settings=settings)
            tokens = []
            for sent in sentences:
                tokens.extend(self._split(sent, keep_formatting=False, settings=settings))
            return self._count_words_present_in_the_dictionary(tokens, settings)

        def _count_words_present_in_the_dictionary(self, words, settings=None):
            dictionary = self.clean_dictionary(
                self._get_split_dictionary(settings=settings)
            )
            dict_cnt = 0
            skip_cnt = 0
            for word in set(words):
                if word in dictionary:
                    if dictionary[word]:
                        dict_cnt += 1
                    else:
                        skip_cnt += 1
                elif word.isdigit():
                    skip_cnt += 1
            return [dict_cnt, skip_cnt]

        @staticmethod
        def clean_dictionary(dictionary, threshold=2):
            del_keys = []
            for key in dictionary:
                if len(key) < threshold:
                    del_keys.append(key)
            for del_key in del_keys:
                del dictionary[del_key]
            return dictionary

        def translate(
            self,
            date_string,
            keep_formatting=False,
            settings=None,
            ignore_surrounding_text=False,
        ):
            """
            Translate the date string to its English equivalent.

            :param date_string:
                A string representing date and/or time in a recognizably valid format.
            :type date_string: str

            :param keep_formatting:
                If True, retain formatting of the date string after translation.
            :type keep_formatting: bool

            :param ignore_surrounding_text:
                If True, tokens that the locale does not recognise are dropped from
                the edges of the date string before translation
                (``IGNORE_SURROUNDING_TEXT`` setting).
            :type ignore_surrounding_text: bool

            :return: translated date string.
            """
            date_string = self._translate_numerals(date_string)
            if settings.NORMALIZE:
                date_string = normalize_unicode(date_string)
            date_string = self._simplify(date_string, settings=settings)
            dictionary = self._get_dictionary(settings)
            date_string_tokens = dictionary.split(date_string, keep_formatting)
            if ignore_surrounding_text:
                date_string_tokens = dictionary._strip_unknown_edge_tokens(
                    date_string_tokens
                )

            relative_translations = self._get_relative_translations(settings=settings)

            for i, word in enumerate(date_string_tokens):
                word = word.lower()
                for pattern, replacement in relative_translations.items():
                    if pattern.match(word):
                        date_string_tokens[i] = pattern.sub(replacement, word)
                        break
                else:
                    if word in dictionary:
                        fallback = word if keep_formatting and not word.isalpha() else ""
                        date_string_tokens[i] = dictionary[word] or fallback
            if "in" in date_string_tokens:
                date_string_tokens = self._clear_future_words(date_string_tokens)

            # Remove empty tokens (skip words) and handle adjacent whitespace
            # When a skip token is removed between spaces, keep the maximum number of spaces
            filtered_tokens = []
            i = 0
            while i < len(date_string_tokens):
                token = date_string_tokens[i]

                # Skip empty tokens (removed skip words)
                if not token:
                    # Count preceding spaces already in filtered_tokens
                    prev_spaces = 0
                    j = len(filtered_tokens) - 1
                    while j >= 0 and filtered_tokens[j] == " ":
                        prev_spaces += 1
                        j -= 1

                    # Count following spaces in the remaining tokens
                    next_spaces = 0
                    j = i + 1
                    while j < len(date_string_tokens) and date_string_tokens[j] == " ":
                        next_spaces += 1
                        j += 1

                    # If surrounded by spaces, keep max(prev_spaces, next_spaces)
                    if prev_spaces > 0 and next_spaces > 0:
                        # Remove prev_spaces from filtered_tokens
                        for _ in range(prev_spaces):
                            filtered_tokens.pop()

                        # Add back the maximum number of spaces
                        max_spaces = max(prev_spaces, next_spaces)
                        for _ in range(max_spaces):
                            filtered_tokens.append(" ")

                        # Skip the empty token and all following spaces
                        i += next_spaces + 1
                        continue

                    i += 1
                    continue

                filtered_tokens.append(token)
                i += 1

            return self._join(
                filtered_tokens,
                separator="" if keep_formatting else " ",
                settings=settings,
            )

        def _translate_numerals(self, date_string):
            date_string_tokens = NUMERAL_PATTERN.split(date_string)
            for i, token in enumerate(date_string_tokens):
                if token.isdecimal():
                    date_string_tokens[i] = str(int(token)).zfill(len(token))
            return "".join(date_string_tokens)

        def _get_relative_translations(self, settings=None):
            if settings.NORMALIZE:
                if self._normalized_relative_translations is None:
                    self._normalized_relative_translations = (
                        self._generate_relative_translations(normalize=True)
                    )
                return self._normalized_relative_translations
            else:
                if self._relative_translations is None:
                    self._relative_translations = self._generate_relative_translations(
                        normalize=False
                    )
                return self._relative_translations

        def _generate_relative_translations(self, normalize=False):
            relative_translations = self.info.get("relative-type-regex", {})
            relative_dictionary = {}
            for key, value in relative_translations.items():
                if normalize:
                    value = list(map(normalize_unicode, value))
                pattern = "|".join(sorted(value, key=len, reverse=True))
                pattern = pattern.replace(r"(\d+", r"(?P<n>\d+")
                pattern = re.compile(
                    r"^(?:{})$".format(pattern), re.UNICODE | re.IGNORECASE
                )
                relative_dictionary[pattern] = key
            return relative_dictionary

        def translate_search(self, search_string, settings=None):
            dashes = ["-", "——", "—", "～"]
            word_joint_unsupported_languages = ["zh", "ja"]
            sentences = self._sentence_split(search_string, settings=settings)
            dictionary = self._get_dictionary(settings=settings)
            translated = []
            original = []
            for sentence in sentences:
                original_tokens, simplified_tokens = self._simplify_split_align(
                    sentence, settings=settings
                )
                translated_chunk = []
                original_chunk = []
                last_token_index = len(simplified_tokens) - 1
                skip_next_token = False
                for i, word in enumerate(simplified_tokens):
                    next_word = simplified_tokens[i + 1] if i < last_token_index else ""
                    current_and_next_joined = self._join_chunk(
                        [word, next_word], settings=settings
                    )
                    if skip_next_token:
                        skip_next_token = False
                        continue

                    if word == "" or word == " ":
                        translated_chunk.append(word)
                        original_chunk.append(original_tokens[i])
                    elif (
                        current_and_next_joined in dictionary
                        and word not in dashes
                        and self.shortname not in word_joint_unsupported_languages
                    ):
                        translated_chunk.append(dictionary[current_and_next_joined])
                        original_chunk.append(
                            self._join_chunk(
                                [original_tokens[i], original_tokens[i + 1]],
                                settings=settings,
                            )
                        )
                        skip_next_token = True
                    elif word in dictionary and word not in dashes:
                        translated_chunk.append(dictionary[word])
                        original_chunk.append(original_tokens[i])
                    elif word.strip("()\"'{}[],.،") in dictionary and word not in dashes:
                        punct = word[len(word.strip("()\"'{}[],.،")) :]
                        if punct and dictionary[word.strip("()\"'{}[],.،")]:
                            translated_chunk.append(
                                dictionary[word.strip("()\"'{}[],.،")] + punct
                            )
                        else:
                            translated_chunk.append(dictionary[word.strip("()\"'{}[],.،")])
                        original_chunk.append(original_tokens[i])
                    elif self._token_with_digits_is_ok(word):
                        translated_chunk.append(word)
                        original_chunk.append(original_tokens[i])
                    # Use original token because word_is_tz is case sensitive
                    elif translated_chunk and word_is_tz(original_tokens[i]):
                        translated_chunk.append(word)
                        original_chunk.append(original_tokens[i])
                    else:
                        if translated_chunk:
                            translated.append(translated_chunk)
                            translated_chunk = []
                            original.append(original_chunk)
                            original_chunk = []
                if translated_chunk:
                    translated.append(translated_chunk)
                    original.append(original_chunk)
            for i in range(len(translated)):
                if "in" in translated[i]:
                    translated[i] = self._clear_future_words(translated[i])
                translated[i] = self._join_chunk(
                    list(filter(bool, translated[i])), settings=settings
                )
                original[i] = self._join_chunk(
                    list(filter(bool, original[i])), settings=settings
                )
            return translated, original

        def _get_abbreviations(self, settings):
            dictionary = self._get_dictionary(settings=settings)
            abbreviations = []
            if self._abbreviations is None:
                for item in dictionary:
                    if item.endswith(".") and len(item) > 1:
                        abbreviations.append(item)
                self._abbreviations = abbreviations
            return self._abbreviations

        def _sentence_split(self, string, settings):
            abbreviations = self._get_abbreviations(settings=settings)
            digit_abbreviations = ["[0-9]"]  # numeric date with full stop
            abbreviation_string = ""

            for abbreviation in abbreviations:
                abbreviation_string += (
                    "(?<! " + abbreviation[:-1] + ")"
                )  # negative lookbehind
            if self.shortname in ["fi", "cs", "hu", "de", "da"]:
                for digit_abbreviation in digit_abbreviations:
                    abbreviation_string += (
                        "(?<!" + digit_abbreviation + ")"
                    )  # negative lookbehind

            splitters_dict = {
                1: r"[\.!?;…\r\n]+(?:\s|$)*",  # most European, Tagalog, Hebrew, Georgian,
                # Indonesian, Vietnamese
                2: r"[\.!?;…\r\n]+(\s*[¡¿]*|$)|[¡¿]+",  # Spanish
                3: r"[|!?;\r\n]+(?:\s|$)+",  # Hindi and Bangla
                4: r"[。…‥\.!?？！;\r\n]+(?:\s|$)+",  # Japanese and Chinese
                5: r"[\r\n]+",  # Thai
                6: r"[\r\n؟!\.…]+(?:\s|$)+",
            }  # Arabic and Farsi
            if "sentence_splitter_group" not in self.info:
                split_reg = abbreviation_string + splitters_dict[1]
                sentences = re.split(split_reg, string)
            else:
                split_reg = (
                    abbreviation_string
                    + splitters_dict[self.info["sentence_splitter_group"]]
                )
                sentences = re.split(split_reg, string)

            sentences = filter(None, sentences)
            return sentences

        def _simplify_split_align(self, original, settings):
            # TODO: Switch to new split method.
            original_tokens = self._word_split(original, settings=settings)
            simplified_tokens = self._word_split(
                self._simplify(normalize_unicode(original), settings=settings),
                settings=settings,
            )
            if len(original_tokens) == len(simplified_tokens):
                return original_tokens, simplified_tokens

            elif len(original_tokens) < len(simplified_tokens):
                add_empty = False
                for i, token in enumerate(simplified_tokens):
                    if i < len(original_tokens):
                        if token == normalize_unicode(original_tokens[i].lower()):
                            add_empty = False
                        else:
                            if not add_empty:
                                add_empty = True
                                continue
                            else:
                                original_tokens.insert(i, "")
                    else:
                        original_tokens.insert(i, "")
            else:
                add_empty = False
                for i, token in enumerate(original_tokens):
                    if i < len(simplified_tokens):
                        if normalize_unicode(token.lower()) == simplified_tokens[i]:
                            add_empty = False
                        else:
                            if not add_empty:
                                add_empty = True
                                continue
                            else:
                                simplified_tokens.insert(i, "")
                    else:
                        simplified_tokens.insert(i, "")

            while len(original_tokens) != len(simplified_tokens):
                if len(original_tokens) > len(simplified_tokens):
                    original_tokens.remove("")
                else:
                    simplified_tokens.remove("")
            return original_tokens, simplified_tokens

        def _get_split_dictionary(self, settings):
            if self._split_dictionary is None:
                settings.NORMALIZE = True
                dictionary = self._get_dictionary(settings=settings)
                self._split_dictionary = self._split_dict(dictionary)
            return self._split_dictionary

        def _split_dict(self, dictionary):
            newdict = {}
            for item in dictionary:
                if " " in item:
                    items = item.split()
                    for i in items:
                        newdict[i] = dictionary[item]
                else:
                    newdict[item] = dictionary[item]
            return newdict

        def _word_split(self, string, settings):
            if "no_word_spacing" in self.info:
                return self._split(string, keep_formatting=True, settings=settings)
            else:
                return string.split()

        def _split(self, date_string, keep_formatting, settings=None):
            tokens = [date_string]
            tokens = list(self._split_tokens_with_regex(tokens, r"(\d+)"))
            tokens = list(
                self._split_tokens_by_known_words(
                    tokens, keep_formatting, settings=settings
                )
            )
            return tokens

        def _split_tokens_with_regex(self, tokens, regex):
            tokens = tokens[:]
            for i, token in enumerate(tokens):
                tokens[i] = re.split(regex, token)
            return filter(bool, chain.from_iterable(tokens))

        def _split_tokens_by_known_words(self, tokens, keep_formatting, settings=None):
            dictionary = self._get_dictionary(settings)
            for i, token in enumerate(tokens):
                tokens[i] = dictionary.split(token, keep_formatting)
            return list(chain.from_iterable(tokens))

        def _join_chunk(self, chunk, settings):
            if "no_word_spacing" in self.info:
                return self._join(chunk, separator="", settings=settings)
            else:
                return re.sub(r"\s{2,}", " ", " ".join(chunk))

        def _token_with_digits_is_ok(self, token):
            if "no_word_spacing" in self.info:
                if re.search(r"[\d\.:\-/]+", token) is not None:
                    return True
                else:
                    return False

            else:
                if re.search(r"\d+", token) is not None:
                    return True
                else:
                    return False

        def _simplify(self, date_string, settings=None):
            date_string = date_string.lower()
            simplifications = self._get_simplifications(settings=settings)

            if self.info.get("name") == "ru":
                date_string = self._process_russian_compound_ordinals(
                    date_string, simplifications
                )
            else:
                date_string = self._apply_simplifications(date_string, simplifications)

            return date_string

        def _apply_simplifications(self, date_string, simplifications):
            for simplification in simplifications:
                pattern, replacement = list(simplification.items())[0]
                date_string = pattern.sub(replacement, date_string).lower()
            return date_string

        def _process_russian_compound_ordinals(self, date_string, simplifications):
            """Process Russian compound ordinals mathematically (двадцать + первое = 21)."""
            date_string = self._apply_simplifications(date_string, simplifications)

            def replace_number_pairs(match):
                first_num = int(match.group(1))
                second_num = int(match.group(2))
                result = first_num + second_num
                if 1 <= result <= 31 and first_num in [20, 30] and 1 <= second_num <= 9:
                    return str(result)
                return match.group(0)

            number_pair_pattern = r"\b(\d+)\s+(\d+)\b"
            date_string = re.sub(number_pair_pattern, replace_number_pairs, date_string)

            return date_string

        def _get_simplifications(self, settings=None):
            no_word_spacing = _parse_bool(self.info.get("no_word_spacing", False))
            if settings.NORMALIZE:
                if self._normalized_simplifications is None:
                    self._normalized_simplifications = []
                    simplifications = self._generate_simplifications(normalize=True)
                    for simplification in simplifications:
                        pattern, replacement = list(simplification.items())[0]
                        if not no_word_spacing:
                            pattern = r"(?<=\A|\W|_)%s(?=\Z|\W|_)" % pattern
                        pattern = re.compile(pattern, flags=re.I | re.U)
                        self._normalized_simplifications.append({pattern: replacement})
                return self._normalized_simplifications

            else:
                if self._simplifications is None:
                    self._simplifications = []
                    simplifications = self._generate_simplifications(normalize=False)
                    for simplification in simplifications:
                        pattern, replacement = list(simplification.items())[0]
                        if not no_word_spacing:
                            pattern = r"(?<=\A|\W|_)%s(?=\Z|\W|_)" % pattern
                        pattern = re.compile(pattern, flags=re.I | re.U)
                        self._simplifications.append({pattern: replacement})
                return self._simplifications

        def _generate_simplifications(self, normalize=False):
            simplifications = []
            for simplification in self.info.get("simplifications", []):
                c_simplification = {}
                key, value = list(simplification.items())[0]
                if normalize:
                    key = normalize_unicode(key)

                if isinstance(value, int):
                    c_simplification[key] = str(value)
                else:
                    c_simplification[key] = normalize_unicode(value) if normalize else value

                simplifications.append(c_simplification)
            return simplifications

        def _clear_future_words(self, words):
            freshness_words = {"day", "week", "month", "year", "hour", "minute", "second"}
            if set(words).isdisjoint(freshness_words):
                words[words.index("in")] = ""
            return words

        def _join(self, tokens, separator=" ", settings=None):
            if not tokens:
                return ""

            capturing_splitters = self._get_splitters(settings)["capturing"]
            joined = tokens[0]
            for i in range(1, len(tokens)):
                left, right = tokens[i - 1], tokens[i]
                if left not in capturing_splitters and right not in capturing_splitters:
                    joined += separator
                joined += right

            return joined

        def _get_dictionary(self, settings=None):
            if not settings.NORMALIZE:
                if self._dictionary is None:
                    self._generate_dictionary()
                self._dictionary._settings = settings
                return self._dictionary
            else:
                if self._normalized_dictionary is None:
                    self._generate_normalized_dictionary()
                self._normalized_dictionary._settings = settings
                return self._normalized_dictionary

        def _get_wordchars(self, settings=None):
            if self._wordchars is None:
                self._set_wordchars(settings)
            return self._wordchars

        def _get_splitters(self, settings=None):
            if self._splitters is None:
                self._set_splitters(settings)
            return self._splitters

        def _set_splitters(self, settings=None):
            splitters = {
                # The ones that split string only if they are not surrounded by letters from both sides:
                "wordchars": set(),
                # The ones that are not filtered out from tokens after split:
                "capturing": set(),
            }
            splitters["capturing"] |= set(ALWAYS_KEEP_TOKENS)

            wordchars = self._get_wordchars(settings)
            skip = set(self.info.get("skip", [])) | splitters["capturing"]
            for token in skip:
                if not re.match(r"^\W+$", token, re.UNICODE):
                    continue
                if token in wordchars:
                    splitters["wordchars"].add(token)

            self._splitters = splitters

        def _set_wordchars(self, settings=None):
            wordchars = set()
            for word in self._get_dictionary(settings):
                if re.match(r"^[\W\d_]+$", word, re.UNICODE):
                    continue
                for char in word:
                    wordchars.add(char.lower())

            self._wordchars = wordchars - {" "} | {
                "0",
                "1",
                "2",
                "3",
                "4",
                "5",
                "6",
                "7",
                "8",
                "9",
            }

        def get_wordchars_for_detection(self, settings):
            if self._wordchars_for_detection is None:
                wordchars = set()
                for word in self._get_dictionary(settings):
                    if re.match(r"^[\W\d_]+$", word, re.UNICODE):
                        continue
                    for char in word:
                        wordchars.add(char.lower())
                self._wordchars_for_detection = wordchars - {
                    "0",
                    "1",
                    "2",
                    "3",
                    "4",
                    "5",
                    "6",
                    "7",
                    "8",
                    "9",
                    ":",
                    "(",
                    ")",
                    "'",
                    "q",
                    "a",
                    "m",
                    "p",
                    " ",
                }
            return self._wordchars_for_detection

        def _generate_dictionary(self, settings=None):
            self._dictionary = Dictionary(self.info, settings=settings)

        def _generate_normalized_dictionary(self, settings=None):
            self._normalized_dictionary = NormalizedDictionary(self.info, settings=settings)

        def to_parserinfo(self, base_cls=parser.parserinfo):
            attributes = {
                "JUMP": self.info.get("skip", []),
                "PERTAIN": self.info.get("pertain", []),
                "WEEKDAYS": [
                    self.info["monday"],
                    self.info["tuesday"],
                    self.info["wednesday"],
                    self.info["thursday"],
                    self.info["friday"],
                    self.info["saturday"],
                    self.info["sunday"],
                ],
                "MONTHS": [
                    self.info["january"],
                    self.info["february"],
                    self.info["march"],
                    self.info["april"],
                    self.info["may"],
                    self.info["june"],
                    self.info["july"],
                    self.info["august"],
                    self.info["september"],
                    self.info["october"],
                    self.info["november"],
                    self.info["december"],
                ],
                "HMS": [self.info["hour"], self.info["minute"], self.info["second"]],
            }
            name = "{language}ParserInfo".format(language=self.info["name"])
            return type(name, bases=[base_cls], dict=attributes)