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
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
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