@larsgw/formica 0.2.0 → 0.3.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/src/module.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  type Schema = Record<string, FieldSpecification>
2
2
  type FieldSpecification = {
3
- required: boolean,
4
- multiple: boolean | FieldSpecificationCallbackMultiple
5
- format?: string[] | RegExp | FieldSpecificationCallbackFormat
3
+ required: boolean,
4
+ multiple: boolean | FieldSpecificationCallbackMultiple
5
+ format?: string[] | RegExp | FieldSpecificationCallbackFormat
6
6
  }
7
7
 
8
8
  type Value = string[] | string
@@ -11,12 +11,12 @@ type FieldSpecificationCallbackMultiple = (entry: Record<string, Value>) => bool
11
11
  type FieldSpecificationCallbackFormat = (value: SingleValue) => boolean;
12
12
 
13
13
  interface FieldError {
14
- field: string,
15
- error: string
14
+ field: string,
15
+ error: string
16
16
  }
17
17
 
18
18
  interface WorkError extends FieldError {
19
- entity: WorkId
19
+ entity: WorkId
20
20
  }
21
21
 
22
22
  type Rank = string
@@ -28,59 +28,59 @@ type ResourceId = string
28
28
  type WorkId = string
29
29
 
30
30
  interface WorkingTaxon {
31
- scientificNameID?: TaxonId,
32
- scientificName?: string,
33
- scientificNameAuthorship?: string,
34
- genericName?: string,
35
- infragenericEpithet?: string,
36
- specificEpithet?: string,
37
- intraspecificEpithet?: string,
38
-
39
- taxonRank?: Rank,
40
- taxonRemarks?: string,
41
- collectionCode?: ResourceId,
42
-
43
- taxonomicStatus?: TaxonStatus,
44
- acceptedNameUsageID?: TaxonId,
45
- acceptedNameUsage?: string,
46
-
47
- parentNameUsageID?: TaxonId,
48
- parentNameUsage?: string,
49
- kingdom?: string,
50
- phylum?: string,
51
- class?: string,
52
- order?: string,
53
- family?: string,
54
- subfamily?: string,
55
- genus?: string,
56
- subgenus?: string,
57
- higherClassification?: string,
58
-
59
- // Non-standard
60
- scientificNameOnly?: string,
61
- incorrect?: WorkingTaxon
31
+ scientificNameID?: TaxonId,
32
+ scientificName?: string,
33
+ scientificNameAuthorship?: string,
34
+ genericName?: string,
35
+ infragenericEpithet?: string,
36
+ specificEpithet?: string,
37
+ intraspecificEpithet?: string,
38
+
39
+ taxonRank?: Rank,
40
+ taxonRemarks?: string,
41
+ collectionCode?: ResourceId,
42
+
43
+ taxonomicStatus?: TaxonStatus,
44
+ acceptedNameUsageID?: TaxonId,
45
+ acceptedNameUsage?: string,
46
+
47
+ parentNameUsageID?: TaxonId,
48
+ parentNameUsage?: string,
49
+ kingdom?: string,
50
+ phylum?: string,
51
+ class?: string,
52
+ order?: string,
53
+ family?: string,
54
+ subfamily?: string,
55
+ genus?: string,
56
+ subgenus?: string,
57
+ higherClassification?: string,
58
+
59
+ // Non-standard
60
+ scientificNameOnly?: string,
61
+ incorrect?: WorkingTaxon
62
62
  }
63
63
 
64
64
  interface Taxon extends WorkingTaxon {
65
- scientificNameID: TaxonId,
66
- scientificName: string,
67
- taxonRank: Rank,
68
- collectionCode: ResourceId,
69
- taxonomicStatus: string
65
+ scientificNameID: TaxonId,
66
+ scientificName: string,
67
+ taxonRank: Rank,
68
+ collectionCode: ResourceId,
69
+ taxonomicStatus: string
70
70
  }
71
71
 
72
72
  interface ResourceMetadata {
73
- levels: Rank[],
74
- scope: string[],
75
- catalog?: object
73
+ levels: Rank[],
74
+ scope: string[],
75
+ catalog?: object
76
76
  }
77
77
 
78
78
  interface Resource {
79
- id: string,
80
- file: string,
81
- workId: string,
82
- metadata: ResourceMetadata,
83
- taxa: Record<TaxonId, Taxon>
79
+ id: string,
80
+ file: string,
81
+ workId: string,
82
+ metadata: ResourceMetadata,
83
+ taxa: Record<TaxonId, Taxon>
84
84
  }
85
85
 
