relaton 3.0.0.pre.alpha.1 → 3.0.0.pre.alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +57 -1
  3. data/bin/console +0 -1
  4. data/lib/relaton/3gpp/bibliography.rb +82 -7
  5. data/lib/relaton/3gpp/data_fetcher.rb +51 -3
  6. data/lib/relaton/3gpp/docidentifier.rb +114 -0
  7. data/lib/relaton/3gpp/item.rb +6 -0
  8. data/lib/relaton/3gpp/parser.rb +1 -1
  9. data/lib/relaton/3gpp/processor.rb +4 -1
  10. data/lib/relaton/3gpp.rb +5 -1
  11. data/lib/relaton/adobe/bibdata.rb +8 -0
  12. data/lib/relaton/adobe/bibitem.rb +8 -0
  13. data/lib/relaton/adobe/bibliography.rb +92 -0
  14. data/lib/relaton/adobe/docidentifier.rb +49 -0
  15. data/lib/relaton/adobe/doctype.rb +14 -0
  16. data/lib/relaton/adobe/ext.rb +32 -0
  17. data/lib/relaton/adobe/item.rb +15 -0
  18. data/lib/relaton/adobe/item_base.rb +18 -0
  19. data/lib/relaton/adobe/item_data.rb +6 -0
  20. data/lib/relaton/adobe/processor.rb +45 -0
  21. data/lib/relaton/adobe/util.rb +8 -0
  22. data/lib/relaton/adobe.rb +37 -0
  23. data/lib/relaton/bib/model/address.rb +2 -2
  24. data/lib/relaton/bib/model/docidentifier.rb +24 -9
  25. data/lib/relaton/bib/model/localized_string.rb +1 -1
  26. data/lib/relaton/bib/model/structured_identifier.rb +10 -9
  27. data/lib/relaton/bib/sanitizer.rb +202 -6
  28. data/lib/relaton/bib.rb +0 -2
  29. data/lib/relaton/bipm/bibliography.rb +159 -10
  30. data/lib/relaton/bipm/data_fetcher.rb +26 -2
  31. data/lib/relaton/bipm/data_outcomes_parser.rb +3 -4
  32. data/lib/relaton/bipm/id_parser.rb +5 -4
  33. data/lib/relaton/bipm/model/structured_identifier.rb +21 -0
  34. data/lib/relaton/bipm/processor.rb +2 -2
  35. data/lib/relaton/bipm/rawdata_bipm_metrologia/fetcher.rb +2 -4
  36. data/lib/relaton/bipm/si_brochure_parser.rb +5 -3
  37. data/lib/relaton/bipm.rb +7 -1
  38. data/lib/relaton/bsi/bibliography.rb +115 -43
  39. data/lib/relaton/bsi/hit.rb +14 -0
  40. data/lib/relaton/bsi/hit_collection.rb +15 -16
  41. data/lib/relaton/bsi/model/docidentifier.rb +107 -1
  42. data/lib/relaton/bsi/processor.rb +1 -0
  43. data/lib/relaton/calconnect/bibliography.rb +12 -14
  44. data/lib/relaton/calconnect/data_fetcher.rb +77 -9
  45. data/lib/relaton/calconnect/docidentifier.rb +80 -0
  46. data/lib/relaton/calconnect/hit_collection.rb +65 -57
  47. data/lib/relaton/calconnect/model/item.rb +7 -0
  48. data/lib/relaton/calconnect/processor.rb +7 -1
  49. data/lib/relaton/calconnect.rb +11 -1
  50. data/lib/relaton/ccsds/data/fetcher.rb +17 -12
  51. data/lib/relaton/ccsds/data/parser.rb +1 -1
  52. data/lib/relaton/ccsds/hit_collection.rb +6 -1
  53. data/lib/relaton/ccsds/model/docidentifier.rb +121 -0
  54. data/lib/relaton/ccsds/model/item.rb +2 -0
  55. data/lib/relaton/cen/bibliography.rb +75 -47
  56. data/lib/relaton/cen/hit.rb +16 -1
  57. data/lib/relaton/cen/hit_collection.rb +59 -11
  58. data/lib/relaton/cen/model/docidentifier.rb +92 -1
  59. data/lib/relaton/cen/processor.rb +13 -8
  60. data/lib/relaton/cen/scraper.rb +13 -5
  61. data/lib/relaton/cen.rb +1 -0
  62. data/lib/relaton/cie/data_fetcher.rb +215 -30
  63. data/lib/relaton/cie/processor.rb +3 -1
  64. data/lib/relaton/cie/scrapper.rb +15 -2
  65. data/lib/relaton/cie.rb +2 -1
  66. data/lib/relaton/core/data_fetcher.rb +150 -3
  67. data/lib/relaton/core/governor.rb +320 -0
  68. data/lib/relaton/core/pacer.rb +134 -0
  69. data/lib/relaton/core/processor.rb +19 -0
  70. data/lib/relaton/core/request_error.rb +14 -0
  71. data/lib/relaton/core.rb +3 -0
  72. data/lib/relaton/db/registry.rb +41 -1
  73. data/lib/relaton/doi/crossref.rb +19 -2
  74. data/lib/relaton/doi/parser.rb +109 -15
  75. data/lib/relaton/easc/bibdata.rb +8 -0
  76. data/lib/relaton/easc/bibitem.rb +8 -0
  77. data/lib/relaton/easc/bibliography.rb +95 -0
  78. data/lib/relaton/easc/docidentifier.rb +100 -0
  79. data/lib/relaton/easc/doctype.rb +14 -0
  80. data/lib/relaton/easc/ext.rb +44 -0
  81. data/lib/relaton/easc/item.rb +13 -0
  82. data/lib/relaton/easc/item_base.rb +18 -0
  83. data/lib/relaton/easc/item_data.rb +6 -0
  84. data/lib/relaton/easc/processor.rb +46 -0
  85. data/lib/relaton/easc/util.rb +8 -0
  86. data/lib/relaton/easc.rb +35 -0
  87. data/lib/relaton/ecma/bibliography.rb +93 -25
  88. data/lib/relaton/ecma/data_fetcher.rb +71 -12
  89. data/lib/relaton/ecma/docidentifier.rb +124 -0
  90. data/lib/relaton/ecma/item.rb +2 -0
  91. data/lib/relaton/ecma/memento_parser.rb +1 -1
  92. data/lib/relaton/ecma/page_fetcher.rb +15 -3
  93. data/lib/relaton/ecma/parser_common.rb +2 -2
  94. data/lib/relaton/ecma/processor.rb +4 -1
  95. data/lib/relaton/ecma/standard_parser.rb +2 -2
  96. data/lib/relaton/ecma.rb +10 -1
  97. data/lib/relaton/etsi/bibliography.rb +67 -2
  98. data/lib/relaton/etsi/data_fetcher.rb +43 -4
  99. data/lib/relaton/etsi/processor.rb +3 -1
  100. data/lib/relaton/etsi.rb +2 -1
  101. data/lib/relaton/gb/bibliography.rb +55 -29
  102. data/lib/relaton/gb/docidentifier.rb +58 -9
  103. data/lib/relaton/gb/processor.rb +3 -0
  104. data/lib/relaton/gb/scraper.rb +27 -10
  105. data/lib/relaton/gost/bibdata.rb +8 -0
  106. data/lib/relaton/gost/bibitem.rb +8 -0
  107. data/lib/relaton/gost/bibliography.rb +107 -0
  108. data/lib/relaton/gost/docidentifier.rb +80 -0
  109. data/lib/relaton/gost/doctype.rb +16 -0
  110. data/lib/relaton/gost/ext.rb +46 -0
  111. data/lib/relaton/gost/item.rb +15 -0
  112. data/lib/relaton/gost/item_base.rb +18 -0
  113. data/lib/relaton/gost/item_data.rb +6 -0
  114. data/lib/relaton/gost/processor.rb +49 -0
  115. data/lib/relaton/gost/util.rb +8 -0
  116. data/lib/relaton/gost.rb +36 -0
  117. data/lib/relaton/iala/bibdata.rb +8 -0
  118. data/lib/relaton/iala/bibitem.rb +8 -0
  119. data/lib/relaton/iala/bibliography.rb +146 -0
  120. data/lib/relaton/iala/docidentifier.rb +89 -0
  121. data/lib/relaton/iala/doctype.rb +18 -0
  122. data/lib/relaton/iala/ext.rb +32 -0
  123. data/lib/relaton/iala/item.rb +21 -0
  124. data/lib/relaton/iala/item_base.rb +18 -0
  125. data/lib/relaton/iala/item_data.rb +6 -0
  126. data/lib/relaton/iala/processor.rb +43 -0
  127. data/lib/relaton/iala/relation.rb +7 -0
  128. data/lib/relaton/iala/util.rb +8 -0
  129. data/lib/relaton/iala.rb +35 -0
  130. data/lib/relaton/iana/bibliography.rb +67 -14
  131. data/lib/relaton/iana/data_fetcher.rb +35 -5
  132. data/lib/relaton/iana/processor.rb +3 -1
  133. data/lib/relaton/iana.rb +12 -1
  134. data/lib/relaton/iec/data_fetcher.rb +7 -1
  135. data/lib/relaton/iec/hit_collection.rb +1 -1
  136. data/lib/relaton/iec/model/docidentifier.rb +9 -5
  137. data/lib/relaton/iec/model/ext.rb +2 -2
  138. data/lib/relaton/iec/processor.rb +1 -0
  139. data/lib/relaton/ieee/bibliography.rb +25 -3
  140. data/lib/relaton/ieee/data_fetcher.rb +158 -17
  141. data/lib/relaton/ieee/idams_parser.rb +18 -11
  142. data/lib/relaton/ieee/processor.rb +4 -1
  143. data/lib/relaton/ieee/rawbib_id_parser.rb +291 -86
  144. data/lib/relaton/ieee.rb +2 -1
  145. data/lib/relaton/ietf/data_fetcher.rb +295 -12
  146. data/lib/relaton/ietf/processor.rb +7 -3
  147. data/lib/relaton/ietf/rfc/entry.rb +39 -3
  148. data/lib/relaton/ietf/scraper.rb +69 -36
  149. data/lib/relaton/ietf.rb +4 -1
  150. data/lib/relaton/iho/bibliography.rb +1 -1
  151. data/lib/relaton/iho/docidentifier.rb +1 -1
  152. data/lib/relaton/index/file_io.rb +11 -11
  153. data/lib/relaton/index/file_storage.rb +6 -1
  154. data/lib/relaton/index/pool.rb +6 -1
  155. data/lib/relaton/index/shard_source.rb +201 -0
  156. data/lib/relaton/index/type.rb +63 -12
  157. data/lib/relaton/index.rb +2 -1
  158. data/lib/relaton/iso/bibliography.rb +20 -15
  159. data/lib/relaton/iso/data_fetcher.rb +3 -3
  160. data/lib/relaton/iso/data_parser.rb +17 -3
  161. data/lib/relaton/iso/hit_collection.rb +27 -15
  162. data/lib/relaton/iso/item_data.rb +22 -0
  163. data/lib/relaton/iso/model/docidentifier.rb +24 -12
  164. data/lib/relaton/iso/processor.rb +1 -0
  165. data/lib/relaton/iso/scraper.rb +19 -3
  166. data/lib/relaton/itu/bibliography.rb +9 -4
  167. data/lib/relaton/itu/data_crawler_r.rb +664 -0
  168. data/lib/relaton/itu/data_fetcher.rb +496 -50
  169. data/lib/relaton/itu/data_merge_r.rb +149 -0
  170. data/lib/relaton/itu/data_parser_r.rb +163 -89
  171. data/lib/relaton/itu/data_parser_t.rb +228 -0
  172. data/lib/relaton/itu/family_cache.rb +177 -0
  173. data/lib/relaton/itu/governor.rb +56 -0
  174. data/lib/relaton/itu/hit.rb +9 -3
  175. data/lib/relaton/itu/hit_collection.rb +258 -86
  176. data/lib/relaton/itu/model/docidentifier.rb +67 -1
  177. data/lib/relaton/itu/model/structured_identifier.rb +19 -0
  178. data/lib/relaton/itu/processor.rb +10 -4
  179. data/lib/relaton/itu/pubid.rb +27 -5
  180. data/lib/relaton/itu/recommendation_fields.rb +334 -0
  181. data/lib/relaton/itu/recommendation_parser.rb +18 -149
  182. data/lib/relaton/itu/scraper.rb +13 -3
  183. data/lib/relaton/itu.rb +2 -1
  184. data/lib/relaton/jcgm/bibdata.rb +8 -0
  185. data/lib/relaton/jcgm/bibitem.rb +8 -0
  186. data/lib/relaton/jcgm/bibliography.rb +97 -0
  187. data/lib/relaton/jcgm/data_fetcher.rb +81 -0
  188. data/lib/relaton/jcgm/docidentifier.rb +102 -0
  189. data/lib/relaton/jcgm/doctype.rb +12 -0
  190. data/lib/relaton/jcgm/ext.rb +23 -0
  191. data/lib/relaton/jcgm/item.rb +20 -0
  192. data/lib/relaton/jcgm/item_base.rb +18 -0
  193. data/lib/relaton/jcgm/item_data.rb +6 -0
  194. data/lib/relaton/jcgm/meetings_parser.rb +175 -0
  195. data/lib/relaton/jcgm/processor.rb +71 -0
  196. data/lib/relaton/jcgm/relation.rb +9 -0
  197. data/lib/relaton/jcgm/structured_identifier.rb +40 -0
  198. data/lib/relaton/jcgm/util.rb +8 -0
  199. data/lib/relaton/jcgm.rb +24 -0
  200. data/lib/relaton/jis/bibliography.rb +8 -10
  201. data/lib/relaton/jis/data_fetcher.rb +21 -19
  202. data/lib/relaton/jis/docidentifier.rb +104 -5
  203. data/lib/relaton/jis/hit.rb +18 -23
  204. data/lib/relaton/jis/hit_collection.rb +19 -18
  205. data/lib/relaton/jis/processor.rb +1 -1
  206. data/lib/relaton/jis.rb +2 -3
  207. data/lib/relaton/logger/channels/gh_issue.rb +78 -13
  208. data/lib/relaton/nist/data_fetcher.rb +63 -13
  209. data/lib/relaton/nist/docidentifier.rb +165 -0
  210. data/lib/relaton/nist/item.rb +2 -0
  211. data/lib/relaton/nist/item_base.rb +16 -0
  212. data/lib/relaton/nist/mods_parser.rb +38 -12
  213. data/lib/relaton/nist/processor.rb +2 -1
  214. data/lib/relaton/nist/relation.rb +3 -0
  215. data/lib/relaton/nist/scraper.rb +6 -3
  216. data/lib/relaton/oasis/bibliography.rb +147 -6
  217. data/lib/relaton/oasis/data_fetcher.rb +41 -5
  218. data/lib/relaton/oasis/data_parser_utils.rb +37 -3
  219. data/lib/relaton/oasis/docidentifier.rb +54 -0
  220. data/lib/relaton/oasis/item.rb +3 -0
  221. data/lib/relaton/oasis/processor.rb +7 -1
  222. data/lib/relaton/oasis.rb +14 -1
  223. data/lib/relaton/ogc/data_fetcher.rb +23 -2
  224. data/lib/relaton/ogc/docidentifier.rb +105 -0
  225. data/lib/relaton/ogc/hit_collection.rb +78 -3
  226. data/lib/relaton/ogc/processor.rb +2 -1
  227. data/lib/relaton/ogc.rb +5 -1
  228. data/lib/relaton/oiml/bibliography.rb +90 -15
  229. data/lib/relaton/oiml/docidentifier.rb +18 -3
  230. data/lib/relaton/omg/docidentifier.rb +67 -0
  231. data/lib/relaton/omg/item.rb +1 -0
  232. data/lib/relaton/omg/processor.rb +1 -0
  233. data/lib/relaton/omg/scraper.rb +61 -16
  234. data/lib/relaton/omg.rb +1 -0
  235. data/lib/relaton/plateau/bibliography.rb +10 -3
  236. data/lib/relaton/plateau/data_fetcher.rb +25 -2
  237. data/lib/relaton/plateau/handbook_parser.rb +8 -1
  238. data/lib/relaton/plateau/hit.rb +10 -2
  239. data/lib/relaton/plateau/hit_collection.rb +31 -11
  240. data/lib/relaton/plateau/processor.rb +3 -1
  241. data/lib/relaton/plateau/technical_report_parser.rb +8 -1
  242. data/lib/relaton/plateau.rb +2 -1
  243. data/lib/relaton/sdo/config.rb +34 -0
  244. data/lib/relaton/sdo/fetcher.rb +52 -0
  245. data/lib/relaton/sdo/logo.rb +95 -0
  246. data/lib/relaton/sdo/name.rb +26 -0
  247. data/lib/relaton/sdo/organization.rb +71 -0
  248. data/lib/relaton/sdo/store.rb +49 -0
  249. data/lib/relaton/sdo.rb +29 -0
  250. data/lib/relaton/version.rb +1 -1
  251. data/lib/relaton/w3c/bibliography.rb +132 -12
  252. data/lib/relaton/w3c/data_fetcher.rb +194 -16
  253. data/lib/relaton/w3c/data_parser.rb +3 -3
  254. data/lib/relaton/w3c/docidentifier.rb +48 -0
  255. data/lib/relaton/w3c/governor.rb +32 -0
  256. data/lib/relaton/w3c/item.rb +3 -0
  257. data/lib/relaton/w3c/pubid.rb +12 -0
  258. data/lib/relaton/w3c/safe_realize.rb +110 -21
  259. data/lib/relaton/w3c.rb +12 -1
  260. data/lib/relaton/xsf/bibliography.rb +61 -1
  261. data/lib/relaton/xsf/data_fetcher.rb +55 -5
  262. data/lib/relaton/xsf/docidentifier.rb +46 -0
  263. data/lib/relaton/xsf/hit_collection.rb +31 -3
  264. data/lib/relaton/xsf/item.rb +6 -0
  265. data/lib/relaton/xsf/processor.rb +1 -0
  266. data/lib/relaton/xsf.rb +5 -1
  267. data/lib/relaton.rb +42 -0
  268. metadata +135 -24
  269. data/lib/relaton/ieee/pub_id.rb +0 -161
  270. data/lib/relaton/index/id_number.rb +0 -30
