Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 79 additions & 12 deletions app/services/parser_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@
re.compile(r"(?<![\d/.-])(?P<month>\d{1,2})/(?P<day>\d{1,2})(?![\d/.-])"),
]

# ์ข…๋ฃŒ์ผ์˜ ์›”์ด ์ƒ๋žต๋œ ๋ฒ”์œ„๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค: 8์›” 20์ผ๋ถ€ํ„ฐ 22์ผ๊นŒ์ง€, 20์ผ ๋ถ€ํ„ฐ 22์ผ๊นŒ์ง€.
INHERITED_MONTH_DATE_RANGE_PATTERN = re.compile(
r"(?<!\d)"
r"(?:(?P<year>\d{4})๋…„\s*)?"
r"(?:(?P<month>\d{1,2})์›”\s*)?"
r"(?P<start_day>\d{1,2})์ผ\s*๋ถ€ํ„ฐ\s*"
r"(?P<end_day>\d{1,2})์ผ\s*๊นŒ์ง€"
)

# ์˜ค์ „/์˜คํ›„, ๋ฐ˜, ๋ถ„, ์ฏค/๊ฒฝ์ด ํฌํ•จ๋œ ์‹œ๊ฐ„ ํ‘œํ˜„์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
TIME_PATTERN = re.compile(
r"(?:(?P<period>์˜ค์ „|์˜คํ›„|์ €๋…|์•„์นจ|๋ฐค|์ƒˆ๋ฒฝ|์ ์‹ฌ|๋‚ฎ)(?:์—)?\s*)?"
Expand Down Expand Up @@ -171,6 +180,8 @@ def _extract_date(source_text: str, reference_date: date | None = None) -> Extra
candidates: list[tuple[int, int, ExtractedValue]] = []
removable_texts: list[str] = []

inherited_month_range = _extract_inherited_month_date_range(source_text, today)

for pattern in DATE_PATTERNS:
for match in pattern.finditer(source_text):
year = int(match.groupdict().get("year") or today.year)
Expand Down Expand Up @@ -307,8 +318,29 @@ def _extract_date(source_text: str, reference_date: date | None = None) -> Extra
)

date_range = _extract_date_range(source_text, candidates, removable_texts)
if date_range.value is not None:
return date_range
range_candidates = [
candidate
for candidate in (date_range, inherited_month_range)
if candidate is not None
]
if range_candidates:
_, selected_range = min(range_candidates, key=lambda candidate: candidate[0])
range_removable_texts = [
removable_text
for _, candidate in range_candidates
for removable_text in candidate.removable_texts
]
return ExtractedValue(
value=selected_range.value,
end_value=selected_range.end_value,
text=selected_range.text,
removable_texts=_dedupe_longest_first(
[*removable_texts, *range_removable_texts]
),
date_source=selected_range.date_source,
is_past=selected_range.is_past,
is_ambiguous=selected_range.is_ambiguous,
)

if candidates:
selected = min(candidates, key=lambda candidate: candidate[0])[2]
Expand All @@ -324,6 +356,38 @@ def _extract_date(source_text: str, reference_date: date | None = None) -> Extra
return ExtractedValue(value=None)


