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
102 changes: 102 additions & 0 deletions dingo/model/rule/scibase/rule_patent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,42 @@
from dingo.model.rule.base import BaseRule

MAX_CLAIMS_LENGTH = 1_000_000
IPC_SUBCLASS_RE = re.compile(r"^[A-H](?:0[1-9]|[1-9][0-9])[A-Z]$")
IPC_CODE_RE = re.compile(
r"^(?P<section>[A-H])"
r"(?P<class>0[1-9]|[1-9][0-9])"
r"(?P<subclass>[A-Z]) "
r"(?P<main_group>[1-9][0-9]{0,2})/"
r"(?P<subgroup>[0-9]{2,6})$"
)
ASCII_ALNUM_RE = re.compile(r"^[A-Za-z0-9]+$")
JP_ERA_CITATION_RE = re.compile(
r"^JP(?P<publication>特開|特公|実開|実公)"
r"(?P<era>[平昭])"
r"(?P<era_year>\d+)-"
r"(?P<serial>\d+)"
r"(?P<kind_code>[A-Z]\d?)?$"
)
JP_GREGORIAN_CITATION_RE = re.compile(
r"^JP特開"
r"(?P<year>\d{4})-"
r"(?P<serial>\d+)"
r"(?:\([^)]*\))?"
r"(?P<kind_code>[A-Z]\d?)?$"
)
JP_REGISTRATION_CITATION_RE = re.compile(
r"^JP(?:特許第|実用新案登録第|第)"
r"(?P<registration_number>\d+)号"
r"(?:\([^)]*\))?"
r"(?P<kind_code>[A-Z]\d?)$"
)
JP_ERA_CODES = {"平": "H", "昭": "S"}
JP_PUBLICATION_DEFAULT_KIND_CODES = {
"特開": "A",
"特公": "B2",
"実開": "U",
"実公": "Y2",
}
ValidationResult = tuple[bool, List[str], List[str]]


Expand Down Expand Up @@ -56,12 +85,19 @@ def check_ipc_unified(ipc_unified: Any) -> ValidationResult:
reasons.append(f"item[{index}] must be a string")
continue

# IPC entries whose main group and subgroup are both zero are
# normalized to the four-character subclass code (for example,
# ``F03G 0/00`` becomes ``F03G``).
if IPC_SUBCLASS_RE.fullmatch(ipc_code):
continue

match = IPC_CODE_RE.fullmatch(ipc_code)
if match is None:
if "invalid_format" not in error_labels:
error_labels.append("invalid_format")
reasons.append(
f"item[{index}] value {ipc_code!r} must match IPC format "
"'<A-H><01-99><A-Z>' or "
"'<A-H><01-99><A-Z> <1-999>/<2-6 digits>'"
)
continue
Expand All @@ -78,9 +114,75 @@ def check_ipc_unified(ipc_unified: Any) -> ValidationResult:
return bool(error_labels), error_labels, reasons


def _expected_jp_document_number(document_number: str) -> str | None:
"""Return the expected normalized value for supported JP citation forms."""
match = JP_ERA_CITATION_RE.fullmatch(document_number)
if match is not None:
kind_code = match.group("kind_code") or JP_PUBLICATION_DEFAULT_KIND_CODES[
match.group("publication")
]
return (
"JP"
f"{JP_ERA_CODES[match.group('era')]}"
f"{match.group('era_year').zfill(2)}"
f"{match.group('serial').zfill(6)}"
f"{kind_code}"
)

match = JP_GREGORIAN_CITATION_RE.fullmatch(document_number)
if match is not None:
kind_code = match.group("kind_code") or "A"
return (
"JP"
f"{match.group('year')}"
f"{match.group('serial').zfill(6)}"
f"{kind_code}"
)

match = JP_REGISTRATION_CITATION_RE.fullmatch(document_number)
if match is not None:
return (
"JP"
f"{match.group('registration_number')}"
f"{match.group('kind_code')}"
)

return None


def check_document_number_unifed(
document_number_unifed: Any,
document_number: Any = None,
) -> ValidationResult:
error_labels: List[str] = []
reasons: List[str] = []