@@ -1,5 +1,4 @@
1
1
  require "mechanize"
2
- require_relative "id_parser"
3
2
 
4
3
  module Relaton::Bipm
5
4
  class Bibliography
@@ -46,11 +45,14 @@ module Relaton::Bipm
46
45
  # @return [RelatonBipm::BipmBibliographicItem]
47
46
  #
48
47
  def get_bipm(reference) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
49
- ref_id = Id.new.parse reference
50
- rows = index.search { |r| ref_id == r[:id] }
51
- return unless rows.any?
48
+ pubid = parse_ref reference
49
+ rows = search_index pubid, reference
50
+ return if rows.empty?
52
51
 
53
- row = rows.sort_by { |r| r[:id][:year] }.last
52
+ # Latest edition wins. Metrologia and SI Brochure rows carry no year,
53
+ # so they all score 0; the file path breaks that tie so a repeated
54
+ # lookup returns the same document (the index sort is not stable).
55
+ row = rows.max_by { |r| [r[:id].year.to_i, r[:file]] }
54
56
  url = "#{GH_ENDPOINT}#{row[:file]}"
55
57
  resp = Mechanize.new.get url
56
58
  return unless resp.code == "200"
@@ -60,16 +62,163 @@ module Relaton::Bipm
60
62
  item
