Exemplo n.º 1
0
class ChineseHolidayParser(BaseHolidayParser):
    def __init__(self):
        config = ChineseHolidayParserConfiguration()
        BaseHolidayParser.__init__(self, config)
        self.__lunar_holiday_regex = RegExpUtility.get_safe_reg_exp(
            ChineseDateTime.LunarHolidayRegex)
        self.__integer_extractor = ChineseIntegerExtractor()
        self.__number_parser = AgnosticNumberParserFactory.get_parser(
            AgnosticNumberParserType.INTEGER,
            ChineseNumberParserConfiguration())
        self.__fixed_holiday_dictionary = dict([
            ('元旦', ChineseHolidayParser.new_year),
            ('元旦节', ChineseHolidayParser.new_year),
            ('教师节', ChineseHolidayParser.teacher_day),
            ('青年节', ChineseHolidayParser.youth_day),
            ('儿童节', ChineseHolidayParser.children_day),
            ('妇女节', ChineseHolidayParser.female_day),
            ('植树节', ChineseHolidayParser.tree_plant_day),
            ('情人节', ChineseHolidayParser.lover_day),
            ('圣诞节', ChineseHolidayParser.christmas_day),
            ('新年', ChineseHolidayParser.new_year),
            ('愚人节', ChineseHolidayParser.fool_day),
            ('五一', ChineseHolidayParser.labor_day),
            ('劳动节', ChineseHolidayParser.labor_day),
            ('万圣节', ChineseHolidayParser.halloween_day),
            ('中秋节', ChineseHolidayParser.midautumn_day),
            ('中秋', ChineseHolidayParser.midautumn_day),
            ('春节', ChineseHolidayParser.spring_day),
            ('除夕', ChineseHolidayParser.new_year_eve),
            ('元宵节', ChineseHolidayParser.lantern_day),
            ('清明节', ChineseHolidayParser.qing_ming_day),
            ('清明', ChineseHolidayParser.qing_ming_day),
            ('端午节', ChineseHolidayParser.dragon_boat_day),
            ('端午', ChineseHolidayParser.dragon_boat_day),
            ('国庆节', ChineseHolidayParser.chs_national_day),
            ('建军节', ChineseHolidayParser.chs_mil_build_day),
            ('女生节', ChineseHolidayParser.girls_day),
            ('光棍节', ChineseHolidayParser.singles_day),
            ('双十一', ChineseHolidayParser.singles_day),
            ('重阳节', ChineseHolidayParser.chong_yang_day)
        ])

    @staticmethod
    def new_year(year: int) -> datetime:
        return datetime(year, 1, 1)

    @staticmethod
    def chs_national_day(year: int) -> datetime:
        return datetime(year, 10, 1)

    @staticmethod
    def labor_day(year: int) -> datetime:
        return datetime(year, 5, 1)

    @staticmethod
    def christmas_day(year: int) -> datetime:
        return datetime(year, 12, 25)

    @staticmethod
    def lover_day(year: int) -> datetime:
        return datetime(year, 2, 14)

    @staticmethod
    def chs_mil_build_day(year: int) -> datetime:
        return datetime(year, 8, 1)

    @staticmethod
    def fool_day(year: int) -> datetime:
        return datetime(year, 4, 1)

    @staticmethod
    def girls_day(year: int) -> datetime:
        return datetime(year, 3, 7)

    @staticmethod
    def tree_plant_day(year: int) -> datetime:
        return datetime(year, 3, 12)

    @staticmethod
    def female_day(year: int) -> datetime:
        return datetime(year, 3, 8)

    @staticmethod
    def children_day(year: int) -> datetime:
        return datetime(year, 6, 1)

    @staticmethod
    def youth_day(year: int) -> datetime:
        return datetime(year, 5, 4)

    @staticmethod
    def teacher_day(year: int) -> datetime:
        return datetime(year, 9, 10)

    @staticmethod
    def singles_day(year: int) -> datetime:
        return datetime(year, 11, 11)

    @staticmethod
    def halloween_day(year: int) -> datetime:
        return datetime(year, 10, 31)

    @staticmethod
    def midautumn_day(year: int) -> datetime:
        return datetime(year, 8, 15)

    @staticmethod
    def spring_day(year: int) -> datetime:
        return datetime(year, 1, 1)

    @staticmethod
    def new_year_eve(year: int) -> datetime:
        return datetime(year, 1, 1) + timedelta(days=-1)

    @staticmethod
    def lantern_day(year: int) -> datetime:
        return datetime(year, 1, 15)

    @staticmethod
    def qing_ming_day(year: int) -> datetime:
        return datetime(year, 4, 4)

    @staticmethod
    def dragon_boat_day(year: int) -> datetime:
        return datetime(year, 5, 5)

    @staticmethod
    def chong_yang_day(year: int) -> datetime:
        return datetime(year, 9, 9)

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

        value = None

        if source.type == self.parser_type_name:
            inner_result = self._parse_holiday_regex_match(
                source.text, reference)

            if inner_result.success:
                inner_result.future_resolution = {
                    TimeTypeConstants.DATE:
                    DateTimeFormatUtil.format_date(inner_result.future_value)
                }
                inner_result.past_resolution = {
                    TimeTypeConstants.DATE:
                    DateTimeFormatUtil.format_date(inner_result.past_value)
                }
                inner_result.is_lunar = self.__is_lunar(source.text)
                value = inner_result

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

        return result

    def __is_lunar(self, source: str) -> bool:
        return self.__lunar_holiday_regex.search(source) is not None

    def _match2date(self, match: Match,
                    reference: datetime) -> DateTimeResolutionResult:
        result = DateTimeResolutionResult()
        holiday_str = self.config.sanitize_holiday_token(
            match.group('holiday').lower())

        if not holiday_str:
            return result

        year = reference.year
        year_num = match.group('year')
        year_chinese = match.group('yearchs')
        year_relative = match.group('yearrel')
        has_year = False

        if year_num:
            has_year = True
            if self.config.get_swift_year(year_num) == 0:
                year_num = year_num[0:len(year_num) - 1]
            year = self.__convert_year(year_num, False)
        elif year_chinese:
            has_year = True
            if self.config.get_swift_year(year_chinese) == 0:
                year_chinese = year_chinese[0:len(year_chinese) - 1]
            year = self.__convert_year(year_chinese, True)
        elif year_relative:
            has_year = True
            year += self.config.get_swift_year(reference.year)

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

        timex = ''
        date = reference
        if holiday_str in self.__fixed_holiday_dictionary:
            date = self.__fixed_holiday_dictionary[holiday_str](year)
            timex = f'-{DateTimeFormatUtil.to_str(date.month, 2)}-{DateTimeFormatUtil.to_str(date.day, 2)}'
        elif holiday_str in self.config.holiday_func_dictionary:
            date = self.config.holiday_func_dictionary[holiday_str](year)
            timex = self.config.variable_holidays_timex_dictionary[holiday_str]
        else:
            return result

        if has_year:
            result.timex = DateTimeFormatUtil.to_str(year, 4) + timex
            result.future_value = datetime(year, date.month, date.day)
            result.past_value = datetime(year, date.month, date.day)
        else:
            result.timex = 'XXXX' + timex
            result.future_value = self.__get_date_value(
                date, reference, holiday_str, 1, lambda d, r: d < r)
            result.past_value = self.__get_date_value(date, reference,
                                                      holiday_str, -1,
                                                      lambda d, r: d >= r)

        result.success = True

        return result

    def __convert_year(self, year_str: str, is_chinese: bool) -> int:
        year = -1
        if is_chinese:
            year_num = 0
            ers = self.__integer_extractor.extract(year_str)
            if ers and ers[-1].type == NumberConstants.SYS_NUM_INTEGER:
                year_num = int(self.__number_parser.parse(ers[-1]).value)

            if year_num < 10:
                year_num = 0
                for char in year_str:
                    year_num *= 10
                    ers = self.__integer_extractor.extract(char)
                    if ers and ers[-1].type == NumberConstants.SYS_NUM_INTEGER:
                        year_num += int(
                            self.__number_parser.parse(ers[-1]).value)
            else:
                year = year_num
        else:
            year = int(year_str)

        return -1 if year == 0 else year

    def __get_date_value(self, date: datetime, reference: datetime,
                         holiday: str, swift: int, comparer) -> datetime:
        result = date
        if comparer(date, reference):
            if holiday in self.__fixed_holiday_dictionary:
                return date + datedelta(years=swift)

            if holiday in self.config.holiday_func_dictionary:
                result = self.config.holiday_func_dictionary[holiday](
                    reference.year + swift)

        return result
Exemplo n.º 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
Exemplo n.º 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