def _extract_inherited_month_date_range(
source_text: str,
today: date,
) -> tuple[int, ExtractedValue] | None:
"""์ƒ๋žต๋œ ์›”์€ ์‹œ์ž‘์ผ์˜ ์›” ๋˜๋Š” ์„œ๋น„์Šค ๊ธฐ์ค€ ํ˜„์žฌ ์›”๋กœ ๋ณด์™„ํ•ด ๋‚ ์งœ ๋ฒ”์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
match = INHERITED_MONTH_DATE_RANGE_PATTERN.search(source_text)
if match is None:
return None

year = int(match.group("year") or today.year)
month = int(match.group("month") or today.month)

try:
start_date = date(year, month, int(match.group("start_day")))
end_date = date(year, month, int(match.group("end_day")))
except ValueError:
return None

removable_text = match.group(0)
return (
match.start(),
ExtractedValue(
value=start_date.isoformat(),
end_value=end_date.isoformat() if end_date >= start_date else None,
text=removable_text,
removable_texts=[removable_text],
date_source="EXPLICIT",
is_past=start_date < today,
),
)


def _add_relative_week_candidates_without_weekday(
source_text: str,
today: date,
Expand Down Expand Up @@ -372,7 +436,7 @@ def _extract_date_range(
source_text: str,
candidates: list[tuple[int, int, ExtractedValue]],
removable_texts: list[str],
) -> ExtractedValue:
) -> tuple[int, ExtractedValue] | None:
"""A๋ถ€ํ„ฐ B๊นŒ์ง€ ํ˜•ํƒœ์˜ ๋‚ ์งœ ๋ฒ”์œ„๋ฅผ ์‹œ์ž‘์ผ/์ข…๋ฃŒ์ผ ํ›„๋ณด๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค."""
sorted_candidates = sorted(candidates, key=lambda candidate: candidate[0])
for start_index, start_end, start_date in sorted_candidates:
Expand All @@ -393,17 +457,20 @@ def _extract_date_range(

removable_text = source_text[start_index : end_end + until_match.end()]
removable_texts.append(removable_text)
return ExtractedValue(
value=start_date.value,
end_value=end_date.value,
text=removable_text,
removable_texts=_dedupe_longest_first(removable_texts),
date_source=_combine_date_source(start_date.date_source, end_date.date_source),
is_past=start_date.is_past,
is_ambiguous=start_date.is_ambiguous or end_date.is_ambiguous,
return (
start_index,
ExtractedValue(
value=start_date.value,
end_value=end_date.value,
text=removable_text,
removable_texts=_dedupe_longest_first(removable_texts),
date_source=_combine_date_source(start_date.date_source, end_date.date_source),
is_past=start_date.is_past,
is_ambiguous=start_date.is_ambiguous or end_date.is_ambiguous,
),
)

return ExtractedValue(value=None)
return None


def _align_bare_weekday_start_to_qualified_week_end(
Expand Down
56 changes: 56 additions & 0 deletions tests/test_parser_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,62 @@ def test_explicit_date_range_sets_start_and_end_date():
assert result.to_embedding == ["๋ถ€์‚ฐ", "์—ฌํ–‰"]


def test_date_range_inherits_start_month_when_end_month_is_omitted():
result = parse_event_text(
"8์›” 20์ผ๋ถ€ํ„ฐ 22์ผ๊นŒ์ง€ ๋ถ€์‚ฐ ์—ฌํ–‰",
reference_date=date(2026, 8, 17),
)

assert result.start_date == "2026-08-20"
assert result.end_date == "2026-08-22"
assert result.date_source == "EXPLICIT"
assert result.to_embedding == ["๋ถ€์‚ฐ", "์—ฌํ–‰"]


def test_date_range_uses_reference_month_when_both_months_are_omitted():
result = parse_event_text(
"20์ผ ๋ถ€ํ„ฐ 22์ผ๊นŒ์ง€ ๋ถ€์‚ฐ ์—ฌํ–‰",
reference_date=date(2026, 8, 17),
)

assert result.start_date == "2026-08-20"
assert result.end_date == "2026-08-22"
assert result.date_source == "EXPLICIT"
assert result.to_embedding == ["๋ถ€์‚ฐ", "์—ฌํ–‰"]


def test_inherited_month_date_range_does_not_roll_inverted_end_to_next_month():
result = parse_event_text(
"8์›” 30์ผ๋ถ€ํ„ฐ 2์ผ๊นŒ์ง€ ๋ถ€์‚ฐ ์—ฌํ–‰",
reference_date=date(2026, 8, 17),
)

assert result.start_date == "2026-08-30"
assert result.end_date is None
assert result.date_source == "EXPLICIT"
assert result.to_embedding == ["๋ถ€์‚ฐ", "์—ฌํ–‰"]


def test_date_range_selects_earliest_standard_range_before_inherited_range():
result = parse_event_text(
"8์›” 18์ผ๋ถ€ํ„ฐ 8์›” 19์ผ๊นŒ์ง€ ์ถœ์žฅ ํ›„ 8์›” 20์ผ๋ถ€ํ„ฐ 22์ผ๊นŒ์ง€ ์—ฌํ–‰",
reference_date=date(2026, 8, 17),
)

assert result.start_date == "2026-08-18"
assert result.end_date == "2026-08-19"


def test_date_range_selects_earliest_inherited_range_before_standard_range():
result = parse_event_text(
"8์›” 20์ผ๋ถ€ํ„ฐ 22์ผ๊นŒ์ง€ ์—ฌํ–‰ ํ›„ 8์›” 24์ผ๋ถ€ํ„ฐ 8์›” 25์ผ๊นŒ์ง€ ์ถœ์žฅ",
reference_date=date(2026, 8, 17),
)

assert result.start_date == "2026-08-20"
assert result.end_date == "2026-08-22"


def test_relative_date_range_sets_start_and_end_date(monkeypatch):
import app.services.parser_service as parser_service

Expand Down
Loading