@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 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
- /* eslint-disable @typescript-eslint/no-explicit-any */
56
- function sortObject(object) {
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
- /* eslint-enable @typescript-eslint/no-explicit-any */
65
- function addTaxon(gbifIndex, gbifId, taxon) {
66
- if (!(gbifId in gbifIndex)) {
67
- gbifIndex[gbifId] = [];
66
+ function addTaxon(index, id, taxon) {
67
+ if (!(id in index)) {
68
+ index[id] = [];
68
69
  }
69
- gbifIndex[gbifId].push(taxon[0]);
70
- gbifIndex[gbifId].sort(util_1.numericSort);
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: ((?:x )?[a-z][^\s.]+)
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+?)\) )?)?((?:x )?[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.genus = parentContext.incorrect.genus = capitalize(genus);
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.specificEpithet = parentContext.incorrect.specificEpithet = species;
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 /, '\u00D7');
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 (compareRanks('group', rank) > 0) {
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 (item.scientificName[0] !== taxon[0]) {
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
- item.specificEpithet = taxon.toLowerCase();
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
- if (item.taxonomicStatus === 'incorrect') {
484
- return "continue";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larsgw/formica",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
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",
@@ -11,22 +11,25 @@ interface AmendedResourceMetadata extends ResourceMetadata {
11
11
  taxonCount: number
12
12
  }
13
13
 
14
- /* eslint-disable @typescript-eslint/no-explicit-any */
15
- function sortObject (object: Record<string, any>): Record<string, any> {
16
- const sorted: Record<string, any> = {}
17
- for (const key of Object.keys(object).sort(numericSort)) {
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 (gbifIndex: Record<string, TaxonId[]>, gbifId: string, taxon: string[]) {
25
- if (!(gbifId in gbifIndex)) {
26
- gbifIndex[gbifId] = []
27
+ function addTaxon (index: Record<string, TaxonId[]>, id: string, taxon: string[]) {
28
+ if (!(id in index)) {
29
+ index[id] = []
27
30
  }
28
- gbifIndex[gbifId].push(taxon[0])
29
- gbifIndex[gbifId].sort(numericSort)
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
@@ -71,8 +71,11 @@ interface Taxon extends WorkingTaxon {
71
71
  verbatimIdentification: string
72
72
  }
73
73
 
74
+ type ResourceFlag = string;
75
+
74
76
  interface ResourceMetadata {
75
77
  levels: Rank[],
78
+ flags?: ResourceFlag[],
76
79
  catalog?: Record<string, Value>
77
80
  }
78
81
 
@@ -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: ((?:x )?[a-z][^\s.]+)
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+?)\) )?)?((?:x )?[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.genus = parentContext.incorrect.genus = capitalize(genus)
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.specificEpithet = parentContext.incorrect.specificEpithet = species
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 /, '\u00D7')
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 (compareRanks('group', rank) > 0) {
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 (item.scientificName[0] !== taxon[0]) {
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
- item.specificEpithet = taxon.toLowerCase()
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
- if (item.taxonomicStatus === 'incorrect') {
514
- continue
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
  })