86
86
  interface ResourceHistory {
@@ -103,3 +103,23 @@ declare enum ResourceDiffType {
103
103
  Modified = '~',
104
104
  Unchanged = '='
105
105
  }
106
+
107
+ interface AmendedTaxon extends Taxon {
108
+ colTaxonID?: string,
109
+ colAcceptedTaxonID?: string,
110
+ gbifTaxonID?: string,
111
+ gbifAcceptedTaxonID?: string
112
+ }
113
+
114
+ interface AmendedResource extends Resource {
115
+ taxa: Record<TaxonId, AmendedTaxon>
116
+ }
117
+
118
+ interface TaxonMatch {
119
+ source: number,
120
+ id: string,
121
+ currentId?: string,
122
+ classificationPath: string[]
123
+ }
124
+
125
+ type GroupedNameMatches = Record<string, Record<string, Record<TaxonId, TaxonMatch>>>
@@ -31,6 +31,16 @@ const RANKS: Rank[] = [
31
31
  'stirps' // not ICZN
32
32
  ]
33
33
 
34
+ const MAIN_RANKS: Rank[] = [
35
+ 'kingdom',
36
+ 'phylum',
37
+ 'class',
38
+ 'order',
39
+ 'family',
40
+ 'genus',
41
+ 'species'
42
+ ]
43
+
34
44
  const DWC_RANKS: DwcRank[] = [
35
45
  'kingdom',
36
46
  'phylum',
@@ -360,11 +370,7 @@ function parseHeader (header: string): ResourceMetadata {
360
370
  return metadata
361
371
  }
362
372
 
363
- function parseResource (resource: string): [ResourceMetadata, string] {
364
- const [header, _, ...rest] = resource.split(/(\n---\n+)/)
365
- const config = parseHeader(header)
366
- const content = rest.join('')
367
-
373
+ function validateResource (config: ResourceMetadata, content: string) {
368
374
  // Check for too much indentation
369
375
  const longerIndent = new RegExp(`^( ){${config.levels.length - 1}}(?! [+=>] ) `, 'm')
370
376
  const longerIndentMatch = content.match(longerIndent)
@@ -377,15 +383,25 @@ ${content.slice(offset).split('\n', 1)}
377
383
  }
378
384
 
379
385
  // Check for missing leaf taxa
380
- const missingLeafTaxa = new RegExp(`^( ){0,${config.levels.length - 2}}(?![+=>] ).*\\n(?!\\1 )`, 'm')
381
- const missingLeafTaxaMatch = content.match(missingLeafTaxa)
382
- if (missingLeafTaxaMatch !== null) {
383
- const offset = missingLeafTaxaMatch.index
384
- const line = (content.slice(0, offset).match(/\n/g) || []).length + 1
385
- throw new SyntaxError(`Missing leaf taxon at ${line}:0
386
+ const leafTaxonRank = config.levels.filter(rank => MAIN_RANKS.includes(rank)).pop() as string
387
+ const leafTaxonParentIndent = config.levels.indexOf(leafTaxonRank) - 1
388
+ if (leafTaxonRank && leafTaxonParentIndent >= 0) {
389
+ const missingLeafTaxa = new RegExp(`^((?: ){0,${leafTaxonParentIndent}})(?![+=> ] ).*\\n(\\1( )+[+=>].*\\n)*(?!\\1 )`, 'm')
390
+ const missingLeafTaxaMatch = content.match(missingLeafTaxa)
391
+ if (missingLeafTaxaMatch !== null) {
392
+ const offset = missingLeafTaxaMatch.index
393
+ const line = (content.slice(0, offset).match(/\n/g) || []).length + 1
394
+ throw new SyntaxError(`Missing leaf taxon at ${line}:0
386
395
  ${content.slice(offset).split('\n', 1)}
387
396
  ^`)
397
+ }
388
398
  }
399
+ }
400
+
401
+ function parseResource (resource: string): [ResourceMetadata, string] {
402
+ const [header, _, ...rest] = resource.split(/(\n---\n+)/)
403
+ const config = parseHeader(header)
404
+ const content = rest.join('')
389
405
 
390
406
  return [config, content]
391
407
  }
@@ -499,6 +515,7 @@ export function parseFile (file: string, id: WorkId, old?: ResourceHistory): Res
499
515
  const oldResources = old ? splitResources(old.txt) : []
500
516
  return splitResources(file).map((resource, index) => {
501
517
  const [config, content] = parseResource(resource)
518
+ validateResource(config, content)
502
519
  const template: Resource = {
503
520
  id: `${id}:${index + 1}`,
504
521
  file: `${id}-${index + 1}`,
@@ -0,0 +1,79 @@
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
+ }