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

1# SPDX-FileCopyrightText: 2022-2026 Arcangelo Massari <arcangelo.massari@unibo.it> 

2# 

3# SPDX-License-Identifier: ISC 

4 

5import re 

6 

7# Split by ';' outside '[]' (any spaces before and after ';'). 

8semicolon_in_people_field = r"\s*;\s*(?=[^\]]*(?:\[|$))" 

9 

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\[\]]+" 

14 

15# It captures a colon preceded and followed by zero or more spaces. 

16colon_and_spaces = r"\s*:\s*" 

17 

18# It captures a comma preceded and followed by zero or more spaces. 

19comma_and_spaces = r"\s*,\s*" 

20 

21# It captures one or more spaces. 

22one_or_more_spaces = r"\s+" 

23 

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) 

28 

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) 

51 

52 

53def split_name_and_ids(text: str) -> tuple[str, str]: 

54 """Parse a responsible-agent / venue cell into ``(name, ids_str)``. 

55 

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

64 

65 

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+") 

70 

71# It captures any pages range separator. 

72pages_separator = r"[^A-Za-z\d]+(?=[A-Za-z\d]+)" 

73 

74# It captures an ORCID 

75orcid_pattern = r"([0-9]{4}-){3}[0-9]{3}[0-9X]" 

76 

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} 

147 

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]