@larsgw/formica 0.4.2 → 0.5.0
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 +41 -0
- package/lib/bin/process-resources.d.ts +5 -1
- package/lib/bin/process-resources.js +143 -144
- package/lib/catalog/value.js +2 -2
- package/lib/csv.js +1 -1
- package/lib/resources/parse-text.js +80 -47
- package/package.json +1 -1
- package/src/bin/process-resources.ts +81 -60
- package/src/catalog/value.ts +2 -2
- package/src/csv.ts +1 -1
- package/src/module.d.ts +16 -4
- package/src/resources/parse-text.ts +76 -36
- package/test/resources.js +34 -0
|
@@ -13,6 +13,7 @@ var __assign = (this && this.__assign) || function () {
|
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.parseFileHeader = exports.parseFile = void 0;
|
|
15
15
|
var yaml = require("js-yaml");
|
|
16
|
+
var work_1 = require("../catalog/tables/work");
|
|
16
17
|
var diff_resource_1 = require("./diff-resource");
|
|
17
18
|
var RANKS = [
|
|
18
19
|
'class',
|
|
@@ -122,9 +123,9 @@ var NAME_PATTERN = new RegExp('^' +
|
|
|
122
123
|
* $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
|
|
123
124
|
* $1.1 genus: ([A-Z]\S+)
|
|
124
125
|
* $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
|
|
125
|
-
* $2 species: ((?:x )?[a-
|
|
126
|
+
* $2 species: ((?:x )?[a-z][^\s.]+)
|
|
126
127
|
*/
|
|
127
|
-
var BINAME_PATTERN = /^(?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?((?:x )?[a-
|
|
128
|
+
var BINAME_PATTERN = /^(?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?((?:x )?[a-z][^\s.]+)(?= |$)/;
|
|
128
129
|
function compareRanks(a, b) {
|
|
129
130
|
return RANKS.indexOf(a) - RANKS.indexOf(b);
|
|
130
131
|
}
|
|
@@ -174,55 +175,62 @@ function parseName(name, rank, parent) {
|
|
|
174
175
|
if (/^\[(_|\d+)\] /.test(name)) {
|
|
175
176
|
name = name.replace(/^\[(_|\d+)\] /, '');
|
|
176
177
|
}
|
|
178
|
+
// Set verbatim identification after subsequent syntax is removed.
|
|
179
|
+
item.verbatimIdentification = name;
|
|
177
180
|
// Parent context is used for parsing and formatting binomial names.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
// For formatting, it needs to match external databases (i.e. be correct).
|
|
182
|
+
// For parsing, it needs to match the current file. If relevant parents
|
|
183
|
+
// (i.e. genus, species) had mistakes that were corrected, the uncorrected
|
|
184
|
+
// genus and species names need to be used.
|
|
185
|
+
var parentContext = {
|
|
186
|
+
genus: parent.genus,
|
|
187
|
+
subgenus: parent.subgenus,
|
|
188
|
+
specificEpithet: parent.specificEpithet,
|
|
189
|
+
incorrect: {
|
|
190
|
+
genus: parent.incorrect && parent.incorrect.genus,
|
|
191
|
+
specificEpithet: parent.incorrect && parent.incorrect.specificEpithet
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
// Both contexts should be amended in the two cases where binomial names
|
|
195
|
+
// are fully used: (1) synonyms and (2) multinomial taxa without parents to
|
|
196
|
+
// provide parts of the name (e.g. bare species without a genus parent, or
|
|
197
|
+
// even subspecies without a species or genus parent).
|
|
185
198
|
if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
|
|
186
199
|
var _a = name.match(BINAME_PATTERN) || [], genus = _a[1], subgenus = _a[2], species = _a[3];
|
|
187
200
|
if (genus) {
|
|
188
|
-
parentContext.genus = capitalize(genus);
|
|
189
|
-
if (parentContext.incorrect)
|
|
190
|
-
parentContext.incorrect.genus = capitalize(genus);
|
|
201
|
+
parentContext.genus = parentContext.incorrect.genus = capitalize(genus);
|
|
191
202
|
}
|
|
192
203
|
if (subgenus) {
|
|
193
204
|
parentContext.subgenus = capitalize(subgenus);
|
|
194
|
-
if (parentContext.incorrect)
|
|
195
|
-
parentContext.incorrect.subgenus = capitalize(subgenus);
|
|
196
205
|
}
|
|
197
206
|
else if (genus) {
|
|
198
|
-
// If a genus is given but no subgenus, remove
|
|
207
|
+
// If a genus is given but no subgenus, remove any existing subgenus
|
|
208
|
+
// from the parent context.
|
|
199
209
|
delete parentContext.subgenus;
|
|
200
|
-
if (parentContext.incorrect)
|
|
201
|
-
delete parentContext.incorrect.subgenus;
|
|
202
210
|
}
|
|
203
211
|
if (species) {
|
|
204
|
-
parentContext.specificEpithet = species;
|
|
205
|
-
if (parentContext.incorrect)
|
|
206
|
-
parentContext.incorrect.specificEpithet = species;
|
|
212
|
+
parentContext.specificEpithet = parentContext.incorrect.specificEpithet = species;
|
|
207
213
|
}
|
|
208
214
|
}
|
|
209
215
|
// In taxa of group, species or lower, the name should just contain the
|
|
210
|
-
// (
|
|
216
|
+
// (infra)specific epithet and the author information & remarks when processing
|
|
211
217
|
// further.
|
|
212
218
|
if (compareRanks('group', rank) <= 0) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
// Remove genus
|
|
220
|
+
var genus = parentContext.incorrect.genus || parentContext.genus || '';
|
|
221
|
+
if (name[0] === genus[0] && name.toLowerCase().startsWith(genus.toLowerCase() + ' ')) {
|
|
222
|
+
name = name.slice(genus.length + 1);
|
|
223
|
+
}
|
|
224
|
+
// Remove subgenus
|
|
225
|
+
name = name.replace(/^\(.*?\) /, '');
|
|
226
|
+
// Infraspecific taxa
|
|
221
227
|
if (compareRanks('species', rank) < 0) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
// Remove specific epithet
|
|
229
|
+
var species = parentContext.incorrect.specificEpithet || parentContext.specificEpithet || '';
|
|
230
|
+
if (name.startsWith(species + ' ')) {
|
|
231
|
+
name = name.slice(species.length + 1);
|
|
225
232
|
}
|
|
233
|
+
// Remove rank abbreviations
|
|
226
234
|
name = name.replace(/^(st|r|ab|f|var|ssp|subsp)\. /, '');
|
|
227
235
|
}
|
|
228
236
|
}
|
|
@@ -236,6 +244,11 @@ function parseName(name, rank, parent) {
|
|
|
236
244
|
if (!nameParts) {
|
|
237
245
|
throw new Error("Taxon \"".concat(name, "\" could not be parsed"));
|
|
238
246
|
}
|
|
247
|
+
// To encode old names with spaces (e.g. "Orsillus pini canariensis Lindberg, 1953")
|
|
248
|
+
// underscores are used, which are replaced here.
|
|
249
|
+
if (nameParts[1].includes('_')) {
|
|
250
|
+
nameParts[1] = nameParts[1].replace(/_/g, ' ');
|
|
251
|
+
}
|
|
239
252
|
var _ = nameParts[0], taxon = nameParts[1], _b = nameParts[2], citation = _b === void 0 ? '' : _b, notes = nameParts[3];
|
|
240
253
|
item.scientificNameAuthorship = capitalizeAuthors(citation);
|
|
241
254
|
item.taxonRemarks = notes;
|
|
@@ -284,21 +297,21 @@ function parseName(name, rank, parent) {
|
|
|
284
297
|
item.genericName = parentContext.genus;
|
|
285
298
|
item.infragenericEpithet = parentContext.subgenus;
|
|
286
299
|
item.specificEpithet = parentContext.specificEpithet;
|
|
287
|
-
item.
|
|
300
|
+
item.infraspecificEpithet = taxon.toLowerCase();
|
|
288
301
|
// If possible, names below species should have abbreviations for ranks,
|
|
289
302
|
// like "subsp."
|
|
290
303
|
var nameParts_1 = [
|
|
291
304
|
item.genericName,
|
|
292
305
|
item.specificEpithet,
|
|
293
|
-
item.
|
|
306
|
+
item.infraspecificEpithet
|
|
294
307
|
];
|
|
295
308
|
if (item.taxonRank in RANK_LABELS) {
|
|
296
309
|
nameParts_1.splice(2, 0, RANK_LABELS[item.taxonRank]);
|
|
297
310
|
}
|
|
298
311
|
item.scientificName = nameParts_1.join(' ');
|
|
299
|
-
if (item.
|
|
312
|
+
if (item.infraspecificEpithet !== taxon) {
|
|
300
313
|
console.log(item, taxon);
|
|
301
|
-
throw new Error("
|
|
314
|
+
throw new Error("Infraspecific epithet should be lowercase: \"".concat(taxon, "\""));
|
|
302
315
|
}
|
|
303
316
|
}
|
|
304
317
|
// Re-add authorship information
|
|
@@ -335,15 +348,8 @@ function parseHeader(header) {
|
|
|
335
348
|
else {
|
|
336
349
|
levels = config.levels;
|
|
337
350
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
scope = [];
|
|
341
|
-
}
|
|
342
|
-
else if (!Array.isArray(config.scope)) {
|
|
343
|
-
throw new SyntaxError('"scope" should be an array');
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
scope = config.scope;
|
|
351
|
+
if ('scope' in config) {
|
|
352
|
+
throw new SyntaxError('"scope" data should go in "catalog"');
|
|
347
353
|
}
|
|
348
354
|
// No taxon ranks
|
|
349
355
|
if (levels.length === 0) {
|
|
@@ -354,9 +360,36 @@ function parseHeader(header) {
|
|
|
354
360
|
if (invalidTaxonRanks.length) {
|
|
355
361
|
throw new SyntaxError("\"levels\" contains invalid values: ".concat(invalidTaxonRanks.join(', ')));
|
|
356
362
|
}
|
|
357
|
-
var metadata = { levels: levels
|
|
363
|
+
var metadata = { levels: levels };
|
|
358
364
|
if ('catalog' in config && typeof config.catalog === 'object' && config.catalog !== null) {
|
|
359
|
-
|
|
365
|
+
var catalog = {};
|
|
366
|
+
for (var key in config.catalog) {
|
|
367
|
+
var value = config.catalog[key];
|
|
368
|
+
if (typeof value === 'number') {
|
|
369
|
+
catalog[key] = value.toString();
|
|
370
|
+
}
|
|
371
|
+
else if (typeof value === 'string') {
|
|
372
|
+
catalog[key] = value;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
throw new SyntaxError("\"catalog\" should contain only strings (\"".concat(key, "\")"));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
var work = new work_1.Work(catalog);
|
|
379
|
+
var errors = work.validate().filter(function (_a) {
|
|
380
|
+
var error = _a.error;
|
|
381
|
+
return error !== 'Value(s) required but missing';
|
|
382
|
+
});
|
|
383
|
+
if (errors.length > 0) {
|
|
384
|
+
throw new SyntaxError("\"catalog\" contains errors: ".concat(errors.map(function (_a) {
|
|
385
|
+
var field = _a.field, error = _a.error;
|
|
386
|
+
return "[".concat(field, "] ").concat(error);
|
|
387
|
+
}).join('; ')));
|
|
388
|
+
}
|
|
389
|
+
metadata.catalog = {};
|
|
390
|
+
for (var key in work.fields) {
|
|
391
|
+
metadata.catalog[key] = work.get(key);
|
|
392
|
+
}
|
|
360
393
|
}
|
|
361
394
|
return metadata;
|
|
362
395
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { promises as fs } from 'fs'
|
|
3
|
+
import { promises as fs, existsSync as doesFileExist } from 'fs'
|
|
4
4
|
import * as path from 'path'
|
|
5
5
|
import { spawn } from 'child_process'
|
|
6
6
|
import * as util from 'util'
|
|
@@ -8,14 +8,20 @@ import * as util from 'util'
|
|
|
8
8
|
import { csv } from '../index'
|
|
9
9
|
import { prompt, promptForAnswers, numericSort, runCommand } from './util'
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
export enum ResourceProcessorSource {
|
|
12
|
+
All = 'all',
|
|
13
|
+
Unprocessed = 'unprocessed',
|
|
14
|
+
Modified = 'modified'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DWC_FIELDS: (keyof AmendedTaxon)[] = [
|
|
12
18
|
'scientificNameID',
|
|
13
19
|
'scientificName',
|
|
14
20
|
'scientificNameAuthorship',
|
|
15
21
|
'genericName',
|
|
16
|
-
'
|
|
22
|
+
'infragenericEpithet',
|
|
17
23
|
'specificEpithet',
|
|
18
|
-
'
|
|
24
|
+
'infraspecificEpithet',
|
|
19
25
|
|
|
20
26
|
'taxonRank',
|
|
21
27
|
'taxonRemarks',
|
|
@@ -36,6 +42,7 @@ const DWC_FIELDS: string[] = [
|
|
|
36
42
|
'genus',
|
|
37
43
|
'subgenus',
|
|
38
44
|
'higherClassification',
|
|
45
|
+
'verbatimIdentification',
|
|
39
46
|
|
|
40
47
|
'colTaxonID',
|
|
41
48
|
'gbifTaxonID',
|
|
@@ -43,7 +50,7 @@ const DWC_FIELDS: string[] = [
|
|
|
43
50
|
'gbifAcceptedTaxonID'
|
|
44
51
|
]
|
|
45
52
|
|
|
46
|
-
const DISPLAY_FIELDS:
|
|
53
|
+
const DISPLAY_FIELDS: (keyof AmendedTaxon)[] = [
|
|
47
54
|
'scientificNameID',
|
|
48
55
|
'taxonRank',
|
|
49
56
|
'scientificName',
|
|
@@ -83,11 +90,22 @@ function runGnverifier (names: string): Promise<string> {
|
|
|
83
90
|
})
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
async function listFiles (directory: string): Promise<string[]> {
|
|
94
|
+
const input = await fs.readdir(directory)
|
|
95
|
+
return input.map(file => path.basename(file, '.txt')).sort(numericSort)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function listUnprocessedFiles (directory: string, outputDirectory: string): Promise<string[]> {
|
|
99
|
+
const input = await listFiles(directory)
|
|
100
|
+
const output = new Set(await fs.readdir(outputDirectory))
|
|
101
|
+
return input.filter(file => output.has(file + '-1'))
|
|
102
|
+
}
|
|
103
|
+
|
|
86
104
|
async function listChangedFiles (directory: string): Promise<string[]> {
|
|
87
105
|
const output = await runCommand('git', ['diff', '--name-only', 'HEAD', '--', directory], {
|
|
88
106
|
cwd: directory
|
|
89
107
|
})
|
|
90
|
-
return output.trimEnd().split('\n').sort(numericSort)
|
|
108
|
+
return output.trimEnd().split('\n').map(file => path.basename(file, '.txt')).sort(numericSort)
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
async function getOldFile (file: string): Promise<string> {
|
|
@@ -111,49 +129,32 @@ class ResourceProcessor {
|
|
|
111
129
|
this.FILE_PROBLEMS = path.join(this.DIR_ROOT, 'problems.csv')
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
async run (): Promise<void> {
|
|
115
|
-
const
|
|
116
|
-
const output = await fs.readdir(this.DIR_DWC)
|
|
117
|
-
|
|
118
|
-
const ids = input
|
|
119
|
-
.map(file => path.basename(file, '.txt'))
|
|
120
|
-
.sort((a, b) => parseInt(a.slice(1)) - parseInt(b.slice(1)))
|
|
121
|
-
|
|
132
|
+
async run (source: ResourceProcessorSource, config: ResourceProcessorConfig): Promise<void> {
|
|
133
|
+
const ids = await this.listWorks(source)
|
|
122
134
|
for (const id of ids) {
|
|
123
|
-
|
|
124
|
-
if (output.some(file => file.startsWith(id + '-'))) {
|
|
125
|
-
continue
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
await this.processWork(id)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async runUpdate (): Promise<void> {
|
|
133
|
-
for (const file of await listChangedFiles(this.DIR_TXT)) {
|
|
134
|
-
const id = path.basename(file, '.txt')
|
|
135
|
-
await this.processWork(id, true)
|
|
135
|
+
await this.processWork(id, config)
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
async
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
139
|
+
async listWorks (source: ResourceProcessorSource): Promise<string[]> {
|
|
140
|
+
switch (source) {
|
|
141
|
+
case ResourceProcessorSource.All:
|
|
142
|
+
return listFiles(this.DIR_TXT)
|
|
143
|
+
case ResourceProcessorSource.Unprocessed:
|
|
144
|
+
return listUnprocessedFiles(this.DIR_TXT, this.DIR_DWC)
|
|
145
|
+
case ResourceProcessorSource.Modified:
|
|
146
|
+
return listChangedFiles(this.DIR_TXT)
|
|
147
|
+
default:
|
|
148
|
+
return []
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
async processWork (id: WorkId,
|
|
152
|
-
const resources = await this.processResources(id,
|
|
152
|
+
async processWork (id: WorkId, config: ResourceProcessorConfig): Promise<void> {
|
|
153
|
+
const resources = await this.processResources(id, config)
|
|
153
154
|
|
|
154
155
|
await Promise.all(resources.map(resource => {
|
|
155
156
|
const header = DWC_FIELDS
|
|
156
|
-
const table = [header]
|
|
157
|
+
const table: string[][] = [header]
|
|
157
158
|
|
|
158
159
|
for (const id in resource.taxa) {
|
|
159
160
|
const taxon = resource.taxa[id] as unknown as Record<string, string | undefined>
|
|
@@ -164,12 +165,12 @@ class ResourceProcessor {
|
|
|
164
165
|
}))
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
async processResources (id: WorkId,
|
|
168
|
-
const resources = await this.processResourceText(id,
|
|
168
|
+
async processResources (id: WorkId, config: ResourceProcessorConfig): Promise<AmendedResource[]> {
|
|
169
|
+
const resources = await this.processResourceText(id, config)
|
|
169
170
|
|
|
170
171
|
const amendedResources = []
|
|
171
172
|
for (const resource of resources) {
|
|
172
|
-
const results = await this.processResourceDwc(resource)
|
|
173
|
+
const results = await this.processResourceDwc(resource, config)
|
|
173
174
|
|
|
174
175
|
const skip = await this.shouldBeSkipped(resource.id)
|
|
175
176
|
|
|
@@ -198,7 +199,7 @@ class ResourceProcessor {
|
|
|
198
199
|
case 'r':
|
|
199
200
|
case 'R': {
|
|
200
201
|
console.log(`${resource.workId}: retrying ${resource.id}`)
|
|
201
|
-
return this.processResources(id,
|
|
202
|
+
return this.processResources(id, config)
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
205
|
}
|
|
@@ -210,14 +211,14 @@ class ResourceProcessor {
|
|
|
210
211
|
return amendedResources
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
async processResourceText (id: WorkId,
|
|
214
|
+
async processResourceText (id: WorkId, config: ResourceProcessorConfig): Promise<Resource[]> {
|
|
214
215
|
try {
|
|
215
216
|
console.log(`${id}: generating Darwin Core`)
|
|
216
217
|
const filePath = path.join(this.DIR_TXT, id + '.txt')
|
|
217
218
|
const file = await fs.readFile(filePath, 'utf-8')
|
|
218
219
|
|
|
219
220
|
let old = undefined
|
|
220
|
-
if (update) {
|
|
221
|
+
if (config.update) {
|
|
221
222
|
const dwc = []
|
|
222
223
|
for (const file of await fs.readdir(this.DIR_DWC)) {
|
|
223
224
|
if (file.startsWith(id + '-')) {
|
|
@@ -243,13 +244,34 @@ class ResourceProcessor {
|
|
|
243
244
|
}
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
return this.processResourceText(id,
|
|
247
|
+
return this.processResourceText(id, config)
|
|
247
248
|
}
|
|
248
249
|
}
|
|
249
250
|
|
|
250
|
-
async processResourceDwc (resource: Resource): Promise<AmendedResource> {
|
|
251
|
+
async processResourceDwc (resource: Resource, config: ResourceProcessorConfig): Promise<AmendedResource> {
|
|
251
252
|
console.log(`${resource.workId}: matching ${resource.id}`)
|
|
252
253
|
|
|
254
|
+
if (!config.updateMappings) {
|
|
255
|
+
const file = path.join(this.DIR_DWC, resource.file + '.csv')
|
|
256
|
+
if (doesFileExist(file)) {
|
|
257
|
+
const [header, ...rows] = csv.parseCsv(await fs.readFile(file, 'utf-8'))
|
|
258
|
+
for (const row of rows) {
|
|
259
|
+
const oldTaxon = row.reduce((taxon, value, index) => {
|
|
260
|
+
taxon[header[index]] = value
|
|
261
|
+
return taxon
|
|
262
|
+
}, {} as Record<string, string>)
|
|
263
|
+
const taxon = resource.taxa[oldTaxon.scientificNameID] as AmendedTaxon
|
|
264
|
+
if (taxon) {
|
|
265
|
+
taxon.colTaxonID = oldTaxon.colTaxonID
|
|
266
|
+
taxon.colAcceptedTaxonID = oldTaxon.colAcceptedTaxonID
|
|
267
|
+
taxon.gbifTaxonID = oldTaxon.gbifTaxonID
|
|
268
|
+
taxon.gbifAcceptedTaxonID = oldTaxon.gbifAcceptedTaxonID
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return resource as AmendedResource
|
|
273
|
+
}
|
|
274
|
+
|
|
253
275
|
const filteredResults: Record<TaxonId, TaxonMatch[]> = {}
|
|
254
276
|
const taxonNames: Record<string, TaxonId[]> = {}
|
|
255
277
|
const names = new Set()
|
|
@@ -429,12 +451,14 @@ class ResourceProcessor {
|
|
|
429
451
|
function main (): void {
|
|
430
452
|
const args = util.parseArgs({
|
|
431
453
|
options: {
|
|
432
|
-
|
|
433
|
-
type: '
|
|
434
|
-
short: '
|
|
454
|
+
source: {
|
|
455
|
+
type: 'string',
|
|
456
|
+
short: 's',
|
|
457
|
+
default: 'unprocessed'
|
|
435
458
|
},
|
|
436
|
-
'
|
|
437
|
-
type: 'boolean'
|
|
459
|
+
'keep-mappings': {
|
|
460
|
+
type: 'boolean',
|
|
461
|
+
short: 'k'
|
|
438
462
|
}
|
|
439
463
|
},
|
|
440
464
|
allowPositionals: true
|
|
@@ -445,16 +469,13 @@ function main (): void {
|
|
|
445
469
|
process.stdout.write('\n')
|
|
446
470
|
})
|
|
447
471
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
task = processor.runMappingsUpdate()
|
|
453
|
-
} else {
|
|
454
|
-
task = processor.run()
|
|
472
|
+
const source = args.values.source as ResourceProcessorSource
|
|
473
|
+
const config: ResourceProcessorConfig = {
|
|
474
|
+
update: source !== 'unprocessed',
|
|
475
|
+
updateMappings: !args.values['keep-mappings']
|
|
455
476
|
}
|
|
456
477
|
|
|
457
|
-
|
|
478
|
+
processor.run(source, config).catch(error => {
|
|
458
479
|
console.error(error)
|
|
459
480
|
process.exit(1)
|
|
460
481
|
})
|
package/src/catalog/value.ts
CHANGED
|
@@ -24,11 +24,11 @@ export const FORMATS = {
|
|
|
24
24
|
|
|
25
25
|
export const CHECK = {
|
|
26
26
|
MULTILANG (entry: Record<string, Value>) {
|
|
27
|
-
return entry.language.length > 1
|
|
27
|
+
return Array.isArray(entry.language) && entry.language.length > 1
|
|
28
28
|
},
|
|
29
29
|
|
|
30
30
|
ISBN (entry: Record<string, Value>) {
|
|
31
|
-
if (entry.ISBN.length < 2) { return false }
|
|
31
|
+
if (!Array.isArray(entry.ISBN) || entry.ISBN.length < 2) { return false }
|
|
32
32
|
const a = entry.ISBN[0].length === 10
|
|
33
33
|
const b = entry.ISBN[0].length === 13
|
|
34
34
|
const c = entry.ISBN[1].length === 10
|
package/src/csv.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function formatCsv (table: string[][], delimiter = ',') {
|
|
|
23
23
|
.map((row: string[]) => {
|
|
24
24
|
return row.map(value => {
|
|
25
25
|
if (/["\n]/.test(value) || value.includes(delimiter)) {
|
|
26
|
-
return `"${value.replace(/"
|
|
26
|
+
return `"${value.replace(/"/g, '""')}"`
|
|
27
27
|
} else {
|
|
28
28
|
return value
|
|
29
29
|
}
|
package/src/module.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ interface WorkingTaxon {
|
|
|
34
34
|
genericName?: string,
|
|
35
35
|
infragenericEpithet?: string,
|
|
36
36
|
specificEpithet?: string,
|
|
37
|
-
|
|
37
|
+
infraspecificEpithet?: string,
|
|
38
38
|
|
|
39
39
|
taxonRank?: Rank,
|
|
40
40
|
taxonRemarks?: string,
|
|
@@ -55,6 +55,7 @@ interface WorkingTaxon {
|
|
|
55
55
|
genus?: string,
|
|
56
56
|
subgenus?: string,
|
|
57
57
|
higherClassification?: string,
|
|
58
|
+
verbatimIdentification?: string,
|
|
58
59
|
|
|
59
60
|
// Non-standard
|
|
60
61
|
scientificNameOnly?: string,
|
|
@@ -66,13 +67,13 @@ interface Taxon extends WorkingTaxon {
|
|
|
66
67
|
scientificName: string,
|
|
67
68
|
taxonRank: Rank,
|
|
68
69
|
collectionCode: ResourceId,
|
|
69
|
-
taxonomicStatus: string
|
|
70
|
+
taxonomicStatus: string,
|
|
71
|
+
verbatimIdentification: string
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
interface ResourceMetadata {
|
|
73
75
|
levels: Rank[],
|
|
74
|
-
|
|
75
|
-
catalog?: object
|
|
76
|
+
catalog?: Record<string, Value>
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
interface Resource {
|
|
@@ -123,3 +124,14 @@ interface TaxonMatch {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
type GroupedNameMatches = Record<string, Record<string, Record<TaxonId, TaxonMatch>>>
|
|
127
|
+
|
|
128
|
+
declare enum ResourceProcessorSource {
|
|
129
|
+
All = 'all',
|
|
130
|
+
Unprocessed = 'unprocessed',
|
|
131
|
+
Modified = 'modified'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface ResourceProcessorConfig {
|
|
135
|
+
update: boolean,
|
|
136
|
+
updateMappings: boolean
|
|
137
|
+
}
|