@larsgw/formica 0.10.1 → 0.10.2

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/README.md CHANGED
@@ -23,10 +23,20 @@ Run locally:
23
23
  - `loir-resources-process [./resources]`:
24
24
  Interactive tool to convert the aforementioned `.txt` files to Darwin Core archives
25
25
  ([documentation](https://github.com/identification-resources/catalog/blob/main/docs/resources-dwc.md)),
26
- and link the taxa to GBIF and Catalogue of Life identifiers. Use the `-u` or
27
- `--update` flag in a Git repo to update existing Darwin Core files while keeping
28
- the identifiers stable (in most cases).
26
+ and link the taxa to GBIF and Catalogue of Life identifiers.
27
+ - Use `-s` or `--source` to specify which files should be processed: `unprocessed`
28
+ (default) for `.txt` files without corresponding Darwin Core files; `modified`
29
+ if in a Git repo to update existing Darwin Core files while keeping the identifiers
30
+ stable (in most cases); `all` for all `.txt` files; or `dwc` for only update
31
+ identifier mappings.
32
+ - Use `-u` or `--update-mappings` to specify which mappings to update: `col` or
33
+ `gbif`. This argument can be repeated. By default, both COL and GBIF are updated.
34
+ - Use `-k` or `--keep-mappings` to not update any mappings. Cannot be used in
35
+ conjunction with `-u`.
36
+ - Use `--mapping-api` to use either `gnverifier` (default) or `clb`, the ChecklistBank
37
+ API, to update the mappings.
29
38
  - `loir-resources-index [./resources]`: Create indices of the Darwin Core archives.
39
+ - `loir-resources-check-diff [./resources/dwc]`: Determine which columns changed.
30
40
 
31
41
  ## API
32
42
 
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
37
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
38
+ return new (P || (P = Promise))(function (resolve, reject) {
39
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
40
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
41
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
42
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
43
+ });
44
+ };
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ // import { promises as fs, existsSync as doesFileExist } from 'fs'
47
+ const path = __importStar(require("path"));
48
+ const util = __importStar(require("util"));
49
+ const util_1 = require("./util");
50
+ function listChangedFiles(directory) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ const output = yield (0, util_1.runCommand)('git', ['diff', '--name-only', 'HEAD', '--', directory], {
53
+ cwd: directory
54
+ });
55
+ return output.trimEnd().split('\n');
56
+ });
57
+ }
58
+ function getOldFile(file) {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ const options = {
61
+ cwd: path.dirname(file)
62
+ };
63
+ const gitRoot = (yield (0, util_1.runCommand)('git', ['rev-parse', '--show-toplevel'], options)).trim();
64
+ return yield (0, util_1.runCommand)('git', ['show', 'HEAD:' + path.relative(gitRoot, file)], options);
65
+ });
66
+ }
67
+ function main() {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ const args = util.parseArgs({
70
+ allowPositionals: true
71
+ });
72
+ const files = yield listChangedFiles(path.resolve(args.positionals[0]));
73
+ console.log(files);
74
+ });
75
+ }
76
+ main().catch(console.error);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
37
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
38
+ return new (P || (P = Promise))(function (resolve, reject) {
39
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
40
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
41
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
42
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
43
+ });
44
+ };
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ const fs_1 = require("fs");
47
+ const path = __importStar(require("path"));
48
+ const util = __importStar(require("util"));
49
+ const index_1 = require("../index");
50
+ const util_1 = require("./util");
51
+ function listChangedFiles(directory) {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ const output = yield (0, util_1.runCommand)('git', ['diff', '--name-only', 'HEAD', '--', directory], {
54
+ cwd: directory
55
+ });
56
+ return output.trimEnd().split('\n');
57
+ });
58
+ }
59
+ function getOldFile(file, gitRoot) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ return (0, util_1.runCommand)('git', ['show', 'HEAD:' + path.relative(gitRoot, file)], {
62
+ cwd: path.dirname(file)
63
+ });
64
+ });
65
+ }
66
+ function main() {
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ var _a;
69
+ const args = util.parseArgs({
70
+ allowPositionals: true
71
+ });
72
+ const base = path.resolve(args.positionals[0]);
73
+ const files = yield listChangedFiles(base);
74
+ const gitRoot = (yield (0, util_1.runCommand)('git', ['rev-parse', '--show-toplevel'], {
75
+ cwd: args.positionals[0]
76
+ })).trim();
77
+ const columnIndex = {};
78
+ for (const fileName of files) {
79
+ const filePath = path.join(gitRoot, fileName);
80
+ const b = index_1.csv.parseCsv(yield fs_1.promises.readFile(filePath, 'utf8'));
81
+ const a = index_1.csv.parseCsv(yield getOldFile(filePath, gitRoot));
82
+ const header = a[0];
83
+ for (let i = 1; i < Math.max(a.length, b.length); i++) {
84
+ for (let j = 0; j < header.length; j++) {
85
+ if (!a[i] || !b[i] || a[i][j] !== b[i][j]) {
86
+ ((_a = columnIndex[header[j]]) !== null && _a !== void 0 ? _a : (columnIndex[header[j]] = new Set())).add(path.basename(fileName, '.csv'));
87
+ }
88
+ }
89
+ }
90
+ }
91
+ const output = Object.entries(columnIndex)
92
+ .sort((a, b) => b[1].size - a[1].size)
93
+ .map(([column, files]) => ({
94
+ column,
95
+ count: files.size,
96
+ files: (files.size > 10 ? [...files].slice(0, 10).concat('...') : [...files]).join(', ')
97
+ }));
98
+ console.table(output);
99
+ });
100
+ }
101
+ main().catch(console.error);
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
37
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
38
+ return new (P || (P = Promise))(function (resolve, reject) {
39
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
40
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
41
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
42
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
43
+ });
44
+ };
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ // import { promises as fs, existsSync as doesFileExist } from 'fs'
47
+ const path = __importStar(require("path"));
48
+ const util = __importStar(require("util"));
49
+ const util_1 = require("./util");
50
+ function listChangedFiles(directory) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ const output = yield (0, util_1.runCommand)('git', ['diff', '--name-only', 'HEAD', '--', directory], {
53
+ cwd: directory
54
+ });
55
+ return output.trimEnd().split('\n');
56
+ });
57
+ }
58
+ function getOldFile(file) {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ const options = {
61
+ cwd: path.dirname(file)
62
+ };
63
+ const gitRoot = (yield (0, util_1.runCommand)('git', ['rev-parse', '--show-toplevel'], options)).trim();
64
+ return yield (0, util_1.runCommand)('git', ['show', 'HEAD:' + path.relative(gitRoot, file)], options);
65
+ });
66
+ }
67
+ function main() {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ const args = util.parseArgs({
70
+ allowPositionals: true
71
+ });
72
+ const files = yield listChangedFiles(args.positionals[0]);
73
+ console.log(files);
74
+ });
75
+ }
76
+ main().catch(console.error);
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- export declare enum ResourceTaxonIdSource {
3
- COL = "col",
4
- GBIF = "gbif"
5
- }
6
2
  export declare enum ResourceProcessorSource {
7
3
  All = "all",
8
4
  Unprocessed = "unprocessed",
@@ -43,18 +43,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  });
44
44
  };
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
- exports.ResourceProcessorSource = exports.ResourceTaxonIdSource = void 0;
46
+ exports.ResourceProcessorSource = void 0;
47
47
  const fs_1 = require("fs");