61
63
  end
62
64
 
65
+ # Look the reference up in the pubid `index-v2`, narrowing the candidate
66
+ # rows before the match runs. `Relaton::Index::Type#search` binary-searches
67
+ # only when given an identifier — the same call shape ISO, OIML and JCGM
68
+ # use. Both sides are now `Pubid::Bipm` objects, so the bespoke `Id`
69
+ # grammar takes no part in a query.
70
+ #
71
+ # Two escapes sit under the narrowing, each covering a case where the
72
+ # query's bsearch key cannot equal its row's:
73
+ #
74
+ # 1. A bare `SI Brochure` names no edition, so it keys to `""` while its
75
+ # row keys to `"9e"`. The rescan finds it.
76
+ # 2. `CCTF Recommendation 2009-02` parses as the literal number
77
+ # `2009-02`, while the row keys on `2`. No row carries that number, so
78
+ # a rescan cannot help — only re-reading the tail as number-plus-year.
79
+ #
80
+ # @param pubid [Pubid::Bipm::Identifier] the parsed reference
81
+ # @param reference [String, nil] the raw reference, for the retry
82
+ # @return [Array<Hash>] the matching index rows
83
+ def search_index(pubid, reference)
84
+ # Reduce the query once, not once per row. `#exclude` copies the
85
+ # identifier, so rebuilding it per candidate dominated the lookup.
86
+ query_stem = stem pubid, pubid
87
+ rows = index.search(pubid) { |r| pubid_match? r[:id], pubid, query_stem }
88
+ rows = index.search { |r| pubid_match? r[:id], pubid, query_stem } if rows.empty?
89
+ return rows unless rows.empty?
90
+
91
+ retry_pubid = year_number_retry reference
92
+ retry_pubid ? search_index(retry_pubid, nil) : rows
93
+ end
94
+
95
+ # Re-read a trailing `YYYY-NN` as number `NN` of year `YYYY`
96
+ # (`CCTF Recommendation 2009-02` → `CCTF Recommendation 2 (2009)`). Tried
97
+ # only after a miss, because `NNNN-NN` is also a real BIPM number
98
+ # (`CIPM 2005-06`), so the literal reading has to be preferred.
99
+ #
100
+ # The re-read string is a machine-derived probe, not the caller's
101
+ # reference, so a probe that does not parse is a miss (nil), never a
102
+ # malformed-reference error -- the same rule as ISO's type/stage probe.
103
+ #
104
+ # @param reference [String, nil]
105
+ # @return [Pubid::Bipm::Identifier, nil]
106
+ def year_number_retry(reference)
107
+ match = reference&.match(/\A(?<stem>.+)\s(?<year>\d{4})-(?<number>\d+)\z/)
108
+ return unless match
109
+
110
+ parse_ref "#{match[:stem]} #{match[:number].sub(/\A0+(?=\d)/, '')} (#{match[:year]})"
111
+ rescue ::Pubid::Errors::ParseError
112
+ nil
113
+ end
114
+
115
+ # Compare a row's identifier with the query. Index rows are stored
116
+ # language- and form-neutral — verified across all committee-document and
117
+ # meeting rows — while a reference may name a language (`(2009, E)`) and
118
+ # always names a form (short `CCTF REC 2` vs long `CCTF Recommendation 2`).
119
+ # The year joins them when the query omits it, mirroring what `Id#==` did
120
+ # with `other_hash.delete(:year) unless hash[:year]`.
121
+ #
122
+ # @param row_id [Pubid::Bipm::Identifier]
123
+ # @param query [Pubid::Bipm::Identifier]
124
+ # @param query_stem [Pubid::Bipm::Identifier, nil] the reduced query, when
125
+ # the caller already built it once for the whole search
126
+ # @return [Boolean]
127
+ def pubid_match?(row_id, query, query_stem = nil)
128
+ # A bare brochure names no edition, so it is a partial reference and
129
+ # cannot equal the `9e v3.01` row. Match any brochure row, which is what
130
+ # the old `{group: "SI", type: "Brochure"}` projection already meant.
131
+ if query.is_a?(::Pubid::Bipm::Identifiers::SiBrochure) && query.edition.nil?
132
+ return row_id.is_a?(::Pubid::Bipm::Identifiers::SiBrochure)
133
+ end
134
+
135
+ return false unless row_id.instance_of?(query.class)
136
+
137
+ # `Id#==` treated a document numbered "1" and a number-less one as the
138
+ # same document when both carried a year, so `CIPM Resolution 1 (1879)`
139
+ # reached `CIPM RES (1879)`. pubid has no such rule and the six
140
+ # ordinal-less declarations are only addressable this way, so keep it.
141
+ if number_collapses?(row_id, query)
142
+ keys = stem_keys(query) + [:number]
143
+ return row_id.exclude(*keys) == query.exclude(*keys)
144
+ end
145
+
146
+ return false unless cheap_reject_passes?(row_id, query)
147
+
148
+ stem(row_id, query) == (query_stem || stem(query, query))
149
+ end
150
+
151
+ # @param row_id [Pubid::Bipm::Identifier]
152
+ # @param query [Pubid::Bipm::Identifier]
153
+ # @return [Boolean] true when one side is number-less, the other is
154
+ # numbered "1", and both carry a year
155
+ def number_collapses?(row_id, query)
156
+ return false unless row_id.year && query.year
157
+
158
+ numbers = [row_id.number.to_s, query.number.to_s]
159
+ numbers.include?("") && numbers.include?("1")
160
+ end
161
+
162
+ # Attributes the stem never removes, so stem equality implies equality on
163
+ # every one of them. Comparing them first cannot change the answer, it
164
+ # only avoids the reduction — and `#exclude` copies the identifier, which
165
+ # is what made an unguarded compare cost roughly 165x a plain attribute
166
+ # read and turned a rescan into seconds.
167
+ CHEAP_KEYS = %i[number group year issue article].freeze
168
+
169
+ # @param row_id [Pubid::Bipm::Identifier]
170
+ # @param query [Pubid::Bipm::Identifier]
171
+ # @return [Boolean] false as soon as one cheap attribute differs
172
+ def cheap_reject_passes?(row_id, query)
173
+ CHEAP_KEYS.all? do |key|
174
+ # `:year` is dropped from the stem when the query has none, so it is
175
+ # only significant when the query supplies one.
176
+ next true if key == :year && query.year.nil?
177
+ next true unless query.respond_to?(key) && row_id.respond_to?(key)
178
+
179
+ row_id.public_send(key).to_s == query.public_send(key).to_s
180
+ end
181
+ end
182
+
183
+ # @param pubid [Pubid::Bipm::Identifier] the identifier to reduce
184
+ # @param query [Pubid::Bipm::Identifier] the query that sets which
185
+ # attributes are significant
186
+ # @return [Pubid::Bipm::Identifier] a copy; the cached row id is untouched
187
+ def stem(pubid, query)
188
+ pubid.exclude(*stem_keys(query))
189
+ end
190
+
191
+ # @param query [Pubid::Bipm::Identifier]
192
+ # @return [Array<Symbol>] the attributes a match must ignore
193
+ def stem_keys(query)
194
+ keys = %i[language form]
195
+ keys << :year if query.year.nil?
196
+ keys
197
+ end
198
+
199
+ # Parse a user reference with `Pubid::Bipm`, which since pubid `a96e9f45`
200
+ # accepts the loose consumer forms this flavor once needed `Id` for
201
+ # (`CCDS Recommendation 2 (2009)`, `CIPM 111e Réunion (2022)`,
202
+ # `CCTF Meeting 14 (1999)`, `SI Brochure Part 1`, …) and normalizes them
203
+ # to BIPM's canonical spelling. An unrecognized reference raises; like
204
+ # ISO and 3GPP we let it propagate -- relaton-cli rescues
205
+ # Pubid::Errors::Error and renders "... is not a recognized standards
206
+ # identifier".
207
+ #
208
+ # @param reference [String]
209
+ # @return [Pubid::Bipm::Identifier]
210
+ # @raise [Pubid::Errors::ParseError]
211
+ def parse_ref(reference)
212
+ ::Pubid::Bipm.parse reference
213
+ end
214
+
63
215
  def index
