@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
@@ -1,18 +1,23 @@
1
+ // We should also get copyright time and copyright/publication times from 26x
2
+ // see publication-time-allow-cons-years for a version allowing consequent years to match
1
3
 
4
+ import {extractPublicationYearFrom773} from '../../../candidate-search/query-list/component.js';
5
+
6
+ const MAX = 0.1;
7
+ const MIN = -1.0;
2
8
 
3
- import {testStringOrNumber} from '../../../matching-utils.js';
4
9
 
5
- // We should also get copyright time and copyright/publication times from 26x
6
- // see publication-time-allow-cons-years for a version allowing consequent years to match
7
10
 
8
11
  export default () => ({
9
12
  name: 'Publication time',
10
13
  extract: ({record}) => {
11
- return getDateData();
12
- //const value = record.get(/^008$/u)?.[0]?.value || undefined;
13
- //return testStringOrNumber(value) ? [String(value).slice(6, 15)] : [];
14
+ let dateData = getDataFrom008();
15
+ const [f773] = record.get(/^773$/u);
16
+ const hostYear = f773 && extractPublicationYearFrom773(f773) || null;
17
+ dateData.hostYear = hostYear;
18
+ return dateData;
14
19
 
15
- function getDateData() {
20
+ function getDataFrom008() {
16
21
  const [f008] = record.get(/^008$/u);
17
22
  if (!f008 || f008.value.length < 16) {
18
23
  return splitDateData('| ');
@@ -29,50 +34,91 @@ export default () => ({
29
34
  },
30
35
 
31
36
  compare: (aa, bb) => {
32
- if (aa.typeOfDate === 'b') { // Berfore Christ. No really makes sense in our domain, though.
37
+ // Be happy with a f773$g match:
38
+ if (aa.hostYear && bb.hostYear && aa.hostYear === bb.hostYear) {
39
+ return MAX;
40
+ }
41
+ // Check 008
42
+ if (aa.typeOfDate === 'b') { // 008/06: Before Christ. No really makes sense in our domain, though.
33
43
  if (bb.typeOfDate === 'b') {
34
44
  return 0;
35
45
  }
36
- return -1.0;
46
+ return MIN;
37
47
  }
38
48
  if (aa.typeOfDate === 'n' || bb.typeOfDate === 'n') { // n=unknown
39
49
  return 0;
40
50
  }
41
51
 
52
+ // Try to handle questionable dates:
53
+ if (aa.typeOfDate === 'q') { // questionable data
54
+ if (bb.typeOfDate !== 'q') {
55
+ if (yearInBetween(aa.date1, bb.date1, aa.date2)) {
56
+ return MAX;
57
+ }
58
+ return MIN;
59
+ }
60
+
61
+ // What if there are questionable dates on both sides?
62
+ if (aa.date1 === bb.date1 && bb.date1 === bb.date2) {
63
+ return MAX;
64
+ }
65
+ // Lazily return 0. (We could check for overlap etc. but not really worth the effort)
66
+ return 0.0;
67
+ }
68
+ if (bb.typeOfDate === 'q') {
69
+ if (yearInBetween(bb.date1, aa.date1, bb.date2)) {
70
+ return MAX;
71
+ }
72
+ return MIN;
73
+ }
74
+
42
75
  const skipList = [' ', '||||', 'uuuu'];
43
76
  if (skipList.includes(aa.date1) || skipList.includes(bb.date1)) { // 008/07-10 carries no information
44
77
  return 0;
45
78
  }
46
79
 
47
80
  if (matchingYears(aa.date1, bb.date1)) { // 'u' support
48
- return 0.1;
81
+ return MAX;
49
82
  }
50
83
 
51
- // TODO: add $q support here
52
-
53
- return -1.0;
54
-
55
- function matchingYears(yyyy1, yyyy2) {
56
- if (yyyy1.length === 0) { // All digits have been succesfully consumed -> success
57
- return true;
58
- }
59
- if (yyyy1[0] === 'u' || yyyy2[0] === 'u') { // Ignore 'u' (it refers to unknown millenia, century, decade or year)
60
- return matchingYears(yyyy1.slice(1), yyyy2.slice(1));
61
- }
62
- if (yyyy1[0] !== yyyy2[0]) {
63
- return false;
84
+ if (isValidYear(aa.date1) && bb.date1) {
85
+ const difference = Math.abs(aa.date1 - bb.date1);
86
+ if (difference === 1) {
87
+ return MIN/8;
64
88
  }
65
- // Should we require that yyyy[0] is a digit at this point?
66
- return matchingYears(yyyy1.slice(1), yyyy2.slice(1));
67
89
  }
68
90
 
69
- /*
70
- function hasFourDigits(yyyy) { // Will be needed by 'q' support
71
- return yyyy.match(/^[0-9]{4}$/u);
72
- }
73
- */
91
+ return MIN;
74
92
  }
75
93
 
76
94
  });
77
95
 
96
+ function yearInBetween(start, curr, end) {
97
+ if (!isValidYear(start) || !isValidYear(curr) || !isValidYear(end)) {
98
+ return false;
99
+ }
100
+ return start <= curr && curr <= end;
101
+ }
102
+
103
+ export function matchingYears(yyyy1, yyyy2) {
104
+ if (yyyy1.length === 0) { // All digits have been succesfully consumed -> success
105
+ return true;
106
+ }
107
+ if (yyyy1[0] === 'u' || yyyy2[0] === 'u') { // Ignore 'u' (it refers to unknown millenia, century, decade or year)
108
+ return matchingYears(yyyy1.slice(1), yyyy2.slice(1));
109
+ }
110
+ if (yyyy1[0] !== yyyy2[0]) {
111
+ return false;
112
+ }
113
+ if (yyyy1[0] >= '0' && yyyy1[0] <= '9') { // Require that yyyy[0] is a digit at this point? (What if year is 983?)
114
+ return matchingYears(yyyy1.slice(1), yyyy2.slice(1));
115
+ }
116
+ return false;
117
+ }
118
+
119
+
120
+ const validYearRegexp = /^(?:1[89][0-9][0-9]|20[012][0-9])$/u;
78
121
 
122
+ function isValidYear(yyyy) {
123
+ return validYearRegexp.test(yyyy); // Currently supports 1800-2029
124
+ }
@@ -3,19 +3,28 @@ import naturalPkg from 'natural';
3
3
  const {LevenshteinDistance: leven} = naturalPkg;
4
4
  import {testStringOrNumber} from '../../../matching-utils.js';
5
5
 
6
- const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:title');
7
- const debugData = debug.extend('data');
8
6
 
7
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/title');
8
+ const debugData = debug.extend('data');
9
9
 
10
- export default ({threshold = 10} = {}) => ({
10
+ export default ({threshold = 0.9} = {}) => ({
11
11
  name: 'Title',
12
12
  extract: ({record, recordExternal}) => {
13
13
  const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
14
- const title = getTitle();
15
- debug(`${label}: title: ${title}`);
14
+ const a = getTitle(record, ['a']);
15
+ debug(`${label}: title: ${a}`);
16
+
17
+ if (a) {
18
+ const b = normalizeTitle(getTitle(record, ['b'])) || '';
19
+ const n = normalizeTitle(getTitle(record, ['n'])) || '';
20
+ const p = normalizeTitle(getTitle(record, ['p'])) || '';
21
+ return [normalizeTitle(a), b, n, p];
22
+ }
23
+
24
+ return ['', '', '', ''];
16
25
 
17
- if (testStringOrNumber(title)) {
18
- const titleAsNormalizedString = String(title)
26
+ function normalizeTitle(title) {
27
+ return title
19
28
  // decompose unicode diacritics
20
29
  .normalize('NFD')
21
30
  // strip non-letters/numbers
@@ -24,48 +33,117 @@ export default ({threshold = 10} = {}) => ({
24
33
  // - see validator normalize-utf8-diacritics for details
25
34
  .replace(/[^\p{Letter}\p{Number}]/gu, '')
26
35
  .toLowerCase();
27
- debug(`${label}: titleString: ${titleAsNormalizedString}`);
28
- return [titleAsNormalizedString];
29
36
  }
30
37
 
31
- return [];
32
-
33
- function getTitle() {
34
- const [field] = record.get(/^245$/u);
35
- debugData(`${label}: titleField: ${JSON.stringify(field)}`);
36
-
37
- if (field) {
38
- return field.subfields
39
- // get also $n:s and $p:s here
40
- .filter(({code}) => ['a', 'b', 'n', 'p'].includes(code))
41
- .map(({value}) => testStringOrNumber(value) ? String(value) : '')
42
- .join('');
43
- }
44
- return false;
45
- }
46
38
  },
47
39
  compare: (a, b) => {
48
- const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/title');
49
- const distance = leven(a[0], b[0]);
40
+ const [aa, ab, an, ap] = a;
41
+ const [ba, bb, bn, bp] = b;
42
+
43
+ if (isEmpty(aa) || isEmpty(ba)) {
44
+ return -1.0;
45
+ }
46
+ // F245$n information is critical; it can not mismatch at all:
47
+ if (an.length && bn.length && an !== bn) { // If these exists, they must be the same (we might convert Roman numbers to Arabic numbers though)
48
+ return -1.0;
49
+ }
50
+
51
+ const aFull = toFullTitle(a);
52
+ const bFull = toFullTitle(b);
53
+
54
+ const [distance, maxLength, correctness] = doLevenshtein(aFull, bFull);
55
+
56
+ debug(`'${aFull}' vs '${bFull}': Max length = ${maxLength}, distance = ${distance}, correctness = ${correctness}`);
50
57
 
51
58
  if (distance === 0) {
52
59
  return 0.5;
53
60
  }
54
61
 
55
- const maxLength = getMaxLength();
56
- const percentage = distance / maxLength * 100;
62
+ if (correctness >= threshold) {
63
+ return 0.4;
64
+ }
65
+
66
+ if (an && bn) {
67
+ // There seems to be some wobble between $b and $p, for example:
68
+ if (ab && isEmpty(ap) && bp && isEmpty(bb)) {
69
+ return compare([aa, ap, an, ab], [ba, bb, bn, bp]);
70
+ }
71
+ if (ap && isEmpty(ab) && bb && isEmpty(bp)) {
72
+ return compare([aa, ab, an, ap], [ba, bp, bn, bb]);
73
+ }
74
+ }
75
+
76
+ // Try the same without $p:
77
+ if (localXor(ap, bp)) {
78
+ const result = compare([aa, ab, an, ''], [ba, bb, bn, '']);
79
+ return result > 0.0 ? result * 0.8 : result;
80
+ }
81
+
82
+ if (isEmpty(ap) && isEmpty(bp) && localXor(ab, bb)) {
83
+ // Try the same without $b ($p is not here)
84
+ const result = compare([aa, '', an, ''], [ba, '', bn, '']);
85
+ return result > 0.0 ? result * 0.8 : result;
86
+ }
87
+
88
+ return -0.5; // Not likely
89
+
90
+ function isEmpty(x) {
91
+ return !x || x.length === 0;
92
+ }
57
93
 
58
- debug(`'${a}' vs '${b}': Max length = ${maxLength}, distance = ${distance}, percentage = ${percentage}`);
94
+ function localXor(x, y) {
95
+ if (isEmpty(x)) {
96
+ return !isEmpty(y);
97
+ }
98
+ // 'x' exists, thus 'y' can not exist:
99
+ return isEmpty(y);
100
+ }
59
101
 
60
- if (percentage <= threshold) {
61
- return 0.3;
102
+ function doLevenshtein(string1, string2) {
103
+ const distance = leven(string1, string2);
104
+ const len = getMaxLength(string1, string2);
105
+ const correctness = 1.0 - (distance / len);
106
+ return [distance, len, correctness];
62
107
  }
63
108
 
64
- return -0.5;
109
+ function toFullTitle(arr) {
110
+ const relevant = arr.filter(val => typeof val === 'string' && val.length);
111
+ return relevant.join(' ');
112
+ }
65
113
 
66
- function getMaxLength() {
67
- return a[0].length > b[0].length ? a[0].length : b[0].length;
114
+ function getMaxLength(str1, str2) {
115
+ return str1.length > str2.length ? str1.length : str2.length;
68
116
  }
69
117
 
70
118
  }
71
119
  });
120
+
121
+ export function getTitle(record, targetSubfieldCodes = ['a', 'b', 'n', 'p']) {
122
+ const [field] = record.get(/^245$/u);
123
+ debugData(`titleField: ${JSON.stringify(field)}`);
124
+
125
+ if (field) {
126
+ const title = field.subfields
127
+ // get also $n:s and $p:s here
128
+ .filter(({code}) => targetSubfieldCodes.includes(code))
129
+ // Would be nice to normalize $n values...
130
+ .map(({value}) => testStringOrNumber(value) ? String(value) : '')
131
+ .join(' ')
132
+ .replace(/\[[^\]]*\]/ug, ' ') // Remove [whatever] stuff. ADD TEST FOR THIS
133
+ .replace(/ [=\/:](?:$| )/ug, ' ') // Strip punctuation
134
+ // Also đ vs d pairs seen:
135
+ //.replace(/(?:đ|ȧ|t̄|ǩ|ǧ|s̆|c̆)/ug, '') // Hack. Saamelaisbibliografia has often dropped this wierd characters (oft old articles, no longer used in sami either)
136
+ // trim:
137
+ .replace(/ +/ug, ' ')
138
+ .replace(/^ +/u, '')
139
+ .replace(/ +$/u, '');
140
+
141
+ // Skip non-filing indicator (note that '9' is a magic indicator value, so we don't do it):
142
+ if (/^[1-8]$/u.test(field.ind2)) { // Skip non-filing characters
143
+ return title.slice(parseInt(field.ind2));
144
+ }
145
+ return title;
146
+
147
+ }
148
+ return false;
149
+ }
@@ -5,7 +5,8 @@ import * as features from './features/index.js';
5
5
  export {features};
6
6
 
7
7
  export default ({strategy, threshold = 0.8999}, returnStrategy = false, localSettings = {}) => ({recordA, recordB, recordAExternal = {}, recordBExternal = {}}) => {
8
- const minProbabilityQuantifier = 0.5;
8
+ const MIN_PROPABILITY_QUALIFIER = 0.4; // Title perfect match: 0.5
9
+ const FORCE_FAIL = -1.0;
9
10
 
10
11
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection');
11
12
  const debugData = debug.extend('data');
@@ -41,13 +42,18 @@ export default ({strategy, threshold = 0.8999}, returnStrategy = false, localSet
41
42
  const featurePairs = generateFeaturePairs(featuresA, featuresB);
42
43
  const similarityVector = generateSimilarityVector(featurePairs);
43
44
 
44
- if (similarityVector.some(v => v >= minProbabilityQuantifier)) {
45
+ if (similarityVector.some(v => v >= MIN_PROPABILITY_QUALIFIER)) {
45
46
  const probability = calculateProbability(similarityVector);
46
47
  debug(`probability: ${probability} (Threshold: ${threshold})`);
48
+ if (similarityVector.some(v => v <= FORCE_FAIL)) {
49
+ debug(`Some feature resulted in utter failure (${FORCE_FAIL} or less)`);
50
+ return returnResult({match: false, probability: 0.0});
51
+ }
52
+
47
53
  return returnResult({match: probability >= threshold, probability});
48
54
  }
49
55
 
50
- debugData(`No feature yielded minimum probability amount of points (${minProbabilityQuantifier})`);
56
+ debugData(`No feature yielded minimum probability amount of points (${MIN_PROPABILITY_QUALIFIER})`);
51
57
  return returnResult({match: false, probability: 0.0});
52
58
  }
53
59
 
@@ -112,3 +112,20 @@ export function getMatchCounts(aValues, bValues) {
112
112
  return aToB < bToA ? aToB : bToA;
113
113
  }
114
114
  }
115
+
116
+
117
+ export function stringAfter(string, substring) {
118
+ const pos = string.indexOf(substring);
119
+ if (pos === -1) {
120
+ return string;
121
+ }
122
+ return string.substring(pos+substring.length);
123
+ }
124
+
125
+ export function stringBefore(string, substring) {
126
+ const pos = string.indexOf(substring);
127
+ if (pos === -1) {
128
+ return string;
129
+ }
130
+ return string.substring(0, pos);
131
+ }
@@ -1,37 +0,0 @@
1
- import createDebugLogger from "debug";
2
- import naturalPkg from "natural";
3
- const { LevenshteinDistance: leven } = naturalPkg;
4
- export default ({ threshold = 10 } = {}) => ({
5
- name: "titleVersionOriginal",
6
- extract: ({ record }) => {
7
- const title = getTitle();
8
- if (title) {
9
- return [title.replace(/[^\p{Letter}\p{Number}]/gu, "").toLowerCase()];
10
- }
11
- return [];
12
- function getTitle() {
13
- const [field] = record.get(/^245$/u);
14
- if (field) {
15
- return field.subfields.filter(({ code }) => ["a", "b"].includes(code)).map(({ value }) => value).join("");
16
- }
17
- }
18
- },
19
- compare: (a, b) => {
20
- const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features/bib/title-version-original");
21
- const distance = leven(a[0], b[0]);
22
- if (distance === 0) {
23
- return 0.5;
24
- }
25
- const maxLength = getMaxLength();
26
- const percentage = distance / maxLength * 100;
27
- debug(`'${a}' vs '${b}': Max length = ${maxLength}, distance = ${distance}, percentage = ${percentage}`);
28
- if (percentage <= threshold) {
29
- return 0.3;
30
- }
31
- return -0.5;
32
- function getMaxLength() {
33
- return a[0].length > b[0].length ? a[0].length : b[0].length;
34
- }
35
- }
36
- });
37
- //# sourceMappingURL=title-version-original.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/match-detection/features/bib/title-version-original.js"],
4
- "sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport naturalPkg from 'natural';\nconst {LevenshteinDistance: leven} = naturalPkg;\n\nexport default ({threshold = 10} = {}) => ({\n name: 'titleVersionOriginal',\n extract: ({record}) => {\n const title = getTitle();\n\n if (title) {\n return [title.replace(/[^\\p{Letter}\\p{Number}]/gu, '').toLowerCase()];\n }\n\n return [];\n\n function getTitle() {\n const [field] = record.get(/^245$/u);\n\n if (field) {\n return field.subfields\n .filter(({code}) => ['a', 'b'].includes(code))\n .map(({value}) => value)\n .join('');\n }\n }\n },\n compare: (a, b) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/title-version-original');\n const distance = leven(a[0], b[0]);\n\n if (distance === 0) {\n return 0.5;\n }\n\n const maxLength = getMaxLength();\n const percentage = distance / maxLength * 100;\n\n debug(`'${a}' vs '${b}': Max length = ${maxLength}, distance = ${distance}, percentage = ${percentage}`);\n\n if (percentage <= threshold) {\n return 0.3;\n }\n\n return -0.5;\n\n function getMaxLength() {\n return a[0].length > b[0].length ? a[0].length : b[0].length;\n }\n\n }\n});\n"],
5
- "mappings": "AACA,OAAO,uBAAuB;AAC9B,OAAO,gBAAgB;AACvB,MAAM,EAAC,qBAAqB,MAAK,IAAI;AAErC,eAAe,CAAC,EAAC,YAAY,GAAE,IAAI,CAAC,OAAO;AAAA,EACzC,MAAM;AAAA,EACN,SAAS,CAAC,EAAC,OAAM,MAAM;AACrB,UAAM,QAAQ,SAAS;AAEvB,QAAI,OAAO;AACT,aAAO,CAAC,MAAM,QAAQ,6BAA6B,EAAE,EAAE,YAAY,CAAC;AAAA,IACtE;AAEA,WAAO,CAAC;AAER,aAAS,WAAW;AAClB,YAAM,CAAC,KAAK,IAAI,OAAO,IAAI,QAAQ;AAEnC,UAAI,OAAO;AACT,eAAO,MAAM,UACV,OAAO,CAAC,EAAC,KAAI,MAAM,CAAC,KAAK,GAAG,EAAE,SAAS,IAAI,CAAC,EAC5C,IAAI,CAAC,EAAC,MAAK,MAAM,KAAK,EACtB,KAAK,EAAE;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,CAAC,GAAG,MAAM;AACjB,UAAM,QAAQ,kBAAkB,uFAAuF;AACvH,UAAM,WAAW,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;AAEjC,QAAI,aAAa,GAAG;AAClB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,aAAa;AAC/B,UAAM,aAAa,WAAW,YAAY;AAE1C,UAAM,IAAI,CAAC,SAAS,CAAC,mBAAmB,SAAS,gBAAgB,QAAQ,kBAAkB,UAAU,EAAE;AAEvG,QAAI,cAAc,WAAW;AAC3B,aAAO;AAAA,IACT;AAEA,WAAO;AAEP,aAAS,eAAe;AACtB,aAAO,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE;AAAA,IACxD;AAAA,EAEF;AACF;",
6
- "names": []
7
- }
@@ -1,52 +0,0 @@
1
-
2
- import createDebugLogger from 'debug';
3
- import naturalPkg from 'natural';
4
- const {LevenshteinDistance: leven} = naturalPkg;
5
-
6
- export default ({threshold = 10} = {}) => ({
7
- name: 'titleVersionOriginal',
8
- extract: ({record}) => {
9
- const title = getTitle();
10
-
11
- if (title) {
12
- return [title.replace(/[^\p{Letter}\p{Number}]/gu, '').toLowerCase()];
13
- }
14
-
15
- return [];
16
-
17
- function getTitle() {
18
- const [field] = record.get(/^245$/u);
19
-
20
- if (field) {
21
- return field.subfields
22
- .filter(({code}) => ['a', 'b'].includes(code))
23
- .map(({value}) => value)
24
- .join('');
25
- }
26
- }
27
- },
28
- compare: (a, b) => {
29
- const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/title-version-original');
30
- const distance = leven(a[0], b[0]);
31
-
32
- if (distance === 0) {
33
- return 0.5;
34
- }
35
-
36
- const maxLength = getMaxLength();
37
- const percentage = distance / maxLength * 100;
38
-
39
- debug(`'${a}' vs '${b}': Max length = ${maxLength}, distance = ${distance}, percentage = ${percentage}`);
40
-
41
- if (percentage <= threshold) {
42
- return 0.3;
43
- }
44
-
45
- return -0.5;
46
-
47
- function getMaxLength() {
48
- return a[0].length > b[0].length ? a[0].length : b[0].length;
49
- }
50
-
51
- }
52
- });