48
48
  const path = __importStar(require("path"));
49
49
  const child_process_1 = require("child_process");
50
50
  const util = __importStar(require("util"));
51
51
  const index_1 = require("../index");
52
52
  const util_1 = require("./util");
53
- var ResourceTaxonIdSource;
54
- (function (ResourceTaxonIdSource) {
55
- ResourceTaxonIdSource["COL"] = "col";
56
- ResourceTaxonIdSource["GBIF"] = "gbif";
57
- })(ResourceTaxonIdSource || (exports.ResourceTaxonIdSource = ResourceTaxonIdSource = {}));
53
+ const { ResourceTaxonIdSource, formatClassificationPath } = index_1.taxonNames;
54
+ var ResourceTaxonIdApi;
55
+ (function (ResourceTaxonIdApi) {
56
+ ResourceTaxonIdApi["GNVerifier"] = "gnverifier";
57
+ ResourceTaxonIdApi["ChecklistBank"] = "clb";
58
+ })(ResourceTaxonIdApi || (ResourceTaxonIdApi = {}));
58
59
  var ResourceProcessorSource;
59
60
  (function (ResourceProcessorSource) {
60
61
  ResourceProcessorSource["All"] = "all";
@@ -137,6 +138,55 @@ function runGnverifier(names, sources) {
137
138
  proc.stdin.end();
138
139
  });
139
140
  }
