@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.
Files changed (47) hide show
  1. package/.github/workflows/melinda-node-tests-and-publish.yml +4 -4
  2. package/dist/candidate-search/query-list/bib.js +84 -67
  3. package/dist/candidate-search/query-list/bib.js.map +3 -3
  4. package/dist/candidate-search/query-list/component.js +94 -28
  5. package/dist/candidate-search/query-list/component.js.map +3 -3
  6. package/dist/cli.js +12 -4
  7. package/dist/cli.js.map +2 -2
  8. package/dist/match-detection/features/bib/f773.js +82 -0
  9. package/dist/match-detection/features/bib/f773.js.map +7 -0
  10. package/dist/match-detection/features/bib/index.js +1 -1
  11. package/dist/match-detection/features/bib/index.js.map +2 -2
  12. package/dist/match-detection/features/bib/index.test.js +1 -1
  13. package/dist/match-detection/features/bib/index.test.js.map +2 -2
  14. package/dist/match-detection/features/bib/isbn.js +47 -48
  15. package/dist/match-detection/features/bib/isbn.js.map +2 -2
  16. package/dist/match-detection/features/bib/issn.js +2 -16
  17. package/dist/match-detection/features/bib/issn.js.map +2 -2
  18. package/dist/match-detection/features/bib/language.js +12 -22
  19. package/dist/match-detection/features/bib/language.js.map +2 -2
  20. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +6 -2
  21. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +2 -2
  22. package/dist/match-detection/features/bib/publication-time.js +62 -16
  23. package/dist/match-detection/features/bib/publication-time.js.map +2 -2
  24. package/dist/match-detection/features/bib/title.js +76 -26
  25. package/dist/match-detection/features/bib/title.js.map +3 -3
  26. package/dist/match-detection/index.js +8 -3
  27. package/dist/match-detection/index.js.map +2 -2
  28. package/dist/matching-utils.js +14 -0
  29. package/dist/matching-utils.js.map +2 -2
  30. package/package.json +11 -12
  31. package/src/candidate-search/query-list/bib.js +100 -77
  32. package/src/candidate-search/query-list/component.js +100 -23
  33. package/src/cli.js +8 -4
  34. package/src/match-detection/features/bib/f773.js +118 -0
  35. package/src/match-detection/features/bib/index.js +1 -1
  36. package/src/match-detection/features/bib/index.test.js +1 -1
  37. package/src/match-detection/features/bib/isbn.js +66 -60
  38. package/src/match-detection/features/bib/issn.js +3 -32
  39. package/src/match-detection/features/bib/language.js +15 -26
  40. package/src/match-detection/features/bib/publication-time-allow-cons-years.js +7 -4
  41. package/src/match-detection/features/bib/publication-time.js +76 -30
  42. package/src/match-detection/features/bib/title.js +112 -34
  43. package/src/match-detection/index.js +9 -3
  44. package/src/matching-utils.js +17 -0
  45. package/dist/match-detection/features/bib/title-version-original.js +0 -37
  46. package/dist/match-detection/features/bib/title-version-original.js.map +0 -7
  47. 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
- const host = !isComponentRecord(record, false, []);
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 [subfieldCodeForGoodValues, subfieldCodeForBadValues] = getSubfieldCodes(aa[0].tag);
27
+ const subfieldCodeForGoodValues = 'a';
28
+ const subfieldCodeForBadValues = 'z';
35
29
 
36
- const [goodValuesA, badValuesA] = getValues(aa);
37
- if (goodValuesA.length) {
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
- const [goodValuesB, badValuesB] = getValues(bb);
45
- if (goodValuesB.length) {
46
- debug(`GOOD VALUES (B): ${goodValuesB.join(', ')}`);
47
- }
48
- if (badValuesB.length) {
49
- debug(`BAD VALUES (B): ${badValuesB.join(', ')}`);
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(goodValuesA, goodValuesB);
50
+ const [sharedGoodValues, goodValuesAOnly, goodValuesBOnly] = getUnionData(aValidValuesA, aValidValuesB);
53
51
 
54
- //debug(`GOOD\tBOTH: ${sharedGoodValues.length}, A only: ${goodValuesAOnly.length}, B only: ${goodValuesBOnly.length}`);
52
+ debug(`GOOD\tBOTH: ${sharedGoodValues.length}, A only: ${goodValuesAOnly.length}, B only: ${goodValuesBOnly.length}`);
55
53
 
56
- const hitScore = scoreHit();
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
- function scoreHit() {
59
- if (sharedGoodValues.length > 0) {
60
- return MAX_SCORE;
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 (goodValuesA.some(valA => badValuesB.includes(valA)) || goodValuesB.some(valB => badValuesA.includes(valB))) {
63
+ if (aValidValuesA.some(valA => zValidValuesB.includes(valA)) || aValidValuesB.some(valB => zValidValuesA.includes(valB))) {
64
64
  return MAX_SCORE;
65
65
  }
66
66
 
67
- // Value is bad, but looks isbn-ish to a human eye (not validating the isbn again, note than invalid isbns in 020$a are considered bad):
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
- if (badValuesA.some(valA => looksGood(valA) && badValuesB.includes(valA)) || badValuesB.some(valB => looksGood(valB) && badValuesA.includes(valB))) {
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
- if (sharedGoodValues.length > 0) {
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 (goodValuesA.length === 0 || goodValuesB === 0) { // At least one record did not have any good ISBNs, so not penalizing here! (Invalid 020$as are counted a bad.)
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
- return -0.75; // Has good ISBNs on both records, but they did not match
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
- return [uniqArray(trueGoodValues), uniqArray([...wannabeGoodValues, ...badValues])];
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 = getRelevantFields();
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 = getRelevantSubfieldCodes();
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 [{leader: clonedRecord.leader, fields: clonedRecord.fields}, label];
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 worrty about language code sources, I daresay.
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(record) {
104
- if (!record.fields) {
102
+ function getFields041(fields) {
103
+ if (!fields) {
105
104
  return [];
106
105
  }
107
- return record.fields.filter(f => f.tag === '041');
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(record, label = 'record') {
222
- if (!record || !record.fields) {
210
+ function get008Value(fields, label = 'record') {
211
+ if (!fields) {
223
212
  return;
224
213
  }
225
- const f008 = record.fields.find(f => f.tag === '008');
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(record) {
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 = record.fields.filter(f => f.tag === '041').flatMap(f => getFieldsLanguageCodes(f));
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
- // If either of years is a non string/number, values are not comparable
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);