if not isinstance(document_number_unifed, str):
return True, ["wrong_type"], ["value must be a string"]

if ASCII_ALNUM_RE.fullmatch(document_number_unifed) is None:
error_labels.append("invalid_format")
reasons.append("value must contain only ASCII letters and digits")

if isinstance(document_number, str):
expected = _expected_jp_document_number(document_number)
if expected is not None and document_number_unifed != expected:
error_labels.append("incorrect_conversion")
reasons.append(
f"conversion from document_number {document_number!r} is incorrect: "
f"expected {expected!r}, got {document_number_unifed!r}"
)

return bool(error_labels), error_labels, reasons


FIELD_VALIDATORS = {
"claims": lambda record: check_claims(record.get("claims")),
"ipc_unified": lambda record: check_ipc_unified(record.get("ipc_unified")),
"document_number_unifed": lambda record: check_document_number_unifed(
record.get("document_number_unifed"),
record.get("document_number"),
),
}


Expand Down
142 changes: 137 additions & 5 deletions test/scripts/model/rule/test_rule_patent.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

from dingo.io.input import Data
from dingo.model.rule.scibase.rule_patent import MAX_CLAIMS_LENGTH, RulePatentFieldValidation, check_ipc_unified
from dingo.model.rule.scibase.rule_patent import MAX_CLAIMS_LENGTH, RulePatentFieldValidation, check_document_number_unifed, check_ipc_unified


def _claims_with_string_length(length: int) -> list[dict]:
Expand All @@ -14,7 +14,14 @@ def test_claims_at_maximum_length_is_valid(self):
claims = _claims_with_string_length(MAX_CLAIMS_LENGTH)
assert len(str(claims)) == MAX_CLAIMS_LENGTH

result = RulePatentFieldValidation().eval(Data(claims=claims, ipc_unified=[]))
result = RulePatentFieldValidation().eval(
Data(
claims=claims,
ipc_unified=[],
document_number="JP2013022164A",
document_number_unifed="JP2013022164A",
)
)

assert result.status is False
assert result.label == ["QUALITY_GOOD"]
Expand All @@ -23,7 +30,14 @@ def test_claims_over_maximum_length_is_invalid(self):
claims = _claims_with_string_length(MAX_CLAIMS_LENGTH + 1)
assert len(str(claims)) == MAX_CLAIMS_LENGTH + 1

result = RulePatentFieldValidation().eval(Data(claims=claims, ipc_unified=[]))
result = RulePatentFieldValidation().eval(
Data(
claims=claims,
ipc_unified=[],
document_number="JP2013022164A",
document_number_unifed="JP2013022164A",
)
)

assert result.status is True
assert result.label == ["claims.too_long"]
Expand All @@ -32,7 +46,13 @@ def test_claims_over_maximum_length_is_invalid(self):
]

def test_missing_claims_is_invalid(self):
result = RulePatentFieldValidation().eval(Data(ipc_unified=[]))
result = RulePatentFieldValidation().eval(
Data(
ipc_unified=[],
document_number="JP2013022164A",
document_number_unifed="JP2013022164A",
)
)

assert result.status is True
assert result.label == ["claims.missing_field"]
Expand All @@ -41,6 +61,7 @@ def test_missing_claims_is_invalid(self):
"ipc_unified",
[
[],
["F03G"],
["A01A 1/00"],
["B60T 8/48", "H99Z 999/999999"],
'["B60T 8/48", "B60T 8/58"]',
Expand All @@ -65,6 +86,7 @@ def test_valid_ipc_unified(self, ipc_unified):
"A01A 1/1234567", # Subgroup must contain at most six digits.
"A01A 1-00", # Groups must be separated by a slash.
"A01A 1/00 A", # Extra suffixes are not allowed.
"F03G 0/00", # Zero groups must be reduced to "F03G".
],
)
def test_invalid_ipc_format(self, ipc_code):
Expand All @@ -83,8 +105,10 @@ def test_reports_every_invalid_item_with_its_value(self):
assert labels == ["invalid_format"]
assert reasons == [
"item[1] value 'C00C 5/27' must match IPC format "
"'<A-H><01-99><A-Z>' or "
"'<A-H><01-99><A-Z> <1-999>/<2-6 digits>'",
"item[3] value 'C00C 15/08' must match IPC format "
"'<A-H><01-99><A-Z>' or "
"'<A-H><01-99><A-Z> <1-999>/<2-6 digits>'",
]

