Class: Autoselect::TaxonName::ColMatch
- Inherits:
-
Object
- Object
- Autoselect::TaxonName::ColMatch
- Defined in:
- lib/autoselect/taxon_name/col_match.rb
Overview
Matches one Catalogue of Life nameusage against a project's TaxonNames and builds the autoselect pseudo-record for it: CoL's own data and classification (Vendor::Colrapi.classified_usage) plus how each row lines up with TaxonWorks, whether the target already exists, and anything that blocks Create (conflicts, homonyms).
Terms:
* target - the CoL name being matched (the last row of the chain, below
its ancestors)
* anchor - the lowest row of CoL's chain (the target, else its nearest
ancestor) that exactly one TaxonName in the project matches, by current
form, rank and code. TaxonWorks' lineage is compared to CoL's from there
up. No anchor: Create would make the whole chain.
Instance Method Summary collapse
-
#agreeing_authorship(candidates, col_authorship) ⇒ Array<TaxonName>
private
Authorship only ever turns a block into a match: anything it can't compare (no year, an abbreviation, a diacritic or date that differs) leaves all the candidates.
-
#authorship_mismatch(anchor, row) ⇒ Hash?
private
The anchor is matched by name, rank and code alone; when its authorship and CoL's can both be compared and differ, they are usually different names that share a string (e.g. CoL's "Trioza minuta Crawford, 1910", a synonym of Bactericera minuta, vs TaxonWorks' Trioza minuta (Mathur, 1975)).
-
#blocked_result(extension, **reason) ⇒ Object
private
No anchor can be trusted: nothing is matched and Create and Select are blocked.
-
#call ⇒ OpenStruct?
Pseudo-record compatible with TaxonName autoselect format_results; nil when the usage has no scientificName.
-
#candidates_for(candidates_by_key, row, expected_code) ⇒ Array<TaxonName>
private
The TaxonNames whose current form is a CoL row, at the row's rank (and code, when CoL gives one) - never a Combination, which has no current form.
-
#find_anchor(full_chain, col_code) ⇒ Array?
private
Walks CoL's chain up from the target to the first row any TaxonName matches, by current form, rank and code.
-
#initialize(col_result, project_id:, dataset_id: nil) ⇒ ColMatch
constructor
A new instance of ColMatch.
-
#match_against_taxonworks(extension, col_name, col_rank) ⇒ Hash
private
Finds the anchor (#find_anchor), walking up from the target, then diffs TaxonWorks' REAL ancestor chain from there against CoL's (ColAncestryComparator).
- #none_alignment(extension) ⇒ Object private
Constructor Details
#initialize(col_result, project_id:, dataset_id: nil) ⇒ ColMatch
Returns a new instance of ColMatch.
22 23 24 25 26 27 28 |
# File 'lib/autoselect/taxon_name/col_match.rb', line 22 def initialize(col_result, project_id:, dataset_id: nil) raise ArgumentError, 'project_id is required' if project_id.blank? @col_result = col_result @project_id = project_id @dataset_id = dataset_id end |
Instance Method Details
#agreeing_authorship(candidates, col_authorship) ⇒ Array<TaxonName> (private)
Authorship only ever turns a block into a match: anything it can't compare (no year, an abbreviation, a diacritic or date that differs) leaves all the candidates.
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/autoselect/taxon_name/col_match.rb', line 226 def (candidates, ) = ::Utilities::AuthorNames.() return candidates if .nil? candidate_keys = candidates.to_h do |taxon_name| [ taxon_name, ::Utilities::AuthorNames.(taxon_name.) ] end [true, false].each do |parentheses| agreeing = candidates.select do |taxon_name| ::Utilities::AuthorNames.( candidate_keys[taxon_name], , parentheses: ) end return agreeing if agreeing.length == 1 end candidates end |
#authorship_mismatch(anchor, row) ⇒ Hash? (private)
The anchor is matched by name, rank and code alone; when its authorship and CoL's can both be compared and differ, they are usually different names that share a string (e.g. CoL's "Trioza minuta Crawford, 1910", a synonym of Bactericera minuta, vs TaxonWorks' Trioza minuta (Mathur, 1975)). Reported, not blocked: the same name can still differ (TaxonWorks' Evansia "Cambridge, O.P., 1900" is CoL's "O. Pickard-Cambridge, 1901"), and a block on an ancestor would stop Create with no way past it short of getting CoL's authorship changed. Parentheses are ignored, CoL adds them by its own placement.
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/autoselect/taxon_name/col_match.rb', line 261 def (anchor, row) = ::Utilities::AuthorNames.(row[:col_authorship]) = ::Utilities::AuthorNames.(anchor.) return nil if .nil? || .nil? same = ::Utilities::AuthorNames .(, ) return nil if same { rank: row[:rank], col_name: row[:col_name], col_authorship: row[:col_authorship], taxonworks_name: anchor.cached, taxonworks_authorship: anchor. } end |
#blocked_result(extension, **reason) ⇒ Object (private)
No anchor can be trusted: nothing is matched and Create and Select are
blocked. reason says why (e.g. :ambiguous_match).
306 307 308 309 310 311 312 |
# File 'lib/autoselect/taxon_name/col_match.rb', line 306 def blocked_result(extension, **reason) { alignment: none_alignment(extension), chain_consistent: false, **reason } end |
#call ⇒ OpenStruct?
Returns pseudo-record compatible with TaxonName autoselect format_results; nil when the usage has no scientificName.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/autoselect/taxon_name/col_match.rb', line 32 def call # Flat nameusage structure: 'id', 'status', 'name' hash, 'label', # 'labelHtml' col_name = @col_result.dig('name', 'scientificName') return nil if col_name.blank? col_rank = @col_result.dig('name', 'rank')&.downcase extension = ::Vendor::Colrapi.classified_usage( @col_result, dataset_id: @dataset_id ) # Flag when the target's rank is not resolvable in TaxonWorks (e.g. # 'domain'). if ::Vendor::Colrapi.rank_class_for( col_rank, col_code: extension[:col_code] ).nil? extension = extension.merge(target_rank_unknown: true) end extension = extension.merge( match_against_taxonworks(extension, col_name, col_rank) ) OpenStruct.new( id: nil, cached: col_name, name: col_name, rank_string: nil, cached_valid_taxon_name_id: nil, _col_extension: extension ) end |
#candidates_for(candidates_by_key, row, expected_code) ⇒ Array<TaxonName> (private)
The TaxonNames whose current form is a CoL row, at the row's rank (and code, when CoL gives one) - never a Combination, which has no current form. Rank is checked here, per row, not by key: a genus and its nominotypical subgenus share a key.
290 291 292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/autoselect/taxon_name/col_match.rb', line 290 def candidates_for(candidates_by_key, row, expected_code) key = ::Vendor::Colrapi::NameTranslation .col_name_key(row[:col_name], row[:rank]) (candidates_by_key[key] || []) .select { |candidate| candidate.form == :current } .map(&:taxon_name) .select do |taxon_name| rank_class = taxon_name.rank_class ::RANKS_LOOKUP[rank_class.to_s] == row[:rank] && (expected_code.nil? || rank_class.nomenclatural_code == expected_code) end end |
#find_anchor(full_chain, col_code) ⇒ Array? (private)
Walks CoL's chain up from the target to the first row any TaxonName matches, by current form, rank and code. One match there is the anchor. More than one is a homonym: the one whose authorship agrees with CoL's is the anchor (#agreeing_authorship), else the caller can't pick between them. A lone match is taken whatever its authorship, a difference is only reported (#authorship_mismatch).
Only the current form counts: a name matching in its published form (a Protonym's original combination, or a Combination) is a taxon TaxonWorks places differently, and its lineage would conflict with CoL's.
A valid TaxonName is preferred over an invalid one of the same name. An invalid one alone still stops the walk, and the caller decides what it means: walking past it would have Create duplicate it.
TODO: still missed - the same genus-group name at another genus-group rank (CoL genus Erasmoneura vs TaxonWorks subgenus Erythroneura (Erasmoneura)): rank must match exactly.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/autoselect/taxon_name/col_match.rb', line 194 def find_anchor(full_chain, col_code) # CoL and TaxonWorks format some names differently (subgenus, species-group # names with/without subgenus, [sic]), so candidates are found and compared # by key. candidates_by_key = ::Vendor::Colrapi::NameTranslation .candidates_by_key(full_chain, project_id: @project_id) expected_code = ::Vendor::Colrapi::COL_CODE_TO_NOMENCLATURE_CODE[col_code] (full_chain.length - 1).downto(0) do |i| row = full_chain[i] candidates = candidates_for(candidates_by_key, row, expected_code) next if candidates.empty? valid = candidates.select(&:cached_is_valid) candidates = valid if valid.any? if candidates.length > 1 candidates = (candidates, row[:col_authorship]) end return [i, candidates] end nil end |
#match_against_taxonworks(extension, col_name, col_rank) ⇒ Hash (private)
Finds the anchor (#find_anchor), walking up from the target, then diffs TaxonWorks' REAL ancestor chain from there against CoL's (ColAncestryComparator).
A homonym at the anchor's row - more than one same-name, same-rank candidate that authorship can't pick between (#agreeing_authorship) - blocks Create, same as a real conflict. So does an anchor that is invalid in TaxonWorks where CoL accepts the name: nothing is created under, above or beside a TaxonWorks synonym. A lone anchor whose authorship differs from CoL's is matched, the difference only reported (#authorship_mismatch).
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/autoselect/taxon_name/col_match.rb', line 85 def match_against_taxonworks(extension, col_name, col_rank) full_chain = extension[:alignment] + [{ rank: col_rank, col_name:, col_id: extension[:col_key], dataset_id: extension[:col_dataset_id], col_authorship: extension[:col_authorship] }] anchor_index, anchor_candidates = find_anchor(full_chain, extension[:col_code]) if anchor_candidates && !extension[:non_accepted] && anchor_candidates.none?(&:cached_is_valid) row = full_chain[anchor_index] invalid = anchor_candidates.first return blocked_result( extension, invalid_match: { rank: row[:rank], col_name: row[:col_name], taxonworks_id: invalid.id, taxonworks_name: invalid.cached, taxonworks_valid_name: invalid.valid_taxon_name&.cached } ) end if anchor_candidates && anchor_candidates.length > 1 row = full_chain[anchor_index] return blocked_result( extension, ambiguous_match: { rank: row[:rank], col_name: row[:col_name], count: anchor_candidates.length } ) end anchor = anchor_candidates&.first if anchor.nil? return { alignment: none_alignment(extension), chain_consistent: true } end # Rows Create would make: everything below the anchor (none when the # target matched). # # TODO: a row here whose name TaxonWorks has only in its published form is # created again, as CoL places it - a duplicate of the taxon, under a # hierarchy TaxonWorks' data says is out of date. Not checked for now: the # Smart level, which usually runs first, already lists such a name for the # target (it searches original combinations), and it's rare. below_rows = full_chain[(anchor_index + 1)..].map do |row| row.merge(taxonworks_id: nil, taxonworks_name: nil, match: 'none') end tw_chain = anchor.self_and_ancestors .where.not(rank_class: 'NomenclaturalRank') # excludes Root .unscope(:order) .order(generations: :desc) .to_a diffed = ::Autoselect::TaxonName::ColAncestryComparator.new( tw_chain:, col_chain: full_chain[0..anchor_index], col_code: extension[:col_code] ).call merged = diffed + below_rows mismatch = (anchor, full_chain[anchor_index]) { alignment: merged[0...-1], chain_consistent: merged.none? { |row| row[:match] == 'conflict' }, **(mismatch ? { authorship_mismatch: mismatch } : {}), **( if below_rows.empty? { target_taxonworks_id: anchor.id, target_global_id: anchor.to_global_id.to_s } else {} end ) } end |
#none_alignment(extension) ⇒ Object (private)
314 315 316 317 318 |
# File 'lib/autoselect/taxon_name/col_match.rb', line 314 def none_alignment(extension) extension[:alignment].map do |row| row.merge(taxonworks_id: nil, taxonworks_name: nil, match: 'none') end end |