64
216
  Relaton::Index.find_or_create(
65
- :bipm, url: "#{GH_ENDPOINT}index-v1.zip", file: INDEXFILE, id_keys: %i[group type number year corr part append]
217
+ :bipm, url: "#{GH_ENDPOINT}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml",
218
+ pubid_class: ::Pubid::Bipm::Identifier
66
219
  )
67
220
  end
68
221
 
69
- # def match_item(ids, ref_id)
70
- # ids.find { |id| Id.new(id) == ref_id }
71
- # end
72
-
73
222
  # @param ref [String] the BIPM standard Code to look up (e..g "BIPM B-11")
74
223
  # @param year [String] not used
75
224
  # @param opts [Hash] not used
@@ -1,4 +1,3 @@
1
- require_relative "id_parser"
2
1
  require_relative "../bipm"
3
2
  require_relative "data_outcomes_parser"
4
3
  require_relative "si_brochure_parser"
@@ -9,13 +8,38 @@ module Relaton::Bipm
9
8
  attr_reader :output, :format, :ext, :files, :index, :errors
10
9
 
11
10
  def index
12
- @index ||= Relaton::Index.find_or_create :bipm, file: INDEXFILE
11
+ @index ||= Relaton::Index.find_or_create(
12
+ :bipm, file: "#{INDEXFILE}.yaml", pubid_class: ::Pubid::Bipm::Identifier
13
+ )
13
14
  end
