@larsgw/formica 0.6.3 → 0.6.5

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,23 @@
1
+ ## [0.6.5](https://github.com/identification-resources/formica/compare/v0.6.4...v0.6.5) (2024-12-20)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **resources:** ignore empty lines in diff mode ([3fb027e](https://github.com/identification-resources/formica/commit/3fb027e8968196910fff2e574e700ac0abee996c))
7
+ * **resources:** improve (ICBN) author parsing ([d3e3faf](https://github.com/identification-resources/formica/commit/d3e3faf411d1a7dccd528a5ee78bd0823a5883b4)), closes [#13](https://github.com/identification-resources/formica/issues/13) [#14](https://github.com/identification-resources/formica/issues/14)
8
+ * **resources:** support "et al." in author name ([82993bf](https://github.com/identification-resources/formica/commit/82993bffac5e7f3ed00d00c28c836f347b264a43))
9
+
10
+
11
+
12
+ ## [0.6.4](https://github.com/identification-resources/formica/compare/v0.6.3...v0.6.4) (2024-12-16)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **resources:** fix matching of some ICBN names ([3c18458](https://github.com/identification-resources/formica/commit/3c184586b64e2a31f1eb7298bcd7c80c1d119069))
18
+
19
+
20
+
1
21
  ## [0.6.3](https://github.com/identification-resources/formica/compare/v0.6.2...v0.6.3) (2024-04-12)
2
22
 
3
23
 
@@ -398,9 +398,9 @@ var ResourceProcessor = /** @class */ (function () {
398
398
  };
399
399
  ResourceProcessor.prototype.processResourceDwc = function (resource, config) {
400
400
  return __awaiter(this, void 0, void 0, function () {
401
- var file, _a, header_1, rows, _b, _c, _i, rows_1, row, oldTaxon, taxon, filteredResults, taxonNames, names, id, name_1, result, _d, _e, results, _f, name_2, matches, _g, matches_1, match, source, currentRank, _h, _j, loirId, taxon, _k, amendResource, groupNameMatches, groupedNameMatches, amendedResource, _l, _m, _o, _p, source, matches;
402
- return __generator(this, function (_q) {
403
- switch (_q.label) {
401
+ var file, _a, header_1, rows, _b, _c, _i, rows_1, row, oldTaxon, taxon, filteredResults, taxonNames, names, id, name_1, result, _loop_2, _d, _e, results, _f, amendResource, groupNameMatches, groupedNameMatches, amendedResource, _g, _h, _j, _k, source, matches;
402
+ return __generator(this, function (_l) {
403
+ switch (_l.label) {
404
404
  case 0:
405
405
  console.log("".concat(resource.workId, ": matching ").concat(resource.id));
406
406
  if (!!config.updateMappings) return [3 /*break*/, 3];
@@ -409,7 +409,7 @@ var ResourceProcessor = /** @class */ (function () {
409
409
  _c = (_b = index_1.csv).parseCsv;
410
410
  return [4 /*yield*/, fs_1.promises.readFile(file, 'utf-8')];
411
411
  case 1:
412
- _a = _c.apply(_b, [_q.sent()]), header_1 = _a[0], rows = _a.slice(1);
412
+ _a = _c.apply(_b, [_l.sent()]), header_1 = _a[0], rows = _a.slice(1);
413
413
  for (_i = 0, rows_1 = rows; _i < rows_1.length; _i++) {
414
414
  row = rows_1[_i];
415
415
  oldTaxon = row.reduce(function (taxon, value, index) {
@@ -424,7 +424,7 @@ var ResourceProcessor = /** @class */ (function () {
424
424
  taxon.gbifAcceptedTaxonID = oldTaxon.gbifAcceptedTaxonID;
425
425
  }
426
426
  }
427
- _q.label = 2;
427
+ _l.label = 2;
428
428
  case 2: return [2 /*return*/, resource];
429
429
  case 3:
430
430
  filteredResults = {};
@@ -441,17 +441,23 @@ var ResourceProcessor = /** @class */ (function () {
441
441
  }
442
442
  return [4 /*yield*/, runGnverifier(Array.from(names).join('\n'))];
443
443
  case 4:
444
- result = _q.sent();
445
- for (_d = 0, _e = result.trim().split('\n'); _d < _e.length; _d++) {
446
- results = _e[_d];
447
- _f = JSON.parse(results), name_2 = _f.name, matches = _f.results;
444
+ result = _l.sent();
445
+ _loop_2 = function (results) {
446
+ var _m = JSON.parse(results), name_2 = _m.name, matches = _m.results;
448
447
  if (!matches) {
449
- continue;
448
+ return "continue";
450
449
  }
451
- for (_g = 0, matches_1 = matches; _g < matches_1.length; _g++) {
452
- match = matches_1[_g];
453
- source = match.dataSourceId;
454
- currentRank = match.classificationRanks.split('|').pop();
450
+ // Fix author scoring for some species, see https://github.com/gnames/gnverifier/issues/129
451
+ matches.sort(function (a, b) {
452
+ if (a.sortScore !== b.sortScore) {
453
+ return b.sortScore - a.sortScore;
454
+ }
455
+ return name_2 === a.matchedName ? -1 : name_2 === b.matchedName ? 1 : 0;
456
+ });
457
+ for (var _o = 0, _p = matches; _o < _p.length; _o++) {
458
+ var match = _p[_o];
459
+ var source = match.dataSourceId;
460
+ var currentRank = match.classificationRanks.split('|').pop();
455
461
  if (match.scoreDetails.cardinalityScore === 0) {
456
462
  // Rank mismatch
457
463
  continue;
@@ -460,9 +466,9 @@ var ResourceProcessor = /** @class */ (function () {
460
466
  // GBIF species like "Nomada spec"
461
467
  continue;
462
468
  }
463
- for (_h = 0, _j = taxonNames[name_2]; _h < _j.length; _h++) {
464
- loirId = _j[_h];
465
- taxon = resource.taxa[loirId];
469
+ for (var _q = 0, _r = taxonNames[name_2]; _q < _r.length; _q++) {
470
+ var loirId = _r[_q];
471
+ var taxon = resource.taxa[loirId];
466
472
  if (source === 11 && !GBIF_RANKS.includes(taxon.taxonRank)) {
467
473
  // Exclude GBIF matches for ranks that are not in GBIF
468
474
  continue;
@@ -483,30 +489,34 @@ var ResourceProcessor = /** @class */ (function () {
483
489
  });
484
490
  }
485
491
  }
492
+ };
493
+ for (_d = 0, _e = result.trim().split('\n'); _d < _e.length; _d++) {
494
+ results = _e[_d];
495
+ _loop_2(results);
486
496
  }
487
497
  return [4 /*yield*/, Promise.resolve().then(function () { return require('../index'); })];
488
498
  case 5:
489
- _k = (_q.sent()).taxonNames, amendResource = _k.amendResource, groupNameMatches = _k.groupNameMatches;
499
+ _f = (_l.sent()).taxonNames, amendResource = _f.amendResource, groupNameMatches = _f.groupNameMatches;
490
500
  groupedNameMatches = groupNameMatches(filteredResults);
491
501
  amendedResource = __assign(__assign({}, resource), { taxa: __assign({}, resource.taxa) });
492
- _l = groupedNameMatches;
493
- _m = [];
494
- for (_o in _l)
495
- _m.push(_o);
496
- _p = 0;
497
- _q.label = 6;
502
+ _g = groupedNameMatches;
503
+ _h = [];
504
+ for (_j in _g)
505
+ _h.push(_j);
506
+ _k = 0;
507
+ _l.label = 6;
498
508
  case 6:
499
- if (!(_p < _m.length)) return [3 /*break*/, 9];
500
- _o = _m[_p];
501
- if (!(_o in _l)) return [3 /*break*/, 8];
502
- source = _o;
509
+ if (!(_k < _h.length)) return [3 /*break*/, 9];
510
+ _j = _h[_k];
511
+ if (!(_j in _g)) return [3 /*break*/, 8];
512
+ source = _j;
503
513
  return [4 /*yield*/, this.selectPrefixes(resource, groupedNameMatches, source)];
504
514
  case 7:
505
- matches = _q.sent();
515
+ matches = _l.sent();
506
516
  amendResource(amendedResource, source, matches);
507
- _q.label = 8;
517
+ _l.label = 8;
508
518
  case 8:
509
- _p++;
519
+ _k++;
510
520
  return [3 /*break*/, 6];
511
521
  case 9: return [2 /*return*/, amendedResource];
512
522
  }
@@ -93,6 +93,18 @@ var RANK_LABELS_REVERSE = {
93
93
  'ssp': 'subspecies',
94
94
  'subsp': 'subspecies'
95
95
  };
96
+ /**
97
+ * 1. Any number of
98
+ * - capitalized words
99
+ * - "&"
100
+ * - " in "
101
+ * - " ex "
102
+ * - lowercase name particles
103
+ * 2. Followed by a capitalized word
104
+ * 3. Optionally, followed by "et al."
105
+ */
106
+ var LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van'].join('|');
107
+ var SIMPLE_AUTHOR_PATTERN = '(?:(?:\\p{Lu}\\S*|&|in|ex|' + LOWERCASE_NAME_PARTICLES + ')\\s*)*\\p{Lu}\\S+(?:\\s+et\\s+al\\.)?';
96
108
  var NAME_PATTERN = new RegExp('^' +
97
109
  // $1 main name part
98
110
  '(\\S+)' +
@@ -101,20 +113,14 @@ var NAME_PATTERN = new RegExp('^' +
101
113
  // but not auct(t)., etc.
102
114
  '(?!auctt?\\.|(?:syn|comb|sp|spec)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.))' +
103
115
  '(' +
104
- // $2.1 anything in parentheses
105
- '\\(.+?\\)' +
116
+ // $2.1 anything in parentheses, followed by optional revising author(s)
117
+ '\\(.+?\\)(?:\\s+' + SIMPLE_AUTHOR_PATTERN + ')?' +
106
118
  '|' +
107
119
  // $2.2 anything followed by a year
108
120
  '.+?\\d{4}\\)?' +
109
121
  '|' +
110
- // $2.3 name(, name)* & name
111
- '.+(?:, .+)* & \\S+' +
112
- '|' +
113
- // $2.4 name y name
114
- '\\S+ [yY] \\S+' +
115
- '|' +
116
- // $2.5 name( in name)
117
- '\\p{Lu}\\S*(?: in \\S+)?' +
122
+ // $2.3 author(s)
123
+ SIMPLE_AUTHOR_PATTERN +
118
124
  '))?' +
119
125
  // $3 optional notes
120
126
  '(?:,? (.+))?' +
@@ -541,6 +547,8 @@ function parseFile(file, id, old) {
541
547
  var diff;
542
548
  if (oldResources[index]) {
543
549
  diff = (0, diff_resource_1.createDiff)(content, parseResource(oldResources[index])[1]);
550
+ // Ignore empty lines
551
+ diff = diff.filter(function (line) { return line.text !== ''; });
544
552
  }
545
553
  else {
546
554
  diff = (0, diff_resource_1.createDiff)(content, content);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larsgw/formica",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
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",
@@ -287,13 +287,38 @@ class ResourceProcessor {
287
287
 
288
288
  const result = await runGnverifier(Array.from(names).join('\n'))
289
289
  for (const results of result.trim().split('\n')) {
290
+ interface MatchScoreDetails {
291
+ cardinalityScore: number;
292
+ }
293
+
294
+ interface Match {
295
+ currentRecordId: string;
296
+ dataSourceId: number;
297
+ matchedName: string;
298
+ recordId: string;
299
+ sortScore: number;
300
+ isSynonym: boolean;
301
+ classificationPath: string;
302
+ classificationRanks: string;
303
+ scoreDetails: MatchScoreDetails;
304
+ }
305
+
290
306
  const { name, results: matches } = JSON.parse(results)
291
307
 
292
308
  if (!matches) {
293
309
  continue
294
310
  }
295
311
 
296
- for (const match of matches) {
312
+ // Fix author scoring for some species, see https://github.com/gnames/gnverifier/issues/129
313
+ matches.sort((a: Match, b: Match) => {
314
+ if (a.sortScore !== b.sortScore) {
315
+ return b.sortScore - a.sortScore
316
+ }
317
+
318
+ return name === a.matchedName ? -1 : name === b.matchedName ? 1 : 0
319
+ })
320
+
321
+ for (const match of matches as Match[]) {
297
322
  const source = match.dataSourceId
298
323
  const currentRank = match.classificationRanks.split('|').pop()
299
324
 
@@ -87,6 +87,19 @@ const RANK_LABELS_REVERSE: Record<string, Rank> = {
87
87
  'subsp': 'subspecies'
88
88
  }
89
89
 
90
+ /**
91
+ * 1. Any number of
92
+ * - capitalized words
93
+ * - "&"
94
+ * - " in "
95
+ * - " ex "
96
+ * - lowercase name particles
97
+ * 2. Followed by a capitalized word
98
+ * 3. Optionally, followed by "et al."
99
+ */
100
+ const LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van'].join('|')
101
+ const SIMPLE_AUTHOR_PATTERN = '(?:(?:\\p{Lu}\\S*|&|in|ex|' + LOWERCASE_NAME_PARTICLES + ')\\s*)*\\p{Lu}\\S+(?:\\s+et\\s+al\\.)?'
102
+
90
103
  const NAME_PATTERN = new RegExp(
91
104
  '^' +
92
105
  // $1 main name part
@@ -96,20 +109,14 @@ const NAME_PATTERN = new RegExp(
96
109
  // but not auct(t)., etc.
97
110
  '(?!auctt?\\.|(?:syn|comb|sp|spec)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.))' +
98
111
  '(' +
99
- // $2.1 anything in parentheses
100
- '\\(.+?\\)' +
112
+ // $2.1 anything in parentheses, followed by optional revising author(s)
113
+ '\\(.+?\\)(?:\\s+' + SIMPLE_AUTHOR_PATTERN + ')?' +
101
114
  '|' +
102
115
  // $2.2 anything followed by a year
103
116
  '.+?\\d{4}\\)?' +
104
117
  '|' +
105
- // $2.3 name(, name)* & name
106
- '.+(?:, .+)* & \\S+' +
107
- '|' +
108
- // $2.4 name y name
109
- '\\S+ [yY] \\S+' +
110
- '|' +
111
- // $2.5 name( in name)
112
- '\\p{Lu}\\S*(?: in \\S+)?' +
118
+ // $2.3 author(s)
119
+ SIMPLE_AUTHOR_PATTERN +
113
120
  '))?' +
114
121
  // $3 optional notes
115
122
  '(?:,? (.+))?' +
@@ -573,6 +580,8 @@ export function parseFile (file: string, id: WorkId, old?: ResourceHistory): Res
573
580
  let diff
574
581
  if (oldResources[index]) {
575
582
  diff = createDiff(content, parseResource(oldResources[index])[1])
583
+ // Ignore empty lines
584
+ diff = diff.filter(line => line.text !== '')
576
585
  } else {
577
586
  diff = createDiff(content, content)
578
587
  }