@natlibfi/melinda-record-matching 5.0.3 → 5.0.5-alpha.2
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/.github/workflows/melinda-node-tests-and-publish.yml +4 -4
- package/dist/candidate-search/query-list/bib.js +84 -67
- package/dist/candidate-search/query-list/bib.js.map +3 -3
- package/dist/candidate-search/query-list/component.js +94 -28
- package/dist/candidate-search/query-list/component.js.map +3 -3
- package/dist/cli.js +12 -4
- package/dist/cli.js.map +2 -2
- package/dist/match-detection/features/bib/f773.js +82 -0
- package/dist/match-detection/features/bib/f773.js.map +7 -0
- package/dist/match-detection/features/bib/index.js +1 -1
- package/dist/match-detection/features/bib/index.js.map +2 -2
- package/dist/match-detection/features/bib/index.test.js +1 -1
- package/dist/match-detection/features/bib/index.test.js.map +2 -2
- package/dist/match-detection/features/bib/isbn.js +47 -48
- package/dist/match-detection/features/bib/isbn.js.map +2 -2
- package/dist/match-detection/features/bib/issn.js +2 -16
- package/dist/match-detection/features/bib/issn.js.map +2 -2
- package/dist/match-detection/features/bib/language.js +12 -22
- package/dist/match-detection/features/bib/language.js.map +2 -2
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +6 -2
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +2 -2
- package/dist/match-detection/features/bib/publication-time.js +62 -16
- package/dist/match-detection/features/bib/publication-time.js.map +2 -2
- package/dist/match-detection/features/bib/title.js +76 -26
- package/dist/match-detection/features/bib/title.js.map +3 -3
- package/dist/match-detection/index.js +8 -3
- package/dist/match-detection/index.js.map +2 -2
- package/dist/matching-utils.js +14 -0
- package/dist/matching-utils.js.map +2 -2
- package/package.json +11 -12
- package/src/candidate-search/query-list/bib.js +100 -77
- package/src/candidate-search/query-list/component.js +100 -23
- package/src/cli.js +8 -4
- package/src/match-detection/features/bib/f773.js +118 -0
- package/src/match-detection/features/bib/index.js +1 -1
- package/src/match-detection/features/bib/index.test.js +1 -1
- package/src/match-detection/features/bib/isbn.js +66 -60
- package/src/match-detection/features/bib/issn.js +3 -32
- package/src/match-detection/features/bib/language.js +15 -26
- package/src/match-detection/features/bib/publication-time-allow-cons-years.js +7 -4
- package/src/match-detection/features/bib/publication-time.js +76 -30
- package/src/match-detection/features/bib/title.js +112 -34
- package/src/match-detection/index.js +9 -3
- package/src/matching-utils.js +17 -0
- package/dist/match-detection/features/bib/title-version-original.js +0 -37
- package/dist/match-detection/features/bib/title-version-original.js.map +0 -7
- package/src/match-detection/features/bib/title-version-original.js +0 -52
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// NB! This checks identifiers ($w=ID, $x=ISSN, $z=ISBN and $o=others) to check whether the host is the same.
|
|
2
|
+
// BB! $g is not checked *yet*. It could very strongly indicate that the records are the same... (I doubt $q could be useful here)
|
|
3
|
+
// Rationale:
|
|
4
|
+
// 773$w indicates sameness. However, non-Melinda records probably refer to non-melinda hosts.
|
|
5
|
+
// Thus check other identifiers subfields ($x, $z and $o as well)
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import createDebugLogger from 'debug';
|
|
10
|
+
|
|
11
|
+
import {isComponentRecord} from '@natlibfi/melinda-commons';
|
|
12
|
+
import {uniqArray} from './issn.js';
|
|
13
|
+
import {parse773g} from '../../../candidate-search/query-list/component.js';
|
|
14
|
+
|
|
15
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:issn');
|
|
16
|
+
const debugData = debug.extend('data');
|
|
17
|
+
|
|
18
|
+
const MAX_IDENTIFIER = 0.1; // This should be pretty low: it only says something about the host
|
|
19
|
+
const MAX_G = 0.5; // $g is about the comp itself, so score can be high here
|
|
20
|
+
|
|
21
|
+
export default () => ({
|
|
22
|
+
name: 'f773 ',
|
|
23
|
+
extract: ({record/*, recordExternal*/}) => {
|
|
24
|
+
//const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
|
|
25
|
+
|
|
26
|
+
if (!isComponentRecord(record, false, [])) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const f773s = record.get(/^773$/u); // Identifier extract handles multiple 773...
|
|
30
|
+
|
|
31
|
+
// I think it's ok, if, say, $o and $x values match, so I'm not keeping the subfield codes
|
|
32
|
+
const values = f773s.map(f => f.subfields)
|
|
33
|
+
.flat()
|
|
34
|
+
.filter(sf => ['w', 'o', 'x', 'z'].includes(sf.code))
|
|
35
|
+
.map(sf => normalizeValue(sf.value));
|
|
36
|
+
|
|
37
|
+
// $g: only on $g is supported
|
|
38
|
+
const gData = parse773g(f773s[0]);
|
|
39
|
+
|
|
40
|
+
return [uniqArray(values), gData];
|
|
41
|
+
|
|
42
|
+
function normalizeValue(value) {
|
|
43
|
+
return value.replace(/(?:\. -|\.)$/u, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
compare: (aa, bb) => {
|
|
50
|
+
const [aIdentifiers, ag] = aa;
|
|
51
|
+
const [bIdentifiers, bg] = bb;
|
|
52
|
+
|
|
53
|
+
const identifierScore = scoreIdentifiers();
|
|
54
|
+
const gScore = scoreG();
|
|
55
|
+
|
|
56
|
+
if (identifierScore === MAX_IDENTIFIER && gScore === MAX_G) { // Pretty impressive hit, even if title matches not
|
|
57
|
+
return 1.0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return identifierScore + gScore;
|
|
61
|
+
|
|
62
|
+
function scoreG() {
|
|
63
|
+
// NB! $g contents are very noise, so wrong values may be extracted. Thus do not overpenalize.
|
|
64
|
+
|
|
65
|
+
// All exist match: things must be pretty good:
|
|
66
|
+
if (ag.number && ag.number === bg.number && ag.pages && ag.pages === bg.pages && ag.year && ag.year === bg.year) {
|
|
67
|
+
return MAX_G;
|
|
68
|
+
}
|
|
69
|
+
// Not comparing volume. It correlates with year.
|
|
70
|
+
return scoreYear() + scoreNumber() + scorePages();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function scoreNumber() {
|
|
74
|
+
if (!ag.number || !bg.number) {
|
|
75
|
+
return 0.0;
|
|
76
|
+
}
|
|
77
|
+
if (ag.number === bg.number) {
|
|
78
|
+
return 0.05;
|
|
79
|
+
}
|
|
80
|
+
return -0.02;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function scorePages() {
|
|
84
|
+
if (!ag.pages || !bg.pages) {
|
|
85
|
+
return 0.0;
|
|
86
|
+
}
|
|
87
|
+
if (ag.pages === bg.pages) { // If pages match, things must be pretty good
|
|
88
|
+
return 0.1;
|
|
89
|
+
}
|
|
90
|
+
return -0.05;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function scoreYear() { // publication-time.js also uses this, so don't score heavily here
|
|
94
|
+
if (!ag.year || !bg.year) {
|
|
95
|
+
return 0.0;
|
|
96
|
+
}
|
|
97
|
+
if (ag.year === bg.year) {
|
|
98
|
+
return 0.02;
|
|
99
|
+
}
|
|
100
|
+
return -0.02;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function scoreIdentifiers() {
|
|
104
|
+
debugData(`Comparing ISSN sets ${JSON.stringify(aIdentifiers)} and ${JSON.stringify(bIdentifiers)}`);
|
|
105
|
+
if (aIdentifiers.length === 0 || bIdentifiers.length === 0) {
|
|
106
|
+
// No data for decision
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const firstSharedIdentifier = aIdentifiers.find(val => bIdentifiers.includes(val));
|
|
110
|
+
if (firstSharedIdentifier) {
|
|
111
|
+
debug(`\t Shared identifier found: '${firstSharedIdentifier}'`);
|
|
112
|
+
return MAX_IDENTIFIER;
|
|
113
|
+
}
|
|
114
|
+
return -0.5;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
@@ -4,7 +4,6 @@ export {default as isbn} from './isbn.js';
|
|
|
4
4
|
export {default as issn} from './issn.js';
|
|
5
5
|
export {default as otherStandardIdentifier} from './other-standard-identifier.js';
|
|
6
6
|
export {default as title} from './title.js';
|
|
7
|
-
export {default as titleVersionOriginal} from './title-version-original.js';
|
|
8
7
|
export {default as authors} from './authors.js';
|
|
9
8
|
export {default as recordType} from './record-type.js';
|
|
10
9
|
export {default as publicationTime} from './publication-time.js';
|
|
@@ -15,3 +14,4 @@ export {default as bibliographicLevel} from './bibliographic-level.js';
|
|
|
15
14
|
export {default as melindaId} from './melinda-id.js';
|
|
16
15
|
export {default as allSourceIds} from './all-source-ids.js';
|
|
17
16
|
export {default as mediaType} from './media-type.js';
|
|
17
|
+
export {default as f773} from './f773.js';
|
|
@@ -42,7 +42,7 @@ describe('match-detection/features/bib/', () => {
|
|
|
42
42
|
const {featuresA, featuresB, expectedPoints} = expectations;
|
|
43
43
|
const {compare} = features[feature](options);
|
|
44
44
|
|
|
45
|
-
assert.equal(compare(featuresA, featuresB), expectedPoints);
|
|
45
|
+
assert.equal(Math.round(compare(featuresA, featuresB) *100)/100, expectedPoints);
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -3,8 +3,6 @@ import createDebugLogger from 'debug';
|
|
|
3
3
|
import {parse as isbnParse} from 'isbn3';
|
|
4
4
|
|
|
5
5
|
import {uniqArray} from './issn.js';
|
|
6
|
-
import {isComponentRecord} from '@natlibfi/melinda-commons';
|
|
7
|
-
|
|
8
6
|
|
|
9
7
|
const debug = createDebugLogger(`@natlibfi/melinda-record-matching:match-detection:features:standard-identifiers:ISBN`);
|
|
10
8
|
const debugData = debug.extend('data');
|
|
@@ -16,12 +14,7 @@ export default () => ({
|
|
|
16
14
|
extract: ({record/*, recordExternal*/}) => {
|
|
17
15
|
//const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (host) {
|
|
22
|
-
return record.get('020').filter(f => f.subfields?.some(sf => ['a', 'z'].includes(sf.code) && sf.value));
|
|
23
|
-
}
|
|
24
|
-
return record.get('773').filter(f => f.subfields?.some(sf => ['z'].includes(sf.code) && sf.value));
|
|
17
|
+
return record.get('020').filter(f => f.subfields?.some(sf => ['a', 'z'].includes(sf.code) && sf.value));
|
|
25
18
|
},
|
|
26
19
|
// eslint-disable-next-line max-statements
|
|
27
20
|
compare: (aa, bb) => {
|
|
@@ -31,52 +24,72 @@ export default () => ({
|
|
|
31
24
|
return 0;
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
const
|
|
27
|
+
const subfieldCodeForGoodValues = 'a';
|
|
28
|
+
const subfieldCodeForBadValues = 'z';
|
|
35
29
|
|
|
36
|
-
const [
|
|
37
|
-
|
|
38
|
-
debug(`GOOD VALUES (A): ${goodValuesA.join(', ')}`);
|
|
39
|
-
}
|
|
40
|
-
if (badValuesA.length) {
|
|
41
|
-
debug(`BAD VALUES (A): ${badValuesA.join(', ')}`);
|
|
42
|
-
}
|
|
30
|
+
const [aValidValuesA, aInvalidValuesA, zValidValuesA, zInvalidValuesA] = getValuesWrapper(aa, 'AA'); // initial 'a' and 'z' refer to 020 subfields codes
|
|
31
|
+
const [aValidValuesB, aInvalidValuesB, zValidValuesB, zInvalidValuesB] = getValuesWrapper(bb, 'BB');
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
function getValuesWrapper(data, prefix) {
|
|
34
|
+
const [aValidValues, aInvalidValues, zValidValues, zInvalidValues] = getValues(data);
|
|
35
|
+
if (aValidValues.length) {
|
|
36
|
+
debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForGoodValues} VALID: ${aValidValues.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
if (aInvalidValues.length) {
|
|
39
|
+
debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForGoodValues} INVALID: ${aInvalidValues.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
if (zValidValues.length) {
|
|
42
|
+
debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForBadValues} VALID: ${zValidValues.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
if (zInvalidValues.length) {
|
|
45
|
+
debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForBadValues} INVALID: ${zInvalidValues.join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
return [aValidValues, aInvalidValues, zValidValues, zInvalidValues];
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
const [sharedGoodValues, goodValuesAOnly, goodValuesBOnly] = getUnionData(
|
|
50
|
+
const [sharedGoodValues, goodValuesAOnly, goodValuesBOnly] = getUnionData(aValidValuesA, aValidValuesB);
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
debug(`GOOD\tBOTH: ${sharedGoodValues.length}, A only: ${goodValuesAOnly.length}, B only: ${goodValuesBOnly.length}`);
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
if (sharedGoodValues.length > 0) {
|
|
55
|
+
// Third argument (aka 'N times') is >= 0
|
|
56
|
+
return scoreData(MAX_SCORE, 0.8, goodValuesAOnly.length + goodValuesBOnly.length);
|
|
57
|
+
}
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
59
|
+
const hitScore = scoreSuboptimalHit();
|
|
60
|
+
|
|
61
|
+
function scoreSuboptimalHit() {
|
|
62
62
|
// One record consider ISBN good and the other record considered it's canceled:
|
|
63
|
-
if (
|
|
63
|
+
if (aValidValuesA.some(valA => zValidValuesB.includes(valA)) || aValidValuesB.some(valB => zValidValuesA.includes(valB))) {
|
|
64
64
|
return MAX_SCORE;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
//
|
|
67
|
+
// Subfield is for cancelled/whatever values, but the value is syntactically valid:
|
|
68
68
|
// Could happen for two canceled ISBNs for example. I'll give this two thirds of the full score
|
|
69
|
-
|
|
69
|
+
const zzValid = zValidValuesA.find(valA => zValidValuesB.includes(valA));
|
|
70
|
+
if (zzValid) {
|
|
71
|
+
debug(`Both contain a valid value in 020$z: ${zzValid}`);
|
|
72
|
+
return MAX_SCORE * 2 / 3;
|
|
73
|
+
}
|
|
74
|
+
// Shared invalid identifiers:
|
|
75
|
+
const aaInvalid = aInvalidValuesA.find(valA => aInvalidValuesB.includes(valA) || zInvalidValuesB.includes(valA)) || aInvalidValuesB.find(valB => zInvalidValuesA.includes(valB));
|
|
76
|
+
if (aaInvalid) {
|
|
77
|
+
debug(`Shared invalid value in 020$a and 020$a-or-$z subfields: ${aaInvalid}`);
|
|
70
78
|
return MAX_SCORE * 2 / 3;
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
/* // Currently I think that paired invalid idenfiers in 020$z are meaningless...
|
|
82
|
+
const zzInvalid = zInvalidValuesB.find(valB => zInvalidValuesA.includes(valB)) || zInvalidValuesA.find(valA => zInvalidValuesB.includes(valA));
|
|
83
|
+
if (zzInvalid) {
|
|
84
|
+
debug(`Shared invalid value in 020$z subfields: ${zzInvalid}`);
|
|
85
|
+
return MAX_SCORE / 3;
|
|
86
|
+
}
|
|
87
|
+
*/
|
|
88
|
+
|
|
73
89
|
return 0;
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
|
|
77
|
-
// Third argument (aka 'N times') is >= 0
|
|
78
|
-
return scoreData(hitScore, 0.8, goodValuesAOnly.length + goodValuesBOnly.length);
|
|
79
|
-
}
|
|
92
|
+
|
|
80
93
|
|
|
81
94
|
if (hitScore === MAX_SCORE) {
|
|
82
95
|
// -1 is needed to make the third argument >= 0 (otherwise min val would be 0)
|
|
@@ -90,41 +103,34 @@ export default () => ({
|
|
|
90
103
|
|
|
91
104
|
// No match:
|
|
92
105
|
|
|
93
|
-
if (
|
|
106
|
+
if (aValidValuesA.length + aInvalidValuesA.length === 0 || aValidValuesB.length + aInvalidValuesB.length === 0) { // At least one record did not have any good ISBNs, so not penalizing here! (Invalid 020$as are counted a bad.)
|
|
94
107
|
return 0.0;
|
|
95
108
|
}
|
|
109
|
+
// We have same matching invalid identifiers. Don't penalize:
|
|
110
|
+
const allA = [...aValidValuesA, ...aInvalidValuesA, ...zValidValuesA, ...zInvalidValuesA];
|
|
111
|
+
const allB = [...aValidValuesB, ...aInvalidValuesB, ...zValidValuesB, ...zInvalidValuesB];
|
|
112
|
+
const [sharedValues, tmp1, tmp2] = getUnionData(allA, allB);
|
|
113
|
+
debug(`WHATEVER\tBOTH: ${sharedGoodValues.length}, A only: ${tmp1.length}, B only: ${tmp2.length}`);
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
function getSubfieldCodes(tag) {
|
|
101
|
-
if (tag === '773') {
|
|
102
|
-
return ['z', undefined];
|
|
103
|
-
}
|
|
104
|
-
return ['a', 'z'];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function looksGood(val) {
|
|
108
|
-
// isbn10 can end in X:
|
|
109
|
-
if (/^([0-9]-?){9}[0-9X]$/u.test(val)) {
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
// isbn13 can not:
|
|
113
|
-
if (/^([0-9]-?){12}[0-9]$/u.test(val)) {
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
115
|
+
if (sharedValues.length > 0) {
|
|
116
|
+
return 0;
|
|
117
117
|
}
|
|
118
|
+
// We have values but they disagree:
|
|
119
|
+
return -0.75; // Has good ISBNs on both records, but they did not match
|
|
118
120
|
|
|
119
121
|
function getValues(fields) {
|
|
122
|
+
// Valid values are normalized to their isbn-13 form. Invalid values get their '-'s removed.
|
|
120
123
|
const goodValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForGoodValues)).map(sf => validatorAndNormalizer(sf.value));
|
|
121
124
|
const trueGoodValues = goodValues.filter(val => val.valid).map(val => val.value);
|
|
122
125
|
const wannabeGoodValues = goodValues.filter(val => !val.valid).map(val => val.value);
|
|
123
126
|
if (!subfieldCodeForBadValues) { // 773
|
|
124
|
-
return [trueGoodValues, wannabeGoodValues];
|
|
127
|
+
return [trueGoodValues, wannabeGoodValues, [], []];
|
|
125
128
|
}
|
|
126
|
-
const badValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForBadValues)).map(sf => sf.value);
|
|
127
|
-
|
|
129
|
+
const badValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForBadValues)).map(sf => validatorAndNormalizer(sf.value));
|
|
130
|
+
const validBadValues = badValues.filter(val => val.valid).map(val => val.value);
|
|
131
|
+
const invalidBadValues = badValues.filter(val => !val.valid).map(val => val.value);
|
|
132
|
+
//const badValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForBadValues)).map(sf => validatorAndNormalizer(sf.value).value);
|
|
133
|
+
return [uniqArray(trueGoodValues), uniqArray(wannabeGoodValues), uniqArray(validBadValues), uniqArray(invalidBadValues)];
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
function validatorAndNormalizer(string) {
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
/*
|
|
2
|
-
//import createInterface from './standard-identifier-factory.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export default () => {
|
|
6
|
-
const {extract, compare} = createInterface({pattern: /^022$/u, subfieldCodes: ['a', 'z', 'y']});
|
|
7
|
-
return {extract, compare, name: 'ISSN'};
|
|
8
|
-
};
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
1
|
import createDebugLogger from 'debug';
|
|
13
2
|
|
|
14
|
-
import {isComponentRecord} from '@natlibfi/melinda-commons';
|
|
15
|
-
|
|
16
3
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:issn');
|
|
17
4
|
const debugData = debug.extend('data');
|
|
18
5
|
|
|
@@ -21,18 +8,16 @@ export default () => ({
|
|
|
21
8
|
extract: ({record/*, recordExternal*/}) => {
|
|
22
9
|
//const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
|
|
23
10
|
|
|
24
|
-
const isHost = !isComponentRecord(record, false, []);
|
|
25
|
-
|
|
26
|
-
// debug(`\t Is HOST: '${isHost}'`);
|
|
27
11
|
return getIssns();
|
|
28
12
|
|
|
29
13
|
function getIssns() {
|
|
30
|
-
const fields =
|
|
14
|
+
const fields = record.get(/^022$/u);
|
|
31
15
|
if (fields.length === 0) {
|
|
32
16
|
return [];
|
|
33
17
|
}
|
|
18
|
+
|
|
34
19
|
debug(`\t ${fields.length} potential ISSN fields (${fields[0].tag})`);
|
|
35
|
-
const subfieldCodes =
|
|
20
|
+
const subfieldCodes = ['a', 'y', 'z']
|
|
36
21
|
//debug(`\t subfield codes: '${subfieldCodes.join("', '")}'`);
|
|
37
22
|
const subfieldValues = fields.flatMap(f => f.subfields.filter(sf => subfieldCodes.includes(sf.code)).map(sf => sf.value));
|
|
38
23
|
//debug(`\t cand values: '${subfieldValues.join("', '")}'`);
|
|
@@ -44,13 +29,6 @@ export default () => ({
|
|
|
44
29
|
return uniqArray(validSubfieldValues);
|
|
45
30
|
}
|
|
46
31
|
|
|
47
|
-
function getRelevantFields() {
|
|
48
|
-
if (isHost) {
|
|
49
|
-
return record.get(/^022$/u);
|
|
50
|
-
}
|
|
51
|
-
return record.get(/^[79]73$/u);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
32
|
function normalizeSubfieldValue(val) {
|
|
55
33
|
return val.replace(/[., :;].*$/u, '');
|
|
56
34
|
}
|
|
@@ -58,13 +36,6 @@ export default () => ({
|
|
|
58
36
|
function isValidIssn(val) {
|
|
59
37
|
return /^[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9X]$/u.test(val);
|
|
60
38
|
}
|
|
61
|
-
|
|
62
|
-
function getRelevantSubfieldCodes() {
|
|
63
|
-
if (isHost) {
|
|
64
|
-
return ['a', 'y', 'z'];
|
|
65
|
-
}
|
|
66
|
-
return ['x'];
|
|
67
|
-
}
|
|
68
39
|
},
|
|
69
40
|
// eslint-disable-next-line max-statements
|
|
70
41
|
compare: (aa, bb) => {
|
|
@@ -31,10 +31,11 @@ export default () => ({
|
|
|
31
31
|
|
|
32
32
|
FixSami041().fix(clonedRecord); // Handle 'smi' adding if needed
|
|
33
33
|
Remove041zxx().fix(clonedRecord); // Remove 'zxx' from f041s
|
|
34
|
+
clonedRecord.fields = clonedRecord.fields.filter(f => ['008', '041'].includes(f.tag));
|
|
34
35
|
// Add other language code normalizations?
|
|
35
36
|
|
|
36
37
|
// Should we return all the fields, or just the relevant fields?
|
|
37
|
-
return [
|
|
38
|
+
return [clonedRecord.fields, label];
|
|
38
39
|
},
|
|
39
40
|
// eslint-disable-next-line max-statements
|
|
40
41
|
compare: (aa, bb) => {
|
|
@@ -50,9 +51,6 @@ export default () => ({
|
|
|
50
51
|
if (score > 0.1) { // Keeps the original max, we might have 0.10 (041) + 0.05 (008/35-37) = 0.15 now
|
|
51
52
|
return 0.1;
|
|
52
53
|
}
|
|
53
|
-
if (score < -1) {
|
|
54
|
-
return -1.0;
|
|
55
|
-
}
|
|
56
54
|
return score;
|
|
57
55
|
}
|
|
58
56
|
|
|
@@ -73,7 +71,7 @@ export default () => ({
|
|
|
73
71
|
return 0.05;
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
if (a008 === 'mul' || b008 === 'mul') {
|
|
74
|
+
if (a008 === 'mul' || b008 === 'mul') { // We'll let 041 decide
|
|
77
75
|
return 0.0;
|
|
78
76
|
}
|
|
79
77
|
|
|
@@ -87,6 +85,7 @@ export default () => ({
|
|
|
87
85
|
function compare041() {
|
|
88
86
|
const a041s = getFields041(a);
|
|
89
87
|
const b041s = getFields041(b);
|
|
88
|
+
|
|
90
89
|
if (a041s.length === 0 || b041s.length === 0) {
|
|
91
90
|
return 0.0; // Should we punish these for badness?
|
|
92
91
|
}
|
|
@@ -97,14 +96,14 @@ export default () => ({
|
|
|
97
96
|
const scoreInd1 = mainPenalty === 0.0 ? 0.0 : indicator1Penalty(a041s[0], b041s[0]);
|
|
98
97
|
return mainPenalty + sourcePenalty + scoreInd1;
|
|
99
98
|
}
|
|
100
|
-
// Things are already complicated (likely to pe penalized, so no nedd to
|
|
99
|
+
// Things are already complicated (likely to pe penalized, so no nedd to worry about language code sources, I daresay.
|
|
101
100
|
return compareLanguageCodeLists(getLanguageCodesFrom041Fields(a), getLanguageCodesFrom041Fields(b));
|
|
102
101
|
|
|
103
|
-
function getFields041(
|
|
104
|
-
if (!
|
|
102
|
+
function getFields041(fields) {
|
|
103
|
+
if (!fields) {
|
|
105
104
|
return [];
|
|
106
105
|
}
|
|
107
|
-
return
|
|
106
|
+
return fields.filter(f => f.tag === '041');
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
}
|
|
@@ -139,7 +138,7 @@ export default () => ({
|
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
function compareLanguageCodeLists(a, b) {
|
|
142
|
-
if (a.length === 0 || b.length === 0) {
|
|
141
|
+
if (a.length === 0 || b.length === 0 || a.every(val => containsNoData(val)) || b.every(val => containsNoData(val))) {
|
|
143
142
|
debugData(`No language to compare`);
|
|
144
143
|
return 0;
|
|
145
144
|
}
|
|
@@ -176,19 +175,9 @@ export default () => ({
|
|
|
176
175
|
|
|
177
176
|
|
|
178
177
|
// Damage control:
|
|
179
|
-
|
|
180
|
-
// Not using the generic solution here as eg. 'und' needs a special treatment
|
|
181
|
-
const sharedValues = getSharedValues(a, b);
|
|
182
|
-
const aOnly = a.filter(val => !sharedValues.includes(val));
|
|
183
|
-
const bOnly = b.filter(val => !sharedValues.includes(val));
|
|
184
|
-
const hasUnd = [...aOnly, ...bOnly].includes('und');
|
|
178
|
+
const sharedValues = getSharedValues(a, b).filter(val => !containsNoData(val));
|
|
185
179
|
|
|
186
180
|
if (sharedValues.length < 1) {
|
|
187
|
-
console.info(`NV: ${sharedValues.join(", ")}`);
|
|
188
|
-
if (aOnly.length === 1 && bOnly.length === 1 && hasUnd) {
|
|
189
|
-
debug(`Both have languages, but none of these match. However, the benefit of doubt is given: '${aOnly[0]}' and '${bOnly[0]}' might mean the same}`);
|
|
190
|
-
return 0;
|
|
191
|
-
}
|
|
192
181
|
debug(`Both have languages, but none of these match.`); // includes 'mul' vs a single language code
|
|
193
182
|
return -1.0;
|
|
194
183
|
}
|
|
@@ -218,11 +207,11 @@ export default () => ({
|
|
|
218
207
|
|
|
219
208
|
});
|
|
220
209
|
|
|
221
|
-
function get008Value(
|
|
222
|
-
if (!
|
|
210
|
+
function get008Value(fields, label = 'record') {
|
|
211
|
+
if (!fields) {
|
|
223
212
|
return;
|
|
224
213
|
}
|
|
225
|
-
const f008 =
|
|
214
|
+
const f008 = fields.find(f => f.tag === '008');
|
|
226
215
|
if (!f008) {
|
|
227
216
|
return undefined;
|
|
228
217
|
}
|
|
@@ -269,12 +258,12 @@ function isLangCodeForALanguage(code, label = 'record', encoding = undefined) {
|
|
|
269
258
|
|
|
270
259
|
|
|
271
260
|
|
|
272
|
-
function getLanguageCodesFrom041Fields(
|
|
261
|
+
function getLanguageCodesFrom041Fields(fields) {
|
|
273
262
|
// NB! We brutally don't check $2 (language code source) as marc's language codes is practically a subset of ISO 639-2,
|
|
274
263
|
// and also ISO 639-2 and ISO 639-3 overlap to a degree. If we ever run into a trouble with some ISO 639-2 vs 639-3 mismatch, then we'll work it out.
|
|
275
264
|
// Also we could write a validator that converts ISO 639-2 to marc if applicable and maybe even partial support for ISO-639-3.
|
|
276
265
|
// Note that ISO 639-2-B values correspond with marc beteer that ISO 639-2-T. Eg. code for Chinese 'zho' should/code be normalized to 'chi'!
|
|
277
|
-
const values =
|
|
266
|
+
const values = fields.filter(f => f.tag === '041').flatMap(f => getFieldsLanguageCodes(f));
|
|
278
267
|
/*
|
|
279
268
|
// .filter(({ind2}) => ind2 === ' ')
|
|
280
269
|
.map(({subfields}) => subfields)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import createDebugLogger from 'debug';
|
|
3
3
|
import {testStringOrNumber} from '../../../matching-utils.js';
|
|
4
|
+
import {matchingYears} from './publication-time.js';
|
|
4
5
|
|
|
5
6
|
// We should also get copyright time and copyright/publication times from 26x
|
|
6
7
|
|
|
@@ -16,14 +17,16 @@ export default () => ({
|
|
|
16
17
|
|
|
17
18
|
const [firstA] = a;
|
|
18
19
|
const [firstB] = b;
|
|
20
|
+
// Sanity check: If either of years is a non string/number, values are not comparable
|
|
21
|
+
if (!testStringOrNumber(firstA) || !testStringOrNumber(firstB)) {
|
|
22
|
+
return 0.0;
|
|
23
|
+
}
|
|
19
24
|
|
|
20
25
|
if (firstA === firstB) {
|
|
21
26
|
return 0.1;
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!testStringOrNumber(firstA) || !testStringOrNumber(firstB)) {
|
|
26
|
-
return 0;
|
|
28
|
+
if (matchingYears(firstA, firstB)) { // Handle 'u' Eg. '18uu' vs '1812'
|
|
29
|
+
return 0.05;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
const firstANumber = parseInt(firstA, 10);
|