14
15
 
15
16
  def log_error(msg)
16
17
  Util.error msg
17
18
  end
18
19
 
20
+ #
21
+ # Index a document by its pubid. Guarded defensively: a docnumber the
22
+ # `Pubid::Bipm` grammar can't parse (a legacy/inconsistent id, or a
23
+ # wrong-flavor orphan) is skipped with a warning rather than allowed to
24
+ # corrupt the whole index (`Relaton::Index` rejects an index if a single
25
+ # row fails to deserialize). Mirrors `Relaton::Jcgm::DataFetcher#add_to_index`.
26
+ #
27
+ # Public so `relaton-data-bipm`'s crawler can index its curated `static/`
28
+ # docs through the same guarded, sorted, `_type: pubid:bipm:*` path.
29
+ #
30
+ # @param [String] docnumber the record's primary docidentifier / docnumber
31
+ # @param [String] path repo-relative path to the YAML file
32
+ #
33
+ def add_to_index(docnumber, path)
34
+ # Store the Pubid::Bipm identifier object (not a hash): with pubid_class
35
+ # set, Relaton::Index sorts the index by id number on save and serializes
36
+ # each id to its `_type: pubid:bipm:*` hash, so the published index is
37
+ # already sorted (no "not sorted by id number" warning on consumer fetch).
38
+ index.add_or_update ::Pubid::Bipm.parse(docnumber), path
39
+ rescue StandardError => e
40
+ Util.warn "Skipping index entry for `#{docnumber}` (#{path}): #{e.message}"
41
+ end
42
+
19
43
  #
20
44
  # Fetch bipm-data-outcomes or si-brochure
21
45
  #
@@ -39,7 +39,7 @@ module Relaton::Bipm
39
39
  # Parse BIPM meeting and write them to YAML files
40
40
  #
41
41
  def parse
