Coverage for oc_meta / lib / master_of_regex.py: 97%
34 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-07-25 10:39 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-07-25 10:39 +0000
1# SPDX-FileCopyrightText: 2022-2026 Arcangelo Massari <arcangelo.massari@unibo.it>
2#
3# SPDX-License-Identifier: ISC
5import re
7# Split by ';' outside '[]' (any spaces before and after ';').
8semicolon_in_people_field = r"\s*;\s*(?=[^\]]*(?:\[|$))"
10# A single id token "schema:value". Square brackets are excluded from both
11# sides of the colon so a stray '[' that leaks into the name (e.g.
12# '[Labour Party[ [omid:ra/123]') cannot be absorbed into the captured id.
13_ID_TOKEN = r"[^\s\[\]]+:[^\s\[\]]+"
15# It captures a colon preceded and followed by zero or more spaces.
16colon_and_spaces = r"\s*:\s*"
18# It captures a comma preceded and followed by zero or more spaces.
19comma_and_spaces = r"\s*,\s*"
21# It captures one or more spaces.
22one_or_more_spaces = r"\s+"
24RE_ENTITY_URI = re.compile(
25 r"^(?P<base>https://w3id\.org/oc/meta)/(?P<short_name>br|ra|ar|re|id)/(?P<supplier_prefix>06[1-9]*0)(?P<entity_number>[1-9]\d*)$"
26)
27RE_SEMICOLON_IN_PEOPLE_FIELD = re.compile(semicolon_in_people_field)
29# Parses a responsible-agent / venue cell into a name and an optional list of
30# ids inside square brackets. Always matches any input: when the '[ids]' block
31# is present the engine captures it (preferring the first occurrence in a
32# multi-RA string), otherwise the whole (trimmed) cell lands in 'name' and
33# 'ids' is None. Downstream code can therefore rely on .groups() always
34# succeeding; a regex failure indicates a real bug and should crash.
35RE_NAME_AND_IDS = re.compile(
36 rf"""
37 \s*
38 (?P<name> .*? ) # name, possibly empty, possibly with junk
39 (?:
40 \s* \[ \s*
41 (?P<ids>
42 (?: {_ID_TOKEN} )? # optional first id ...
43 (?: \s+ {_ID_TOKEN} )* # ... followed by any number of space-separated ids
44 )
45 \s* \]
46 | \s* \Z # or no '[ids]' block at all
47 )
48 """,
49 re.VERBOSE,
50)
53def split_name_and_ids(text: str) -> tuple[str, str]:
54 """Parse a responsible-agent / venue cell into ``(name, ids_str)``.
56 ``RE_NAME_AND_IDS`` is built to match any input, so a ``None`` here
57 signals a regression in the pattern itself and must be raised loudly
58 rather than silently fallen back to.
59 """
60 match = RE_NAME_AND_IDS.match(text)
61 if match is None:
62 raise RuntimeError(f"RE_NAME_AND_IDS failed to match {text!r}")
63 return match["name"], match["ids"] or ""
66RE_COLON_AND_SPACES = re.compile(colon_and_spaces)
67RE_COMMA_AND_SPACES = re.compile(comma_and_spaces)
68RE_ONE_OR_MORE_SPACES = re.compile(one_or_more_spaces)
69RE_MULTIPLE_SPACES = re.compile(r"\s+")
71# It captures any pages range separator.
72pages_separator = r"[^A-Za-z\d]+(?=[A-Za-z\d]+)"
74# It captures an ORCID
75orcid_pattern = r"([0-9]{4}-){3}[0-9]{3}[0-9X]"
77# A series of patterns useful to clean invalid "volume" and "issue" fields
78good_sep = r"\-"
79bad_sep = r"&\/_,\.:+;\(\[\|"
80separators = good_sep + bad_sep
81alphabets = (
82 r"a-zà-öø-ÿ\u0430-я\u0391-ω" # basic latin, latin-1 supplement, cyrillic, greek
83)
84vi_pattern = rf"((?:[{alphabets}]*\d[{alphabets}\d]*|[ivxlcdm]+)(?:\s?(?:[{separators}]|and|\()\s?[{alphabets}\d]+\)?)*?)"
85numero = r"(?:№|no?(?!v)\.?|n[º°]\.?|n[uú]m(?:[eé]ro)?|number)"
86year_pattern = r"(\d{4})"
87valid_vi_patterns = [
88 vi_pattern,
89 rf"[‹<\()]?[{alphabets}]+?([{separators}\s]?[{alphabets}])*[\)›>]?",
90 rf"[{alphabets}{separators}\s]+{vi_pattern}",
91 rf"[{alphabets}\d\-'/]*\d[{alphabets}\d\-'/]*(,?\s[{alphabets}\d\-'/]+)+",
92 rf"\(?s(uppl([eéi]ment(ary|um)?))?\)?\s?(part)?\s?(s?{vi_pattern})?",
93 rf"({vi_pattern}_)?({vi_pattern}\s)?[\(_]?supp?(plement|pl)?l?[\s\._]*({vi_pattern}|[{alphabets}])?\)?\.?",
94 rf"{vi_pattern}*,?\s?part[\s_]{vi_pattern}(\sof\s{vi_pattern})?(,\sno?\.\s?{vi_pattern})?",
95 rf"{vi_pattern}*[_\s]?pt?[_\s\.]{vi_pattern}",
96 r"(ed|pt|d)\sinside(d|r)",
97 r"p(ublish\s)?a(head\sof\s)?p(rint)?",
98 "預刊文章",
99 "[\u0621-\u064a]+",
100 rf"\[{year_pattern}\]\s(\d\s)?[{alphabets}]+",
101 rf"{vi_pattern}\s\[\+CDROM\]",
102 rf"{vi_pattern}[{separators}\s]?\({vi_pattern}\)(\s{vi_pattern})?",
103 rf"([{alphabets}]+\.)?[{alphabets}]+\.?",
104 rf"[{alphabets}]+-\d+",
105 rf"[{alphabets}]+(_[{alphabets}]+)+",
106 rf"{numero}:?\s?{vi_pattern}(,?\s({year_pattern}|\({vi_pattern}\)))?",
107 r"historica\svol\.\s\d+(,\d+(-\d+)?)?",
108 r"\d+\(\d+\)\d{2,4}",
109 rf"(\[{year_pattern}\]\s)?(\d+\s)?vl?r(\s\([a-z]+\))?",
110 rf"\({vi_pattern}\/{vi_pattern}\)\s[{alphabets}]+(-[{alphabets}]+)?",
111]
112volumes_valid_patterns = [
113 r"original\sseries,\svolume\s\d+",
114 rf"(vol(ume)?|tome|cilt)\s?[{separators}]?\s?{vi_pattern}",
115]
116issues_valid_patterns = [
117 rf"issue[\.,]?\s{vi_pattern}",
118 rf'({vi_pattern}\s)?e?sp?e?(ecial)?[\s_\-\.](issue)?(_number_)?[\s_-]?({vi_pattern})?(["“][{alphabets}\s]+?["”])?',
119 rf"ö(zel)?(\ss(ayı)?|\(special\))?(\s?{vi_pattern})?",
120 rf"({numero}[{separators}\s]?)?hors[{separators}\s]série[{separators}\s]{vi_pattern}",
121 "특별호",
122 rf"([{alphabets}]+\s{year_pattern}\s)?\(?(jan(uary)?|feb(ruary)?|mar(ch)?|apr(il)?|may|jun(e)?|jul(y)?|aug(ust)?|sep(tember)?|oct(ober)?|(nov|dec)(ember)?|spring|summer|autumn|winter)(\s{year_pattern})?\)?",
123 rf"{vi_pattern},\spart\s{vi_pattern}\sof\s{vi_pattern}",
124 rf"sayı[{separators}\s]\s?{vi_pattern}",
125 rf"issues?\s{vi_pattern},\s(supplement|part)\s{vi_pattern}",
126 rf"issues?\s{vi_pattern}\.?\spp\.\s[a-z\d]+[^a-z\d]+[a-z\d]+",
127]
128invalid_vi_patterns = {
129 rf".*?(?:vol\.?(?:ume)?|tome)(?:[{separators}]?\s?){vi_pattern}[\-&\/_,\.:+;\(\)\[\]|\s]*(?:{numero}|issues?)[{separators}|\s]*(?:sp[eé]cial\s)?{vi_pattern}": "vol_iss",
130 rf"{vi_pattern},\s?{numero}\s?{vi_pattern}": "vol_iss",
131 rf"tập\s?{vi_pattern},?\s?số\s?{vi_pattern}": "vol_iss",
132 rf"issues?\s{vi_pattern}\svol\.?(?:ume)?\s{vi_pattern}(?:.*?{year_pattern}.*?)?": "iss_vol_year",
133 rf"{vi_pattern}\s?\({vi_pattern}'{year_pattern}\)": "vol_iss_year",
134 rf"cilt[{separators}\s]\s?{vi_pattern}[{separators}\s]sayı[{separators}\s]\s?{vi_pattern}(?:[{separators}\s]\s?temmuz\s{year_pattern})?": "vol_iss_year",
135 r"&na;|n\/a|not\savailable": "del",
136 r"[\:\-\.`ё/]": "del",
137 rf"\${{[{alphabets}]+(\.[{alphabets}]+)?}}": "del",
138 rf"[&\/_,:+;\|`'#]\s*{vi_pattern}": "all",
139 rf"[\->+]{vi_pattern}": "do_nothing",
140 rf"{vi_pattern}[\.+]": "do_nothing",
141 rf"{numero}?[{separators}]?\s?{vi_pattern}[&\/_,:;\|`'\(\[\{{]": "all",
142 rf"{vi_pattern}\(\)": "all",
143 rf"n[�?]+{vi_pattern}": "all",
144 rf"{vi_pattern}(?:â\x80[\x92\x93\x94]|�+|â|\?+){vi_pattern}": "sep",
145 rf"{vi_pattern}\s?\(first\sserie": "s)",
146}
148RE_INVALID_VI_PATTERNS = {
149 re.compile(f"^{pattern}$", re.IGNORECASE): strategy
150 for pattern, strategy in invalid_vi_patterns.items()
151}
152RE_VOLUMES_VALID_PATTERNS = [
153 re.compile(f"^{pattern}$", re.IGNORECASE) for pattern in volumes_valid_patterns
154]
155RE_ISSUES_VALID_PATTERNS = [
156 re.compile(f"^{pattern}$", re.IGNORECASE) for pattern in issues_valid_patterns
157]