@larsgw/formica 0.8.7 → 0.9.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 +24 -0
- package/lib/bin/process-resources.js +1 -0
- package/lib/resources/parse-name.js +15 -4
- package/lib/resources/parse-text.js +27 -0
- package/package.json +1 -1
- package/src/bin/process-resources.ts +2 -0
- package/src/module.d.ts +3 -1
- package/src/resources/parse-name.ts +17 -4
- package/src/resources/parse-text.ts +32 -0
- package/test/resources.js +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
# [0.9.0](https://github.com/identification-resources/formica/compare/v0.8.8...v0.9.0) (2026-02-04)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **resources:** improve taxonomic status ([0ea8fd2](https://github.com/identification-resources/formica/commit/0ea8fd220571f5f6ff05f517069aba10148c6888))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **resources:** include cluster data in DwC files ([60a28ac](https://github.com/identification-resources/formica/commit/60a28ace18b0760696735ca416a2e8d014d1c928))
|
|
12
|
+
* **resources:** include uncertainty of synonymy in DwC if specified ([03b6734](https://github.com/identification-resources/formica/commit/03b6734851dc9c331ec88fab21e90ad3f0303c3c))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [0.8.8](https://github.com/identification-resources/formica/compare/v0.8.7...v0.8.8) (2025-12-25)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **resources:** add 'subclass' rank ([4b10405](https://github.com/identification-resources/formica/commit/4b10405b32840c1675caf7ef24bf2bbfd821a236))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
1
25
|
## [0.8.7](https://github.com/identification-resources/formica/compare/v0.8.6...v0.8.7) (2025-11-01)
|
|
2
26
|
|
|
3
27
|
|
|
@@ -13,6 +13,7 @@ exports.RANKS = [
|
|
|
13
13
|
'phylum',
|
|
14
14
|
'subphylum',
|
|
15
15
|
'class',
|
|
16
|
+
'subclass',
|
|
16
17
|
'infraclass',
|
|
17
18
|
'superorder',
|
|
18
19
|
'order',
|
|
@@ -42,7 +43,7 @@ exports.RANKS = [
|
|
|
42
43
|
];
|
|
43
44
|
const TAXONOMIC_STATUS = {
|
|
44
45
|
'>': 'incorrect',
|
|
45
|
-
'+': '
|
|
46
|
+
'+': 'proparte synonym',
|
|
46
47
|
'=': 'synonym'
|
|
47
48
|
};
|
|
48
49
|
const RANK_LABELS = {
|
|
@@ -112,6 +113,7 @@ const SUBGENUS_PATTERN = /^([A-Z]\S+) (?:\(([A-Z]\S+?)\))(?= |$)/;
|
|
|
112
113
|
* $2c intergeneric hybrid: [A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+
|
|
113
114
|
*/
|
|
114
115
|
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-]+)(?= |$)/;
|
|
116
|
+
const CLUSTER_PATTERN = /^\[(_|\d+)\] /;
|
|
115
117
|
function compareRanks(a, b) {
|
|
116
118
|
return exports.RANKS.indexOf(a) - exports.RANKS.indexOf(b);
|
|
117
119
|
}
|
|
@@ -157,15 +159,21 @@ function parseName(name, rank, parent) {
|
|
|
157
159
|
const isSynonym = /^[+=>] /.test(name);
|
|
158
160
|
if (isSynonym) {
|
|
159
161
|
item.taxonomicStatus = TAXONOMIC_STATUS[name[0]];
|
|
160
|
-
name = name.
|
|
162
|
+
name = name.slice(2);
|
|
163
|
+
if (/^\? ?/.test(name)) {
|
|
164
|
+
item.dynamicProperties = '{"synonymUncertain":true}';
|
|
165
|
+
name = name.replace(/^\? ?/, '');
|
|
166
|
+
}
|
|
161
167
|
rank = getSynonymRank(name, parent.taxonRank);
|
|
162
168
|
}
|
|
163
169
|
else {
|
|
164
170
|
item.taxonomicStatus = 'accepted';
|
|
165
171
|
}
|
|
166
172
|
// Clusters
|
|
167
|
-
if (
|
|
168
|
-
|
|
173
|
+
if (CLUSTER_PATTERN.test(name)) {
|
|
174
|
+
const [match, cluster] = name.match(CLUSTER_PATTERN);
|
|
175
|
+
item.cluster = cluster;
|
|
176
|
+
name = name.slice(match.length);
|
|
169
177
|
}
|
|
170
178
|
// Set verbatim identification after subsequent syntax is removed.
|
|
171
179
|
item.verbatimIdentification = name.replace(/(?<=^| )x(?=$| )/g, HYBRID_SIGN).replace(/_/g, ' ');
|
|
@@ -350,5 +358,8 @@ function parseName(name, rank, parent) {
|
|
|
350
358
|
if (item.scientificNameAuthorship) {
|
|
351
359
|
item.scientificName += ` ${item.scientificNameAuthorship}`;
|
|
352
360
|
}
|
|
361
|
+
if (isSynonym && /^auctt?\./.test(item.taxonRemarks)) {
|
|
362
|
+
item.taxonomicStatus = 'misapplied';
|
|
363
|
+
}
|
|
353
364
|
return item;
|
|
354
365
|
}
|
|
@@ -153,6 +153,32 @@ function parseResource(resource) {
|
|
|
153
153
|
const offsetLine = resource.offsetLine + (header + _).split('\n').length - 1;
|
|
154
154
|
return [config, { content, offsetLine }];
|
|
155
155
|
}
|
|
156
|
+
function getTaxonChildren(parent, taxa) {
|
|
157
|
+
const children = [];
|
|
158
|
+
for (const id in taxa) {
|
|
159
|
+
if (taxa[id].parentNameUsageID === parent) {
|
|
160
|
+
children.push(taxa[id]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return children;
|
|
164
|
+
}
|
|
165
|
+
function processClusters(taxa) {
|
|
166
|
+
for (const id in taxa) {
|
|
167
|
+
const taxon = taxa[id];
|
|
168
|
+
if (taxon.taxonomicStatus !== 'accepted' || !taxon.cluster) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const dynamicProperties = taxon.dynamicProperties ? JSON.parse(taxon.dynamicProperties) : {};
|
|
172
|
+
if (taxon.cluster === '_') {
|
|
173
|
+
dynamicProperties.identifiable = false;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const siblings = getTaxonChildren(taxon.parentNameUsageID, taxa).filter(sibling => sibling.scientificNameID !== id);
|
|
177
|
+
dynamicProperties.indistinguishableFrom = siblings.filter(sibling => sibling.cluster === taxon.cluster).map(sibling => sibling.scientificNameID);
|
|
178
|
+
}
|
|
179
|
+
taxon.dynamicProperties = JSON.stringify(dynamicProperties);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
156
182
|
function parseResourceContent(content, resource, oldIds, offsetLine) {
|
|
157
183
|
var _a, _b;
|
|
158
184
|
const leafTaxonIndex = resource.metadata.levels.reduce((last, rank, i) => MAIN_RANKS.includes(rank) ? i : last, 0);
|
|
@@ -311,6 +337,7 @@ function parseResourceContent(content, resource, oldIds, offsetLine) {
|
|
|
311
337
|
if (errors.length) {
|
|
312
338
|
throw mergeParserErrors(errors);
|
|
313
339
|
}
|
|
340
|
+
processClusters(data);
|
|
314
341
|
return resource;
|
|
315
342
|
}
|
|
316
343
|
function splitResources(file) {
|
package/package.json
CHANGED
package/src/module.d.ts
CHANGED
|
@@ -56,12 +56,14 @@ interface TaxonBase {
|
|
|
56
56
|
subgenus?: string,
|
|
57
57
|
higherClassification?: string,
|
|
58
58
|
verbatimIdentification?: string,
|
|
59
|
+
dynamicProperties?: string,
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
interface WorkingTaxon extends TaxonBase {
|
|
62
63
|
// Non-standard
|
|
63
64
|
scientificNameOnly?: string,
|
|
64
|
-
incorrect?: WorkingTaxon
|
|
65
|
+
incorrect?: WorkingTaxon,
|
|
66
|
+
cluster?: string
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
interface Taxon extends TaxonBase {
|
|
@@ -11,6 +11,7 @@ export const RANKS: Rank[] = [
|
|
|
11
11
|
'phylum',
|
|
12
12
|
'subphylum',
|
|
13
13
|
'class',
|
|
14
|
+
'subclass',
|
|
14
15
|
'infraclass',
|
|
15
16
|
'superorder',
|
|
16
17
|
'order',
|
|
@@ -41,7 +42,7 @@ export const RANKS: Rank[] = [
|
|
|
41
42
|
|
|
42
43
|
const TAXONOMIC_STATUS: Record<string, TaxonStatus> = {
|
|
43
44
|
'>': 'incorrect',
|
|
44
|
-
'+': '
|
|
45
|
+
'+': 'proparte synonym',
|
|
45
46
|
'=': 'synonym'
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -122,6 +123,8 @@ const SUBGENUS_PATTERN = /^([A-Z]\S+) (?:\(([A-Z]\S+?)\))(?= |$)/
|
|
|
122
123
|
*/
|
|
123
124
|
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-]+)(?= |$)/
|
|
124
125
|
|
|
126
|
+
const CLUSTER_PATTERN = /^\[(_|\d+)\] /
|
|
127
|
+
|
|
125
128
|
function compareRanks (a: Rank, b: Rank): number {
|
|
126
129
|
return RANKS.indexOf(a) - RANKS.indexOf(b)
|
|
127
130
|
}
|
|
@@ -172,15 +175,21 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
|
|
|
172
175
|
const isSynonym = /^[+=>] /.test(name)
|
|
173
176
|
if (isSynonym) {
|
|
174
177
|
item.taxonomicStatus = TAXONOMIC_STATUS[name[0]]
|
|
175
|
-
name = name.
|
|
178
|
+
name = name.slice(2)
|
|
179
|
+
if (/^\? ?/.test(name)) {
|
|
180
|
+
item.dynamicProperties = '{"synonymUncertain":true}'
|
|
181
|
+
name = name.replace(/^\? ?/, '')
|
|
182
|
+
}
|
|
176
183
|
rank = getSynonymRank(name, parent.taxonRank as Rank)
|
|
177
184
|
} else {
|
|
178
185
|
item.taxonomicStatus = 'accepted'
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
// Clusters
|
|
182
|
-
if (
|
|
183
|
-
|
|
189
|
+
if (CLUSTER_PATTERN.test(name)) {
|
|
190
|
+
const [match, cluster] = name.match(CLUSTER_PATTERN) as string[]
|
|
191
|
+
item.cluster = cluster
|
|
192
|
+
name = name.slice(match.length)
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
// Set verbatim identification after subsequent syntax is removed.
|
|
@@ -375,5 +384,9 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
|
|
|
375
384
|
item.scientificName += ` ${item.scientificNameAuthorship}`
|
|
376
385
|
}
|
|
377
386
|
|
|
387
|
+
if (isSynonym && /^auctt?\./.test(item.taxonRemarks)) {
|
|
388
|
+
item.taxonomicStatus = 'misapplied'
|
|
389
|
+
}
|
|
390
|
+
|
|
378
391
|
return item
|
|
379
392
|
}
|
|
@@ -134,6 +134,36 @@ function parseResource (resource: FilePart): [ResourceMetadata, FilePart] {
|
|
|
134
134
|
return [config, { content, offsetLine }]
|
|
135
135
|
}
|
|
136
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
|
+
|
|
137
167
|
function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds: number[], offsetLine: number): Resource {
|
|
138
168
|
const leafTaxonIndex = resource.metadata.levels.reduce((last, rank, i) => MAIN_RANKS.includes(rank) ? i : last, 0)
|
|
139
169
|
const data = resource.taxa as Record<TaxonId, WorkingTaxon>
|
|
@@ -307,6 +337,8 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
307
337
|
throw mergeParserErrors(errors)
|
|
308
338
|
}
|
|
309
339
|
|
|
340
|
+
processClusters(data)
|
|
341
|
+
|
|
310
342
|
return resource
|
|
311
343
|
}
|
|
312
344
|
|
package/test/resources.js
CHANGED
|
@@ -166,6 +166,27 @@ Polistes Latreille, 1802
|
|
|
166
166
|
assert.strictEqual(resource.taxa['T1:1:2'].genericName, 'Polistes')
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
+
test('handles clusters', () => {
|
|
170
|
+
const [resource] = resources.parseTextFile(`---
|
|
171
|
+
levels: [genus, species]
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
Eurydema Laporte, 1833
|
|
175
|
+
oleracea (Linnaeus, 1758)
|
|
176
|
+
[1] rotundicollis (Dohrn, 1860)
|
|
177
|
+
[1] fieberi Schummel, 1837
|
|
178
|
+
[2] ornata (Linnaeus, 1758)
|
|
179
|
+
[2] ventralis Kolenati, 1846
|
|
180
|
+
[_] eckerleini Josifov, 1961
|
|
181
|
+
`, 'T1')
|
|
182
|
+
assert.strictEqual(resource.taxa['T1:1:2'].dynamicProperties, undefined)
|
|
183
|
+
assert.strictEqual(resource.taxa['T1:1:3'].dynamicProperties, '{"indistinguishableFrom":["T1:1:4"]}')
|
|
184
|
+
assert.strictEqual(resource.taxa['T1:1:4'].dynamicProperties, '{"indistinguishableFrom":["T1:1:3"]}')
|
|
185
|
+
assert.strictEqual(resource.taxa['T1:1:5'].dynamicProperties, '{"indistinguishableFrom":["T1:1:6"]}')
|
|
186
|
+
assert.strictEqual(resource.taxa['T1:1:6'].dynamicProperties, '{"indistinguishableFrom":["T1:1:5"]}')
|
|
187
|
+
assert.strictEqual(resource.taxa['T1:1:7'].dynamicProperties, '{"identifiable":false}')
|
|
188
|
+
})
|
|
189
|
+
|
|
169
190
|
suite('leaf taxa checks', () => {
|
|
170
191
|
test('errors for missing leaf taxa', () => {
|
|
171
192
|
assert.throws(() => {
|