42
- dirs = "cctf,cgpm,cipm,ccauv,ccem,ccl,ccm,ccpr,ccqm,ccri,cct,ccu,jcgm,jcrb"
42
+ dirs = "cctf,cgpm,cipm,ccauv,ccem,ccl,ccm,ccpr,ccqm,ccri,cct,ccu,jcrb"
43
43
  source_path = File.join "bipm-data-outcomes", "{#{dirs}}"
44
44
  Dir[source_path].each { |body_dir| fetch_body(body_dir) }
45
45
  end
@@ -270,9 +270,8 @@ module Relaton::Bipm
270
270
  # @param [Relaton::Bipm::ItemData] item bibliographic item
271
271
  # @param [String] path path to YAML file
272
272
  #
273
- def add_to_index(item, path) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
274
- key = Id.new.parse(item.docnumber).to_hash
275
- @data_fetcher.index.add_or_update key, path
273
+ def add_to_index(item, path)
274
+ @data_fetcher.add_to_index item.docnumber, path
276
275
  end
277
276
 
278
277
  #
@@ -92,7 +92,7 @@ module Relaton
92
92
  #
93
93
  def parse(id)
94
94
  # str = StringScanner.new id
95
- match = parse_outcome(id) || parse_brochure(id) || parse_metrologia(id) || parse_jcgm(id)
95
+ match = parse_outcome(id) || parse_brochure(id) || parse_metrologia(id)
96
96
  @id = match.named_captures.compact.transform_keys(&:to_sym)
97
97
  self
98
98
  rescue StandardError => e
@@ -167,9 +167,10 @@ module Relaton
167
167
  %r{^(?<group>Metrologia)(?:\s(?<number>[a-zA-Z0-9\s]+))?$}.match(id)
168
168
  end
169
169
 
170
- def parse_jcgm(id)
171
- %r{^#{group}\s#{number}(?::#{year})?(?:\s(?<corr>Corrigendum))?$}.match(id)
172
- end
170
+ # NOTE: JCGM is no longer served by the BIPM flavor — it has its own
171
+ # `Relaton::Jcgm` flavor (pubid-backed, fetching from relaton-data-jcgm).
172
+ # The old `parse_jcgm` branch and the `JCGM` default-prefix entry were
173
+ # removed together with the `jcgm` harvest dir in data_outcomes_parser.
173
174
 
174
175
  # def parse_gorup_num(str)
175
176
  # return unless group = parse_group(str)
@@ -10,6 +10,27 @@ module Relaton
10
10
  map_element "part", to: :part
11
11
  map_element "appendix", to: :appendix
12
12
  end
13
+
14
+ # `Bib::ItemData#ext_to_all_parts!` / `#ext_remove_date` broadcast to
15
+ # `ext.structuredidentifier` as well as to `docidentifier`. This class
16
+ # descends from `Lutaml::Model::Serializable` rather than
17
+ # `Bib::StructuredIdentifier` (BIPM stores a single mapping, not a
18
+ # sequence), so it does not inherit that class's no-op defaults and the
19
+ # broadcast raised `NoMethodError` on every BIPM item.
20
+ #
21
+ # BIPM structured identifiers carry no date, so `remove_date!` has nothing
22
+ # to strip; the year lives on the docidentifier. `part` is real and is
23
+ # cleared. (Identical to `Relaton::Jcgm::StructuredIdentifier`.)
24
+
25
+ def remove_part!
26
+ self.part = nil
27
+ end
28
+
29
+ def remove_date!; end
30
+
31
+ def to_all_parts!
32
+ remove_part!
33
+ end
13
34
  end
14
35
  end
15
36
  end
@@ -8,7 +8,7 @@ module Relaton
8
8
  def initialize
9
9
  @short = :relaton_bipm
10
10
  @prefix = "BIPM"
11
- @defaultprefix = %r{^(?:BIPM|CCTF|CCDS|CGPM|CIPM|JCRB|JCGM)(?!\w)}
11
+ @defaultprefix = %r{^(?:BIPM|CCTF|CCDS|CGPM|CIPM|JCRB)(?!\w)}
12
12
  @idtype = "BIPM"
13
13
  @datasets = %w[bipm-data-outcomes bipm-si-brochure rawdata-bipm-metrologia]
14
14
  end
@@ -62,7 +62,7 @@ module Relaton
62
62
  #
63
63
  def remove_index_file
64
64
  require_relative "../bipm"
65
- Relaton::Index.find_or_create(:bipm, url: true, file: INDEXFILE).remove_file
65
+ Relaton::Index.find_or_create(:bipm, url: true, file: "#{INDEXFILE}.yaml").remove_file
66
66
  end
67
67
  end
68
68
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../id_parser"
4
3
  require_relative "niso_jats_parser"
5
4
 
6
5
  module Relaton::Bipm
@@ -37,8 +36,7 @@ module Relaton::Bipm
37
36
  item = NisoJatsParser.parse path, @data_fetcher.errors
38
37
  file = "#{item.docidentifier.first.content.downcase.tr(' ', '-')}.#{@data_fetcher.ext}"
39
38
  out_path = File.join(@data_fetcher.output, file)
40
- key = Relaton::Bipm::Id.new.parse(item.docidentifier.first.content).to_hash
41
- @data_fetcher.index.add_or_update key, out_path
39
+ @data_fetcher.add_to_index item.docidentifier.first.content, out_path
42
40
  @data_fetcher.write_file out_path, item
43
41
  end
44
42
  end
@@ -80,7 +78,7 @@ module Relaton::Bipm
80
78
  )
81
79
  file = "#{id.downcase.gsub(' ', '-')}.#{@data_fetcher.ext}"
82
80
  path = File.join(@data_fetcher.output, file)
83
- @data_fetcher.index.add_or_update Id.new.parse(id).to_hash, path
81
+ @data_fetcher.add_to_index id, path
84
82
  @data_fetcher.write_file path, item
85
83
  end
86
84
 
@@ -1,5 +1,4 @@
1
1
  require "nokogiri"
2
- require_relative "id_parser"
3
2
 
4
3
  module Relaton::Bipm
5
4
  class SiBrochureParser
@@ -51,8 +50,11 @@ module Relaton::Bipm
51
50
  fix_si_brochure_id item1
