@larsgw/formica 0.2.1 → 0.3.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larsgw/formica",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "SDK and tools for data from the Library of Identification Resources",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -21,6 +21,14 @@ function sortObject (object: Record<string, any>): Record<string, any> {
21
21
  }
22
22
  /* eslint-enable @typescript-eslint/no-explicit-any */
23
23
 
24
+ function addTaxon (gbifIndex: Record<string, TaxonId[]>, gbifId: string, taxon: string[]) {
25
+ if (!(gbifId in gbifIndex)) {
26
+ gbifIndex[gbifId] = []
27
+ }
28
+ gbifIndex[gbifId].push(taxon[0])
29
+ gbifIndex[gbifId].sort(numericSort)
30
+ }
31
+
24
32
  async function main (args: string[]): Promise<void> {
25
33
  const REPO_ROOT = path.resolve(args[0])
26
34
 
@@ -50,11 +58,10 @@ async function main (args: string[]): Promise<void> {
50
58
  for (const taxon of dwc) {
51
59
  const gbifId = taxon[25]
52
60
  if (gbifId) {
53
- if (!(gbifId in gbifIndex)) {
54
- gbifIndex[gbifId] = []
61
+ addTaxon(gbifIndex, gbifId, taxon)
62
+ if (taxon[27] !== taxon[25]) {
63
+ addTaxon(gbifIndex, taxon[27], taxon)
55
64
  }
56
- gbifIndex[gbifId].push(taxon[0])
57
- gbifIndex[gbifId].sort(numericSort)
58
65
  }
59
66
  amendedResource.taxonCount += 1
60
67
  }
@@ -8,17 +8,6 @@ import * as util from 'util'
8
8
  import { csv } from '../index'
9
9
  import { prompt, promptForAnswers, numericSort, runCommand } from './util'
10
10
 
11
- interface AmendedTaxon extends Taxon {
12
- colTaxonID?: string,
13
- gbifTaxonID?: string
14
- }
15
-
16
- interface AmendedResource extends Resource {
17
- taxa: Record<TaxonId, AmendedTaxon>
18
- }
19
-
20
- type Classifications = Record<string, Array<[AmendedTaxon, string]>>
21
-
22
11
  const DWC_FIELDS: string[] = [
23
12
  'scientificNameID',
24
13
  'scientificName',
@@ -49,7 +38,9 @@ const DWC_FIELDS: string[] = [
49
38
  'higherClassification',
50
39
 
51
40
  'colTaxonID',
52
- 'gbifTaxonID'
41
+ 'gbifTaxonID',
42
+ 'colAcceptedTaxonID',
43
+ 'gbifAcceptedTaxonID'
53
44
  ]
54
45
 
55
46
  const DISPLAY_FIELDS: string[] = [
@@ -74,17 +65,9 @@ const GBIF_RANKS: Rank[] = [
74
65
  'variety'
75
66
  ]
76
67
 
77
- const VALID_COMMON_PREFIXES = [
78
- 'Plantae|Tracheophyta',
79
- 'Fungi',
80
- 'Fungi|Ascomycota',
81
- 'Fungi|Basidiomycota',
82
- 'Fungi|Zygomycota'
83
- ]
84
-
85
68
  function runGnverifier (names: string): Promise<string> {
86
69
  return new Promise((resolve, reject) => {
87
- const proc = spawn('gnverifier', ['-s', '1,11', '-M'])
70
+ const proc = spawn('gnverifier', ['-s', '1,11', '-f', 'compact', '-M'])
88
71
  let stdout = ''
89
72
  proc.stdout.on('data', data => { stdout += data })
90
73
  proc.stderr.pipe(process.stdout)
@@ -153,6 +136,18 @@ class ResourceProcessor {
153
136
  }
154
137
  }
155
138
 
139
+ async runMappingsUpdate (): Promise<void> {
140
+ const input = await fs.readdir(this.DIR_TXT)
141
+
142
+ const ids = input
143
+ .map(file => path.basename(file, '.txt'))
144
+ .sort((a, b) => parseInt(a.slice(1)) - parseInt(b.slice(1)))
145
+
146
+ for (const id of ids) {
147
+ await this.processWork(id, true)
148
+ }
149
+ }
150
+
156
151
  async processWork (id: WorkId, update?: boolean): Promise<void> {
157
152
  const resources = await this.processResources(id, update)
158
153
 
@@ -174,11 +169,7 @@ class ResourceProcessor {
174
169
 
175
170
  const amendedResources = []
176
171
  for (const resource of resources) {
177
- const [results, classifications] = await this.processResourceDwc(resource)
178
-
179
- for (const source in classifications) {
180
- await this.checkPrefix(resource, classifications, source)
181
- }
172
+ const results = await this.processResourceDwc(resource)
182
173
 
183
174
  const skip = await this.shouldBeSkipped(resource.id)
184
175
 
@@ -256,100 +247,153 @@ class ResourceProcessor {
256
247
  }
257
248
  }
258
249
 
259
- async processResourceDwc (resource: Resource): Promise<[AmendedResource, Classifications]> {
250
+ async processResourceDwc (resource: Resource): Promise<AmendedResource> {
260
251
  console.log(`${resource.workId}: matching ${resource.id}`)
261
- const taxa: Record<string, Record<TaxonId, AmendedTaxon>> = {}
262
- const names = []
252
+
253
+ const filteredResults: Record<TaxonId, TaxonMatch[]> = {}
254
+ const taxonNames: Record<string, TaxonId[]> = {}
255
+ const names = new Set()
263
256
  for (const id in resource.taxa) {
264
257
  const name = resource.taxa[id].scientificName
265
258
 
266
- if (!taxa[name]) { taxa[name] = {} }
267
- taxa[name][id] = { ...resource.taxa[id] }
259
+ if (!taxonNames[name]) { taxonNames[name] = [] }
260
+ taxonNames[name].push(id)
268
261
 
269
- names.push(name)
262
+ names.add(name)
263
+ filteredResults[id] = []
270
264
  }
271
265
 
272
- const result = await runGnverifier(names.join('\n'))
273
- const classifications: Classifications = { '1': [], '11': [] }
274
-
275
- const [header, ...matches] = csv.parseCsv(result)
276
- for (const match of matches) {
277
- const name = match[header.indexOf('ScientificName')]
278
- const source = match[header.indexOf('DataSourceId')]
279
- const id = match[header.indexOf('TaxonId')]
280
- const classification = match[header.indexOf('ClassificationPath')]
281
-
282
- for (const loirId in taxa[name]) {
283
- const taxon = taxa[name][loirId]
284
- if (source === '1' && !taxon.colTaxonID) {
285
- taxon.colTaxonID = id
286
- classifications[source].push([taxon, classification])
266
+ const result = await runGnverifier(Array.from(names).join('\n'))
267
+ for (const results of result.trim().split('\n')) {
268
+ const { name, results: matches } = JSON.parse(results)
269
+
270
+ if (!matches) {
271
+ continue
272
+ }
273
+
274
+ for (const match of matches) {
275
+ const source = match.dataSourceId
276
+ const currentRank = match.classificationRanks.split('|').pop()
277
+
278
+ if (match.scoreDetails.cardinalityScore === 0) {
279
+ // Rank mismatch
280
+ continue
281
+ } else if (source === 11 && currentRank === 'species' && match.classificationPath.endsWith(' spec')) {
282
+ // GBIF species like "Nomada spec"
283
+ continue
287
284
  }
288
- if (source === '11' && GBIF_RANKS.includes(taxon.taxonRank) && !taxon.gbifTaxonID) {
289
- taxon.gbifTaxonID = id
290
- classifications[source].push([taxon, classification])
285
+
286
+ for (const loirId of taxonNames[name]) {
287
+ const taxon = resource.taxa[loirId]
288
+
289
+ if (source === 11 && !GBIF_RANKS.includes(taxon.taxonRank)) {
290
+ // Exclude GBIF matches for ranks that are not in GBIF
291
+ continue
292
+ } else if (source === 11 && !match.isSynonym && currentRank !== taxon.taxonRank) {
293
+ // Exclude matches with rank mismatches (only possible
294
+ // for non-synonyms).
295
+ continue
296
+ }
297
+
298
+ if (!filteredResults[loirId]) {
299
+ filteredResults[loirId] = []
300
+ }
301
+
302
+ filteredResults[loirId].push({
303
+ source,
304
+ id: match.recordId,
305
+ currentId: match.currentRecordId,
306
+ classificationPath: match.classificationPath.split('|')
307
+ })
291
308
  }
292
309
  }
293
310
  }
294
311
 
295
- const results = {
296
- ...resource,
297
- taxa: Object.fromEntries(Object.values(resource.taxa).map(taxon => [
298
- taxon.scientificNameID,
299
- taxa[taxon.scientificName][taxon.scientificNameID]
300
- ]))
312
+ const { taxonNames: { amendResource, groupNameMatches } } = await import('../index')
313
+ const groupedNameMatches = groupNameMatches(filteredResults)
314
+
315
+ const amendedResource: AmendedResource = { ...resource, taxa: { ...resource.taxa } }
316
+ for (const source in groupedNameMatches) {
317
+ const matches = await this.selectPrefixes(resource, groupedNameMatches, source)
318
+ amendResource(amendedResource, source, matches)
301
319
  }
302
320
 
303
- return [results, classifications]
321
+ return amendedResource
304
322
  }
305
323
 
306
- async checkPrefix (resource: AmendedResource, classifications: Classifications, source: string): Promise<void> {
307
- const lists = classifications[source]
308
- if (!lists.length) { return }
309
- const prefix = lists[0][1].split('|')
310
-
311
- for (const [taxon, list] of lists.slice(1)) {
312
- const parts = list.split('|')
313
- for (let i = 0; i < parts.length; i++) {
314
- if (parts[i] !== prefix[i] && i < 3) {
315
- let choice
316
-
317
- if (source === '1' || taxon.taxonomicStatus !== 'accepted') {
318
- choice = 'd'
319
- } else if (VALID_COMMON_PREFIXES.includes(prefix.slice(0, i).join('|'))) {
320
- choice = 'k'
321
- } else {
322
- console.log(`${resource.workId}: source ${source} results in short prefix "${prefix.slice(0, i).join('|')}" (${i} taxa)`)
323
- console.log(` taxon: ${taxon.scientificNameID} "${taxon.scientificName}"`)
324
- console.log(` class: ${parts.join('|')}`)
325
- console.log(` prefx: ${prefix.join('|')}`)
326
-
327
- choice = await promptForAnswers(` Keep or delete (k/d)? `, ['k', 'K', 'd', 'D'])
328
- }
324
+ async selectPrefixes (resource: Resource, groupedNameMatches: GroupedNameMatches, source: string): Promise<Record<TaxonId, TaxonMatch>> {
325
+ const prefixes = Object.keys(groupedNameMatches[source])
326
+ if (prefixes.length === 0) {
327
+ return {}
328
+ } else if (prefixes.length === 1) {
329
+ return groupedNameMatches[source][prefixes[0]]
330
+ }
329
331
 
330
- switch (choice) {
331
- case 'k':
332
- case 'K': {
333
- console.log(` keeping...`)
334
- break
335
- }
332
+ // Count total mapped taxa
333
+ const mappedTaxa: Record<TaxonId, boolean> = {}
334
+ for (const prefix of prefixes) {
335
+ for (const taxon in groupedNameMatches[source][prefix]) {
336
+ mappedTaxa[taxon] = true
337
+ }
338
+ }
339
+ const missedTaxonCount = Object.keys(mappedTaxa).length - Object.keys(groupedNameMatches[source][prefixes[0]]).length
336
340
 
337
- case 'd':
338
- case 'D': {
339
- console.log(` deleting...`)
340
- if (source === '1') {
341
- delete taxon.colTaxonID
342
- } else if (source === '11') {
343
- delete taxon.gbifTaxonID
344
- }
345
- break
346
- }
347
- }
341
+ if (missedTaxonCount === 0) {
342
+ // Multiple prefixes but the first one maps all taxa (not counting that are unmapped in all prefixes)
343
+ return groupedNameMatches[source][prefixes[0]]
344
+ }
345
+
346
+ console.error(`${resource.workId}: source ${source} results in multiple prefixes`)
347
+
348
+ let choice
349
+ if (missedTaxonCount <= 5) {
350
+ console.error(` Most common prefix misses ${missedTaxonCount} taxa: automatically selecting most common prefix...`)
351
+ choice = '1'
352
+ } else if (source === '1') {
353
+ console.error(` Catalogue of Life: automatically selecting most common prefix...`)
354
+ choice = '1'
355
+ } else {
356
+ for (let i = 0; i < prefixes.length; i++) {
357
+ const prefix = prefixes[i]
358
+ const taxa = groupedNameMatches[source][prefix]
359
+ const taxonIds = Object.keys(taxa)
360
+
361
+ console.error(` [${i + 1}] ${prefix} (${taxonIds.length} taxa)`)
362
+ for (let j = 0; j < Math.min(9, taxonIds.length); j++) {
363
+ const taxonId = taxonIds[j]
364
+ const taxon = resource.taxa[taxonId]
365
+ const match = taxa[taxonId]
366
+ console.error(` taxon: ${taxonId} "${taxon.scientificName}" - ${match.classificationPath.join('|')}`)
367
+ }
368
+ if (taxonIds.length > 9) {
369
+ console.error(` ...`)
370
+ }
371
+ }
372
+
373
+ do {
374
+ choice = await prompt(` Select prefixes (1-${prefixes.length})? `)
375
+ } while (!/^(|\d+(,\d+)*)$/.test(choice))
376
+ }
377
+
378
+ console.error(` Applying selection...`)
379
+
380
+ if (choice === '') {
381
+ return {}
382
+ }
348
383
 
349
- break
384
+ const matches: Record<TaxonId, TaxonMatch> = {}
385
+ for (const i of choice.split(',')) {
386
+ const prefix = prefixes[parseInt(i) - 1]
387
+ const taxa = groupedNameMatches[source][prefix]
388
+ for (const id in taxa) {
389
+ if (id in matches) {
390
+ continue
350
391
  }
392
+ matches[id] = taxa[id]
351
393
  }
352
394
  }
395
+
396
+ return matches
353
397
  }
354
398
 
355
399
  checkResults (resource: AmendedResource): boolean {
@@ -388,6 +432,9 @@ function main (): void {
388
432
  update: {
389
433
  type: 'boolean',
390
434
  short: 'u'
435
+ },
436
+ 'update-mappings': {
437
+ type: 'boolean'
391
438
  }
392
439
  },
393
440
  allowPositionals: true
@@ -398,7 +445,15 @@ function main (): void {
398
445
  process.stdout.write('\n')
399
446
  })
400
447
 
401
- const task = args.values.update ? processor.runUpdate() : processor.run()
448
+ let task
449
+ if (args.values.update) {
450
+ task = processor.runUpdate()
451
+ } else if (args.values['update-mappings']) {
452
+ task = processor.runMappingsUpdate()
453
+ } else {
454
+ task = processor.run()
455
+ }
456
+
402
457
  task.catch(error => {
403
458
  console.error(error)
404
459
  process.exit(1)
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * as catalog from './catalog/index'
2
2
  export * as resources from './resources/index'
3
+ export * as taxonNames from './taxon-names/index'
3
4
  export * as csv from './csv'
package/src/module.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  type Schema = Record<string, FieldSpecification>
2
2
  type FieldSpecification = {
3
- required: boolean,
4
- multiple: boolean | FieldSpecificationCallbackMultiple
5
- format?: string[] | RegExp | FieldSpecificationCallbackFormat
3
+ required: boolean,
4
+ multiple: boolean | FieldSpecificationCallbackMultiple
5
+ format?: string[] | RegExp | FieldSpecificationCallbackFormat
6
6
  }
7
7
 
8
8
  type Value = string[] | string
@@ -11,12 +11,12 @@ type FieldSpecificationCallbackMultiple = (entry: Record<string, Value>) => bool
11
11
  type FieldSpecificationCallbackFormat = (value: SingleValue) => boolean;
12
12
 
13
13
  interface FieldError {
14
- field: string,
15
- error: string
14
+ field: string,
15
+ error: string
16
16
  }
17
17
 
18
18
  interface WorkError extends FieldError {
19
- entity: WorkId
19
+ entity: WorkId
20
20
  }
21
21
 
22
22
  type Rank = string
@@ -28,59 +28,59 @@ type ResourceId = string
28
28
  type WorkId = string
29
29
 
30
30
  interface WorkingTaxon {
31
- scientificNameID?: TaxonId,
32
- scientificName?: string,
33
- scientificNameAuthorship?: string,
34
- genericName?: string,
35
- infragenericEpithet?: string,
36
- specificEpithet?: string,
37
- intraspecificEpithet?: string,
38
-
39
- taxonRank?: Rank,
40
- taxonRemarks?: string,
41
- collectionCode?: ResourceId,
42
-
43
- taxonomicStatus?: TaxonStatus,
44
- acceptedNameUsageID?: TaxonId,
45
- acceptedNameUsage?: string,
46
-
47
- parentNameUsageID?: TaxonId,
48
- parentNameUsage?: string,
49
- kingdom?: string,
50
- phylum?: string,
51
- class?: string,
52
- order?: string,
53
- family?: string,
54
- subfamily?: string,
55
- genus?: string,
56
- subgenus?: string,
57
- higherClassification?: string,
58
-
59
- // Non-standard
60
- scientificNameOnly?: string,
61
- incorrect?: WorkingTaxon
31
+ scientificNameID?: TaxonId,
32
+ scientificName?: string,
33
+ scientificNameAuthorship?: string,
34
+ genericName?: string,
35
+ infragenericEpithet?: string,
36
+ specificEpithet?: string,
37
+ intraspecificEpithet?: string,
38
+
39
+ taxonRank?: Rank,
40
+ taxonRemarks?: string,
41
+ collectionCode?: ResourceId,
42
+
43
+ taxonomicStatus?: TaxonStatus,
44
+ acceptedNameUsageID?: TaxonId,
45
+ acceptedNameUsage?: string,
46
+
47
+ parentNameUsageID?: TaxonId,
48
+ parentNameUsage?: string,
49
+ kingdom?: string,
50
+ phylum?: string,
51
+ class?: string,
52
+ order?: string,
53
+ family?: string,
54
+ subfamily?: string,
55
+ genus?: string,
56
+ subgenus?: string,
57
+ higherClassification?: string,
58
+
59
+ // Non-standard
60
+ scientificNameOnly?: string,
61
+ incorrect?: WorkingTaxon
62
62
  }
63
63
 
64
64
  interface Taxon extends WorkingTaxon {
65
- scientificNameID: TaxonId,
66
- scientificName: string,
67
- taxonRank: Rank,
68
- collectionCode: ResourceId,
69
- taxonomicStatus: string
65
+ scientificNameID: TaxonId,
66
+ scientificName: string,
67
+ taxonRank: Rank,
68
+ collectionCode: ResourceId,
69
+ taxonomicStatus: string
70
70
  }
71
71
 
72
72
  interface ResourceMetadata {
73
- levels: Rank[],
74
- scope: string[],
75
- catalog?: object
73
+ levels: Rank[],
74
+ scope: string[],
75
+ catalog?: object
76
76
  }
77
77
 
78
78
  interface Resource {
79
- id: string,
80
- file: string,
81
- workId: string,
82
- metadata: ResourceMetadata,
83
- taxa: Record<TaxonId, Taxon>
79
+ id: string,
80
+ file: string,
81
+ workId: string,
82
+ metadata: ResourceMetadata,
83
+ taxa: Record<TaxonId, Taxon>
84
84
  }
85
85
 
86
86
  interface ResourceHistory {
@@ -103,3 +103,23 @@ declare enum ResourceDiffType {
103
103
  Modified = '~',
104
104
  Unchanged = '='
105
105
  }
106
+
107
+ interface AmendedTaxon extends Taxon {
108
+ colTaxonID?: string,
109
+ colAcceptedTaxonID?: string,
110
+ gbifTaxonID?: string,
111
+ gbifAcceptedTaxonID?: string
112
+ }
113
+
114
+ interface AmendedResource extends Resource {
115
+ taxa: Record<TaxonId, AmendedTaxon>
116
+ }
117
+
118
+ interface TaxonMatch {
119
+ source: number,
120
+ id: string,
121
+ currentId?: string,
122
+ classificationPath: string[]
123
+ }
124
+
125
+ type GroupedNameMatches = Record<string, Record<string, Record<TaxonId, TaxonMatch>>>
@@ -372,7 +372,7 @@ function parseHeader (header: string): ResourceMetadata {
372
372
 
373
373
  function validateResource (config: ResourceMetadata, content: string) {
374
374
  // Check for too much indentation
375
- const longerIndent = new RegExp(`^( ){${config.levels.length - 1}}(?! [+=>] ) `, 'm')
375
+ const longerIndent = new RegExp(`^( ){${config.levels.length - 1}}(?! [+=>] | > ) `, 'm')
376
376
  const longerIndentMatch = content.match(longerIndent)
377
377
  if (longerIndentMatch !== null) {
378
378
  const offset = longerIndentMatch.index
@@ -425,8 +425,8 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
425
425
  const lineIndent = (line.match(/^ */) as string[])[0].length
426
426
 
427
427
  if (lineIndent > groupIndent) {
428
- // Do not count synonyms as parents
429
- if (data[previousId] && data[previousId].taxonomicStatus === 'accepted') {
428
+ // Do not count synonyms as parents (unless this is correcting a typo in the synonym)
429
+ if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line)) {
430
430
  parents.push(previousId)
431
431
  } else {
432
432
  parents.push(null)
@@ -0,0 +1,79 @@
1
+ const MINIMUM_PREFIX_LENGTH = 3
2
+ const VALID_COMMON_PREFIXES = new Set([
3
+ 'Plantae|Tracheophyta',
4
+ 'Fungi',
5
+ 'Fungi|Ascomycota',
6
+ 'Fungi|Basidiomycota',
7
+ 'Fungi|Zygomycota'
8
+ ])
9
+
10
+ function getCommonPrefix (a: string[], b: string[]): string[] {
11
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
12
+ if (a[i] !== b[i]) {
13
+ return a.slice(0, i)
14
+ }
15
+ }
16
+ return a.slice()
17
+ }
18
+
19
+ function isValidPrefix (a: string[], b: string[]): boolean {
20
+ const prefix = getCommonPrefix(a, b)
21
+ return VALID_COMMON_PREFIXES.has(prefix.join('|')) || prefix.length >= MINIMUM_PREFIX_LENGTH
22
+ }
23
+
24
+ export function groupNameMatches (results: Record<TaxonId, TaxonMatch[]>): GroupedNameMatches {
25
+ const prefixes: Record<string, [string[], Record<TaxonId, TaxonMatch>][]> = {}
26
+
27
+ for (const scientificNameID in results) {
28
+ for (const result of results[scientificNameID]) {
29
+ if (!prefixes[result.source]) {
30
+ prefixes[result.source] = []
31
+ }
32
+
33
+ let prefix = prefixes[result.source].find(prefix => isValidPrefix(prefix[0], result.classificationPath))
34
+
35
+ if (!prefix) {
36
+ prefix = [result.classificationPath, {}]
37
+ prefixes[result.source].push(prefix)
38
+ } else {
39
+ prefix[0] = getCommonPrefix(prefix[0], result.classificationPath)
40
+ }
41
+
42
+ if (scientificNameID in prefix[1]) {
43
+ continue
44
+ }
45
+
46
+ prefix[1][scientificNameID] = result
47
+ }
48
+ }
49
+
50
+ const groupedNameMatches: GroupedNameMatches = {}
51
+ for (const source in prefixes) {
52
+ groupedNameMatches[source] = prefixes[source]
53
+ .sort((a, b) => Object.keys(b[1]).length - Object.keys(a[1]).length)
54
+ .reduce((map: Record<string, Record<TaxonId, TaxonMatch>>, [prefix, taxa]) => {
55
+ map[prefix.join('|')] = taxa
56
+ return map
57
+ }, {})
58
+ }
59
+
60
+ return groupedNameMatches
61
+ }
62
+
63
+ export function amendResource (resource: AmendedResource, source: string, matches: Record<TaxonId, TaxonMatch>) {
64
+ for (const id in matches) {
65
+ const match = matches[id]
66
+
67
+ if (source === '1') {
68
+ resource.taxa[id].colTaxonID = match.id
69
+ if (match.currentId) {
70
+ resource.taxa[id].colAcceptedTaxonID = match.currentId
71
+ }
72
+ } else if (source === '11') {
73
+ resource.taxa[id].gbifTaxonID = match.id
74
+ if (match.currentId) {
75
+ resource.taxa[id].gbifAcceptedTaxonID = match.currentId
76
+ }
77
+ }
78
+ }
79
+ }
package/test/resources.js CHANGED
@@ -4,7 +4,33 @@ const assert = require('assert')
4
4
  const { resources } = require('../lib')
5
5
 
6
6
  test('resources', async (t) => {
7
- await t.test('missing leaf taxa', (t) => {
7
+ await t.test('parses author with initials', (t) => {
8
+ const [resource] = resources.parseTextFile(`---
9
+ levels: [family, genus, species]
10
+ ---
11
+
12
+ Dolichurus haemorrhous A. Costa, 1886
13
+ Dolichurus A. Costa, 1886
14
+ Dolichurus indet.
15
+ Sphecidae A. Costa, 1886
16
+ Sphecidae indet.
17
+ `, 'T1')
18
+ assert.deepStrictEqual(Object.values(resource.taxa).map(taxon => taxon.scientificNameAuthorship), Array(3).fill('A. Costa, 1886'))
19
+ })
20
+
21
+ await t.test('does not validate name with correction', (t) => {
22
+ const [resource] = resources.parseTextFile(`---
23
+ levels: [species]
24
+ ---
25
+
26
+ Clytochrysus lapidarius (Panzer, 1804)
27
+ = Crabo chrysostomus Lepeletier & Brullé, 1835
28
+ > Crabro chrysostomus Lepeletier & Brullé, 1835
29
+ `, 'T1')
30
+ assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Clytochrysus lapidarius (Panzer, 1804)')
31
+ })
32
+
33
+ await t.test('errors for missing leaf taxa', (t) => {
8
34
  assert.throws(() => {
9
35
  resources.parseTextFile(`---
10
36
  levels: [family, genus, species]