@larsgw/formica 0.6.6 → 0.6.8
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/CHANGELOG.md +27 -0
- package/lib/bin/process-resources-index.js +23 -11
- package/lib/bin/process-resources-problems.js +177 -0
- package/lib/resources/parse-text.js +82 -47
- package/package.json +1 -1
- package/src/bin/process-resources-index.ts +26 -10
- package/src/bin/process-resources.ts +0 -1
- package/src/module.d.ts +3 -0
- package/src/resources/parse-text.ts +91 -52
- package/test/resources.js +39 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## [0.6.8](https://github.com/identification-resources/formica/compare/v0.6.7...v0.6.8) (2025-03-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **resources:** add support for intergeneric hybrids without parents ([3cb0c0c](https://github.com/identification-resources/formica/commit/3cb0c0cd60fc5a4ba88add623fbadc3b90d8530c))
|
|
7
|
+
* **resources:** create index of CoL identifiers ([3792903](https://github.com/identification-resources/formica/commit/37929035374bc0c67ed2403e775e06a904bdbd4d))
|
|
8
|
+
* **resources:** support intergeneric hybrids ([0a6696c](https://github.com/identification-resources/formica/commit/0a6696c5fd0603a0366c80c2298b6544636ed0c6))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## [0.6.7](https://github.com/identification-resources/formica/compare/v0.6.6...v0.6.7) (2025-02-26)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **resources:** keep uncorrected name in verbatimIdentification ([dc4d5d1](https://github.com/identification-resources/formica/commit/dc4d5d17e7a412a94538f6162dfd839d808cbc30))
|
|
18
|
+
* **resources:** update heuristics for recognizing author citations ([bca6a7f](https://github.com/identification-resources/formica/commit/bca6a7f5a6c2dc3dc421d885f6270f5464fc5ea5))
|
|
19
|
+
* **resources:** use corrected generic name in output ([b472384](https://github.com/identification-resources/formica/commit/b4723842e87515f076230c603246082bc3bbec9d))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **resources:** add support for completeness flags ([55b5427](https://github.com/identification-resources/formica/commit/55b5427125b4f1072191cadbc461ba31d37e6f2e))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
1
28
|
## [0.6.6](https://github.com/identification-resources/formica/compare/v0.6.5...v0.6.6) (2025-02-16)
|
|
2
29
|
|
|
3
30
|
|
|
@@ -52,26 +52,27 @@ var fs_1 = require("fs");
|
|
|
52
52
|
var path = require("path");
|
|
53
53
|
var index_1 = require("../index");
|
|
54
54
|
var util_1 = require("./util");
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
function alphabeticSort(a, b) {
|
|
56
|
+
return a > b ? 1 : a < b ? -1 : 0;
|
|
57
|
+
}
|
|
58
|
+
function sortObject(object, sorter) {
|
|
57
59
|
var sorted = {};
|
|
58
|
-
for (var _i = 0, _a = Object.keys(object).sort(util_1.numericSort); _i < _a.length; _i++) {
|
|
60
|
+
for (var _i = 0, _a = Object.keys(object).sort(sorter !== null && sorter !== void 0 ? sorter : util_1.numericSort); _i < _a.length; _i++) {
|
|
59
61
|
var key = _a[_i];
|
|
60
62
|
sorted[key] = object[key];
|
|
61
63
|
}
|
|
62
64
|
return sorted;
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
gbifIndex[gbifId] = [];
|
|
66
|
+
function addTaxon(index, id, taxon) {
|
|
67
|
+
if (!(id in index)) {
|
|
68
|
+
index[id] = [];
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
index[id].push(taxon[0]);
|
|
71
|
+
index[id].sort(util_1.numericSort);
|
|
71
72
|
}
|
|
72
73
|
function main(args) {
|
|
73
74
|
return __awaiter(this, void 0, void 0, function () {
|
|
74
|
-
var REPO_ROOT, files, gbifIndex, resourceIndex;
|
|
75
|
+
var REPO_ROOT, files, gbifIndex, colIndex, resourceIndex;
|
|
75
76
|
return __generator(this, function (_a) {
|
|
76
77
|
switch (_a.label) {
|
|
77
78
|
case 0:
|
|
@@ -80,6 +81,7 @@ function main(args) {
|
|
|
80
81
|
case 1:
|
|
81
82
|
files = _a.sent();
|
|
82
83
|
gbifIndex = {};
|
|
84
|
+
colIndex = {};
|
|
83
85
|
resourceIndex = {};
|
|
84
86
|
return [4 /*yield*/, Promise.all(files.map(function (fileName) {
|
|
85
87
|
return __awaiter(this, void 0, void 0, function () {
|
|
@@ -96,7 +98,7 @@ function main(args) {
|
|
|
96
98
|
file = _a.sent();
|
|
97
99
|
return [2 /*return*/, Promise.all(index_1.resources.parseTextFileHeader(file).map(function (resource, index) {
|
|
98
100
|
return __awaiter(this, void 0, void 0, function () {
|
|
99
|
-
var amendedResource, dwcFile, _a, header, dwc, _b, _c, gbifColumn, gbifAcceptedColumn, _i, dwc_1, taxon, gbifId;
|
|
101
|
+
var amendedResource, dwcFile, _a, header, dwc, _b, _c, gbifColumn, gbifAcceptedColumn, colColumn, colAcceptedColumn, _i, dwc_1, taxon, gbifId, colId;
|
|
100
102
|
return __generator(this, function (_d) {
|
|
101
103
|
switch (_d.label) {
|
|
102
104
|
case 0:
|
|
@@ -111,6 +113,8 @@ function main(args) {
|
|
|
111
113
|
_a = _c.apply(_b, [_d.sent()]), header = _a[0], dwc = _a.slice(1);
|
|
112
114
|
gbifColumn = header.indexOf('gbifTaxonID');
|
|
113
115
|
gbifAcceptedColumn = header.indexOf('gbifAcceptedTaxonID');
|
|
116
|
+
colColumn = header.indexOf('colTaxonID');
|
|
117
|
+
colAcceptedColumn = header.indexOf('colAcceptedTaxonID');
|
|
114
118
|
for (_i = 0, dwc_1 = dwc; _i < dwc_1.length; _i++) {
|
|
115
119
|
taxon = dwc_1[_i];
|
|
116
120
|
gbifId = taxon[gbifColumn];
|
|
@@ -120,6 +124,13 @@ function main(args) {
|
|
|
120
124
|
addTaxon(gbifIndex, taxon[gbifAcceptedColumn], taxon);
|
|
121
125
|
}
|
|
122
126
|
}
|
|
127
|
+
colId = taxon[colColumn];
|
|
128
|
+
if (colId) {
|
|
129
|
+
addTaxon(colIndex, colId, taxon);
|
|
130
|
+
if (taxon[colAcceptedColumn] !== taxon[colColumn]) {
|
|
131
|
+
addTaxon(colIndex, taxon[colAcceptedColumn], taxon);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
123
134
|
amendedResource.taxonCount += 1;
|
|
124
135
|
}
|
|
125
136
|
resourceIndex[amendedResource.id] = amendedResource;
|
|
@@ -136,6 +147,7 @@ function main(args) {
|
|
|
136
147
|
_a.sent();
|
|
137
148
|
return [4 /*yield*/, Promise.all([
|
|
138
149
|
fs_1.promises.writeFile(path.join(REPO_ROOT, 'gbif.index.json'), JSON.stringify(sortObject(gbifIndex), null, 2)),
|
|
150
|
+
fs_1.promises.writeFile(path.join(REPO_ROOT, 'col.index.json'), JSON.stringify(sortObject(colIndex, alphabeticSort), null, 2)),
|
|
139
151
|
fs_1.promises.writeFile(path.join(REPO_ROOT, 'index.json'), JSON.stringify(sortObject(resourceIndex), null, 2))
|
|
140
152
|
])];
|
|
141
153
|
case 3:
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __assign = (this && this.__assign) || function () {
|
|
4
|
+
__assign = Object.assign || function(t) {
|
|
5
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
6
|
+
s = arguments[i];
|
|
7
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
8
|
+
t[p] = s[p];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
return __assign.apply(this, arguments);
|
|
13
|
+
};
|
|
14
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
15
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
16
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
17
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
18
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
19
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
20
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
24
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
25
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
26
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
27
|
+
function step(op) {
|
|
28
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
29
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
30
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
31
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
32
|
+
switch (op[0]) {
|
|
33
|
+
case 0: case 1: t = op; break;
|
|
34
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
35
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
36
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
37
|
+
default:
|
|
38
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
39
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
40
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
41
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
42
|
+
if (t[2]) _.ops.pop();
|
|
43
|
+
_.trys.pop(); continue;
|
|
44
|
+
}
|
|
45
|
+
op = body.call(thisArg, _);
|
|
46
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
47
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
var fs_1 = require("fs");
|
|
52
|
+
var path = require("path");
|
|
53
|
+
var util = require("util");
|
|
54
|
+
var index_1 = require("../index");
|
|
55
|
+
var GBIF_RANKS = [
|
|
56
|
+
'kingdom',
|
|
57
|
+
'phyllum',
|
|
58
|
+
'class',
|
|
59
|
+
'order',
|
|
60
|
+
'family',
|
|
61
|
+
'genus',
|
|
62
|
+
'species',
|
|
63
|
+
'subspecies',
|
|
64
|
+
'variety'
|
|
65
|
+
];
|
|
66
|
+
var DISPLAY_FIELDS = [
|
|
67
|
+
'scientificNameID',
|
|
68
|
+
'taxonRank',
|
|
69
|
+
'scientificName',
|
|
70
|
+
'taxonomicStatus',
|
|
71
|
+
'taxonRemarks',
|
|
72
|
+
'colTaxonID',
|
|
73
|
+
'gbifTaxonID'
|
|
74
|
+
];
|
|
75
|
+
var ResourceProblemsProcessor = /** @class */ (function () {
|
|
76
|
+
function ResourceProblemsProcessor(collectionPath) {
|
|
77
|
+
this.DIR_ROOT = path.resolve(collectionPath);
|
|
78
|
+
this.DIR_DWC = path.join(this.DIR_ROOT, 'dwc');
|
|
79
|
+
this.FILE_INDEX = path.join(this.DIR_ROOT, 'index.json');
|
|
80
|
+
this.FILE_PROBLEMS = path.join(this.DIR_ROOT, 'problems.csv');
|
|
81
|
+
}
|
|
82
|
+
ResourceProblemsProcessor.prototype.run = function () {
|
|
83
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
84
|
+
var problems, _a, _b, resources, _c, _d, _i, problems_1, problem, _work, resourceId, _problemText, resource, missing;
|
|
85
|
+
return __generator(this, function (_e) {
|
|
86
|
+
switch (_e.label) {
|
|
87
|
+
case 0:
|
|
88
|
+
_b = (_a = index_1.csv).parseCsv;
|
|
89
|
+
return [4 /*yield*/, fs_1.promises.readFile(this.FILE_PROBLEMS, 'utf8')];
|
|
90
|
+
case 1:
|
|
91
|
+
problems = _b.apply(_a, [_e.sent()]).slice(1);
|
|
92
|
+
_d = (_c = JSON).parse;
|
|
93
|
+
return [4 /*yield*/, fs_1.promises.readFile(this.FILE_INDEX, 'utf8')];
|
|
94
|
+
case 2:
|
|
95
|
+
resources = _d.apply(_c, [_e.sent()]);
|
|
96
|
+
_i = 0, problems_1 = problems;
|
|
97
|
+
_e.label = 3;
|
|
98
|
+
case 3:
|
|
99
|
+
if (!(_i < problems_1.length)) return [3 /*break*/, 6];
|
|
100
|
+
problem = problems_1[_i];
|
|
101
|
+
_work = problem[0], resourceId = problem[1], _problemText = problem[2];
|
|
102
|
+
return [4 /*yield*/, this.readAmendedResource(resourceId, resources)];
|
|
103
|
+
case 4:
|
|
104
|
+
resource = _e.sent();
|
|
105
|
+
missing = this.getMissingMappings(resource);
|
|
106
|
+
if (missing.length === 0) {
|
|
107
|
+
this.removeProblem(resourceId);
|
|
108
|
+
}
|
|
109
|
+
_e.label = 5;
|
|
110
|
+
case 5:
|
|
111
|
+
_i++;
|
|
112
|
+
return [3 /*break*/, 3];
|
|
113
|
+
case 6: return [2 /*return*/];
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
ResourceProblemsProcessor.prototype.readAmendedResource = function (resourceId, resources) {
|
|
119
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
120
|
+
var resource, resourceFileId, resourceFile, _a, header, taxonRows, _b, _c, taxa, _i, taxonRows_1, row, taxon;
|
|
121
|
+
return __generator(this, function (_d) {
|
|
122
|
+
switch (_d.label) {
|
|
123
|
+
case 0:
|
|
124
|
+
resource = resources[resourceId];
|
|
125
|
+
resourceFileId = resourceId.split(':').join('-');
|
|
126
|
+
resourceFile = path.join(this.DIR_DWC, "".concat(resourceFileId, ".csv"));
|
|
127
|
+
_c = (_b = index_1.csv).parseCsv;
|
|
128
|
+
return [4 /*yield*/, fs_1.promises.readFile(resourceFile, 'utf8')];
|
|
129
|
+
case 1:
|
|
130
|
+
_a = _c.apply(_b, [_d.sent()]), header = _a[0], taxonRows = _a.slice(1);
|
|
131
|
+
taxa = {};
|
|
132
|
+
for (_i = 0, taxonRows_1 = taxonRows; _i < taxonRows_1.length; _i++) {
|
|
133
|
+
row = taxonRows_1[_i];
|
|
134
|
+
taxon = row.reduce(function (taxon, value, index) {
|
|
135
|
+
taxon[header[index]] = value;
|
|
136
|
+
return taxon;
|
|
137
|
+
}, {});
|
|
138
|
+
taxa[taxon.scientificNameID] = taxon;
|
|
139
|
+
}
|
|
140
|
+
return [2 /*return*/, __assign(__assign({}, resource), { taxa: taxa })];
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
ResourceProblemsProcessor.prototype.getMissingMappings = function (resource) {
|
|
146
|
+
var missing = [];
|
|
147
|
+
for (var id in resource.taxa) {
|
|
148
|
+
var taxon = resource.taxa[id];
|
|
149
|
+
if (taxon.taxonomicStatus !== 'accepted') {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
var missingCol = false; // !taxon.colTaxonID
|
|
153
|
+
var missingGbif = GBIF_RANKS.includes(taxon.taxonRank) && !taxon.gbifTaxonID;
|
|
154
|
+
if (missingCol || missingGbif) {
|
|
155
|
+
missing.push(taxon);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return missing;
|
|
159
|
+
};
|
|
160
|
+
ResourceProblemsProcessor.prototype.removeProblem = function (resourceId) {
|
|
161
|
+
resourceId;
|
|
162
|
+
//
|
|
163
|
+
};
|
|
164
|
+
return ResourceProblemsProcessor;
|
|
165
|
+
}());
|
|
166
|
+
function main() {
|
|
167
|
+
var args = util.parseArgs({ allowPositionals: true });
|
|
168
|
+
var processor = new ResourceProblemsProcessor(args.positionals[0]);
|
|
169
|
+
process.on('exit', function () {
|
|
170
|
+
process.stdout.write('\n');
|
|
171
|
+
});
|
|
172
|
+
processor.run().catch(function (error) {
|
|
173
|
+
console.error(error);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
main();
|
|
@@ -64,6 +64,11 @@ var DWC_RANKS = [
|
|
|
64
64
|
'genus',
|
|
65
65
|
'subgenus'
|
|
66
66
|
];
|
|
67
|
+
var FLAGS = [
|
|
68
|
+
'MISSING_TAXA',
|
|
69
|
+
'MISSING_PARENT_TAXA',
|
|
70
|
+
'MISSING_SYNONYMS'
|
|
71
|
+
];
|
|
67
72
|
var TAXONOMIC_STATUS = {
|
|
68
73
|
'>': 'incorrect',
|
|
69
74
|
'+': 'heterotypic synonym',
|
|
@@ -93,6 +98,7 @@ var RANK_LABELS_REVERSE = {
|
|
|
93
98
|
'ssp': 'subspecies',
|
|
94
99
|
'subsp': 'subspecies'
|
|
95
100
|
};
|
|
101
|
+
var HYBRID_SIGN = '\u00D7';
|
|
96
102
|
/**
|
|
97
103
|
* 1. Any number of
|
|
98
104
|
* - capitalized words
|
|
@@ -103,7 +109,7 @@ var RANK_LABELS_REVERSE = {
|
|
|
103
109
|
* 2. Followed by a capitalized word
|
|
104
110
|
* 3. Optionally, followed by "et al."
|
|
105
111
|
*/
|
|
106
|
-
var LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van'].join('|');
|
|
112
|
+
var LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van', 'von'].join('|');
|
|
107
113
|
var SIMPLE_AUTHOR_PATTERN = '(?:(?:\\p{Lu}\\S*|&|in|ex|' + LOWERCASE_NAME_PARTICLES + ')\\s*)*\\p{Lu}\\S+(?:\\s+et\\s+al\\.)?';
|
|
108
114
|
var NAME_PATTERN = new RegExp('^' +
|
|
109
115
|
// $1 main name part
|
|
@@ -111,7 +117,7 @@ var NAME_PATTERN = new RegExp('^' +
|
|
|
111
117
|
// $2 optional author citation
|
|
112
118
|
'(?: ' +
|
|
113
119
|
// but not auct(t)., etc.
|
|
114
|
-
'(?!auctt?\\.|(?:syn|comb|sp|spec)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.))' +
|
|
120
|
+
'(?!auctt?\\.|(?:syn|comb|sp|spec|nom)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.)|in part|partim)' +
|
|
115
121
|
'(' +
|
|
116
122
|
// $2.1 anything in parentheses, followed by optional revising author(s)
|
|
117
123
|
'\\(.+?\\)(?:\\s+' + SIMPLE_AUTHOR_PATTERN + ')?' +
|
|
@@ -128,22 +134,30 @@ var NAME_PATTERN = new RegExp('^' +
|
|
|
128
134
|
/**
|
|
129
135
|
* Structure
|
|
130
136
|
* $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
|
|
131
|
-
* $1.1 genus: ([A-Z]\S+)
|
|
137
|
+
* $1.1 genus: ((?:x )?[A-Z]\S+)
|
|
132
138
|
* $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
|
|
133
|
-
* $2 species: (
|
|
139
|
+
* $2 species: (x [a-z]+|[a-z][^\s.]+(?: x [a-z]+)?|[A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+)
|
|
140
|
+
* $2a: x [a-z]+
|
|
141
|
+
* $2b hybrid: [a-z][^\s.]+(?: x [a-z]+)?
|
|
142
|
+
* $2c intergeneric hybrid: [A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+
|
|
134
143
|
*/
|
|
135
|
-
var BINAME_PATTERN = /^(?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?(
|
|
144
|
+
var BINAME_PATTERN = /^(?:((?:x )?[A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?(x [a-z]+|[a-z][^\s.]+(?: x [a-z]+)?|[A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+)(?= |$)/;
|
|
136
145
|
function compareRanks(a, b) {
|
|
137
146
|
return RANKS.indexOf(a) - RANKS.indexOf(b);
|
|
138
147
|
}
|
|
139
148
|
function capitalize(name) {
|
|
140
149
|
return name[0].toUpperCase() + name.slice(1).toLowerCase();
|
|
141
150
|
}
|
|
151
|
+
function capitalizeGenericName(name) {
|
|
152
|
+
if (name[0] === HYBRID_SIGN) {
|
|
153
|
+
return HYBRID_SIGN + capitalize(name.slice(1));
|
|
154
|
+
}
|
|
155
|
+
return capitalize(name);
|
|
156
|
+
}
|
|
142
157
|
function isUpperCase(name) {
|
|
143
158
|
return name === name.toUpperCase();
|
|
144
159
|
}
|
|
145
160
|
function getSynonymRank(name, rank) {
|
|
146
|
-
var BINAME_PATTERN = /^([A-Z]\S+ (\([A-Z]\S+\) )?)?(x )?[a-z0-9-]+(?= |$)/;
|
|
147
161
|
var rest = name.replace(BINAME_PATTERN, '');
|
|
148
162
|
var rankPrefix = rest.match(/^(?: |^)(st|r|ab|f|var|ssp|subsp)\. /);
|
|
149
163
|
if (rankPrefix) {
|
|
@@ -183,7 +197,7 @@ function parseName(name, rank, parent) {
|
|
|
183
197
|
name = name.replace(/^\[(_|\d+)\] /, '');
|
|
184
198
|
}
|
|
185
199
|
// Set verbatim identification after subsequent syntax is removed.
|
|
186
|
-
item.verbatimIdentification = name;
|
|
200
|
+
item.verbatimIdentification = name.replace(/(?<=^| )x(?=$| )/g, HYBRID_SIGN).replace(/_/g, ' ');
|
|
187
201
|
// Parent context is used for parsing and formatting binomial names.
|
|
188
202
|
// For formatting, it needs to match external databases (i.e. be correct).
|
|
189
203
|
// For parsing, it needs to match the current file. If relevant parents
|
|
@@ -205,7 +219,8 @@ function parseName(name, rank, parent) {
|
|
|
205
219
|
if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
|
|
206
220
|
var _a = name.match(BINAME_PATTERN) || [], genus = _a[1], subgenus = _a[2], species = _a[3];
|
|
207
221
|
if (genus) {
|
|
208
|
-
parentContext.
|
|
222
|
+
parentContext.incorrect.genus = genus;
|
|
223
|
+
parentContext.genus = capitalizeGenericName(genus.replace(/(^| )x /, HYBRID_SIGN));
|
|
209
224
|
}
|
|
210
225
|
if (subgenus) {
|
|
211
226
|
parentContext.subgenus = capitalize(subgenus);
|
|
@@ -215,8 +230,9 @@ function parseName(name, rank, parent) {
|
|
|
215
230
|
// from the parent context.
|
|
216
231
|
delete parentContext.subgenus;
|
|
217
232
|
}
|
|
218
|
-
if (species) {
|
|
219
|
-
parentContext.
|
|
233
|
+
if (species && compareRanks('species', rank) < 0) {
|
|
234
|
+
parentContext.incorrect.specificEpithet = species;
|
|
235
|
+
parentContext.specificEpithet = species.replace(/(^| )x /, HYBRID_SIGN);
|
|
220
236
|
}
|
|
221
237
|
}
|
|
222
238
|
// In taxa of group, species or lower, the name should just contain the
|
|
@@ -242,8 +258,11 @@ function parseName(name, rank, parent) {
|
|
|
242
258
|
}
|
|
243
259
|
}
|
|
244
260
|
// Hybrids
|
|
261
|
+
if (rank === 'genus' && name.startsWith('x ')) {
|
|
262
|
+
name = HYBRID_SIGN + name.slice(2);
|
|
263
|
+
}
|
|
245
264
|
if (rank === 'species' && /(^| )x /.test(name)) {
|
|
246
|
-
name = name.replace(/(^| )x /,
|
|
265
|
+
name = name.replace(/(^| )x /, HYBRID_SIGN);
|
|
247
266
|
}
|
|
248
267
|
// Divide the name into the main scientific name (only the epithet for taxa
|
|
249
268
|
// lower than genus), the authorship information, and optionally remarks
|
|
@@ -252,7 +271,9 @@ function parseName(name, rank, parent) {
|
|
|
252
271
|
throw new Error("Taxon \"".concat(name, "\" could not be parsed"));
|
|
253
272
|
}
|
|
254
273
|
// To encode old names with spaces (e.g. "Orsillus pini canariensis Lindberg, 1953")
|
|
255
|
-
// underscores are used, which are replaced here.
|
|
274
|
+
// underscores are used, which are replaced here. This is also used for undescribed
|
|
275
|
+
// species (e.g. "Leiobunum species A") and intergeneric hybrids (e.g. "×Festulpia
|
|
276
|
+
// Festuca rubra × Vulpia bromoides")
|
|
256
277
|
if (nameParts[1].includes('_')) {
|
|
257
278
|
nameParts[1] = nameParts[1].replace(/_/g, ' ');
|
|
258
279
|
}
|
|
@@ -264,9 +285,15 @@ function parseName(name, rank, parent) {
|
|
|
264
285
|
throw new Error("Taxon name contains unexpected characters: \"".concat(taxon, "\""));
|
|
265
286
|
}
|
|
266
287
|
// Validate names and recompose binomial and trinomial names
|
|
267
|
-
if (
|
|
288
|
+
if (rank === 'genus') {
|
|
289
|
+
item.scientificName = capitalizeGenericName(taxon);
|
|
290
|
+
if (taxon[0].toUpperCase() !== taxon[0] || (taxon[0] === HYBRID_SIGN && taxon[1].toUpperCase() !== taxon[1])) {
|
|
291
|
+
throw new Error("Generic epithet should be capitalized: \"".concat(taxon, "\""));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else if (compareRanks('group', rank) > 0) {
|
|
268
295
|
item.scientificName = capitalize(taxon);
|
|
269
|
-
if (
|
|
296
|
+
if (taxon[0].toUpperCase() !== taxon[0]) {
|
|
270
297
|
throw new Error("Taxon name (".concat(rank, ") should be capitalized: \"").concat(taxon, "\""));
|
|
271
298
|
}
|
|
272
299
|
}
|
|
@@ -293,12 +320,12 @@ function parseName(name, rank, parent) {
|
|
|
293
320
|
else if (rank === 'species') {
|
|
294
321
|
item.genericName = parentContext.genus;
|
|
295
322
|
item.infragenericEpithet = parentContext.subgenus;
|
|
296
|
-
|
|
297
|
-
item.scientificName = "".concat(item.genericName, " ").concat(item.specificEpithet);
|
|
298
|
-
if (item.specificEpithet !== taxon) {
|
|
323
|
+
if (taxon.toLowerCase() !== taxon && !/^[A-Z][a-z]+ [a-z]+\xD7[A-Z][a-z]+ [a-z]+$/.test(taxon)) {
|
|
299
324
|
console.log(item, taxon);
|
|
300
325
|
throw new Error("Specific epithet should be lowercase: \"".concat(taxon, "\""));
|
|
301
326
|
}
|
|
327
|
+
item.specificEpithet = taxon;
|
|
328
|
+
item.scientificName = "".concat(item.genericName, " ").concat(item.specificEpithet);
|
|
302
329
|
}
|
|
303
330
|
else if (compareRanks('species', rank) < 0) {
|
|
304
331
|
item.genericName = parentContext.genus;
|
|
@@ -326,17 +353,6 @@ function parseName(name, rank, parent) {
|
|
|
326
353
|
if (item.scientificNameAuthorship) {
|
|
327
354
|
item.scientificName += " ".concat(item.scientificNameAuthorship);
|
|
328
355
|
}
|
|
329
|
-
// Amend "parent" with corrections
|
|
330
|
-
if (item.taxonomicStatus === 'incorrect') {
|
|
331
|
-
var itemAsObject = item;
|
|
332
|
-
var parentAsObject = parent;
|
|
333
|
-
parent.incorrect = __assign({}, parent);
|
|
334
|
-
for (var key in item) {
|
|
335
|
-
if (key !== 'taxonomicStatus') {
|
|
336
|
-
parentAsObject[key] = itemAsObject[key];
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
356
|
return item;
|
|
341
357
|
}
|
|
342
358
|
function parseHeader(header) {
|
|
@@ -401,6 +417,16 @@ function parseHeader(header) {
|
|
|
401
417
|
metadata.catalog[key] = work.get(key);
|
|
402
418
|
}
|
|
403
419
|
}
|
|
420
|
+
if ('flags' in config) {
|
|
421
|
+
if (!Array.isArray(config.flags)) {
|
|
422
|
+
throw new SyntaxError('"flags" should be an array if present');
|
|
423
|
+
}
|
|
424
|
+
var invalidFlags = config.flags.filter(function (flag) { return !FLAGS.includes(flag); });
|
|
425
|
+
if (invalidFlags.length) {
|
|
426
|
+
throw new SyntaxError("\"flags\" contains invalid values: ".concat(invalidFlags.join(', ')));
|
|
427
|
+
}
|
|
428
|
+
metadata.flags = config.flags;
|
|
429
|
+
}
|
|
404
430
|
return metadata;
|
|
405
431
|
}
|
|
406
432
|
function validateResource(config, content) {
|
|
@@ -480,27 +506,11 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
480
506
|
var rank = resource.metadata.levels[groupIndent / 2];
|
|
481
507
|
var item = parseName(name_1, rank, parent_1);
|
|
482
508
|
var isSynonym = item.taxonomicStatus !== 'accepted';
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (type === diff_resource_1.ResourceDiffType.Added) {
|
|
487
|
-
newIdOffset++;
|
|
488
|
-
item.scientificNameID = idBase + newIdOffset.toString();
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
id++;
|
|
492
|
-
item.scientificNameID = idBase + (oldIds[id - 1] || id).toString();
|
|
493
|
-
}
|
|
494
|
-
previousId = item.scientificNameID;
|
|
495
|
-
item.parentNameUsageID = isSynonym ? undefined : parent_1.scientificNameID;
|
|
496
|
-
item.parentNameUsage = isSynonym ? undefined : parent_1.scientificName;
|
|
497
|
-
item.acceptedNameUsageID = isSynonym ? parent_1.scientificNameID : undefined;
|
|
498
|
-
item.acceptedNameUsage = isSynonym ? parent_1.scientificName : undefined;
|
|
499
|
-
item.collectionCode = idBase.slice(0, -1);
|
|
509
|
+
// Add higher classification info
|
|
510
|
+
var itemAsObject = item;
|
|
511
|
+
var parentAsObject = parent_1;
|
|
500
512
|
for (var _b = 0, DWC_RANKS_1 = DWC_RANKS; _b < DWC_RANKS_1.length; _b++) {
|
|
501
513
|
var rank_1 = DWC_RANKS_1[_b];
|
|
502
|
-
var itemAsObject = item;
|
|
503
|
-
var parentAsObject = parent_1;
|
|
504
514
|
itemAsObject[rank_1] = undefined;
|
|
505
515
|
if (parentAsObject[rank_1]) {
|
|
506
516
|
itemAsObject[rank_1] = parentAsObject[rank_1];
|
|
@@ -524,6 +534,31 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
524
534
|
else if (parentId) {
|
|
525
535
|
item.higherClassification = parent_1.scientificNameOnly;
|
|
526
536
|
}
|
|
537
|
+
// Amend "parent" with corrections
|
|
538
|
+
if (item.taxonomicStatus === 'incorrect') {
|
|
539
|
+
parent_1.incorrect = __assign({}, parent_1);
|
|
540
|
+
for (var key in item) {
|
|
541
|
+
if (key !== 'taxonomicStatus' && key !== 'verbatimIdentification') {
|
|
542
|
+
parentAsObject[key] = itemAsObject[key];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return "continue";
|
|
546
|
+
}
|
|
547
|
+
// Set identifiers
|
|
548
|
+
if (type === diff_resource_1.ResourceDiffType.Added) {
|
|
549
|
+
newIdOffset++;
|
|
550
|
+
item.scientificNameID = idBase + newIdOffset.toString();
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
id++;
|
|
554
|
+
item.scientificNameID = idBase + (oldIds[id - 1] || id).toString();
|
|
555
|
+
}
|
|
556
|
+
previousId = item.scientificNameID;
|
|
557
|
+
item.parentNameUsageID = isSynonym ? undefined : parent_1.scientificNameID;
|
|
558
|
+
item.parentNameUsage = isSynonym ? undefined : parent_1.scientificName;
|
|
559
|
+
item.acceptedNameUsageID = isSynonym ? parent_1.scientificNameID : undefined;
|
|
560
|
+
item.acceptedNameUsage = isSynonym ? parent_1.scientificName : undefined;
|
|
561
|
+
item.collectionCode = idBase.slice(0, -1);
|
|
527
562
|
data[item.scientificNameID] = item;
|
|
528
563
|
};
|
|
529
564
|
for (var _i = 0, content_1 = content; _i < content_1.length; _i++) {
|
package/package.json
CHANGED
|
@@ -11,22 +11,25 @@ interface AmendedResourceMetadata extends ResourceMetadata {
|
|
|
11
11
|
taxonCount: number
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
type SortObjectCallback = (a: string, b: string) => number
|
|
15
|
+
function alphabeticSort (a: string, b: string): number {
|
|
16
|
+
return a > b ? 1 : a < b ? -1 : 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sortObject (object: Record<string, unknown>, sorter?: SortObjectCallback): Record<string, unknown> {
|
|
20
|
+
const sorted: Record<string, unknown> = {}
|
|
21
|
+
for (const key of Object.keys(object).sort(sorter ?? numericSort)) {
|
|
18
22
|
sorted[key] = object[key]
|
|
19
23
|
}
|
|
20
24
|
return sorted
|
|
21
25
|
}
|
|
22
|
-
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
23
26
|
|
|
24
|
-
function addTaxon (
|
|
25
|
-
if (!(
|
|
26
|
-
|
|
27
|
+
function addTaxon (index: Record<string, TaxonId[]>, id: string, taxon: string[]) {
|
|
28
|
+
if (!(id in index)) {
|
|
29
|
+
index[id] = []
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
index[id].push(taxon[0])
|
|
32
|
+
index[id].sort(numericSort)
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
async function main (args: string[]): Promise<void> {
|
|
@@ -35,6 +38,7 @@ async function main (args: string[]): Promise<void> {
|
|
|
35
38
|
const files = await fs.readdir(path.join(REPO_ROOT, 'txt'))
|
|
36
39
|
|
|
37
40
|
const gbifIndex: Record<string, TaxonId[]> = {}
|
|
41
|
+
const colIndex: Record<string, TaxonId[]> = {}
|
|
38
42
|
const resourceIndex: Record<TaxonId, AmendedResourceMetadata> = {}
|
|
39
43
|
|
|
40
44
|
await Promise.all(files.map(async function (fileName) {
|
|
@@ -57,6 +61,8 @@ async function main (args: string[]): Promise<void> {
|
|
|
57
61
|
const [header, ...dwc] = csv.parseCsv(await fs.readFile(dwcFile, 'utf-8'))
|
|
58
62
|
const gbifColumn = header.indexOf('gbifTaxonID')
|
|
59
63
|
const gbifAcceptedColumn = header.indexOf('gbifAcceptedTaxonID')
|
|
64
|
+
const colColumn = header.indexOf('colTaxonID')
|
|
65
|
+
const colAcceptedColumn = header.indexOf('colAcceptedTaxonID')
|
|
60
66
|
for (const taxon of dwc) {
|
|
61
67
|
const gbifId = taxon[gbifColumn]
|
|
62
68
|
if (gbifId) {
|
|
@@ -65,6 +71,15 @@ async function main (args: string[]): Promise<void> {
|
|
|
65
71
|
addTaxon(gbifIndex, taxon[gbifAcceptedColumn], taxon)
|
|
66
72
|
}
|
|
67
73
|
}
|
|
74
|
+
|
|
75
|
+
const colId = taxon[colColumn]
|
|
76
|
+
if (colId) {
|
|
77
|
+
addTaxon(colIndex, colId, taxon)
|
|
78
|
+
if (taxon[colAcceptedColumn] !== taxon[colColumn]) {
|
|
79
|
+
addTaxon(colIndex, taxon[colAcceptedColumn], taxon)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
amendedResource.taxonCount += 1
|
|
69
84
|
}
|
|
70
85
|
|
|
@@ -74,6 +89,7 @@ async function main (args: string[]): Promise<void> {
|
|
|
74
89
|
|
|
75
90
|
await Promise.all([
|
|
76
91
|
fs.writeFile(path.join(REPO_ROOT, 'gbif.index.json'), JSON.stringify(sortObject(gbifIndex), null, 2)),
|
|
92
|
+
fs.writeFile(path.join(REPO_ROOT, 'col.index.json'), JSON.stringify(sortObject(colIndex, alphabeticSort), null, 2)),
|
|
77
93
|
fs.writeFile(path.join(REPO_ROOT, 'index.json'), JSON.stringify(sortObject(resourceIndex), null, 2))
|
|
78
94
|
])
|
|
79
95
|
}
|
|
@@ -175,7 +175,6 @@ class ResourceProcessor {
|
|
|
175
175
|
const skip = await this.shouldBeSkipped(resource.id)
|
|
176
176
|
|
|
177
177
|
if (!skip) {
|
|
178
|
-
// TODO const correct = checkResults(results, classifications)
|
|
179
178
|
const correct = this.checkResults(results)
|
|
180
179
|
if (!correct) {
|
|
181
180
|
const choice = await promptForAnswers(
|
package/src/module.d.ts
CHANGED
|
@@ -54,6 +54,12 @@ const DWC_RANKS: DwcRank[] = [
|
|
|
54
54
|
'subgenus'
|
|
55
55
|
]
|
|
56
56
|
|
|
57
|
+
const FLAGS: ResourceFlag[] = [
|
|
58
|
+
'MISSING_TAXA',
|
|
59
|
+
'MISSING_PARENT_TAXA',
|
|
60
|
+
'MISSING_SYNONYMS'
|
|
61
|
+
]
|
|
62
|
+
|
|
57
63
|
const TAXONOMIC_STATUS: Record<string, TaxonStatus> = {
|
|
58
64
|
'>': 'incorrect',
|
|
59
65
|
'+': 'heterotypic synonym',
|
|
@@ -87,6 +93,8 @@ const RANK_LABELS_REVERSE: Record<string, Rank> = {
|
|
|
87
93
|
'subsp': 'subspecies'
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
const HYBRID_SIGN = '\u00D7'
|
|
97
|
+
|
|
90
98
|
/**
|
|
91
99
|
* 1. Any number of
|
|
92
100
|
* - capitalized words
|
|
@@ -97,7 +105,7 @@ const RANK_LABELS_REVERSE: Record<string, Rank> = {
|
|
|
97
105
|
* 2. Followed by a capitalized word
|
|
98
106
|
* 3. Optionally, followed by "et al."
|
|
99
107
|
*/
|
|
100
|
-
const LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van'].join('|')
|
|
108
|
+
const LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van', 'von'].join('|')
|
|
101
109
|
const SIMPLE_AUTHOR_PATTERN = '(?:(?:\\p{Lu}\\S*|&|in|ex|' + LOWERCASE_NAME_PARTICLES + ')\\s*)*\\p{Lu}\\S+(?:\\s+et\\s+al\\.)?'
|
|
102
110
|
|
|
103
111
|
const NAME_PATTERN = new RegExp(
|
|
@@ -107,7 +115,7 @@ const NAME_PATTERN = new RegExp(
|
|
|
107
115
|
// $2 optional author citation
|
|
108
116
|
'(?: ' +
|
|
109
117
|
// but not auct(t)., etc.
|
|
110
|
-
'(?!auctt?\\.|(?:syn|comb|sp|spec)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.))' +
|
|
118
|
+
'(?!auctt?\\.|(?:syn|comb|sp|spec|nom)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.)|in part|partim)' +
|
|
111
119
|
'(' +
|
|
112
120
|
// $2.1 anything in parentheses, followed by optional revising author(s)
|
|
113
121
|
'\\(.+?\\)(?:\\s+' + SIMPLE_AUTHOR_PATTERN + ')?' +
|
|
@@ -127,11 +135,14 @@ const NAME_PATTERN = new RegExp(
|
|
|
127
135
|
/**
|
|
128
136
|
* Structure
|
|
129
137
|
* $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
|
|
130
|
-
* $1.1 genus: ([A-Z]\S+)
|
|
138
|
+
* $1.1 genus: ((?:x )?[A-Z]\S+)
|
|
131
139
|
* $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
|
|
132
|
-
* $2 species: (
|
|
140
|
+
* $2 species: (x [a-z]+|[a-z][^\s.]+(?: x [a-z]+)?|[A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+)
|
|
141
|
+
* $2a: x [a-z]+
|
|
142
|
+
* $2b hybrid: [a-z][^\s.]+(?: x [a-z]+)?
|
|
143
|
+
* $2c intergeneric hybrid: [A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+
|
|
133
144
|
*/
|
|
134
|
-
const BINAME_PATTERN = /^(?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?(
|
|
145
|
+
const BINAME_PATTERN = /^(?:((?:x )?[A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?(x [a-z]+|[a-z][^\s.]+(?: x [a-z]+)?|[A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+)(?= |$)/
|
|
135
146
|
|
|
136
147
|
function compareRanks (a: Rank, b: Rank): number {
|
|
137
148
|
return RANKS.indexOf(a) - RANKS.indexOf(b)
|
|
@@ -141,12 +152,19 @@ function capitalize (name: string): string {
|
|
|
141
152
|
return name[0].toUpperCase() + name.slice(1).toLowerCase()
|
|
142
153
|
}
|
|
143
154
|
|
|
155
|
+
function capitalizeGenericName (name: string): string {
|
|
156
|
+
if (name[0] === HYBRID_SIGN) {
|
|
157
|
+
return HYBRID_SIGN + capitalize(name.slice(1))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return capitalize(name)
|
|
161
|
+
}
|
|
162
|
+
|
|
144
163
|
function isUpperCase (name: string): boolean {
|
|
145
164
|
return name === name.toUpperCase()
|
|
146
165
|
}
|
|
147
166
|
|
|
148
167
|
function getSynonymRank (name: string, rank: Rank): Rank {
|
|
149
|
-
const BINAME_PATTERN = /^([A-Z]\S+ (\([A-Z]\S+\) )?)?(x )?[a-z0-9-]+(?= |$)/
|
|
150
168
|
const rest = name.replace(BINAME_PATTERN, '')
|
|
151
169
|
const rankPrefix = rest.match(/^(?: |^)(st|r|ab|f|var|ssp|subsp)\. /)
|
|
152
170
|
if (rankPrefix) {
|
|
@@ -188,7 +206,7 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
188
206
|
}
|
|
189
207
|
|
|
190
208
|
// Set verbatim identification after subsequent syntax is removed.
|
|
191
|
-
item.verbatimIdentification = name
|
|
209
|
+
item.verbatimIdentification = name.replace(/(?<=^| )x(?=$| )/g, HYBRID_SIGN).replace(/_/g, ' ')
|
|
192
210
|
|
|
193
211
|
// Parent context is used for parsing and formatting binomial names.
|
|
194
212
|
// For formatting, it needs to match external databases (i.e. be correct).
|
|
@@ -212,7 +230,8 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
212
230
|
if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
|
|
213
231
|
const [, genus, subgenus, species] = name.match(BINAME_PATTERN) || []
|
|
214
232
|
if (genus) {
|
|
215
|
-
parentContext.
|
|
233
|
+
parentContext.incorrect.genus = genus
|
|
234
|
+
parentContext.genus = capitalizeGenericName(genus.replace(/(^| )x /, HYBRID_SIGN))
|
|
216
235
|
}
|
|
217
236
|
if (subgenus) {
|
|
218
237
|
parentContext.subgenus = capitalize(subgenus)
|
|
@@ -221,8 +240,9 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
221
240
|
// from the parent context.
|
|
222
241
|
delete parentContext.subgenus
|
|
223
242
|
}
|
|
224
|
-
if (species) {
|
|
225
|
-
parentContext.
|
|
243
|
+
if (species && compareRanks('species', rank) < 0) {
|
|
244
|
+
parentContext.incorrect.specificEpithet = species
|
|
245
|
+
parentContext.specificEpithet = species.replace(/(^| )x /, HYBRID_SIGN)
|
|
226
246
|
}
|
|
227
247
|
}
|
|
228
248
|
|
|
@@ -253,8 +273,12 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
// Hybrids
|
|
276
|
+
if (rank === 'genus' && name.startsWith('x ')) {
|
|
277
|
+
name = HYBRID_SIGN + name.slice(2)
|
|
278
|
+
}
|
|
279
|
+
|
|
256
280
|
if (rank === 'species' && /(^| )x /.test(name)) {
|
|
257
|
-
name = name.replace(/(^| )x /,
|
|
281
|
+
name = name.replace(/(^| )x /, HYBRID_SIGN)
|
|
258
282
|
}
|
|
259
283
|
|
|
260
284
|
// Divide the name into the main scientific name (only the epithet for taxa
|
|
@@ -265,7 +289,9 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
265
289
|
}
|
|
266
290
|
|
|
267
291
|
// To encode old names with spaces (e.g. "Orsillus pini canariensis Lindberg, 1953")
|
|
268
|
-
// underscores are used, which are replaced here.
|
|
292
|
+
// underscores are used, which are replaced here. This is also used for undescribed
|
|
293
|
+
// species (e.g. "Leiobunum species A") and intergeneric hybrids (e.g. "×Festulpia
|
|
294
|
+
// Festuca rubra × Vulpia bromoides")
|
|
269
295
|
if (nameParts[1].includes('_')) {
|
|
270
296
|
nameParts[1] = nameParts[1].replace(/_/g, ' ')
|
|
271
297
|
}
|
|
@@ -280,9 +306,14 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
280
306
|
}
|
|
281
307
|
|
|
282
308
|
// Validate names and recompose binomial and trinomial names
|
|
283
|
-
if (
|
|
309
|
+
if (rank === 'genus') {
|
|
310
|
+
item.scientificName = capitalizeGenericName(taxon)
|
|
311
|
+
if (taxon[0].toUpperCase() !== taxon[0] || (taxon[0] === HYBRID_SIGN && taxon[1].toUpperCase() !== taxon[1])) {
|
|
312
|
+
throw new Error(`Generic epithet should be capitalized: "${taxon}"`)
|
|
313
|
+
}
|
|
314
|
+
} else if (compareRanks('group', rank) > 0) {
|
|
284
315
|
item.scientificName = capitalize(taxon)
|
|
285
|
-
if (
|
|
316
|
+
if (taxon[0].toUpperCase() !== taxon[0]) {
|
|
286
317
|
throw new Error(`Taxon name (${rank}) should be capitalized: "${taxon}"`)
|
|
287
318
|
}
|
|
288
319
|
} else if (rank === 'group') {
|
|
@@ -306,12 +337,12 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
306
337
|
} else if (rank === 'species') {
|
|
307
338
|
item.genericName = parentContext.genus
|
|
308
339
|
item.infragenericEpithet = parentContext.subgenus
|
|
309
|
-
|
|
310
|
-
item.scientificName = `${item.genericName} ${item.specificEpithet}`
|
|
311
|
-
if (item.specificEpithet !== taxon) {
|
|
340
|
+
if (taxon.toLowerCase() !== taxon && !/^[A-Z][a-z]+ [a-z]+\xD7[A-Z][a-z]+ [a-z]+$/.test(taxon)) {
|
|
312
341
|
console.log(item, taxon)
|
|
313
342
|
throw new Error(`Specific epithet should be lowercase: "${taxon}"`)
|
|
314
343
|
}
|
|
344
|
+
item.specificEpithet = taxon
|
|
345
|
+
item.scientificName = `${item.genericName} ${item.specificEpithet}`
|
|
315
346
|
} else if (compareRanks('species', rank) < 0) {
|
|
316
347
|
item.genericName = parentContext.genus
|
|
317
348
|
item.infragenericEpithet = parentContext.subgenus
|
|
@@ -342,19 +373,6 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
342
373
|
item.scientificName += ` ${item.scientificNameAuthorship}`
|
|
343
374
|
}
|
|
344
375
|
|
|
345
|
-
// Amend "parent" with corrections
|
|
346
|
-
if (item.taxonomicStatus === 'incorrect') {
|
|
347
|
-
const itemAsObject = item as { [index: string]: unknown }
|
|
348
|
-
const parentAsObject = parent as { [index: string]: unknown }
|
|
349
|
-
|
|
350
|
-
parent.incorrect = { ...parent }
|
|
351
|
-
for (const key in item) {
|
|
352
|
-
if (key !== 'taxonomicStatus') {
|
|
353
|
-
parentAsObject[key] = itemAsObject[key]
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
376
|
return item
|
|
359
377
|
}
|
|
360
378
|
|
|
@@ -418,6 +436,19 @@ function parseHeader (header: string): ResourceMetadata {
|
|
|
418
436
|
}
|
|
419
437
|
}
|
|
420
438
|
|
|
439
|
+
if ('flags' in config) {
|
|
440
|
+
if (!Array.isArray(config.flags)) {
|
|
441
|
+
throw new SyntaxError('"flags" should be an array if present')
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const invalidFlags = config.flags.filter(flag => !FLAGS.includes(flag))
|
|
445
|
+
if (invalidFlags.length) {
|
|
446
|
+
throw new SyntaxError(`"flags" contains invalid values: ${invalidFlags.join(', ')}`)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
metadata.flags = config.flags
|
|
450
|
+
}
|
|
451
|
+
|
|
421
452
|
return metadata
|
|
422
453
|
}
|
|
423
454
|
|
|
@@ -510,29 +541,10 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
510
541
|
const item = parseName(name, rank, parent)
|
|
511
542
|
const isSynonym = item.taxonomicStatus !== 'accepted'
|
|
512
543
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (type === ResourceDiffType.Added) {
|
|
518
|
-
newIdOffset++
|
|
519
|
-
item.scientificNameID = idBase + newIdOffset.toString()
|
|
520
|
-
} else {
|
|
521
|
-
id++
|
|
522
|
-
item.scientificNameID = idBase + (oldIds[id - 1] || id).toString()
|
|
523
|
-
}
|
|
524
|
-
previousId = item.scientificNameID
|
|
525
|
-
|
|
526
|
-
item.parentNameUsageID = isSynonym ? undefined : parent.scientificNameID
|
|
527
|
-
item.parentNameUsage = isSynonym ? undefined : parent.scientificName
|
|
528
|
-
item.acceptedNameUsageID = isSynonym ? parent.scientificNameID : undefined
|
|
529
|
-
item.acceptedNameUsage = isSynonym ? parent.scientificName : undefined
|
|
530
|
-
item.collectionCode = idBase.slice(0, -1)
|
|
531
|
-
|
|
544
|
+
// Add higher classification info
|
|
545
|
+
const itemAsObject = item as { [index: string]: unknown }
|
|
546
|
+
const parentAsObject = parent as { [index: string]: unknown }
|
|
532
547
|
for (const rank of DWC_RANKS) {
|
|
533
|
-
const itemAsObject = item as { [index: string]: unknown }
|
|
534
|
-
const parentAsObject = parent as { [index: string]: unknown }
|
|
535
|
-
|
|
536
548
|
itemAsObject[rank] = undefined
|
|
537
549
|
if (parentAsObject[rank]) {
|
|
538
550
|
itemAsObject[rank] = parentAsObject[rank]
|
|
@@ -557,6 +569,33 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
557
569
|
item.higherClassification = parent.scientificNameOnly
|
|
558
570
|
}
|
|
559
571
|
|
|
572
|
+
// Amend "parent" with corrections
|
|
573
|
+
if (item.taxonomicStatus === 'incorrect') {
|
|
574
|
+
parent.incorrect = { ...parent }
|
|
575
|
+
for (const key in item) {
|
|
576
|
+
if (key !== 'taxonomicStatus' && key !== 'verbatimIdentification') {
|
|
577
|
+
parentAsObject[key] = itemAsObject[key]
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
continue
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Set identifiers
|
|
584
|
+
if (type === ResourceDiffType.Added) {
|
|
585
|
+
newIdOffset++
|
|
586
|
+
item.scientificNameID = idBase + newIdOffset.toString()
|
|
587
|
+
} else {
|
|
588
|
+
id++
|
|
589
|
+
item.scientificNameID = idBase + (oldIds[id - 1] || id).toString()
|
|
590
|
+
}
|
|
591
|
+
previousId = item.scientificNameID
|
|
592
|
+
|
|
593
|
+
item.parentNameUsageID = isSynonym ? undefined : parent.scientificNameID
|
|
594
|
+
item.parentNameUsage = isSynonym ? undefined : parent.scientificName
|
|
595
|
+
item.acceptedNameUsageID = isSynonym ? parent.scientificNameID : undefined
|
|
596
|
+
item.acceptedNameUsage = isSynonym ? parent.scientificName : undefined
|
|
597
|
+
item.collectionCode = idBase.slice(0, -1)
|
|
598
|
+
|
|
560
599
|
data[item.scientificNameID] = item
|
|
561
600
|
}
|
|
562
601
|
|
package/test/resources.js
CHANGED
|
@@ -136,4 +136,43 @@ Tilia x vulgaris
|
|
|
136
136
|
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Rumex conglomeratus×maritimus')
|
|
137
137
|
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Tilia ×vulgaris')
|
|
138
138
|
})
|
|
139
|
+
|
|
140
|
+
await t.test('outputs corrected generic names', (t) => {
|
|
141
|
+
const [resource] = resources.parseTextFile(`---
|
|
142
|
+
levels: [genus, species]
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
Bogdania Kerzhner, 1964
|
|
146
|
+
> Bogdiana Kerzhner, 1964
|
|
147
|
+
myrmica Kerzhner, 1964
|
|
148
|
+
`, 'T1')
|
|
149
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Bogdiana Kerzhner, 1964')
|
|
150
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Bogdiana myrmica Kerzhner, 1964')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await t.test('parses cross-genus hybrids', (t) => {
|
|
154
|
+
const [resource] = resources.parseTextFile(`---
|
|
155
|
+
levels: [genus, species]
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
x Triticosecale
|
|
159
|
+
Triticosecale indet.
|
|
160
|
+
x Festulpia
|
|
161
|
+
Festuca_rubra x Vulpia_bromoides
|
|
162
|
+
`, 'T1')
|
|
163
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, '×Triticosecale')
|
|
164
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, '×Festulpia')
|
|
165
|
+
assert.strictEqual(resource.taxa['T1:1:3'].scientificName, '×Festulpia Festuca rubra×Vulpia bromoides')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await t.test('parses cross-genus hybrids without parent context', (t) => {
|
|
169
|
+
const [resource] = resources.parseTextFile(`---
|
|
170
|
+
levels: [species]
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
x Festulpia Festuca_rubra x Vulpia_bromoides
|
|
174
|
+
`, 'T1')
|
|
175
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, '×Festulpia Festuca rubra×Vulpia bromoides')
|
|
176
|
+
assert.strictEqual(resource.taxa['T1:1:1'].verbatimIdentification, '× Festulpia Festuca rubra × Vulpia bromoides')
|
|
177
|
+
})
|
|
139
178
|
})
|