52
51
  basename = File.join @data_fetcher.output, File.basename(f).sub(/(?:-(?:en|fr))?\.rxl$/, "")
53
52
  outfile = "#{basename}.#{@data_fetcher.ext}"
54
- key = item1.docnumber || basename
55
- @data_fetcher.index.add_or_update Id.new.parse(key).to_hash, outfile
53
+ # Index by the primary docidentifier content ("BIPM SI Brochure 9e
54
+ # v3.01 (2019/2024, E)"), which `Pubid::Bipm` parses — unlike the bare
55
+ # `docnumber` ("SI Brochure 9e …", no "BIPM" prefix), which it rejects.
56
+ key = primary_id(item1) || item1.docnumber || basename
57
+ @data_fetcher.add_to_index key, outfile
56
58
  item =
57
59
  if File.exist? outfile
58
60
  warn_duplicate = false
data/lib/relaton/bipm.rb CHANGED
@@ -4,8 +4,10 @@ require "parslet"
4
4
  require "relaton/bib"
5
5
  require "relaton/index"
6
6
  require "relaton/core"
7
+ require "pubid"
7
8
  require_relative "version"
8
9
  require_relative "bipm/util"
10
+ require_relative "bipm/id_parser"
9
11
  require_relative "bipm/item_data"
10
12
  require_relative "bipm/model/item"
11
13
  require_relative "bipm/model/bibitem"
@@ -16,7 +18,11 @@ module Relaton
16
18
  module Bipm
17
19
  class Error < StandardError; end
18
20
 
19
- INDEXFILE = "index-v1.yaml".freeze
21
+ # The runtime index is the pubid-backed `index-v2` (`_type: pubid:bipm:*`).
22
+ # The legacy bespoke `{group,type,number,year}` `index-v1` is still published
23
+ # by `relaton-data-bipm`'s crawler (using the retained `Relaton::Bipm::Id`),
24
+ # but this flavor no longer reads or writes it.
25
+ INDEXFILE = "index-v2".freeze
20
26
 
21
27
  # Returns hash of gems versions used to generate the data model.
22
28
  # @return [String]
@@ -3,6 +3,9 @@
3
3
  module Relaton::Bsi
4
4
  # Class methods for search ISO standards.
5
5
  class Bibliography
6
+ # pubid identifier types referenced when navigating the parsed object graph.
7
+ Ids = ::Pubid::Bsi::Identifiers
8
+
6
9
  class << self
7
10
  # @param text [String]
8
11
  # @return [Relaton::Bsi::HitCollection]
@@ -24,52 +27,127 @@ module Relaton::Bsi
24
27
  #
25
28
  # @return [String] Relaton XML serialisation of reference
26
29
  def get(code, year = nil, opts = {})
27
- # y = code.split(":")[1]
28
- year ||= code_parts(code)[:year]
30
+ year ||= publication_year(code)
29
31
  ret = bib_get(code, year, opts)
30
32
  return nil if ret.nil?
31
33
 
32
34
  ret = ret.to_most_recent_reference unless year || opts[:keep_year]
33
- # ret = ret.to_all_parts if opts[:all_parts]
34
35
  ret
35
36
  end
36
37
 
37
- #
38
- # Destruct code to its parts.
39
- #
38
+ # Parse a BSI reference with pubid. An unrecognized reference raises;
39
+ # like ISO and 3GPP we let it propagate -- relaton-cli rescues
40
+ # Pubid::Errors::Error and renders "... is not a recognized standards
41
+ # identifier". A catalogue hit code is a different case: `Hit#pubid`
42
+ # rescues, because one odd Algolia row must not abort the search.
40
43
  # @param [String] code document identifier
44
+ # @return [Pubid::Bsi::Identifier]
45
+ # @raise [Pubid::Errors::ParseError]
46
+ def parse(code)
47
+ ::Pubid::Bsi::Identifier.parse(code)
48
+ end
49
+
50
+ # Publication year of a reference's base document.
51
+ # @param [String] code document identifier
52
+ # @return [String, nil]
53
+ def publication_year(code)
54
+ year_of parse(code)
55
+ end
56
+
57
+ # Whether a query and a candidate hit denote the same BSI document, at the
58
+ # fuzziness the hit filter asks for. Built entirely on pubid: `base_document`
59
+ # peels supplements/wrappers, `matches?` compares ignoring the publication
60
+ # date, and the `Amendment`/`Corrigendum` `#supplement_type`, `#number` and
61
+ # `#year` supply the amendment type, number and year.
41
62
  #
42
- # @return [MatchData] parts of the code
43
- #
44
- def code_parts(code)
45
- %r{
46
- ^(?:BSI\s)?(?<code>(?:[A-Z]+\s)*[^:\s+]+(?:\s\d+)?)
47
- (?::(?<year>\d{4})(?:-\d{2})?)?
48
- (?:\+(?<a>[^:\s]+)(?::(?<y>\d{4}))?)?
49
- (?:\s(?<rest>.+))?
50
- }x.match code
63
+ # @param query [Pubid::Bsi::Identifier]
64
+ # @param hit [Pubid::Bsi::Identifier]
65
+ # @param skip_rest [Boolean] ignore the free-text suffix (ExComm / Flex version)
66
+ # @param drop_amd [Boolean] reduce the hit to its base document before
67
+ # matching, so a query for the base document matches a catalogue entry
68
+ # that only exists in amended form (the returned id is de-amended too)
69
+ # @return [Boolean]
70
+ def same_reference?(query, hit, skip_rest: false, drop_amd: false)
71
+ hit = hit.base_document if drop_amd
72
+
73
+ base_match?(query, hit) &&
74
+ same_supplement?(supplement_of(query), supplement_of(hit)) &&
75
+ (skip_rest || rest_marker(query) == rest_marker(hit))
51
76
  end
52
77
 
53
78
  private
54
79
 