141
+ const CLB_SOURCES = {
142
+ gbif: '53147',
143
+ col: '3LXR'
144
+ };
145
+ function getBatchedFetch(limit = 20) {
146
+ const queue = [];
147
+ let count = 0;
148
+ return function (...args) {
149
+ return __awaiter(this, void 0, void 0, function* () {
150
+ if (count >= limit) {
151
+ yield new Promise(resolve => { queue.push(resolve); });
152
+ }
153
+ count++;
154
+ try {
155
+ return yield fetch(...args);
156
+ }
157
+ finally {
158
+ count--;
159
+ if (count < limit) {
160
+ const resolver = queue.shift();
161
+ if (resolver) {
162
+ resolver();
163
+ }
164
+ }
165
+ }
166
+ });
167
+ };
168
+ }
169
+ function runClbNameParser(taxa, sources) {
170
+ const fetchBatched = getBatchedFetch(50);
171
+ const promises = [];
172
+ for (const id in taxa) {
173
+ const { scientificName, scientificNameAuthorship: authorship, taxonRank: rank } = taxa[id];
174
+ const params = new URLSearchParams({
175
+ scientificName: authorship ? scientificName.slice(0, -1 - authorship.length) : scientificName,
176
+ authorship: authorship !== null && authorship !== void 0 ? authorship : '',
177
+ rank: rank !== null && rank !== void 0 ? rank : ''
178
+ });
179
+ const queryString = params.toString();
180
+ for (const source of sources) {
181
+ const url = `https://api.checklistbank.org/dataset/${CLB_SOURCES[source]}/match/nameusage?verbose=true&${queryString}`;
182
+ const promise = fetchBatched(url)
183
+ .then(response => response.text())
184
+ .then(response => [taxa[id].scientificNameID, source, response]);
185
+ promises.push(promise);
186
+ }
187
+ }
188
+ return Promise.all(promises);
189
+ }
140
190
  function listFiles(directory) {
141
191
  return __awaiter(this, void 0, void 0, function* () {
142
192
  const input = yield fs_1.promises.readdir(directory);
@@ -349,6 +399,22 @@ class ResourceProcessor {
349
399
  if (config.updateMappings.length === 0) {
350
400
  return resource;
351
401
  }
402
+ const filteredResults = config.mappingsApi === ResourceTaxonIdApi.ChecklistBank
403
+ ? yield this.getClbNameParserResults(resource, config)
404
+ : yield this.getGnverifierResults(resource, config);
405
+ const { taxonNames: { amendResource, groupNameMatches } } = yield Promise.resolve().then(() => __importStar(require('../index')));
406
+ const groupedNameMatches = groupNameMatches(filteredResults);
407
+ const amendedResource = Object.assign(Object.assign({}, resource), { taxa: Object.assign({}, resource.taxa) });
408
+ for (const key in groupedNameMatches) {
409
+ const source = key;
410
+ const matches = yield this.selectPrefixes(resource, groupedNameMatches[source], source);
411
+ amendResource(amendedResource, source, matches);
412
+ }
413
+ return amendedResource;
414
+ });
415
+ }
416
+ getGnverifierResults(resource, config) {
417
+ return __awaiter(this, void 0, void 0, function* () {
352
418
  const filteredResults = {};
353
419
  const taxonNames = {};
354
420
  const names = new Set();
@@ -385,6 +451,8 @@ class ResourceProcessor {
385
451
  // GBIF species like "Nomada spec"
386
452
  continue;
387
453
  }
454
+ const classificationRanks = match.classificationRanks.split('|');
455
+ const classificationPath = match.classificationPath.split('|').map((name, i) => ({ name, rank: classificationRanks[i] }));
388
456
  for (const loirId of taxonNames[name]) {
389
457
  const taxon = resource.taxa[loirId];
390
458
  if (source === 11 && !GBIF_RANKS.includes(taxon.taxonRank)) {
@@ -410,44 +478,82 @@ class ResourceProcessor {
410
478
  filteredResults[loirId] = [];
411
479
  }
412
480
  filteredResults[loirId].push({
413
- source,
481
+ source: source === 11 ? ResourceTaxonIdSource.GBIF : ResourceTaxonIdSource.COL,
414
482
  id: match.recordId,
415
483
  currentId: match.currentRecordId,
416
- classificationPath: match.classificationPath.split('|')
484
+ classificationPath
417
485
  });
418
486
  }
419
487
  }
420
488
  }
421
- const { taxonNames: { amendResource, groupNameMatches } } = yield Promise.resolve().then(() => __importStar(require('../index')));
422
- const groupedNameMatches = groupNameMatches(filteredResults);
423
- const amendedResource = Object.assign(Object.assign({}, resource), { taxa: Object.assign({}, resource.taxa) });
424
- for (const source in groupedNameMatches) {
425
- const matches = yield this.selectPrefixes(resource, groupedNameMatches, source);
426
- amendResource(amendedResource, source, matches);
489
+ return filteredResults;
490
+ });
491
+ }
492
+ getClbNameParserResults(resource, config) {
493
+ return __awaiter(this, void 0, void 0, function* () {
494
+ const filteredResults = {};
495
+ const result = yield runClbNameParser(resource.taxa, config.updateMappings);
496
+ for (const [loirId, source, results] of result) {
497
+ const { match, type: matchType, usage, alternatives = [] } = JSON.parse(results);
498
+ if (!match) {
499
+ continue;
500
+ }
501
+ filteredResults[loirId] = [];
502
+ const matches = [usage, ...alternatives];
503
+ for (const match of matches) {
504
+ const isSynonym = match.status !== 'accepted' && match.status !== 'provisionally accepted';
505
+ const currentRank = isSynonym ? match.classification[0].rank : match.rank;
506
+ const currentName = isSynonym ? match.classification[0].name : match.name;
507
+ const currentId = isSynonym ? match.classification[0].id : match.id;
508
+ if (matchType === 'higherrank') {
509
+ // Rank mismatch
510
+ continue;
511
+ }
512
+ if (source === ResourceTaxonIdSource.GBIF && currentRank === 'species' && currentName.endsWith(' spec')) {
513
+ // GBIF species like "Nomada spec"
514
+ continue;
515
+ }
516
+ const taxon = resource.taxa[loirId];
517
+ if (source === ResourceTaxonIdSource.GBIF && !GBIF_RANKS.includes(taxon.taxonRank)) {
518
+ // Exclude GBIF matches for ranks that are not in GBIF
519
+ continue;
520
+ }
521
+ else if (source === ResourceTaxonIdSource.GBIF && !isSynonym && currentRank !== taxon.taxonRank) {
522
+ // Exclude matches with rank mismatches (only possible
523
+ // for non-synonyms).
524
+ continue;
525
+ }
526
+ filteredResults[loirId].push({
527
+ source,
528
+ id: match.id,
529
+ currentId: currentId,
530
+ classificationPath: match.classification.map(({ name, rank }) => ({ name, rank })).reverse()
531
+ });
532
+ }
427
533
  }
428
- return amendedResource;
534
+ return filteredResults;
429
535
  });
430
536
  }
431
537
  selectPrefixes(resource, groupedNameMatches, source) {
432
538
  return __awaiter(this, void 0, void 0, function* () {
433
- const prefixes = Object.keys(groupedNameMatches[source]);
539
+ const prefixes = Object.keys(groupedNameMatches);
434
540
  if (prefixes.length === 0) {
435
541
  return {};
436
542
  }
437
543
  else if (prefixes.length === 1) {
438
- return groupedNameMatches[source][prefixes[0]];
544
+ return groupedNameMatches[prefixes[0]];
439
545
  }
440
546
  // Count total mapped taxa
441
547
  const mappedTaxa = {};
442
548
  for (const prefix of prefixes) {
443
- for (const taxon in groupedNameMatches[source][prefix]) {
549
+ for (const taxon in groupedNameMatches[prefix]) {
444
550
  mappedTaxa[taxon] = true;
445
551
  }
446
552
  }
447
- const missedTaxonCount = Object.keys(mappedTaxa).length - Object.keys(groupedNameMatches[source][prefixes[0]]).length;
553
+ const missedTaxonCount = Object.keys(mappedTaxa).length - Object.keys(groupedNameMatches[prefixes[0]]).length;
448
554
  if (missedTaxonCount === 0) {
449
555
  // Multiple prefixes but the first one maps all taxa (not counting that are unmapped in all prefixes)
450
- return groupedNameMatches[source][prefixes[0]];
556
+ return groupedNameMatches[prefixes[0]];
451
557
  }
452
558
  console.error(`${resource.workId}: source ${source} results in multiple prefixes`);
453
559
  let choice;
@@ -455,21 +561,17 @@ class ResourceProcessor {
455
561
  console.error(` Most common prefix misses ${missedTaxonCount} taxa: automatically selecting most common prefix...`);
456
562
  choice = '1';
457
563
  }
458
- else if (source === '1') {
459
- console.error(` Catalogue of Life: automatically selecting most common prefix...`);
460
- choice = '1';
461
- }
462
564
  else {
463
565
  for (let i = 0; i < prefixes.length; i++) {
464
566
  const prefix = prefixes[i];
465
- const taxa = groupedNameMatches[source][prefix];
567
+ const taxa = groupedNameMatches[prefix];
466
568
  const taxonIds = Object.keys(taxa);
467
569
  console.error(` [${i + 1}] ${prefix} (${taxonIds.length} taxa)`);
468
570
  for (let j = 0; j < Math.min(9, taxonIds.length); j++) {
469
571
  const taxonId = taxonIds[j];
470
572
  const taxon = resource.taxa[taxonId];
471
573
  const match = taxa[taxonId];
472
- console.error(` taxon: ${taxonId} "${taxon.scientificName}" - ${match.classificationPath.join('|')}`);
574
+ console.error(` taxon: ${taxonId} "${taxon.scientificName}" - ${formatClassificationPath(match.classificationPath)}`);
473
575
  }
474
576
  if (taxonIds.length > 9) {
475
577
  console.error(` ...`);
@@ -486,7 +588,7 @@ class ResourceProcessor {
486
588
  const matches = {};
487
589
  for (const i of choice.split(',')) {
488
590
  const prefix = prefixes[parseInt(i) - 1];
489
- const taxa = groupedNameMatches[source][prefix];
591
+ const taxa = groupedNameMatches[prefix];
490
592
  for (const id in taxa) {
491
593
  if (id in matches) {
492
594
  continue;
@@ -540,6 +642,10 @@ function main() {
540
642
  type: 'string',
541
643
  short: 'u',
542
644
  multiple: true
645
+ },
646
+ 'mapping-api': {
647
+ type: 'string',
648
+ default: 'gnverifier'
543
649
  }
544
650
  },
545
651
  allowPositionals: true
@@ -563,6 +669,9 @@ function main() {
563
669
  }
564
670
  updateMappings = args.values['update-mappings'];
565
671
  }
672
+ if (!Object.values(ResourceTaxonIdApi).includes(args.values['mapping-api'])) {
673
+ throw new Error(`Unknown argument "--mapping-api ${args.values['mapping-api']}"`);
674
+ }
566
675
  const processor = new ResourceProcessor(args.positionals[0]);
567
676
  process.on('exit', () => {
568
677
  process.stdout.write('\n');
@@ -570,7 +679,8 @@ function main() {
570
679
  const source = args.values.source;
571
680
  const config = {
572
681
  update: source === ResourceProcessorSource.All || source === ResourceProcessorSource.Modified,
573
- updateMappings
682
+ updateMappings,
683
+ mappingsApi: args.values['mapping-api']
574
684
  };
575
685
  processor.run(source, config).catch(error => {
576
686
  console.error(error);
@@ -104,15 +104,15 @@ class GbifTaxaValidator extends TaxaValidator {
104
104
  });
105
105
  }
106
106
  }
107
- class ColTaxaValidator extends TaxaValidator {
108
- constructor(taxa) {
109
- super(taxa, {
110
- id: 'col',
111
- children: 'children_col',
112
- ancestors: 'ancestors_col'
113
- });
114
- }
115
- }
107
+ // class ColTaxaValidator extends TaxaValidator {
108
+ // constructor (taxa: catalog.Entities) {
109
+ // super(taxa, {
110
+ // id: 'col',
111
+ // children: 'children_col',
112
+ // ancestors: 'ancestors_col'
113
+ // })
114
+ // }
115
+ // }
116
116
  function validateFile(arg) {
117
117
  return __awaiter(this, void 0, void 0, function* () {
118
118
  const filePath = path.resolve(arg);
@@ -1,2 +1,7 @@
1
+ export declare enum ResourceTaxonIdSource {
2
+ COL = "col",
3
+ GBIF = "gbif"
4
+ }
5
+ export declare function formatClassificationPath(path: TaxonMatchClassificationPath): string;
1
6
  export declare function groupNameMatches(results: Record<TaxonId, TaxonMatch[]>): GroupedNameMatches;
2
- export declare function amendResource(resource: AmendedResource, source: string, matches: Record<TaxonId, TaxonMatch>): void;
7
+ export declare function amendResource(resource: AmendedResource, source: ResourceTaxonIdSource, matches: Record<TaxonId, TaxonMatch>): void;
@@ -1,38 +1,108 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ResourceTaxonIdSource = void 0;
4
+ exports.formatClassificationPath = formatClassificationPath;
3
5
  exports.groupNameMatches = groupNameMatches;
4
6
  exports.amendResource = amendResource;
5
- const MINIMUM_PREFIX_LENGTH = 3;
6
- const VALID_COMMON_PREFIXES = new Set([
7
- 'Plantae|Tracheophyta',
8
- 'Fungi',
9
- 'Fungi|Ascomycota',
10
- 'Fungi|Basidiomycota',
11
- 'Fungi|Zygomycota'
12
- ]);
7
+ const RANKS = [
8
+ 'superdomain',
9
+ 'domain',
10
+ 'kingdom',
11
+ 'subkingdom',
12
+ 'phylum',
13
+ 'subphylum',
14
+ 'infraphylum',
15
+ 'parvphylum',
16
+ 'gigaclass',
17
+ 'megaclass',
18
+ 'superclass',
19
+ 'class',
20
+ 'subclass',
21
+ 'infraclass',
22
+ 'subterclass',
23
+ 'superorder',
24
+ 'order',
25
+ 'suborder',
26
+ 'infraorder',
27
+ 'parvorder',
28
+ 'section zoology',
29
+ 'subsection zoology',
30
+ 'series zoology',
31
+ 'superfamily',
32
+ 'epifamily',
33
+ 'family',
34
+ 'subfamily',
35
+ 'tribe',
36
+ 'subtribe',
37
+ 'genus',
38
+ 'subgenus',
39
+ 'section',
40
+ 'group',
41
+ 'aggregate',
42
+ 'complex',
43
+ 'species',
44
+ 'subspecies',
45
+ 'variety',
46
+ 'form'
47
+ ];
48
+ const MINIMUM_PREFIX_RANK = 'class';
49
+ const VALID_COMMON_PREFIXES = {
50
+ col: new Set([
51
+ 'Eukaryota|Animalia|Mollusca',
52
+ 'Eukaryota|Plantae|Bryobiotina|Bryophyta',
53
+ 'Eukaryota|Plantae|Pteridobiotina|Tracheophyta',
54
+ 'Eukaryota|Fungi',
55
+ 'Eukaryota|Fungi|Ascomycota',
56
+ 'Eukaryota|Fungi|Basidiomycota',
57
+ 'Eukaryota|Fungi|Zygomycota'
58
+ ]),
59
+ gbif: new Set([
60
+ 'Plantae|Tracheophyta',
61
+ 'Fungi',
62
+ 'Fungi|Ascomycota',
63
+ 'Fungi|Basidiomycota',
64
+ 'Fungi|Zygomycota'
65
+ ])
66
+ };
67
+ function compareRanks(a, b) {
68
+ return RANKS.indexOf(a) - RANKS.indexOf(b);
69
+ }
13
70
  function getCommonPrefix(a, b) {
14
71
  for (let i = 0; i < Math.max(a.length, b.length); i++) {
15
- if (a[i] !== b[i]) {
72
+ if (a[i] && b[i] && a[i].name !== b[i].name) {
16
73
  return a.slice(0, i);
17
74
  }
18
75
  }
19
76
  return a.slice();
20
77
  }
21
- function isValidPrefix(a, b) {
78
+ function isValidPrefix(a, b, source) {
22
79
  const prefix = getCommonPrefix(a, b);
23
- return VALID_COMMON_PREFIXES.has(prefix.join('|')) || prefix.length >= MINIMUM_PREFIX_LENGTH;
80
+ if (prefix.length === 0) {
81
+ return false;
82
+ }
83
+ if (VALID_COMMON_PREFIXES[source].has(formatClassificationPath(prefix))) {
84
+ return true;
85
+ }
86
+ return compareRanks(prefix[prefix.length - 1].rank, MINIMUM_PREFIX_RANK) >= 0;
87
+ }
88
+ var ResourceTaxonIdSource;
89
+ (function (ResourceTaxonIdSource) {
90
+ ResourceTaxonIdSource["COL"] = "col";
91
+ ResourceTaxonIdSource["GBIF"] = "gbif";
92
+ })(ResourceTaxonIdSource || (exports.ResourceTaxonIdSource = ResourceTaxonIdSource = {}));
93
+ function formatClassificationPath(path) {
94
+ return path.map(part => part.name).join('|');
24
95
  }
25
96
  function groupNameMatches(results) {
97
+ var _a;
26
98
  const prefixes = {};
27
99
  for (const scientificNameID in results) {
28
100
  for (const result of results[scientificNameID]) {
29
- if (!prefixes[result.source]) {
30
- prefixes[result.source] = [];
31
- }
32
- let prefix = prefixes[result.source].find(prefix => isValidPrefix(prefix[0], result.classificationPath));
101
+ const matchGroups = (_a = prefixes[result.source]) !== null && _a !== void 0 ? _a : (prefixes[result.source] = []);
102
+ let prefix = matchGroups.find(prefix => isValidPrefix(prefix[0], result.classificationPath, result.source));
33
103
  if (!prefix) {
34
104
  prefix = [result.classificationPath, {}];
35
- prefixes[result.source].push(prefix);
105
+ matchGroups.push(prefix);
36
106
  }
37
107
  else {
38
108
  prefix[0] = getCommonPrefix(prefix[0], result.classificationPath);
@@ -44,11 +114,11 @@ function groupNameMatches(results) {
44
114
  }
45
115
  }
46
116
  const groupedNameMatches = {};
47
- for (const source in prefixes) {
48
- groupedNameMatches[source] = prefixes[source]
117
+ for (const [source, matchGroups] of Object.entries(prefixes)) {
118
+ groupedNameMatches[source] = matchGroups
49
119
  .sort((a, b) => Object.keys(b[1]).length - Object.keys(a[1]).length)
50
120
  .reduce((map, [prefix, taxa]) => {
51
- map[prefix.join('|')] = taxa;
121
+ map[formatClassificationPath(prefix)] = taxa;
52
122
  return map;
53
123
  }, {});
54
124
  }
@@ -57,13 +127,13 @@ function groupNameMatches(results) {
57
127
  function amendResource(resource, source, matches) {
58
128
  for (const id in matches) {
59
129
  const match = matches[id];
60
- if (source === '1' || source === '13') {
130
+ if (source === ResourceTaxonIdSource.COL) {
61
131
  resource.taxa[id].colTaxonID = match.id;
62
132
  if (match.currentId) {
63
133
  resource.taxa[id].colAcceptedTaxonID = match.currentId;
64
134
  }
65
135
  }
66
- else if (source === '11') {
136
+ else if (source === ResourceTaxonIdSource.GBIF) {
67
137
  resource.taxa[id].gbifTaxonID = match.id;
68
138
  if (match.currentId) {
69
139
  resource.taxa[id].gbifAcceptedTaxonID = match.currentId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larsgw/formica",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
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",
@@ -12,6 +12,7 @@
12
12
  "loir-validate-resources": "lib/bin/validate-resources-text.js",
13
13
  "loir-resources-process": "lib/bin/process-resources.js",
14
14
  "loir-resources-index": "lib/bin/process-resources-index.js",
15
+ "loir-resources-check-diff": "lib/bin/check-resources-diff.js",
15
16
  "loir-generate-linked-data": "lib/bin/generate-linked-data.js"
16
17
  },
17
18
  "scripts": {