Expand Down Expand Up @@ -113,9 +137,117 @@ def test_invalid_ipc_unified_value(self, ipc_unified, expected_label):

def test_rule_reports_invalid_ipc_unified(self):
result = RulePatentFieldValidation().eval(
Data(claims=[], ipc_unified=["A01A 01/00"])
Data(
claims=[],
ipc_unified=["A01A 01/00"],
document_number="JP2013022164A",
document_number_unifed="JP2013022164A",
)
)

assert result.status is True
assert result.label == ["ipc_unified.invalid_format"]
assert result.reason[0].startswith("ipc_unified: item[0]")

@pytest.mark.parametrize(
("document_number", "document_number_unifed"),
[
("JP特開平11-73838A", "JPH11073838A"),
("JP特開平5-263266A", "JPH05263266A"),
("JP特開昭63-123456A", "JPS63123456A"),
("JP特開昭63-123456", "JPS63123456A"),
("JP特開2003-75095(P2003-75095A)A", "JP2003075095A"),
("JP特公平7-46852B2", "JPH07046852B2"),
("JP特公昭48-1234B2", "JPS48001234B2"),
("JP特許第3405959号(P3405959)B2", "JP3405959B2"),
("JP実用新案登録第3082161号(U3082161)U", "JP3082161U"),
("JP実開平6-10396U", "JPH06010396U"),
("JP実公平7-15070Y2", "JPH07015070Y2"),
("JP第2891579号B2", "JP2891579B2"),
],
)
def test_valid_document_number_conversion(
self,
document_number,
document_number_unifed,
):
assert check_document_number_unifed(
document_number_unifed,
document_number,
) == (False, [], [])

@pytest.mark.parametrize(
"document_number_unifed",
[
"",
"JP特開平11-73838A",
"JP2013-022164A",
"JP2013022164 A",
],
)
def test_document_number_unifed_must_be_ascii_alphanumeric(
self,
document_number_unifed,
):
invalid, labels, reasons = check_document_number_unifed(
document_number_unifed
)

assert invalid is True
assert labels == ["invalid_format"]
assert reasons == ["value must contain only ASCII letters and digits"]

@pytest.mark.parametrize("document_number_unifed", [None, 123, []])
def test_document_number_unifed_must_be_string(
self,
document_number_unifed,
):
assert check_document_number_unifed(document_number_unifed) == (
True,
["wrong_type"],
["value must be a string"],
)

def test_reports_incorrect_document_number_conversion(self):
invalid, labels, reasons = check_document_number_unifed(
"JPH11073839A",
"JP特開平11-73838A",
)

assert invalid is True
assert labels == ["incorrect_conversion"]
assert reasons == [
"conversion from document_number 'JP特開平11-73838A' is incorrect: "
"expected 'JPH11073838A', got 'JPH11073839A'"
]

def test_rule_reports_invalid_document_number_unifed(self):
result = RulePatentFieldValidation().eval(
Data(
claims=[],
ipc_unified=[],
document_number="JP特開平11-73838A",
document_number_unifed="JPH11073839A",
)
)

assert result.status is True
assert result.label == ["document_number_unifed.incorrect_conversion"]
assert result.reason == [
"document_number_unifed: conversion from document_number "
"'JP特開平11-73838A' is incorrect: expected 'JPH11073838A', "
"got 'JPH11073839A'"
]

def test_missing_document_number_unifed_is_invalid(self):
result = RulePatentFieldValidation().eval(
Data(
claims=[],
ipc_unified=[],
document_number="JP2013022164A",
)
)

assert result.status is True
assert result.label == ["document_number_unifed.missing_field"]
assert result.reason == ["document_number_unifed: missing field"]
Loading