コード例 #1
0
class ChineseDateTimePeriodParser(BaseDateTimePeriodParser):
    def __init__(self):
        super().__init__(ChineseDateTimePeriodParserConfiguration())
        self.tmo_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DateTimePeriodMORegex)
        self.tmi_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DateTimePeriodMIRegex)
        self.taf_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DateTimePeriodAFRegex)
        self.tev_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DateTimePeriodEVRegex)
        self.tni_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DateTimePeriodNIRegex)
        self.unit_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DateTimePeriodUnitRegex)
        self.time_of_day_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.TimeOfDayRegex)
        self.single_date_extractor = ChineseDateExtractor()
        self.time_period_extractor = ChineseTimePeriodExtractor()
        self.cardinal_extractor = ChineseCardinalExtractor()
        self.cardinal_parser = CJKNumberParser(
            ChineseNumberParserConfiguration())

    def parse(self,
              source: ExtractResult,
              reference: datetime = None) -> Optional[DateTimeParseResult]:
        if reference is None:
            reference = datetime.now()

        result = DateTimeParseResult(source)

        if source.type is self.parser_type_name:
            source_text = source.text.strip().lower()

            inner_result = self.merge_date_and_time_periods(
                source_text, reference)

            if not inner_result.success:
                inner_result = self.merge_two_time_points(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self.parse_specific_time_of_day(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_number_with_unit(
                    source_text, reference)

            if inner_result.success:
                inner_result.future_resolution[
                    TimeTypeConstants.
                    START_DATETIME] = DateTimeFormatUtil.format_date_time(
                        inner_result.future_value[0])
                inner_result.future_resolution[
                    TimeTypeConstants.
                    END_DATETIME] = DateTimeFormatUtil.format_date_time(
                        inner_result.future_value[1])
                inner_result.past_resolution[
                    TimeTypeConstants.
                    START_DATETIME] = DateTimeFormatUtil.format_date_time(
                        inner_result.past_value[0])
                inner_result.past_resolution[
                    TimeTypeConstants.
                    END_DATETIME] = DateTimeFormatUtil.format_date_time(
                        inner_result.past_value[1])
                result.value = inner_result
                result.timex_str = inner_result.timex if inner_result is not None else ''
                result.resolution_str = ''

        return result

    def merge_date_and_time_periods(
            self, trimmed_source: str,
            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        extracted_result1 = self.single_date_extractor.extract(
            trimmed_source, reference)
        extracted_result2 = self.time_period_extractor.extract(
            trimmed_source, reference)
        if len(extracted_result1) != 1 or len(extracted_result2) != 1:
            return result

        parsed_result1 = self.config.date_parser.parse(extracted_result1[0],
                                                       reference)
        parsed_result2 = self.config.time_period_parser.parse(
            extracted_result2[0], reference)
        time_range = tuple(parsed_result2.value.future_value)
        begin_time = time_range[0]
        end_time = time_range[1]
        future_date = parsed_result1.value.future_value
        past_date = parsed_result1.value.past_value

        result.future_value = (DateUtils.safe_create_from_min_value(
            future_date.year, future_date.month, future_date.day,
            begin_time.hour, begin_time.minute, begin_time.second),
                               DateUtils.safe_create_from_min_value(
                                   future_date.year, future_date.month,
                                   future_date.day, end_time.hour,
                                   end_time.minute, end_time.second))

        result.past_value = (DateUtils.safe_create_from_min_value(
            past_date.year, past_date.month, past_date.day, begin_time.hour,
            begin_time.minute, begin_time.second),
                             DateUtils.safe_create_from_min_value(
                                 past_date.year, past_date.month,
                                 past_date.day, end_time.hour, end_time.minute,
                                 end_time.second))

        split = parsed_result2.timex_str.split('T')
        if len(split) != 4:
            return result

        date_str = parsed_result1.timex_str

        result.timex = split[0] + date_str + 'T' + split[
            1] + date_str + 'T' + split[2] + 'T' + split[3]
        result.success = True
        return result

    def merge_two_time_points(self, source: str,
                              reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        prs: BeginEnd = None
        time_ers = self.config.time_extractor.extract(source, reference)
        datetime_ers = self.config.date_time_extractor.extract(
            source, reference)

        both_has_date = False
        begin_has_date = False
        end_has_date = False

        if len(datetime_ers) == 2:
            prs = self.get_two_points(datetime_ers[0], datetime_ers[1],
                                      self.config.date_time_parser,
                                      self.config.date_time_parser, reference)
            both_has_date = True
        elif len(datetime_ers) == 1 and len(time_ers) == 2:
            if datetime_ers[0].overlap(time_ers[0]):
                prs = self.get_two_points(datetime_ers[0], time_ers[1],
                                          self.config.date_time_parser,
                                          self.config.time_parser, reference)
                begin_has_date = True
            else:
                prs = self.get_two_points(time_ers[0], datetime_ers[0],
                                          self.config.time_parser,
                                          self.config.date_time_parser,
                                          reference)
                end_has_date = True
        elif len(datetime_ers) == 1 and len(time_ers) == 1:
            if time_ers[0].start < datetime_ers[0].start:
                prs = self.get_two_points(time_ers[0], datetime_ers[0],
                                          self.config.time_parser,
                                          self.config.date_time_parser,
                                          reference)
                end_has_date = True
            else:
                prs = self.get_two_points(datetime_ers[0], time_ers[0],
                                          self.config.date_time_parser,
                                          self.config.time_parser, reference)
                begin_has_date = True

        if prs is None or not prs.begin.value or not prs.end.value:
            return result

        begin: DateTimeResolutionResult = prs.begin.value
        end: DateTimeResolutionResult = prs.end.value

        future_begin: datetime = begin.future_value
        future_end: datetime = end.future_value
        past_begin: datetime = begin.past_value
        past_end: datetime = end.past_value

        if future_begin > future_end:
            future_begin = past_begin

        if past_end < past_begin:
            past_end = future_end

        right_time = DateUtils.safe_create_from_min_value_date_time(reference)
        left_time = DateUtils.safe_create_from_min_value_date_time(reference)

        if both_has_date:
            right_time = DateUtils.safe_create_from_min_value_date_time(
                future_end)
            left_time = DateUtils.safe_create_from_min_value_date_time(
                future_begin)
        elif begin_has_date:
            # TODO: Handle "明天下午两点到五点"
            future_end = self.get_datetime(future_begin, future_end)
            past_end = self.get_datetime(past_begin, past_end)
            left_time = DateUtils.safe_create_from_min_value_date_time(
                future_begin)
        elif end_has_date:
            # TODO: Handle "明天下午两点到五点"
            future_begin = self.get_datetime(future_end, future_begin)
            past_begin = self.get_datetime(past_end, past_begin)
            right_time = DateUtils.safe_create_from_min_value_date_time(
                future_end)

        left: DateTimeResolutionResult = prs.begin.value
        right: DateTimeResolutionResult = prs.end.value
        left_result_time: datetime = left.future_value
        right_result_time: datetime = right.future_value

        left_time += timedelta(hours=left_result_time.hour,
                               minutes=left_result_time.minute,
                               seconds=left_result_time.second)
        right_time += timedelta(hours=right_result_time.hour,
                                minutes=right_result_time.minute,
                                seconds=right_result_time.second)

        # the right side time contains "ampm", while the left side doesn't
        if right.comment == 'ampm' and not left.comment and right_time < left_time:
            right_time += timedelta(hours=12)

        if right_time < left_time:
            right_time += timedelta(days=1)

        result.future_value = [left_time, right_time]
        result.past_value = [left_time, right_time]

        fuzzy_timex = 'X' in prs.begin.timex_str or 'X' in prs.end.timex_str
        left_timex = prs.begin.timex_str if fuzzy_timex else DateTimeFormatUtil.luis_date_time(
            left_time)
        right_timex = prs.end.timex_str if fuzzy_timex else DateTimeFormatUtil.luis_date_time(
            right_time)
        total_hours = DateUtils.total_hours(left_time, right_time)
        result.timex = f'({left_timex},{right_timex},PT{total_hours}H)'

        result.success = True
        return result

    def parse_specific_time_of_day(
            self, source: str,
            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()
        trimmed_source = source.strip()
        begin_hour = end_hour = end_min = 0

        # Handle 昨晚,今晨
        if RegExpUtility.is_exact_match(self.config.specific_time_of_day_regex,
                                        trimmed_source, True):
            values = self.config.get_matched_time_range(source)
            if not values:
                return result

            swift = values.swift
            date = reference.date() + timedelta(days=swift)
            day = date.day
            month = date.month
            year = date.year

            result.timex = DateTimeFormatUtil.format_date(
                date) + values.time_str
            result.future_value = result.past_value = [
                DateUtils.safe_create_from_min_value(year, month, day,
                                                     values.begin_hour, 0, 0),
                DateUtils.safe_create_from_min_value(year, month, day,
                                                     values.end_hour,
                                                     values.end_min,
                                                     values.end_min)
            ]

            result.success = True
            return result

        # handle morning, afternoon..
        if regex.search(self.tmo_regex, source):
            time_str = 'TMO'
            begin_hour = 8
            end_hour = 12
        elif regex.search(self.tmi_regex, source):
            time_str = 'TMI'
            begin_hour = 11
            end_hour = 13
        elif regex.search(self.taf_regex, source):
            time_str = 'TAF'
            begin_hour = Constants.HALF_DAY_HOUR_COUNT
            end_hour = 16
        elif regex.search(self.tev_regex, source):
            time_str = 'TEV'
            begin_hour = 16
            end_hour = 20
        elif regex.search(self.tni_regex, source):
            time_str = 'TNI'
            begin_hour = 20
            end_hour = 23
            end_min = 59
        else:
            return result

        if RegExpUtility.is_exact_match(self.config.specific_time_of_day_regex,
                                        trimmed_source, True):
            swift = 0
            if regex.search(self.config.next_regex, trimmed_source):
                swift = 1
            elif regex.search(self.config.last_regex, trimmed_source):
                swift = -1

            date = reference.date() + timedelta(days=swift)
            day = date.day
            month = date.month
            year = date.year

            result.timex = DateTimeFormatUtil.format_date(date) + time_str
            result.future_value = result.past_value = [
                DateUtils.safe_create_from_min_value(year, month, day,
                                                     begin_hour, 0, 0),
                DateUtils.safe_create_from_min_value(year, month, day,
                                                     end_hour, end_min,
                                                     end_min)
            ]

            result.success = True
            return result

        # handle Date followed by morning, afternoon
        match = regex.search(self.config.time_of_day_regex, trimmed_source)
        if match:
            before_str = trimmed_source[0:match.start()].strip()
            extracted_results = self.single_date_extractor.extract(
                before_str, reference)

            if len(extracted_results
                   ) == 0 or extracted_results[0].length != len(before_str):
                return result

            parse_result = self.config.date_parser.parse(
                extracted_results[0], reference)
            future_date = parse_result.value.future_value
            past_date = parse_result.value.past_value

            result.timex = parse_result.timex_str + time_str

            result.future_value = (DateUtils.safe_create_from_min_value(
                future_date.year, future_date.month, future_date.day,
                begin_hour, 0, 0),
                                   DateUtils.safe_create_from_min_value(
                                       future_date.year, future_date.month,
                                       future_date.day, end_hour, end_min,
                                       end_min))

            result.past_value = (DateUtils.safe_create_from_min_value(
                past_date.year, past_date.month, past_date.day, begin_hour, 0,
                0),
                                 DateUtils.safe_create_from_min_value(
                                     past_date.year, past_date.month,
                                     past_date.day, end_hour, end_min,
                                     end_min))

            result.success = True
            return result

        return result

    def _parse_number_with_unit(
            self, source: str,
            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()
        ers = self.cardinal_extractor.extract(source)

        if len(ers) == 1:
            er = ers[0]
            pr = self.cardinal_parser.parse(er)
            source_unit: str = source[er.start + er.length:].strip().lower()

            if source_unit.startswith('个'):
                source_unit = source_unit[1:]

            before_str = source[:er.start].strip().lower()

            return self.__parse_common_duration_with_unit(
                before_str, source_unit, pr.resolution_str, float(pr.value),
                reference)

        # handle "last hour"
        match = regex.search(self.unit_regex, source)

        if match:
            source_unit = RegExpUtility.get_group(match, 'unit')
            before_str = source[:match.start()].strip().lower()

            return self.__parse_common_duration_with_unit(
                before_str, source_unit, '1', 1, reference)

        return result

    def __parse_common_duration_with_unit(self, before: str, unit: str, num: str, swift: float, reference: datetime)\
            -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        if unit not in self.config.unit_map:
            return result

        unit_str = self.config.unit_map[unit]

        past_match = regex.search(self.config.past_regex, before)
        has_past = past_match and len(past_match.group()) == len(before)

        future_match = regex.search(self.config.future_regex, before)
        has_future = future_match and len(future_match.group()) == len(before)

        if not has_future and not has_past:
            return result

        begin_date = reference
        end_date = reference

        if unit_str == 'H':
            if has_past:
                begin_date += timedelta(hours=-swift)
            if has_future:
                end_date += timedelta(hours=swift)
        elif unit_str == 'M':
            if has_past:
                begin_date += timedelta(minutes=-swift)
            if has_future:
                end_date += timedelta(minutes=swift)
        elif unit_str == 'S':
            if has_past:
                begin_date += timedelta(seconds=-swift)
            if has_future:
                end_date += timedelta(seconds=swift)
        else:
            return result

        begin_timex = DateTimeFormatUtil.luis_date_from_datetime(
            begin_date) + 'T' + DateTimeFormatUtil.luis_time_from_datetime(
                begin_date)
        end_timex = DateTimeFormatUtil.luis_date_from_datetime(
            end_date) + 'T' + DateTimeFormatUtil.luis_time_from_datetime(
                end_date)

        result.timex = f'({begin_timex},{end_timex},PT{num}{unit_str[0]})'

        result.future_value = [begin_date, end_date]
        result.past_value = [begin_date, end_date]

        result.success = True
        return result
コード例 #2
0
class ChineseDatePeriodParser(BaseDatePeriodParser):
    def __init__(self):
        super().__init__(ChineseDatePeriodParserConfiguration())
        self.integer_extractor = ChineseIntegerExtractor()
        self.number_parser = CJKNumberParser(
            ChineseNumberParserConfiguration())
        self.year_in_chinese_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DatePeriodYearInChineseRegex)
        self.number_combined_with_unit_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.NumberCombinedWithUnit)
        self.unit_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.UnitRegex)
        self.year_and_month_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.YearAndMonth)
        self.pure_number_year_and_month_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.PureNumYearAndMonth)
        self.year_to_year_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.YearToYear)
        self.year_to_year_suffix_required = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.YearToYearSuffixRequired)
        self.chinese_year_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DatePeriodYearInChineseRegex)
        self.season_with_year_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.SeasonWithYear)
        self.decade_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DecadeRegex)
        self.date_this_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DatePeriodThisRegex)
        self.date_last_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DatePeriodLastRegex)
        self.date_next_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DatePeriodNextRegex)

    def parse(self,
              source: ExtractResult,
              reference: datetime = None) -> Optional[DateTimeParseResult]:
        result_value = None
        if not reference:
            reference = datetime.now()

        if source.type == self.parser_type_name:
            source_text = source.text.strip().lower()

            inner_result = self._parse_simple_cases(source_text, reference)
            if not inner_result.success:
                inner_result = self._parse_one_word_period(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._merge_two_times_points(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_number_with_unit(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_duration(source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_year_and_month(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_year_to_year(source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_year(source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_week_of_month(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_season(source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_quarter(source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_decade(source_text, reference)

            if inner_result.success:
                if inner_result.future_value and inner_result.past_value:
                    inner_result.future_resolution = {
                        TimeTypeConstants.START_DATE:
                        DateTimeFormatUtil.format_date(
                            inner_result.future_value[0]),
                        TimeTypeConstants.END_DATE:
                        DateTimeFormatUtil.format_date(
                            inner_result.future_value[1])
                    }
                    inner_result.past_resolution = {
                        TimeTypeConstants.START_DATE:
                        DateTimeFormatUtil.format_date(
                            inner_result.past_value[0]),
                        TimeTypeConstants.END_DATE:
                        DateTimeFormatUtil.format_date(
                            inner_result.past_value[1])
                    }
                else:
                    inner_result.future_resolution = {}
                    inner_result.past_resolution = {}
                result_value = inner_result

        result = DateTimeParseResult(source)
        result.value = result_value
        result.timex_str = result_value.timex if result_value else ''
        result.resolution_str = ''

        return result

    def _parse_simple_cases(self, source: str,
                            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()
        year = reference.year
        month = reference.month
        no_year = False
        input_year = False

        match = regex.search(self.config.simple_cases_regex, source)

        if not match or match.start() != 0 or len(
                match.group()) != len(source):
            return result

        days = RegExpUtility.get_group_list(match, Constants.DAY_GROUP_NAME)
        begin_day = self.config.day_of_month[days[0]]
        end_day = self.config.day_of_month[days[1]]

        month_str = RegExpUtility.get_group(match, Constants.MONTH_GROUP_NAME)

        if month_str.strip() != '':
            month = self.config.month_of_year[month_str]
        else:
            month_str = RegExpUtility.get_group(match, Constants.REL_MONTH)
            month += self.config.get_swift_day_or_month(month_str)

            if month < 0:
                month = 0
                year -= 1
            elif month > 11:
                month = 11
                year += 1

        year_str = RegExpUtility.get_group(match, Constants.YEAR_GROUP_NAME)
        if year_str.strip() != '':
            year = int(year_str)
            input_year = True
        else:
            no_year = True

        begin_date_luis = DateTimeFormatUtil.luis_date(
            year if input_year or self.config.is_future(month_str) else -1,
            month, begin_day)
        end_date_luis = DateTimeFormatUtil.luis_date(
            year if input_year or self.config.is_future(month_str) else -1,
            month, end_day)

        future_past_begin_date = DateUtils.generate_dates(
            no_year, reference, year, month, begin_day)
        future_past_end_date = DateUtils.generate_dates(
            no_year, reference, year, month, end_day)

        result.timex = f'({begin_date_luis},{end_date_luis},P{end_day - begin_day}D)'

        result.future_value = [
            future_past_begin_date[0], future_past_end_date[0]
        ]
        result.past_value = [
            future_past_begin_date[1], future_past_end_date[1]
        ]
        result.success = True
        return result

    def _parse_number_with_unit(
            self, source: str,
            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        # if there are NO spaces between number and unit
        match = regex.search(self.number_combined_with_unit_regex, source)
        if not match:
            return result

        source_unit = RegExpUtility.get_group(match,
                                              Constants.UNIT).strip().lower()
        if source_unit not in self.config.unit_map:
            return result

        num_str = RegExpUtility.get_group(match, Constants.NUM)
        before_str = source[:match.start()].strip().lower()

        return self.__parse_common_duration_with_unit(before_str, source_unit,
                                                      num_str, reference)

    def _parse_duration(self, source: str,
                        reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        # for case "前两年" "后三年"
        duration_result = next(
            iter(self.config.duration_extractor.extract(source, reference)),
            None)
        if not duration_result:
            return result

        match = regex.search(self.unit_regex, duration_result.text)
        if not match:
            return result

        source_unit = RegExpUtility.get_group(match,
                                              Constants.UNIT).strip().lower()
        if source_unit not in self.config.unit_map:
            return result

        before_str = source[:duration_result.start].strip().lower()
        number_str = duration_result.text[:match.start()].strip().lower()
        number_val = self.__convert_chinese_to_number(number_str)
        num_str = str(number_val)

        return self.__parse_common_duration_with_unit(before_str, source_unit,
                                                      num_str, reference)

    def __parse_common_duration_with_unit(
            self, before: str, unit: str, num: str,
            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        unit_str = self.config.unit_map[unit]

        past_match = regex.search(self.config.past_regex, before)
        has_past = past_match and len(past_match.group()) == len(before)

        future_match = regex.search(self.config.future_regex, before)
        has_future = future_match and len(future_match.group()) == len(before)

        if not has_future and not has_past:
            return result

        begin_date = reference
        end_date = reference
        difference = float(num)

        if unit_str == Constants.UNIT_D:
            if has_past:
                begin_date += timedelta(days=-difference)
            if has_future:
                end_date += timedelta(days=difference)
        elif unit_str == Constants.UNIT_W:
            if has_past:
                begin_date += timedelta(days=-7 * difference)
            if has_future:
                end_date += timedelta(days=7 * difference)
        elif unit_str == Constants.UNIT_MON:
            if has_past:
                begin_date += datedelta(months=int(-difference))
            if has_future:
                end_date += datedelta(months=int(difference))
        elif unit_str == Constants.UNIT_Y:
            if has_past:
                begin_date += datedelta(years=int(-difference))
            if has_future:
                end_date += datedelta(years=int(difference))
        else:
            return result

        if has_future:
            begin_date += timedelta(days=1)
            end_date += timedelta(days=1)

        begin_timex = DateTimeFormatUtil.luis_date_from_datetime(begin_date)
        end_timex = DateTimeFormatUtil.luis_date_from_datetime(end_date)

        result.timex = f'({begin_timex},{end_timex},P{num}{unit_str[0]})'

        result.future_value = [begin_date, end_date]
        result.past_value = [begin_date, end_date]

        result.success = True
        return result

    def __convert_chinese_to_number(self, source: str) -> int:
        num = -1
        er = next(iter(self.integer_extractor.extract(source)), None)

        if er and er.type == NumberConstants.SYS_NUM_INTEGER:
            num = int(self.number_parser.parse(er).value)

        return num

    def _parse_year_and_month(self, source: str,
                              reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        match = regex.search(self.year_and_month_regex, source)

        if not match or len(match.group()) != len(source):
            match = regex.search(self.pure_number_year_and_month_regex, source)

        if not match or len(match.group()) != len(source):
            return result

        year = reference.year
        year_num = RegExpUtility.get_group(match, Constants.YEAR_GROUP_NAME)
        year_chinese = RegExpUtility.get_group(match, Constants.YEAR_CHINESE)
        year_relative = RegExpUtility.get_group(match, Constants.YEAR_RELATIVE)

        if year_num.strip() != '':
            if self.config.is_year_only(year_num):
                year_num = year_num[:-1]
            year = self._convert_year(year_num, False)
        elif year_chinese.strip() != '':
            if self.config.is_year_only(year_chinese):
                year_chinese = year_chinese[:-1]
            year = self._convert_year(year_chinese, True)
        elif year_relative.strip() != '':
            year += self.config.get_swift_day_or_month(year_relative)

        if 100 > year >= 90:
            year += 1900
        elif year < 100 and year < 20:
            year += 2000

        month_str = RegExpUtility.get_group(match, Constants.MONTH_GROUP_NAME)
        month = self.config.month_of_year.get(month_str, 0) % 12
        if month == 0:
            month = 12

        begin_date = DateUtils.safe_create_from_min_value(year, month, 1)
        end_date = DateUtils.safe_create_from_min_value(
            year, month, 1) + datedelta(months=1)
        result.future_value = [begin_date, end_date]
        result.past_value = [begin_date, end_date]

        result.timex = f'{year:04d}-{month:02d}'

        result.success = True
        return result

    def _parse_year_to_year(self, source: str,
                            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        match = regex.search(self.year_to_year_regex, source)

        if not match:
            match = regex.search(self.year_to_year_suffix_required, source)
            if not match:
                return result

        year_matches = list(regex.finditer(self.config.year_regex, source))
        chinese_year_matches = list(
            regex.finditer(self.chinese_year_regex, source))

        begin_year = 0
        end_year = 0

        if len(year_matches) == 2:
            begin_year = self.__convert_chinese_to_number(
                RegExpUtility.get_group(year_matches[0],
                                        Constants.YEAR_GROUP_NAME))
            end_year = self.__convert_chinese_to_number(
                RegExpUtility.get_group(year_matches[1],
                                        Constants.YEAR_GROUP_NAME))
        elif len(chinese_year_matches) == 2:
            begin_year = self._convert_year(
                RegExpUtility.get_group(chinese_year_matches[0],
                                        Constants.YEAR_CHINESE), True)
            end_year = self._convert_year(
                RegExpUtility.get_group(chinese_year_matches[1],
                                        Constants.YEAR_CHINESE), True)
        elif len(year_matches) == 1 and len(chinese_year_matches) == 1:
            if year_matches[0].start() < chinese_year_matches[0].start():
                begin_year = self.__convert_chinese_to_number(
                    RegExpUtility.get_group(year_matches[0],
                                            Constants.YEAR_GROUP_NAME))
                end_year = self.__convert_chinese_to_number(
                    RegExpUtility.get_group(chinese_year_matches[0],
                                            Constants.YEAR_CHINESE))
            else:
                begin_year = self.__convert_chinese_to_number(
                    RegExpUtility.get_group(chinese_year_matches[0],
                                            Constants.YEAR_CHINESE))
                end_year = self.__convert_chinese_to_number(
                    RegExpUtility.get_group(year_matches[0],
                                            Constants.YEAR_GROUP_NAME))

        begin_year = self.__sanitize_year(begin_year)
        end_year = self.__sanitize_year(end_year)

        begin_date = DateUtils.safe_create_from_min_value(begin_year, 1, 1)
        end_date = DateUtils.safe_create_from_min_value(end_year, 1, 1)
        result.future_value = [begin_date, end_date]
        result.past_value = [begin_date, end_date]

        begin_timex = DateTimeFormatUtil.luis_date_from_datetime(begin_date)
        end_timex = DateTimeFormatUtil.luis_date_from_datetime(end_date)
        result.timex = f'({begin_timex},{end_timex},P{end_year - begin_year}Y)'

        result.success = True
        return result

    @staticmethod
    def __sanitize_year(year: int) -> int:
        result = year
        if 100 > year >= 90:
            result += 1900
        elif year < 100 and year < 20:
            result += 2000
        return result

    def _parse_year(self, source: str,
                    reference: datetime) -> DateTimeResolutionResult:
        source = source.strip().lower()
        result = DateTimeResolutionResult()
        is_chinese = False

        match = regex.search(self.config.year_regex, source)
        if not match or len(match.group()) != len(source):
            match = regex.search(self.year_in_chinese_regex, source)
            is_chinese = match and len(match.group()) == len(source)

        if not match or len(match.group()) != len(source):
            return result

        year_str = match.group()
        if self.config.is_year_only(year_str):
            year_str = year_str[:-1].strip()

        year = self._convert_year(year_str, is_chinese)
        if len(year_str) == 2:
            if 100 > year >= 30:
                year += 1900
            elif year < 30:
                year += 2000

        begin_day = DateUtils.safe_create_from_min_value(year, 1, 1)
        end_day = DateUtils.safe_create_from_min_value(year + 1, 1, 1)

        result.timex = f'{year:04d}'
        result.future_value = [begin_day, end_day]
        result.past_value = [begin_day, end_day]

        result.success = True
        return result

    def _convert_year(self, year_str: str, is_chinese: bool) -> int:
        year = -1
        if is_chinese:
            dynasty_year = parse_chinese_dynasty_year(
                year_str, self.config.dynasty_year_regex,
                self.config.dynasty_start_year, self.config.dynasty_year_map,
                self.integer_extractor, self.number_parser)
            if dynasty_year is not None:
                return dynasty_year

            year_num = 0
            er = next(iter(self.integer_extractor.extract(year_str)), None)
            if er and er.type == NumberConstants.SYS_NUM_INTEGER:
                year_num = int(self.number_parser.parse(er).value)

            if year_num < 10:
                year_num = 0
                for char in year_str:
                    year_num *= 10
                    er = next(iter(self.integer_extractor.extract(char)), None)
                    if er and er.type == NumberConstants.SYS_NUM_INTEGER:
                        year_num += int(self.number_parser.parse(er).value)
                year = year_num
            else:
                year = year_num
        else:
            year = int(year_str)

        return -1 if year == 0 else year

    def _get_week_of_month(self, cardinal, month, year, reference,
                           no_year) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()
        seed_date = self._compute_date(cardinal, DayOfWeek.MONDAY, month, year)

        future_date = seed_date
        past_date = seed_date

        if no_year and future_date < reference:
            future_date = self._compute_date(cardinal, DayOfWeek.MONDAY, month,
                                             year + 1)
            if not future_date.month == month:
                future_date = future_date + timedelta(days=-7)

        if no_year and past_date >= reference:
            past_date = self._compute_date(cardinal, DayOfWeek.MONDAY, month,
                                           year - 1)
            if not past_date.month == month:
                past_date = past_date + timedelta(days=-7)

        result.timex = ('XXXX' if no_year else
                        f'{year:04d}') + f'-{month:02d}-W{cardinal:02d}'

        days_to_add = 6 if self._inclusive_end_period else 7
        result.future_value = [
            future_date, future_date + timedelta(days=days_to_add)
        ]
        result.past_value = [
            past_date, past_date + timedelta(days=days_to_add)
        ]

        result.success = True
        return result

    def _compute_date(self, cardinal: int, weekday: DayOfWeek, month: int,
                      year: int):
        first_day = datetime(year, month, 1)
        first_week_day = DateUtils.this(first_day, weekday)

        if weekday == 0:
            weekday = 7

        first_day_of_week = first_day.isoweekday()

        if first_day_of_week == 7:
            first_day_of_week = 0

        if weekday < first_day_of_week:
            first_week_day = DateUtils.next(first_day, weekday)

        first_week_day = first_week_day + timedelta(days=7 * (cardinal - 1))

        return first_week_day

    def _parse_season(self, source: str,
                      reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        match = regex.search(self.season_with_year_regex, source)

        if not match or len(match.group()) != len(source):
            return result

        year = reference.year
        year_num = RegExpUtility.get_group(match, Constants.YEAR_GROUP_NAME)
        year_chinese = RegExpUtility.get_group(match, Constants.YEAR_CHINESE)
        year_relative = RegExpUtility.get_group(match, Constants.YEAR_RELATIVE)
        has_year = False

        if year_num.strip() != '':
            has_year = True
            if self.config.is_year_only(year_num):
                year_num = year_num[:-1]
            year = self._convert_year(year_num, False)
        elif year_chinese.strip() != '':
            has_year = True
            if self.config.is_year_only(year_chinese):
                year_chinese = year_chinese[:-1]
            year = self._convert_year(year_chinese, True)
        elif year_relative.strip() != '':
            has_year = True
            year += self.config.get_swift_day_or_month(year_relative)

        if 100 > year >= 90:
            year += 1900
        elif year < 100 and year < 20:
            year += 2000

        season_str = RegExpUtility.get_group(match, Constants.SEASON)
        season = self.config.season_map.get(season_str, None)

        if has_year:
            result.timex = f'{year:02d}-{season}'

        result.success = True
        return result

    def _parse_quarter(self, source: str,
                       reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        match = regex.search(self.config.quarter_regex, source)

        if not match or len(match.group()) != len(source):
            return result

        year = reference.year
        year_num = RegExpUtility.get_group(match, Constants.YEAR_GROUP_NAME)
        year_chinese = RegExpUtility.get_group(match, Constants.YEAR_CHINESE)
        year_relative = RegExpUtility.get_group(match, Constants.YEAR_RELATIVE)
        has_year = False

        if year_num.strip() != '':
            has_year = True
            if self.config.is_year_only(year_num):
                year_num = year_num[:-1]
            year = self._convert_year(year_num, False)
        elif year_chinese.strip() != '':
            has_year = True
            if self.config.is_year_only(year_chinese):
                year_chinese = year_chinese[:-1]
            year = self._convert_year(year_chinese, True)
        elif year_relative.strip() != '':
            has_year = True
            year += self.config.get_swift_day_or_month(year_relative)

        if 100 > year >= 90:
            year += 1900
        elif year < 100 and year < 20:
            year += 2000

        cardinal_str = RegExpUtility.get_group(match, Constants.CARDINAL)
        quarter_num = self.config.cardinal_map.get(cardinal_str, None)

        begin_date = DateUtils.safe_create_from_min_value(
            year, quarter_num * 3 - 2, 1)
        end_date = DateUtils.safe_create_from_min_value(
            year, quarter_num * 3 + 1, 1)
        result.future_value = [begin_date, end_date]
        result.past_value = [begin_date, end_date]

        begin_luis = DateTimeFormatUtil.luis_date_from_datetime(begin_date)
        end_luis = DateTimeFormatUtil.luis_date_from_datetime(end_date)
        result.timex = f'({begin_luis},{end_luis},P3M)'

        result.success = True
        return result

    def _parse_decade(self, source: str,
                      reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        century = int(reference.year / 100) + 1
        decade_last_year = 10
        input_century = False

        match = regex.search(self.decade_regex, source)

        if not match or len(match.group()) != len(source):
            return result

        decade_str = RegExpUtility.get_group(match, Constants.DECADE)
        decade = self.__convert_chinese_to_number(decade_str)
        century_str = RegExpUtility.get_group(match, Constants.CENTURY)
        if century_str != "":
            century = self.__convert_chinese_to_number(century_str)
            input_century = True
        else:
            century_str = RegExpUtility.get_group(match, Constants.REL_CENTURY)
            if century_str != "":
                century_str = century_str.strip().lower()

                this_match = regex.search(self.date_this_regex, century_str)
                next_match = regex.search(self.date_next_regex, century_str)
                last_match = regex.search(self.date_last_regex, century_str)

                if next_match:
                    century += 1
                elif last_match:
                    century -= 1

                input_century = True

        begin_year = ((century - 1) * 100) + decade
        end_year = begin_year + decade_last_year

        if input_century:
            begin_luis_str = DateTimeFormatUtil.luis_date(begin_year, 1, 1)
            end_luis_str = DateTimeFormatUtil.luis_date(end_year, 1, 1)
        else:
            begin_year_str = "XX{:02d}".format(decade)
            begin_luis_str = DateTimeFormatUtil.luis_date(-1, 1, 1)
            begin_luis_str = begin_luis_str.replace("XXXX", begin_year_str)

            end_year_str = "XX{:02d}".format(end_year % 100)
            end_luis_str = DateTimeFormatUtil.luis_date(-1, 1, 1)
            end_luis_str = end_luis_str.replace("XXXX", end_year_str)

        result.timex = f"({begin_luis_str},{end_luis_str},P10Y)"

        future_year, past_year = begin_year, begin_year
        start_date = DateUtils.safe_create_from_min_value(begin_year, 1, 1)
        if not input_century and start_date < reference:
            future_year += 100
        if not input_century and start_date >= reference:
            past_year -= 100

        result.future_value = [
            DateUtils.safe_create_from_min_value(future_year, 1, 1),
            DateUtils.safe_create_from_min_value(
                future_year + decade_last_year, 1, 1)
        ]
        result.past_value = [
            DateUtils.safe_create_from_min_value(past_year, 1, 1),
            DateUtils.safe_create_from_min_value(past_year + decade_last_year,
                                                 1, 1)
        ]
        result.success = True

        return result
コード例 #3
0
class ChineseDatePeriodParser(BaseDatePeriodParser):
    def __init__(self):
        super().__init__(ChineseDatePeriodParserConfiguration())
        self.integer_extractor = ChineseIntegerExtractor()
        self.number_parser = CJKNumberParser(
            ChineseNumberParserConfiguration())
        self.year_in_chinese_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DatePeriodYearInChineseRegex)
        self.number_combined_with_unit_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.NumberCombinedWithUnit)
        self.unit_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.UnitRegex)
        self.year_and_month_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.YearAndMonth)
        self.pure_number_year_and_month_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.PureNumYearAndMonth)
        self.year_to_year_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.YearToYear)
        self.chinese_year_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.DatePeriodYearInChineseRegex)
        self.season_with_year_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.SeasonWithYear)

    def parse(self,
              source: ExtractResult,
              reference: datetime = None) -> Optional[DateTimeParseResult]:
        if not reference:
            reference = datetime.now()

        if source.type == self.parser_type_name:
            source_text = source.text.strip().lower()

            inner_result = self._parse_simple_cases(source_text, reference)
            if not inner_result.success:
                inner_result = self._parse_one_word_period(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._merge_two_times_points(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_number_with_unit(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_duration(source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_year_and_month(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_year_to_year(source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_year(source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_week_of_month(
                    source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_season(source_text, reference)

            if not inner_result.success:
                inner_result = self._parse_quarter(source_text, reference)

            if inner_result.success:
                if inner_result.future_value and inner_result.past_value:
                    inner_result.future_resolution = {
                        TimeTypeConstants.START_DATE:
                        FormatUtil.format_date(inner_result.future_value[0]),
                        TimeTypeConstants.END_DATE:
                        FormatUtil.format_date(inner_result.future_value[1])
                    }
                    inner_result.past_resolution = {
                        TimeTypeConstants.START_DATE:
                        FormatUtil.format_date(inner_result.past_value[0]),
                        TimeTypeConstants.END_DATE:
                        FormatUtil.format_date(inner_result.past_value[1])
                    }
                else:
                    inner_result.future_resolution = {}
                    inner_result.past_resolution = {}
                result_value = inner_result

        result = DateTimeParseResult(source)
        result.value = result_value
        result.timex_str = result_value.timex if result_value else ''
        result.resolution_str = ''

        return result

    def _parse_simple_cases(self, source: str,
                            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()
        year = reference.year
        month = reference.month
        no_year = False
        input_year = False

        match = regex.search(self.config.simple_cases_regex, source)

        if not match or match.start() != 0 or len(
                match.group()) != len(source):
            return result

        days = RegExpUtility.get_group_list(match, 'day')
        begin_day = self.config.day_of_month[days[0]]
        end_day = self.config.day_of_month[days[1]]

        month_str = RegExpUtility.get_group(match, 'month')

        if month_str.strip() != '':
            month = self.config.month_of_year[month_str]
        else:
            month_str = RegExpUtility.get_group(match, 'relmonth')
            month += self.config.get_swift_day_or_month(month_str)

            if month < 0:
                month = 0
                year -= 1
            elif month > 11:
                month = 11
                year += 1

        year_str = RegExpUtility.get_group(match, 'year')
        if year_str.strip() != '':
            year = int(year_str)
            input_year = True
        else:
            no_year = True

        begin_date_luis = FormatUtil.luis_date(
            year if input_year or self.config.is_future(month_str) else -1,
            month, begin_day)
        end_date_luis = FormatUtil.luis_date(
            year if input_year or self.config.is_future(month_str) else -1,
            month, end_day)

        future_year = year
        past_year = year

        start_date = DateUtils.safe_create_from_min_value(
            year, month, begin_day)

        if no_year and start_date < reference:
            future_year += 1

        if no_year and start_date >= reference:
            past_year -= 1

        result.timex = f'({begin_date_luis},{end_date_luis},P{end_day - begin_day}D)'

        result.future_value = [
            DateUtils.safe_create_from_min_value(future_year, month,
                                                 begin_day),
            DateUtils.safe_create_from_min_value(future_year, month, end_day)
        ]
        result.past_value = [
            DateUtils.safe_create_from_min_value(past_year, month, begin_day),
            DateUtils.safe_create_from_min_value(past_year, month, end_day)
        ]

        result.success = True
        return result

    def _parse_number_with_unit(
            self, source: str,
            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        # if there are NO spaces between number and unit
        match = regex.search(self.number_combined_with_unit_regex, source)
        if not match:
            return result

        source_unit = RegExpUtility.get_group(match, 'unit').strip().lower()
        if source_unit not in self.config.unit_map:
            return result

        num_str = RegExpUtility.get_group(match, 'num')
        before_str = source[:match.start()].strip().lower()

        return self.__parse_common_duration_with_unit(before_str, source_unit,
                                                      num_str, reference)

    def _parse_duration(self, source: str,
                        reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        # for case "前两年" "后三年"
        duration_result = next(
            iter(self.config.duration_extractor.extract(source, reference)),
            None)
        if not duration_result:
            return result

        match = regex.search(self.unit_regex, duration_result.text)
        if not match:
            return result

        source_unit = RegExpUtility.get_group(match, 'unit').strip().lower()
        if source_unit not in self.config.unit_map:
            return result

        before_str = source[:duration_result.start].strip().lower()
        number_str = duration_result.text[:match.start()].strip().lower()
        number_val = self.__convert_chinese_to_number(number_str)
        num_str = str(number_val)

        return self.__parse_common_duration_with_unit(before_str, source_unit,
                                                      num_str, reference)

    def __parse_common_duration_with_unit(
            self, before: str, unit: str, num: str,
            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        unit_str = self.config.unit_map[unit]

        past_match = regex.search(self.config.past_regex, before)
        has_past = past_match and len(past_match.group()) == len(before)

        future_match = regex.search(self.config.future_regex, before)
        has_future = future_match and len(future_match.group()) == len(before)

        if not has_future and not has_past:
            return result

        begin_date = reference
        end_date = reference
        difference = float(num)

        if unit_str == 'D':
            if has_past:
                begin_date += timedelta(days=-difference)
            if has_future:
                end_date += timedelta(days=difference)
        elif unit_str == 'W':
            if has_past:
                begin_date += timedelta(days=-7 * difference)
            if has_future:
                end_date += timedelta(days=7 * difference)
        elif unit_str == 'MON':
            if has_past:
                begin_date += datedelta(months=int(-difference))
            if has_future:
                end_date += datedelta(months=int(difference))
        elif unit_str == 'Y':
            if has_past:
                begin_date += datedelta(years=int(-difference))
            if has_future:
                end_date += datedelta(years=int(difference))
        else:
            return result

        if has_future:
            begin_date += timedelta(days=1)
            end_date += timedelta(days=1)

        begin_timex = FormatUtil.luis_date_from_datetime(begin_date)
        end_timex = FormatUtil.luis_date_from_datetime(end_date)

        result.timex = f'({begin_timex},{end_timex},P{num}{unit_str[0]})'

        result.future_value = [begin_date, end_date]
        result.past_value = [begin_date, end_date]

        result.success = True
        return result

    def __convert_chinese_to_number(self, source: str) -> int:
        num = -1
        er = next(iter(self.integer_extractor.extract(source)), None)

        if er and er.type == NumberConstants.SYS_NUM_INTEGER:
            num = int(self.number_parser.parse(er).value)

        return num

    def _parse_year_and_month(self, source: str,
                              reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        match = regex.search(self.year_and_month_regex, source)

        if not match or len(match.group()) != len(source):
            match = regex.search(self.pure_number_year_and_month_regex, source)

        if not match or len(match.group()) != len(source):
            return result

        year = reference.year
        year_num = RegExpUtility.get_group(match, 'year')
        year_chinese = RegExpUtility.get_group(match, 'yearchs')
        year_relative = RegExpUtility.get_group(match, 'yearrel')

        if year_num.strip() != '':
            if self.config.is_year_only(year_num):
                year_num = year_num[:-1]
            year = self._convert_year(year_num, False)
        elif year_chinese.strip() != '':
            if self.config.is_year_only(year_chinese):
                year_chinese = year_chinese[:-1]
            year = self._convert_year(year_chinese, True)
        elif year_relative.strip() != '':
            year += self.config.get_swift_day_or_month(year_relative)

        if year < 100 and year >= 90:
            year += 1900
        elif year < 100 and year < 20:
            year += 2000

        month_str = RegExpUtility.get_group(match, 'month')
        month = self.config.month_of_year.get(month_str, 0) % 12

        begin_date = DateUtils.safe_create_from_min_value(year, month, 1)
        end_date = DateUtils.safe_create_from_min_value(
            year, month, 1) + datedelta(months=1)
        result.future_value = [begin_date, end_date]
        result.past_value = [begin_date, end_date]

        result.timex = f'{year:04d}-{month:02d}'

        result.success = True
        return result

    def _parse_year_to_year(self, source: str,
                            reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        match = regex.search(self.year_to_year_regex, source)

        if not match:
            return result

        year_matches = list(regex.finditer(self.config.year_regex, source))
        chinese_year_matches = list(
            regex.finditer(self.chinese_year_regex, source))

        begin_year = 0
        end_year = 0

        if len(year_matches) == 2:
            begin_year = self.__convert_chinese_to_number(
                RegExpUtility.get_group(year_matches[0], 'year'))
            end_year = self.__convert_chinese_to_number(
                RegExpUtility.get_group(year_matches[1], 'year'))
        elif len(chinese_year_matches) == 2:
            begin_year = self.__convert_chinese_to_number(
                RegExpUtility.get_group(chinese_year_matches[0], 'yearchs'))
            end_year = self.__convert_chinese_to_number(
                RegExpUtility.get_group(chinese_year_matches[1], 'yearchs'))
        elif len(year_matches) == 1 and len(chinese_year_matches) == 1:
            if year_matches[0].start() < chinese_year_matches[0].start():
                begin_year = self.__convert_chinese_to_number(
                    RegExpUtility.get_group(year_matches[0], 'year'))
                end_year = self.__convert_chinese_to_number(
                    RegExpUtility.get_group(chinese_year_matches[0],
                                            'yearchs'))
            else:
                begin_year = self.__convert_chinese_to_number(
                    RegExpUtility.get_group(chinese_year_matches[0],
                                            'yearchs'))
                end_year = self.__convert_chinese_to_number(
                    RegExpUtility.get_group(year_matches[0], 'year'))

        begin_year = self.__sanitize_year(begin_year)
        end_year = self.__sanitize_year(end_year)

        begin_date = DateUtils.safe_create_from_min_value(begin_year, 1, 1)
        end_date = DateUtils.safe_create_from_min_value(end_year, 1, 1)
        result.future_value = [begin_date, end_date]
        result.past_value = [begin_date, end_date]

        begin_timex = FormatUtil.luis_date_from_datetime(begin_date)
        end_timex = FormatUtil.luis_date_from_datetime(end_date)
        result.timex = f'({begin_timex},{end_timex},P{end_year - begin_year}Y)'

        result.success = True
        return result

    def __sanitize_year(self, year: int) -> int:
        result = year
        if year < 100 and year >= 90:
            result += 1900
        elif year < 100 and year < 20:
            result += 2000
        return result

    def _parse_year(self, source: str,
                    reference: datetime) -> DateTimeResolutionResult:
        source = source.strip().lower()
        result = DateTimeResolutionResult()
        is_chinese = False

        match = regex.search(self.config.year_regex, source)
        if not match or len(match.group()) != len(source):
            match = regex.search(self.year_in_chinese_regex, source)
            is_chinese = match and len(match.group()) == len(source)

        if not match or len(match.group()) != len(source):
            return result

        year_str = match.group()
        if self.config.is_year_only(year_str):
            year_str = year_str[:-1].strip()

        year = self._convert_year(year_str, is_chinese)
        if len(year_str) == 2:
            if year < 100 and year >= 30:
                year += 1900
            elif year < 30:
                year += 2000

        begin_day = DateUtils.safe_create_from_min_value(year, 1, 1)
        end_day = DateUtils.safe_create_from_min_value(year + 1, 1, 1)

        result.timex = f'{year:04d}'
        result.future_value = [begin_day, end_day]
        result.past_value = [begin_day, end_day]

        result.success = True
        return result

    def _convert_year(self, year_str: str, is_chinese: bool) -> int:
        year = -1
        if is_chinese:
            year_num = 0
            er = next(iter(self.integer_extractor.extract(year_str)), None)
            if er and er.type == NumberConstants.SYS_NUM_INTEGER:
                year_num = int(self.number_parser.parse(er).value)

            if year_num < 10:
                year_num = 0
                for char in year_str:
                    year_num *= 10
                    er = next(iter(self.integer_extractor.extract(char)), None)
                    if er and er.type == NumberConstants.SYS_NUM_INTEGER:
                        year_num += int(self.number_parser.parse(er).value)
            else:
                year = year_num
        else:
            year = int(year_str)

        return -1 if year == 0 else year

    def _get_week_of_month(self, cardinal, month, year, reference,
                           no_year) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()
        seed_date = self._compute_date(cardinal, 1, month, year)

        future_date = seed_date
        past_date = seed_date

        if no_year and future_date < reference:
            future_date = self._compute_date(cardinal, 1, month, year + 1)
            if not future_date.month == month:
                future_date = future_date + timedelta(days=-7)

        if no_year and past_date >= reference:
            past_date = self._compute_date(cardinal, 1, month, year - 1)
            if not past_date.month == month:
                past_date = past_date + timedelta(days=-7)

        result.timex = ('XXXX' if no_year else
                        f'{year:04d}') + f'-{month:02d}-W{cardinal:02d}'

        days_to_add = 6 if self._inclusive_end_period else 7
        result.future_value = [
            future_date, future_date + timedelta(days=days_to_add)
        ]
        result.past_value = [
            past_date, past_date + timedelta(days=days_to_add)
        ]

        result.success = True
        return result

    def _compute_date(self, cardinal: int, weekday: int, month: int,
                      year: int):
        first_day = datetime(year, month, 1)
        first_week_day = DateUtils.this(first_day, weekday)

        if weekday == 0:
            weekday = 7

        first_day_of_week = first_day.isoweekday()

        if first_day_of_week == 7:
            first_day_of_week = 0

        if weekday < first_day_of_week:
            first_week_day = DateUtils.next(first_day, weekday)

        first_week_day = first_week_day + timedelta(days=7 * (cardinal - 1))

        return first_week_day

    def _parse_season(self, source: str,
                      reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        match = regex.search(self.season_with_year_regex, source)

        if not match or len(match.group()) != len(source):
            return result

        year = reference.year
        year_num = RegExpUtility.get_group(match, 'year')
        year_chinese = RegExpUtility.get_group(match, 'yearchs')
        year_relative = RegExpUtility.get_group(match, 'yearrel')
        has_year = False

        if year_num.strip() != '':
            has_year = True
            if self.config.is_year_only(year_num):
                year_num = year_num[:-1]
            year = self._convert_year(year_num, False)
        elif year_chinese.strip() != '':
            has_year = True
            if self.config.is_year_only(year_chinese):
                year_chinese = year_chinese[:-1]
            year = self._convert_year(year_chinese, True)
        elif year_relative.strip() != '':
            has_year = True
            year += self.config.get_swift_day_or_month(year_relative)

        if year < 100 and year >= 90:
            year += 1900
        elif year < 100 and year < 20:
            year += 2000

        season_str = RegExpUtility.get_group(match, 'season')
        season = self.config.season_map.get(season_str, None)

        if has_year:
            result.timex = f'{year:02d}-{season}'

        result.success = True
        return result

    def _parse_quarter(self, source: str,
                       reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()

        match = regex.search(self.config.quarter_regex, source)

        if not match or len(match.group()) != len(source):
            return result

        year = reference.year
        year_num = RegExpUtility.get_group(match, 'year')
        year_chinese = RegExpUtility.get_group(match, 'yearchs')
        year_relative = RegExpUtility.get_group(match, 'yearrel')
        has_year = False

        if year_num.strip() != '':
            has_year = True
            if self.config.is_year_only(year_num):
                year_num = year_num[:-1]
            year = self._convert_year(year_num, False)
        elif year_chinese.strip() != '':
            has_year = True
            if self.config.is_year_only(year_chinese):
                year_chinese = year_chinese[:-1]
            year = self._convert_year(year_chinese, True)
        elif year_relative.strip() != '':
            has_year = True
            year += self.config.get_swift_day_or_month(year_relative)

        if year < 100 and year >= 90:
            year += 1900
        elif year < 100 and year < 20:
            year += 2000

        cardinal_str = RegExpUtility.get_group(match, 'cardinal')
        quarter_num = self.config.cardinal_map.get(cardinal_str, None)

        begin_date = DateUtils.safe_create_from_min_value(
            year, quarter_num * 3 - 2, 1)
        end_date = DateUtils.safe_create_from_min_value(
            year, quarter_num * 3 + 1, 1)
        result.future_value = [begin_date, end_date]
        result.past_value = [begin_date, end_date]

        begin_luis = FormatUtil.luis_date_from_datetime(begin_date)
        end_luis = FormatUtil.luis_date_from_datetime(end_date)
        result.timex = f'({begin_luis},{end_luis},P3M)'

        result.success = True
        return result