55
- def fetch_ref_err(code, year, missed_years) # rubocop:disable Metrics/MethodLength
56
- # y = code_parts(code)[:year]
57
- # id = year && !y ? "#{code}:#{year}" : code
58
- # Util.warn "WARNING: no match found online for `#{id}`. " \
59
- # "The code must be exactly like it is on the standards website."
80
+ # The year of the dated document at the root of a reference. pubid holds
81
+ # an adopted standard (`BS EN ISO 8848:2021`) in `base`, so its
82
+ # `base_document.year` is nil; `#root` reaches the dated document for
83
+ # every form, on the pubid before that change and after it.
84
+ # @param id [Pubid::Bsi::Identifier, nil]
85
+ # @return [String, nil]
86
+ def year_of(id)
87
+ id&.root&.year&.to_s
88
+ end
89
+
90
+ # Same base document, ignoring the publication date. `:month` is excluded
91
+ # alongside `:date` because Flex identifiers store the month separately, so
92
+ # `exclude(:date)` alone leaves them unequal.
93
+ def base_match?(query, hit)
94
+ query.base_document.matches?(hit.base_document, ignore: %i[date month])
95
+ end
96
+
97
+ # The `Amendment`/`Corrigendum` supplement of a reference, if any — peeling
98
+ # an Expert-commentary wrapper first. Both supplements expose
99
+ # `#supplement_type`, `#number` and `#year`.
100
+ def supplement_of(id)
101
+ while id.is_a?(Ids::ExpertCommentary)
102
+ inner = id.base
103
+ break if inner.nil? || inner.equal?(id)
104
+
105
+ id = inner
106
+ end
107
+ return nil unless id.respond_to?(:identifiers) && id.identifiers
108
+
109
+ id.identifiers.find { |i| i.respond_to?(:supplement_type) }
110
+ end
111
+
112
+ # Same supplement, with the amendment year treated as optional when the
113
+ # query omits it (a query for "…+A2" matches a hit for "…+A2:2020").
114
+ def same_supplement?(query_supp, hit_supp)
115
+ return hit_supp.nil? if query_supp.nil?
116
+ return false if hit_supp.nil?
117
+
118
+ query_supp.supplement_type == hit_supp.supplement_type &&
119
+ supplement_number(query_supp) == supplement_number(hit_supp) &&
120
+ (supplement_year(query_supp).nil? ||
121
+ supplement_year(query_supp) == supplement_year(hit_supp))
122
+ end
123
+
124
+ # The number and the year of a supplement. pubid removes the
125
+ # `#supplement_number` / `#supplement_year` aliases and stores the values
126
+ # in `#number` / `#year`. An older pubid still answers the aliases and
127
+ # leaves `#number` / `#year` nil, so the alias wins while it exists.
128
+ # TODO: read `#number` / `#year` directly once pubid `main` has no alias.
129
+ def supplement_number(supp)
130
+ supp.respond_to?(:supplement_number) ? supp.supplement_number : supp.number
131
+ end
132
+
133
+ def supplement_year(supp)
134
+ supp.respond_to?(:supplement_year) ? supp.supplement_year : supp.year
135
+ end
136
+
137
+ # Canonical marker for the free-text suffix pubid parses into a typed
138
+ # wrapper (so an `ExComm` query matches an `Expert Commentary` hit).
139
+ def rest_marker(id)
140
+ return :expert_commentary if id.is_a?(Ids::ExpertCommentary)
141
+ return :"flex_#{id.edition}" if id.is_a?(Ids::Flex) && id.edition
142
+
143
+ nil
144
+ end
145
+
146
+ def fetch_ref_err(_code, year, missed_years)
60
147
  unless missed_years.empty?
61
148
  Util.info "There was no match for `#{year}`, though there " \
62
149
  "were matches found for `#{missed_years.join('`, `')}`."
63
150
  end
64
- # if /\d-\d/.match? code
65
- # warn "[relaton-bsi] The provided document part may not exist, or "\
66
- # "the document may no longer be published in parts."
67
- # else
68
- # warn "[relaton-bsi] If you wanted to cite all document parts for "\
69
- # "the reference, use \"#{code} (all parts)\".\nIf the document "\
70
- # "is not a standard, use its document type abbreviation (TS, TR, "\
71
- # "PAS, Guide)."
72
- # end
73
151
  nil
74
152
  end
75
153
 
@@ -81,26 +159,20 @@ module Relaton::Bsi
81
159
  # @return [Relaton::Bsi::HitCollection] a collection of hits
82
160
  #
83
161
  def search_filter(code)
84
- cp = code_parts code
162
+ query = parse(code)
85
163
  Util.info "Fetching from shop.bsigroup.com ...", key: code
86
- unless cp
87
- Util.info "Could not parse the reference", key: code
88
- return []
89
- end
90
-
91
- search(code).filter_hits!(cp)
164
+ search(code).filter_hits!(query)
92
165
  end
93
166
 
94
- # Sort through the results from Isobib, fetching them three at a time,
95
- # and return the first result that matches the code,
96
- # matches the year (if provided), and which # has a title (amendments do not).
97
- # Only expects the first page of results to be populated.
98
- # Does not match corrigenda etc (e.g. ISO 3166-1:2006/Cor 1:2007)
99
- # If no match, returns any years which caused mismatch, for error reporting
167
+ # Sort through the results, and return the first result that matches the
168
+ # code and matches the year (if provided), and which has a title.
169
+ # If no match, returns any years which caused mismatch, for error reporting.
100
170
  def results_filter(result, year)
101
171
  missed_years = []
102
172
  result.each do |r|
103
- pyear = code_parts(r.hit[:code])[:year]
173
+ # The hit's own parse: a kept hit always parsed, and its base
174
+ # document's year is the same after a `drop_amd` rewrite.
175
+ pyear = year_of r.pubid
104
176
  return { ret: r.item } if (!year || year == pyear) && r.item
105
177
 
106
178
  missed_years << pyear
@@ -10,5 +10,19 @@ module Relaton::Bsi
10
10
  def item
11
11
  @item ||= Scraper.parse_page self
12
12
  end
13
+
14
+ # The hit's reference parsed with pubid, memoized so the (up to three) hit
15
+ # filter passes don't re-parse it. `nil` when pubid can't parse the code:
16
+ # the Algolia catalogue carries codes like `BS 381C SET:1996 (R2002)`, and
17
+ # one such row must not abort the search. (A malformed *query* raises; see
18
+ # `Bibliography.parse`.)
19
+ # @return [Pubid::Bsi::Identifier, nil]
20
+ def pubid
21
+ return @pubid if defined?(@pubid)
22
+
23
+ @pubid = Bibliography.parse(hit[:code])
24
+ rescue StandardError
25
+ @pubid = nil
26
+ end
13
27
  end
14
28
  end