@larsgw/formica 0.9.2 → 0.10.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/lib/bin/process-resources-index.js +0 -0
- package/lib/bin/process-resources.d.ts +6 -1
- package/lib/bin/process-resources.js +116 -25
- package/lib/bin/validate-catalog.js +0 -0
- package/lib/bin/validate-resources-text.js +3 -1
- package/lib/resources/parse-text.js +6 -3
- package/lib/taxon-names/index.js +1 -1
- package/package.json +15 -13
- package/.gitattributes +0 -1
- package/.github/workflows/ci.yml +0 -27
- package/CHANGELOG.md +0 -390
- package/eslint.config.js +0 -34
- package/lib/bin/SHEETS.js +0 -0
- package/lib/bin/clean-links.d.ts +0 -2
- package/lib/bin/clean-links.js +0 -170
- package/lib/bin/download-place-shapes.js +0 -188
- package/lib/bin/index-place-shapes.js +0 -115
- package/lib/bin/process-resources-problems.js +0 -177
- package/lib/bin/validate-linked-data.js +0 -0
- package/lib/resources/content/clavis.js +0 -10
- package/lib/resources/content/index.js +0 -0
- package/lib/resources/content/sdd.js +0 -151
- package/lib/resources/sdd.js +0 -78
- package/src/bin/generate-linked-data.ts +0 -762
- package/src/bin/process-resources-index.ts +0 -100
- package/src/bin/process-resources.ts +0 -510
- package/src/bin/util.ts +0 -74
- package/src/bin/validate-catalog.ts +0 -122
- package/src/bin/validate-resources-text.ts +0 -25
- package/src/catalog/entities.ts +0 -62
- package/src/catalog/entity.ts +0 -116
- package/src/catalog/index.ts +0 -33
- package/src/catalog/tables/author.ts +0 -15
- package/src/catalog/tables/index.ts +0 -14
- package/src/catalog/tables/place.ts +0 -14
- package/src/catalog/tables/publisher.ts +0 -15
- package/src/catalog/tables/taxon.ts +0 -17
- package/src/catalog/tables/work.ts +0 -65
- package/src/catalog/value.ts +0 -51
- package/src/csv.ts +0 -33
- package/src/index.ts +0 -4
- package/src/module.d.ts +0 -148
- package/src/resources/diff-resource.ts +0 -226
- package/src/resources/index.ts +0 -4
- package/src/resources/parse-name.ts +0 -392
- package/src/resources/parse-text.ts +0 -408
- package/src/resources/resource.ts +0 -10
- package/src/taxon-names/index.ts +0 -79
- package/test/resources.js +0 -374
- package/tsconfig.json +0 -15
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import * as yaml from 'js-yaml'
|
|
2
|
-
import { WorkResource } from './resource'
|
|
3
|
-
import { createDiff, ResourceDiffType } from './diff-resource'
|
|
4
|
-
import { parseName, RANKS, RecoverableSyntaxError } from './parse-name'
|
|
5
|
-
|
|
6
|
-
const MAIN_RANKS: Rank[] = [
|
|
7
|
-
'kingdom',
|
|
8
|
-
'phylum',
|
|
9
|
-
'class',
|
|
10
|
-
'order',
|
|
11
|
-
'family',
|
|
12
|
-
'genus',
|
|
13
|
-
'species'
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
const DWC_RANKS: DwcRank[] = [
|
|
17
|
-
'kingdom',
|
|
18
|
-
'phylum',
|
|
19
|
-
'class',
|
|
20
|
-
'order',
|
|
21
|
-
'family',
|
|
22
|
-
'subfamily',
|
|
23
|
-
'genus',
|
|
24
|
-
'subgenus'
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
const FLAGS: ResourceFlag[] = [
|
|
28
|
-
'MISSING_TAXA',
|
|
29
|
-
'MISSING_PARENT_TAXA',
|
|
30
|
-
'MISSING_SYNONYMS',
|
|
31
|
-
'MISSING_AUTHORSHIP'
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
const RESOURCE_DELIMITER = '\n\n===\n\n'
|
|
35
|
-
const INDENT = 2
|
|
36
|
-
|
|
37
|
-
function makeParseError (message: string, line: number, column: number = 1): SyntaxError {
|
|
38
|
-
return new SyntaxError(`[${line}:${column}] ${message}`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function mergeParserErrors (errors: SyntaxError[]): SyntaxError {
|
|
42
|
-
return new SyntaxError(errors.map(error => error.message).join('\n'))
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseHeader (header: string): ResourceMetadata {
|
|
46
|
-
const config = yaml.load(header)
|
|
47
|
-
|
|
48
|
-
if (typeof config !== 'object' || Array.isArray(config) || config === null) {
|
|
49
|
-
throw new SyntaxError('yaml header should be an object')
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Invalid configuration
|
|
53
|
-
let levels
|
|
54
|
-
if (!('levels' in config)) {
|
|
55
|
-
levels = []
|
|
56
|
-
} else if (!Array.isArray(config.levels)) {
|
|
57
|
-
throw new SyntaxError('"levels" should be an array')
|
|
58
|
-
} else {
|
|
59
|
-
levels = config.levels
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if ('scope' in config) {
|
|
63
|
-
throw new SyntaxError('"scope" data should go in "catalog"')
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// No taxon ranks
|
|
67
|
-
if (levels.length === 0) {
|
|
68
|
-
throw new SyntaxError('Resource contains no taxa')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Invalid taxon ranks
|
|
72
|
-
const invalidTaxonRanks = levels.filter(rank => !RANKS.includes(rank))
|
|
73
|
-
if (invalidTaxonRanks.length) {
|
|
74
|
-
throw new SyntaxError(`"levels" contains invalid values: ${invalidTaxonRanks.join(', ')}`)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const metadata: ResourceMetadata = { levels }
|
|
78
|
-
|
|
79
|
-
if ('catalog' in config && typeof config.catalog === 'object' && config.catalog !== null) {
|
|
80
|
-
const catalog: Record<string, string> = {}
|
|
81
|
-
if ('id' in config.catalog) {
|
|
82
|
-
throw new SyntaxError('"catalog" should not contain id')
|
|
83
|
-
}
|
|
84
|
-
for (const key in config.catalog) {
|
|
85
|
-
const value = config.catalog[key as keyof object]
|
|
86
|
-
if (typeof value === 'number') {
|
|
87
|
-
catalog[key] = (value as number).toString()
|
|
88
|
-
} else if (typeof value === 'string') {
|
|
89
|
-
catalog[key] = value
|
|
90
|
-
} else {
|
|
91
|
-
throw new SyntaxError(`"catalog" should contain only strings ("${key}")`)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const work = new WorkResource(catalog)
|
|
95
|
-
const errors = work.validate().filter(({ error }) => error !== 'Value(s) required but missing')
|
|
96
|
-
if (errors.length > 0) {
|
|
97
|
-
throw new SyntaxError(`"catalog" contains errors: ${errors.map(({ field, error }) => `[${field}] ${error}`).join('; ')}`)
|
|
98
|
-
}
|
|
99
|
-
metadata.catalog = {}
|
|
100
|
-
for (const key in work.fields) {
|
|
101
|
-
metadata.catalog[key] = work.get(key) as Value
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if ('flags' in config) {
|
|
106
|
-
if (!Array.isArray(config.flags)) {
|
|
107
|
-
throw new SyntaxError('"flags" should be an array if present')
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const invalidFlags = config.flags.filter(flag => !FLAGS.includes(flag))
|
|
111
|
-
if (invalidFlags.length) {
|
|
112
|
-
throw new SyntaxError(`"flags" contains invalid values: ${invalidFlags.join(', ')}`)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
metadata.flags = config.flags
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return metadata
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function parseResource (resource: FilePart): [ResourceMetadata, FilePart] {
|
|
122
|
-
const [header, _, ...rest] = resource.content.split(/(\n---\n+)/)
|
|
123
|
-
let config
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
config = parseHeader(header)
|
|
127
|
-
} catch (error) {
|
|
128
|
-
throw makeParseError(error.message, resource.offsetLine + 1)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const content = rest.join('')
|
|
132
|
-
const offsetLine = resource.offsetLine + (header + _).split('\n').length - 1
|
|
133
|
-
|
|
134
|
-
return [config, { content, offsetLine }]
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getTaxonChildren (parent: TaxonId|undefined, taxa: Record<TaxonId, WorkingTaxon>): WorkingTaxon[] {
|
|
138
|
-
const children = []
|
|
139
|
-
for (const id in taxa) {
|
|
140
|
-
if (taxa[id].parentNameUsageID === parent) {
|
|
141
|
-
children.push(taxa[id])
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return children
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function processClusters (taxa: Record<TaxonId, WorkingTaxon>) {
|
|
148
|
-
for (const id in taxa) {
|
|
149
|
-
const taxon = taxa[id]
|
|
150
|
-
if (taxon.taxonomicStatus !== 'accepted' || !taxon.cluster) {
|
|
151
|
-
continue
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const dynamicProperties = taxon.dynamicProperties ? JSON.parse(taxon.dynamicProperties) : {}
|
|
155
|
-
|
|
156
|
-
if (taxon.cluster === '_') {
|
|
157
|
-
dynamicProperties.identifiable = false
|
|
158
|
-
} else {
|
|
159
|
-
const siblings = getTaxonChildren(taxon.parentNameUsageID, taxa).filter(sibling => sibling.scientificNameID !== id)
|
|
160
|
-
dynamicProperties.indistinguishableFrom = siblings.filter(sibling => sibling.cluster === taxon.cluster).map(sibling => sibling.scientificNameID)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
taxon.dynamicProperties = JSON.stringify(dynamicProperties)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds: number[], offsetLine: number): Resource {
|
|
168
|
-
const leafTaxonIndex = resource.metadata.levels.reduce((last, rank, i) => MAIN_RANKS.includes(rank) ? i : last, 0)
|
|
169
|
-
const data = resource.taxa as Record<TaxonId, WorkingTaxon>
|
|
170
|
-
const errors = []
|
|
171
|
-
|
|
172
|
-
let id = 0
|
|
173
|
-
let newId = Math.max(...oldIds)
|
|
174
|
-
let lineNumber = offsetLine
|
|
175
|
-
|
|
176
|
-
const parents: Array<TaxonId | null> = []
|
|
177
|
-
const previous = { id: '', indent: 0, group: { isLeaf: false, indent: 0 }, errors: <SyntaxError[]>[] }
|
|
178
|
-
|
|
179
|
-
for (const line of content) {
|
|
180
|
-
const hasOriginalId = line.type !== ResourceDiffType.Added && !/^\s*(\[indet\]|> )/.test(line.original ?? line.text as string)
|
|
181
|
-
if (hasOriginalId) {
|
|
182
|
-
id++
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (line.type === ResourceDiffType.Deleted) {
|
|
186
|
-
continue
|
|
187
|
-
} else {
|
|
188
|
-
lineNumber++
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const [indentation, name] = (line.text as string).match(/^(\s*)(.*)/)!.slice(1)
|
|
192
|
-
const lineIndent = indentation.length
|
|
193
|
-
|
|
194
|
-
// Validate line
|
|
195
|
-
if (lineIndent % INDENT === 1) {
|
|
196
|
-
errors.push(makeParseError('Too much or little indentation', lineNumber))
|
|
197
|
-
continue
|
|
198
|
-
} else if (lineIndent / INDENT >= resource.metadata.levels.length && !/^[+=>] /.test(name)) {
|
|
199
|
-
errors.push(makeParseError('Too much indentation', lineNumber))
|
|
200
|
-
continue
|
|
201
|
-
} else if (lineIndent <= previous.group.indent && (data[previous.id] && !previous.group.isLeaf)) {
|
|
202
|
-
errors.push(makeParseError('Missing leaf taxon', lineNumber - 1))
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Update parentage
|
|
206
|
-
if (lineIndent > previous.indent) {
|
|
207
|
-
// Do not count synonyms as parents (unless this is correcting a typo in the synonym)
|
|
208
|
-
if (data[previous.id] && data[previous.id].taxonomicStatus === 'accepted' || name.startsWith('> ')) {
|
|
209
|
-
parents.push(previous.id)
|
|
210
|
-
} else {
|
|
211
|
-
parents.push(null)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Handle skips in indentation levels,
|
|
215
|
-
// e.g. if a certain genus has only species
|
|
216
|
-
// whereas other genera in the same key also
|
|
217
|
-
// have subgenera
|
|
218
|
-
for (let i = previous.indent + INDENT; i < lineIndent; i += INDENT) {
|
|
219
|
-
parents.push(null)
|
|
220
|
-
}
|
|
221
|
-
} else if (lineIndent < previous.indent) {
|
|
222
|
-
parents.splice(lineIndent / INDENT)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
previous.indent = lineIndent
|
|
226
|
-
|
|
227
|
-
// Do not process "indet" lines further, as they only serve to indicate
|
|
228
|
-
// that subtaxa are explicitely omitted
|
|
229
|
-
if (name.startsWith('[indet]')) {
|
|
230
|
-
errors.push(...previous.errors)
|
|
231
|
-
previous.errors.length = 0
|
|
232
|
-
previous.group.isLeaf = lineIndent / INDENT >= leafTaxonIndex
|
|
233
|
-
continue
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const parentId = parents.reduce((grandparent, parent) => parent ?? grandparent, null)
|
|
237
|
-
const parent = parentId === null ? {} as WorkingTaxon : data[parentId]
|
|
238
|
-
let item
|
|
239
|
-
const itemErrors = []
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
const rank = resource.metadata.levels[parents.length]
|
|
243
|
-
item = parseName(name, rank, parent)
|
|
244
|
-
} catch (error) {
|
|
245
|
-
if (error instanceof RecoverableSyntaxError) {
|
|
246
|
-
itemErrors.push(makeParseError(error.message, lineNumber))
|
|
247
|
-
item = error.result
|
|
248
|
-
} else {
|
|
249
|
-
errors.push(makeParseError(error.message, lineNumber))
|
|
250
|
-
continue
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Add higher classification info
|
|
255
|
-
const itemAsObject = item as { [index: string]: unknown }
|
|
256
|
-
const parentAsObject = parent as { [index: string]: unknown }
|
|
257
|
-
for (const rank of DWC_RANKS) {
|
|
258
|
-
itemAsObject[rank] = undefined
|
|
259
|
-
if (parentAsObject[rank]) {
|
|
260
|
-
itemAsObject[rank] = parentAsObject[rank]
|
|
261
|
-
}
|
|
262
|
-
if (item.taxonRank === rank) {
|
|
263
|
-
itemAsObject[rank] = item.scientificNameOnly
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (item.genericName && !item.genus) {
|
|
268
|
-
item.genus = item.genericName
|
|
269
|
-
}
|
|
270
|
-
if (item.infragenericEpithet && !item.subgenus) {
|
|
271
|
-
item.subgenus = item.infragenericEpithet
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Amend "parent" with corrections, exit
|
|
275
|
-
if (item.taxonomicStatus === 'incorrect') {
|
|
276
|
-
if (parent.taxonomicStatus !== 'accepted') {
|
|
277
|
-
// Remove corrected synonym from parentage
|
|
278
|
-
parents[parents.length - 1] = null
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (parent.incorrect) {
|
|
282
|
-
errors.push(makeParseError('Cannot apply a correction to a previous correction', lineNumber))
|
|
283
|
-
continue
|
|
284
|
-
} else if (parentId === null) {
|
|
285
|
-
errors.push(makeParseError('Cannot apply a correction to nothing', lineNumber))
|
|
286
|
-
continue
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
parent.incorrect = { ...parent }
|
|
290
|
-
for (const key in item) {
|
|
291
|
-
if (key !== 'taxonomicStatus' && key !== 'verbatimIdentification') {
|
|
292
|
-
parentAsObject[key] = itemAsObject[key]
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// If "parent" is corrected, its errors can be dropped
|
|
297
|
-
previous.errors.length = 0
|
|
298
|
-
// ...but errors associated with the corrected name are added immediately
|
|
299
|
-
errors.push(...itemErrors)
|
|
300
|
-
|
|
301
|
-
continue
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Add more classification info
|
|
305
|
-
const isSynonym = item.taxonomicStatus !== 'accepted'
|
|
306
|
-
if (isSynonym) {
|
|
307
|
-
item.higherClassification = parent.higherClassification
|
|
308
|
-
} else if (parent.higherClassification) {
|
|
309
|
-
item.higherClassification = parent.higherClassification + ` | ${parent.scientificNameOnly}`
|
|
310
|
-
} else if (parentId) {
|
|
311
|
-
item.higherClassification = parent.scientificNameOnly
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Set identifiers
|
|
315
|
-
item.scientificNameID = `${resource.id}:${hasOriginalId ? (oldIds[id - 1] ?? id) : ++newId}`
|
|
316
|
-
|
|
317
|
-
item.parentNameUsageID = isSynonym ? undefined : parent.scientificNameID
|
|
318
|
-
item.parentNameUsage = isSynonym ? undefined : parent.scientificName
|
|
319
|
-
item.acceptedNameUsageID = isSynonym ? parent.scientificNameID : undefined
|
|
320
|
-
item.acceptedNameUsage = isSynonym ? parent.scientificName : undefined
|
|
321
|
-
item.collectionCode = resource.id
|
|
322
|
-
|
|
323
|
-
data[item.scientificNameID] = item
|
|
324
|
-
|
|
325
|
-
// Update loop state
|
|
326
|
-
errors.push(...previous.errors)
|
|
327
|
-
previous.errors = itemErrors
|
|
328
|
-
previous.id = item.scientificNameID
|
|
329
|
-
if (item.taxonomicStatus === 'accepted') {
|
|
330
|
-
previous.group.indent = previous.indent
|
|
331
|
-
previous.group.isLeaf = lineIndent / INDENT >= leafTaxonIndex
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
errors.push(...previous.errors)
|
|
336
|
-
if (errors.length) {
|
|
337
|
-
throw mergeParserErrors(errors)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
processClusters(data)
|
|
341
|
-
|
|
342
|
-
return resource
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
interface FilePart {
|
|
346
|
-
content: string
|
|
347
|
-
offsetLine: number
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function splitResources (file: string): FilePart[] {
|
|
351
|
-
const resources: FilePart[] = []
|
|
352
|
-
let offsetLine = 0
|
|
353
|
-
for (const content of file.split(RESOURCE_DELIMITER)) {
|
|
354
|
-
resources.push({ content, offsetLine })
|
|
355
|
-
offsetLine += (content + RESOURCE_DELIMITER).split('\n').length - 1
|
|
356
|
-
}
|
|
357
|
-
return resources
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export function parseFile (file: string, id: WorkId, old?: ResourceHistory): Resource[] {
|
|
361
|
-
const oldResources = old ? splitResources(old.txt) : []
|
|
362
|
-
const newResources = splitResources(file)
|
|
363
|
-
const resources: Resource[] = []
|
|
364
|
-
const errors = []
|
|
365
|
-
|
|
366
|
-
for (let index = 0; index < newResources.length; index++) {
|
|
367
|
-
const [config, content] = parseResource(newResources[index])
|
|
368
|
-
const template: Resource = {
|
|
369
|
-
id: `${id}:${index + 1}`,
|
|
370
|
-
file: `${id}-${index + 1}`,
|
|
371
|
-
workId: id,
|
|
372
|
-
metadata: config,
|
|
373
|
-
taxa: {}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
let diff
|
|
377
|
-
if (oldResources[index]) {
|
|
378
|
-
diff = createDiff(content.content, parseResource(oldResources[index])[1].content)
|
|
379
|
-
// Ignore empty lines
|
|
380
|
-
diff = diff.filter(line => line.text !== '')
|
|
381
|
-
} else {
|
|
382
|
-
diff = createDiff(content.content, content.content)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const oldIds = []
|
|
386
|
-
if (old) {
|
|
387
|
-
for (const row of old.dwc[index].slice(1)) {
|
|
388
|
-
oldIds.push(parseInt(row[0].split(':')[2]))
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
try {
|
|
393
|
-
resources.push(parseResourceContent(diff, template, oldIds, content.offsetLine))
|
|
394
|
-
} catch (error) {
|
|
395
|
-
errors.push(error)
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (errors.length) {
|
|
400
|
-
throw mergeParserErrors(errors)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return resources
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export function parseFileHeader (file: string): ResourceMetadata[] {
|
|
407
|
-
return splitResources(file).map(resource => parseResource(resource)[0])
|
|
408
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { Work } from '../catalog/tables/work'
|
|
2
|
-
|
|
3
|
-
export class WorkResource extends Work {
|
|
4
|
-
constructor (values: Record<string, string>) {
|
|
5
|
-
super(values)
|
|
6
|
-
|
|
7
|
-
this.schema.version_of.format = /^B[1-9]\d*:[1-9]\d*$/
|
|
8
|
-
this.schema.duplicate_of.format = /^B[1-9]\d*:[1-9]\d*$/
|
|
9
|
-
}
|
|
10
|
-
}
|
package/src/taxon-names/index.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
const MINIMUM_PREFIX_LENGTH = 3
|
|
2
|
-
const VALID_COMMON_PREFIXES = new Set([
|
|
3
|
-
'Plantae|Tracheophyta',
|
|
4
|
-
'Fungi',
|
|
5
|
-
'Fungi|Ascomycota',
|
|
6
|
-
'Fungi|Basidiomycota',
|
|
7
|
-
'Fungi|Zygomycota'
|
|
8
|
-
])
|
|
9
|
-
|
|
10
|
-
function getCommonPrefix (a: string[], b: string[]): string[] {
|
|
11
|
-
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
12
|
-
if (a[i] !== b[i]) {
|
|
13
|
-
return a.slice(0, i)
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return a.slice()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isValidPrefix (a: string[], b: string[]): boolean {
|
|
20
|
-
const prefix = getCommonPrefix(a, b)
|
|
21
|
-
return VALID_COMMON_PREFIXES.has(prefix.join('|')) || prefix.length >= MINIMUM_PREFIX_LENGTH
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function groupNameMatches (results: Record<TaxonId, TaxonMatch[]>): GroupedNameMatches {
|
|
25
|
-
const prefixes: Record<string, [string[], Record<TaxonId, TaxonMatch>][]> = {}
|
|
26
|
-
|
|
27
|
-
for (const scientificNameID in results) {
|
|
28
|
-
for (const result of results[scientificNameID]) {
|
|
29
|
-
if (!prefixes[result.source]) {
|
|
30
|
-
prefixes[result.source] = []
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let prefix = prefixes[result.source].find(prefix => isValidPrefix(prefix[0], result.classificationPath))
|
|
34
|
-
|
|
35
|
-
if (!prefix) {
|
|
36
|
-
prefix = [result.classificationPath, {}]
|
|
37
|
-
prefixes[result.source].push(prefix)
|
|
38
|
-
} else {
|
|
39
|
-
prefix[0] = getCommonPrefix(prefix[0], result.classificationPath)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (scientificNameID in prefix[1]) {
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
prefix[1][scientificNameID] = result
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const groupedNameMatches: GroupedNameMatches = {}
|
|
51
|
-
for (const source in prefixes) {
|
|
52
|
-
groupedNameMatches[source] = prefixes[source]
|
|
53
|
-
.sort((a, b) => Object.keys(b[1]).length - Object.keys(a[1]).length)
|
|
54
|
-
.reduce((map: Record<string, Record<TaxonId, TaxonMatch>>, [prefix, taxa]) => {
|
|
55
|
-
map[prefix.join('|')] = taxa
|
|
56
|
-
return map
|
|
57
|
-
}, {})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return groupedNameMatches
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function amendResource (resource: AmendedResource, source: string, matches: Record<TaxonId, TaxonMatch>) {
|
|
64
|
-
for (const id in matches) {
|
|
65
|
-
const match = matches[id]
|
|
66
|
-
|
|
67
|
-
if (source === '1') {
|
|
68
|
-
resource.taxa[id].colTaxonID = match.id
|
|
69
|
-
if (match.currentId) {
|
|
70
|
-
resource.taxa[id].colAcceptedTaxonID = match.currentId
|
|
71
|
-
}
|
|
72
|
-
} else if (source === '11') {
|
|
73
|
-
resource.taxa[id].gbifTaxonID = match.id
|
|
74
|
-
if (match.currentId) {
|
|
75
|
-
resource.taxa[id].gbifAcceptedTaxonID = match.currentId
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|