@natlibfi/melinda-record-matching 5.0.1 → 5.0.2-alpha.1

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 (40) hide show
  1. package/dist/candidate-search/index.js +6 -2
  2. package/dist/candidate-search/index.js.map +2 -2
  3. package/dist/candidate-search/query-list/bib.js +10 -2
  4. package/dist/candidate-search/query-list/bib.js.map +3 -3
  5. package/dist/cli.js +1 -1
  6. package/dist/cli.js.map +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/index.js.map +2 -2
  9. package/dist/match-detection/features/bib/authors.js +2 -2
  10. package/dist/match-detection/features/bib/authors.js.map +2 -2
  11. package/dist/match-detection/features/bib/bibliographic-level.js +12 -1
  12. package/dist/match-detection/features/bib/bibliographic-level.js.map +2 -2
  13. package/dist/match-detection/features/bib/host-component.js +1 -1
  14. package/dist/match-detection/features/bib/host-component.js.map +2 -2
  15. package/dist/match-detection/features/bib/isbn.js +122 -15
  16. package/dist/match-detection/features/bib/isbn.js.map +2 -2
  17. package/dist/match-detection/features/bib/issn.js +62 -5
  18. package/dist/match-detection/features/bib/issn.js.map +2 -2
  19. package/dist/match-detection/features/bib/language.js +176 -59
  20. package/dist/match-detection/features/bib/language.js.map +3 -3
  21. package/dist/match-detection/features/bib/title-version-original.js +2 -2
  22. package/dist/match-detection/features/bib/title-version-original.js.map +2 -2
  23. package/dist/match-detection/features/bib/title.js +2 -2
  24. package/dist/match-detection/features/bib/title.js.map +2 -2
  25. package/dist/match-detection/index.js +5 -5
  26. package/dist/match-detection/index.js.map +2 -2
  27. package/package.json +9 -4
  28. package/src/candidate-search/index.js +7 -2
  29. package/src/candidate-search/query-list/bib.js +22 -15
  30. package/src/cli.js +1 -1
  31. package/src/index.js +3 -3
  32. package/src/match-detection/features/bib/authors.js +2 -2
  33. package/src/match-detection/features/bib/bibliographic-level.js +13 -1
  34. package/src/match-detection/features/bib/host-component.js +1 -1
  35. package/src/match-detection/features/bib/isbn.js +162 -14
  36. package/src/match-detection/features/bib/issn.js +83 -1
  37. package/src/match-detection/features/bib/language.js +261 -73
  38. package/src/match-detection/features/bib/title-version-original.js +2 -2
  39. package/src/match-detection/features/bib/title.js +5 -4
  40. package/src/match-detection/index.js +5 -5
@@ -1,7 +1,89 @@
1
+ /*
2
+ //import createInterface from './standard-identifier-factory.js';
1
3
 
2
- import createInterface from './standard-identifier-factory.js';
3
4
 
4
5
  export default () => {
5
6
  const {extract, compare} = createInterface({pattern: /^022$/u, subfieldCodes: ['a', 'z', 'y']});
6
7
  return {extract, compare, name: 'ISSN'};
7
8
  };
9
+ */
10
+
11
+
12
+ import createDebugLogger from 'debug';
13
+
14
+ import {isComponentRecord} from '@natlibfi/melinda-commons';
15
+
16
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:issn');
17
+ const debugData = debug.extend('data');
18
+
19
+ export default () => ({
20
+ name: 'ISSN',
21
+ extract: ({record/*, recordExternal*/}) => {
22
+ //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
23
+
24
+ const isHost = !isComponentRecord(record, false, []);
25
+
26
+ // debug(`\t Is HOST: '${isHost}'`);
27
+ return getIssns();
28
+
29
+ function getIssns() {
30
+ const fields = getRelevantFields();
31
+ if (fields.length === 0) {
32
+ return [];
33
+ }
34
+ debug(`\t ${fields.length} potential ISSN fields (${fields[0].tag})`);
35
+ const subfieldCodes = getRelevantSubfieldCodes();
36
+ //debug(`\t subfield codes: '${subfieldCodes.join("', '")}'`);
37
+ const subfieldValues = fields.flatMap(f => f.subfields.filter(sf => subfieldCodes.includes(sf.code)).map(sf => sf.value));
38
+ //debug(`\t cand values: '${subfieldValues.join("', '")}'`);
39
+ // Stripping punctuaction with substring here is pretty quick and dirty approach...
40
+ const validSubfieldValues = subfieldValues?.map(val => normalizeSubfieldValue(val)).filter(val => isValidIssn(val));
41
+ if (!validSubfieldValues) {
42
+ return [];
43
+ }
44
+ return uniqArray(validSubfieldValues);
45
+ }
46
+
47
+ function getRelevantFields() {
48
+ if (isHost) {
49
+ return record.get(/^022$/u);
50
+ }
51
+ return record.get(/^[79]73$/u);
52
+ }
53
+
54
+ function normalizeSubfieldValue(val) {
55
+ return val.replace(/[., :;].*$/u, '');
56
+ }
57
+
58
+ function isValidIssn(val) {
59
+ return /^[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9X]$/u.test(val);
60
+ }
61
+
62
+ function getRelevantSubfieldCodes() {
63
+ if (isHost) {
64
+ return ['a', 'y', 'z'];
65
+ }
66
+ return ['x'];
67
+ }
68
+ },
69
+ // eslint-disable-next-line max-statements
70
+ compare: (aa, bb) => {
71
+ debugData(`Comparing ISSN sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);
72
+ if (aa.length === 0 || bb.length === 0) {
73
+ // No data for decision
74
+ return 0;
75
+ }
76
+ const firstSharedIssn = aa.find(val => bb.includes(val));
77
+ if (firstSharedIssn) {
78
+ debug(`\t Shared ISSN found: '${firstSharedIssn}'`);
79
+ // Maybe less for comps?
80
+ return 0.2;
81
+ }
82
+ return -0.2;
83
+
84
+ }
85
+ });
86
+
87
+ export function uniqArray(arr) {
88
+ return arr.filter((val, i) => arr.indexOf(val) === i);
89
+ }
@@ -1,111 +1,299 @@
1
1
 
2
2
  import createDebugLogger from 'debug';
3
+
4
+ import {MarcRecord} from '@natlibfi/marc-record';
5
+
6
+ import {FixSami041, Remove041zxx} from '@natlibfi/marc-record-validators-melinda';
7
+
3
8
  import {getMatchCounts} from '../../../matching-utils.js';
4
9
 
5
10
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:language');
6
11
  const debugData = debug.extend('data');
7
12
 
13
+ // NB! 2025-12-17: I changed the logic drastically. However, I tried to keep the scores as same as possible.
14
+ // 'extract' no longer extracts anything. Instead it clones the record, and does some preprocessing (using validators) instead.
15
+ // 'compare' had multiple changes
16
+ // - f041 ind1 differences ('0' vs '1') now result in a small penalty
17
+ // - Two sami languages related changes:
18
+ // -- validator adds a 'smi' subfield before a corresponding sma/sme/...subfield, if needed (national)
19
+ // -- 'smi' only vs 'smi'+'sma' does not cause penalty
20
+ // - 'mul' vs 'fin'+'swe' does not cause a penalty. However, 'mul' vs 'fin' alone triggers penalty.
21
+ // - 008/35-37 and f041$a/$d are calculated separately
22
+ // - a threshold is applied: return value is always between -1.0 and +0.1 (as it was before)
23
+ // - we try to handle 041 $2 ISO 639-2 and ISO 639-3 as well.
24
+ // - 'und' vs one other language does not cause a penalty in certain contexts. In other contexts it's treated as a normal "language code". (Tune/alleviate penalties later on.)
25
+
8
26
  export default () => ({
9
27
  name: 'Language',
10
28
  extract: ({record, recordExternal}) => {
11
29
  const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
30
+ const clonedRecord = new MarcRecord(record, record._validationOptions); // clone(record); // NB! This loses record.get()...
31
+
32
+ FixSami041().fix(clonedRecord); // Handle 'smi' adding if needed
33
+ Remove041zxx().fix(clonedRecord); // Remove 'zxx' from f041s
34
+ // Add other language code normalizations?
12
35
 
13
- const value008 = get008Value();
14
- const values041 = get041Values();
15
- debugData(`${label}: 008: ${JSON.stringify(value008)}, 041: ${JSON.stringify(values041)}`);
36
+ // Should we return all the fields, or just the relevant fields?
37
+ return [{leader: clonedRecord.leader, fields: clonedRecord.fields}, label];
38
+ },
39
+ // eslint-disable-next-line max-statements
40
+ compare: (aa, bb) => {
41
+ const [a, aLabel] = aa;
42
+ const [b, bLabel] = bb;
43
+ debugData(`Comparing language ${JSON.stringify(a)} and ${JSON.stringify(b)}`);
44
+
45
+ const score008 = compare008();
46
+ const score041 = compare041();
47
+ return applyLimits(score008 + score041);
16
48
 
17
- if (!value008 && values041.length < 1) {
18
- debugData(`${label}: No actual values found`);
19
- return [];
49
+ function applyLimits(score) {
50
+ if (score > 0.1) { // Keeps the original max, we might have 0.10 (041) + 0.05 (008/35-37) = 0.15 now
51
+ return 0.1;
52
+ }
53
+ if (score < -1) {
54
+ return -1.0;
55
+ }
56
+ return score;
20
57
  }
21
58
 
22
- const allValues = value008 === undefined ? values041 : values041.concat(value008);
23
- const uniqueSortedValues = [...new Set(allValues)].sort();
59
+ function compare008() {
60
+ const a008 = get008Value(a, aLabel);
61
+ const b008 = get008Value(b, bLabel);
62
+ // Something seriously wrong with 008:
63
+ if (a008 === undefined || b008 === undefined) {
64
+ return 0.0; // Punish for generic badness?
65
+ }
24
66
 
25
- return uniqueSortedValues;
67
+ if (containsNoData(a008) || containsNoData(b008)){
68
+ // Nothing to compare
69
+ return 0.0;
70
+ }
26
71
 
27
- function get008Value() {
28
- const value = record.get(/^008$/u)?.[0]?.value || undefined;
29
- debugData(`${label}: 008 value: ${value}`);
72
+ if (a008 === b008) {
73
+ return 0.05;
74
+ }
30
75
 
31
- if (!value) {
32
- return undefined;
76
+ if (a008 === 'mul' || b008 === 'mul') {
77
+ return 0.0;
33
78
  }
34
79
 
35
- const code = value.slice(35, 38);
36
- debugData(`${label}: 008 code: ${code}`);
37
- return isLangCodeForALanguage(code) ? code : undefined;
80
+ return -0.2;
38
81
  }
39
82
 
40
- // Uses only f041s that have 2nd ind ' ', which means that the codes used are MARC 21 language codes
41
83
 
42
- function get041Values() {
43
- return record.get(/^041$/u)
44
- .filter(({ind2}) => ind2 === ' ')
45
- .map(({subfields}) => subfields)
46
- .flat()
47
- .filter(({code}) => code === 'a' || code === 'd')
48
- .filter(({value}) => value && isLangCodeForALanguage(value))
49
- .map(({value}) => value);
50
- }
51
84
 
52
- // Check if a string is a possible, validly formed language code for a single language
53
- // Currently accept also codes in capitals
54
- function isLangCodeForALanguage(code) {
55
- if (code.length !== 3) {
56
- debugData(`Code ${code} is not correct length (3) for a language code.`);
57
- return false;
85
+
86
+
87
+ function compare041() {
88
+ const a041s = getFields041(a);
89
+ const b041s = getFields041(b);
90
+ if (a041s.length === 0 || b041s.length === 0) {
91
+ return 0.0; // Should we punish these for badness?
58
92
  }
59
- if (code === '|||' || code === ' ' || code === '^^^' || code === 'mul' || code === 'zxx') {
60
- debugData(`Code ${code} is not code for a spesific language.`);
61
- return false;
93
+ if (a041s.length === 1 && b041s.length === 1) {
94
+ // The language codes are typically same between different sources. Incur a small penalty for differences though...
95
+ const sourcePenalty = getSourceOfLanguageCode(a041s[0]) === getSourceOfLanguageCode(b041s[0]) ? 0.0 : -0.05;
96
+ const mainPenalty = compareLanguageCodeLists(getFieldsLanguageCodes(a041s[0]), getFieldsLanguageCodes(b041s[0]));
97
+ const scoreInd1 = mainPenalty === 0.0 ? 0.0 : indicator1Penalty(a041s[0], b041s[0]);
98
+ return mainPenalty + sourcePenalty + scoreInd1;
62
99
  }
63
- const langCodePattern = /^[a-z][a-z][a-z]$/ui;
64
- if (!langCodePattern.test(code)) {
65
- debugData(`Code ${code} is not valid as a language code`);
66
- return false;
100
+ // Things are already complicated (likely to pe penalized, so no nedd to worrty about language code sources, I daresay.
101
+ return compareLanguageCodeLists(getLanguageCodesFrom041Fields(a), getLanguageCodesFrom041Fields(b));
102
+
103
+ function getFields041(record) {
104
+ if (!record.fields) {
105
+ return [];
106
+ }
107
+ return record.fields.filter(f => f.tag === '041');
67
108
  }
68
- return true;
109
+
69
110
  }
70
111
 
71
- },
72
- // eslint-disable-next-line max-statements
73
- compare: (a, b) => {
74
- debugData(`Comparing ${JSON.stringify(a)} and ${JSON.stringify(b)}`);
112
+ function getSourceOfLanguageCode(field) {
113
+ const sf2 = field.subfields.find(sf => sf.code === '2');
114
+ if (!sf2) {
115
+ return 'MARC';
116
+ }
117
+ // Normalize the two relevant language code names:
118
+ if (sf2.value.match(/639.2/u)) {
119
+ return 'ISO 639-2';
120
+ }
121
+ if (sf2.value.match(/639.3/u)) {
122
+ return 'ISO 639-3';
123
+ }
124
+ return sf2.value; // We don't actually have anything else in Melinda though
125
+ }
75
126
 
76
- if (a.length === 0 || b.length === 0) {
77
- debugData(`No language to compare`);
78
- return 0;
127
+ function indicator1Penalty(aField, bField) {
128
+ if (aField.ind1 === ' ' || bField.ind1 === ' '|| aField.ind1 === bField.ind1 ) {
129
+ return 0.0;
130
+ }
131
+ debug(`\t Indicator penalty: '${aField.ind1}' vs '${bField.ind1}'`);
132
+ return -0.1;
79
133
  }
80
134
 
81
- if (a.length === b.length && a.every((element, index) => element === b[index])) {
82
- debugData(`All languages match`);
83
- return 0.1;
135
+
136
+
137
+ function containsNoData(languageCode) {
138
+ return [' ', '|||', 'und'].includes(languageCode);
84
139
  }
85
140
 
86
- const {matchingValues, possibleMatchValues, maxValues} = getMatchCounts(a, b);
141
+ function compareLanguageCodeLists(a, b) {
142
+ if (a.length === 0 || b.length === 0) {
143
+ debugData(`No language to compare`);
144
+ return 0;
145
+ }
146
+
147
+ // If either one of the sets has only the generic 'smi', remove the specific sami languages codes from the other as well:
148
+ const samiLanguageCodes = ['sma', 'sme', 'smj', 'smn', 'sms']; // NB! Don't put generic 'smi' here!
149
+
150
+ if (a.includes('smi') && b.includes('smi')) {
151
+ if (a.some(code => samiLanguageCodes.includes(code)) && !b.some(code => samiLanguageCodes.includes(code))) {
152
+ return compareLanguageCodeLists(a.filter(val => !samiLanguageCodes.includes(val)), b); // recurse
153
+ }
154
+ if (b.some(code => samiLanguageCodes.includes(code)) && !a.some(code => samiLanguageCodes.includes(code))) {
155
+ return compareLanguageCodeLists(a, b.filter(val => !samiLanguageCodes.includes(val))); // recurse
156
+ }
157
+ }
158
+
159
+
160
+
161
+
162
+ if (a.length === b.length && a.every((element, index) => element === b[index])) {
163
+ debugData(`All languages match`);
164
+ return 0.1;
165
+ }
166
+
167
+ // Handle 'mul'. (Should we check whether the other array contains mul as well, probably not...)
168
+ // (If 'mul' does not appear alone, then it is very iffy... )
169
+ if (a.length === 1 && a[0] === 'mul' && b.length > 1) {
170
+ return 0;
171
+ }
172
+ if (b.length === 1 && b[0] === 'mul' && a.length > 1) {
173
+ return 0;
174
+ }
175
+
176
+
177
+
178
+ // 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');
185
+
186
+ 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
+ debug(`Both have languages, but none of these match.`); // includes 'mul' vs a single language code
193
+ return -1.0;
194
+ }
87
195
 
88
- if (matchingValues < 1) {
89
- debug(`Both have languages, but none of these match.`);
90
- return -1.0;
196
+ const {matchingValues, possibleMatchValues, maxValues} = getMatchCounts(a, b);
197
+
198
+ debug(`Both have languages, ${matchingValues}/${possibleMatchValues} valid languages match.`);
199
+ // ignore non-matches if there is mismatching amount of values
200
+ debug(`Possible matches: ${possibleMatchValues}/${maxValues}`);
201
+ // we give some kind of penalty for mismatching amount of values instead of simple divide?
202
+ const missingCount = maxValues - possibleMatchValues;
203
+ const misMatchCount = possibleMatchValues - matchingValues;
204
+ debug(`\t missing: ${missingCount}`);
205
+ debug(`\t mismatches: ${misMatchCount}`);
206
+
207
+ const penaltyForMissing = 0.02 * (maxValues - possibleMatchValues);
208
+ const penaltyForMisMatch = 0.05 * (possibleMatchValues - matchingValues);
209
+ debug(`\t points: penaltyForMissing: ${penaltyForMissing}`);
210
+ debug(`\t points: penaltyForMisMatch: ${penaltyForMisMatch}`);
211
+
212
+ const points = Number(Number(0.1 - penaltyForMisMatch - penaltyForMissing).toFixed(2));
213
+ debug(`Total points: ${points}`);
214
+
215
+ return points;
91
216
  }
92
- debug(`Both have languages, ${matchingValues}/${possibleMatchValues} valid languages match.`);
93
- // ignore non-matches if there is mismatching amount of values
94
- debug(`Possible matches: ${possibleMatchValues}/${maxValues}`);
95
- //we give some kind of penalty for mismatching amount of values instead of simple divide?
96
- const missingCount = maxValues - possibleMatchValues;
97
- const misMatchCount = possibleMatchValues - matchingValues;
98
- debug(`\t missing: ${missingCount}`);
99
- debug(`\t mismatches: ${misMatchCount}`);
100
-
101
- const penaltyForMissing = 0.02 * (maxValues - possibleMatchValues);
102
- const penaltyForMisMatch = 0.05 * (possibleMatchValues - matchingValues);
103
- debug(`\t points: penaltyForMissing: ${penaltyForMissing}`);
104
- debug(`\t points: penaltyForMisMatch: ${penaltyForMisMatch}`);
105
-
106
- const points = Number(Number(0.1 - penaltyForMisMatch - penaltyForMissing).toFixed(2));
107
- debug(`Total points: ${points}`);
108
-
109
- return points;
110
217
  }
218
+
111
219
  });
220
+
221
+ function get008Value(record, label = 'record') {
222
+ if (!record || !record.fields) {
223
+ return;
224
+ }
225
+ const f008 = record.fields.find(f => f.tag === '008');
226
+ if (!f008) {
227
+ return undefined;
228
+ }
229
+ const value = f008.value || defaultValue;
230
+
231
+ if (!value) {
232
+ debugData(`${label}: Failed to extact 008/35-37 value from '${value}'`);
233
+ return defaultValue;
234
+ }
235
+
236
+ const code = value.slice(35, 38);
237
+ debugData(`${label}: 008 code: '${code}'`);
238
+ return code;
239
+ }
240
+
241
+ // Check if a string is a possible, validly formed language code for a single language
242
+ // Currently accept also codes in capitals
243
+ function isLangCodeForALanguage(code, label = 'record', encoding = undefined) {
244
+ if (!code) {
245
+ return false;
246
+ }
247
+
248
+ if ([undefined, 'ISO 639-2', 'ISO-639-3'].includes(encoding) && code.length !== 3) {
249
+ debugData(`${label}: Code ${code} is not correct length (3) for a language code.`);
250
+ return false;
251
+ }
252
+ if (!encoding) {
253
+ // 'mul' should be passed as 'mul' can match 'fin' + 'swe' etc.
254
+ // 'zxx' should be removed by a validator
255
+ // '^^^' is Aleph-specific corruption, not supporting it anymore
256
+ if (code === '|||' || code === ' ' ) { // || code === '^^^' || code === 'mul' || code === 'zxx') {
257
+ debugData(`${label}: Code ${code} is not code for a spesific language.`);
258
+ return false;
259
+ }
260
+ }
261
+ // Marc, ISO 639-2 and ISO 639-3 are all three-letters long. Not other language codes seen in Melinda:
262
+ const langCodePattern = /^[a-z][a-z][a-z]$/ui;
263
+ if (!langCodePattern.test(code)) {
264
+ debugData(`${label}: Code ${code} is not valid as a language code`);
265
+ return false;
266
+ }
267
+ return true;
268
+ }
269
+
270
+
271
+
272
+ function getLanguageCodesFrom041Fields(record) {
273
+ // NB! We brutally don't check $2 (language code source) as marc's language codes is practically a subset of ISO 639-2,
274
+ // 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
+ // 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
+ // 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));
278
+ /*
279
+ // .filter(({ind2}) => ind2 === ' ')
280
+ .map(({subfields}) => subfields)
281
+ .flat()
282
+ .filter(({code}) => code === 'a' || code === 'd')
283
+ .filter(({value}) => value && isLangCodeForALanguage(value))
284
+ .map(({value}) => value);
285
+ */
286
+ return [...new Set(values)].sort();
287
+ }
288
+
289
+ function getFieldsLanguageCodes(field) {
290
+ const relevantSubfields = field.subfields.filter(sf => sf.code === 'a' || sf.code === 'd').filter(sf => isLangCodeForALanguage(sf.value, 'field'));
291
+
292
+ const values = relevantSubfields.map(sf => sf.value).flat();
293
+
294
+ return [...new Set(values)].sort();
295
+ }
296
+
297
+ function getSharedValues(list1, list2) {
298
+ return list1.filter(val => list2.includes(val));
299
+ }
@@ -3,7 +3,7 @@ import createDebugLogger from 'debug';
3
3
  import naturalPkg from 'natural';
4
4
  const {LevenshteinDistance: leven} = naturalPkg;
5
5
 
6
- export default ({treshold = 10} = {}) => ({
6
+ export default ({threshold = 10} = {}) => ({
7
7
  name: 'titleVersionOriginal',
8
8
  extract: ({record}) => {
9
9
  const title = getTitle();
@@ -38,7 +38,7 @@ export default ({treshold = 10} = {}) => ({
38
38
 
39
39
  debug(`'${a}' vs '${b}': Max length = ${maxLength}, distance = ${distance}, percentage = ${percentage}`);
40
40
 
41
- if (percentage <= treshold) {
41
+ if (percentage <= threshold) {
42
42
  return 0.3;
43
43
  }
44
44
 
@@ -7,7 +7,7 @@ const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detecti
7
7
  const debugData = debug.extend('data');
8
8
 
9
9
 
10
- export default ({treshold = 10} = {}) => ({
10
+ export default ({threshold = 10} = {}) => ({
11
11
  name: 'Title',
12
12
  extract: ({record, recordExternal}) => {
13
13
  const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
@@ -19,8 +19,9 @@ export default ({treshold = 10} = {}) => ({
19
19
  // decompose unicode diacritics
20
20
  .normalize('NFD')
21
21
  // strip non-letters/numbers
22
- // - note: combined with decomposing unicode diactics this normalizes both 'saa' and 'sää' as 'saa'
23
- // - we could precompose the finnish letters back to avoid this
22
+ // - note: combined with decomposing unicode diacritics this normalizes both 'saa' and 'sää' as 'saa'
23
+ // - we could precompose the Finnish letters back to avoid this
24
+ // - see validator normalize-utf8-diacritics for details
24
25
  .replace(/[^\p{Letter}\p{Number}]/gu, '')
25
26
  .toLowerCase();
26
27
  debug(`${label}: titleString: ${titleAsNormalizedString}`);
@@ -56,7 +57,7 @@ export default ({treshold = 10} = {}) => ({
56
57
 
57
58
  debug(`'${a}' vs '${b}': Max length = ${maxLength}, distance = ${distance}, percentage = ${percentage}`);
58
59
 
59
- if (percentage <= treshold) {
60
+ if (percentage <= threshold) {
60
61
  return 0.3;
61
62
  }
62
63
 
@@ -4,13 +4,13 @@ import * as features from './features/index.js';
4
4
 
5
5
  export {features};
6
6
 
7
- export default ({strategy, treshold = 0.9}, returnStrategy = false, localSettings = {}) => ({recordA, recordB, recordAExternal = {}, recordBExternal = {}}) => {
7
+ export default ({strategy, threshold = 0.8999}, returnStrategy = false, localSettings = {}) => ({recordA, recordB, recordAExternal = {}, recordBExternal = {}}) => {
8
8
  const minProbabilityQuantifier = 0.5;
9
9
 
10
10
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection');
11
11
  const debugData = debug.extend('data');
12
12
 
13
- debugData(`Strategy: ${JSON.stringify(strategy)}, Treshold: ${JSON.stringify(treshold)}, ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
13
+ debugData(`Strategy: ${JSON.stringify(strategy)}, Threshold: ${JSON.stringify(threshold)}, ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
14
14
  debugData(`Records: A: ${recordA}\nB: ${recordB}`);
15
15
  debug(`Externals: A: ${JSON.stringify(recordAExternal)}, B: ${JSON.stringify(recordBExternal)}`);
16
16
  // We could add here labels for records if we didn't get external labels
@@ -43,8 +43,8 @@ export default ({strategy, treshold = 0.9}, returnStrategy = false, localSetting
43
43
 
44
44
  if (similarityVector.some(v => v >= minProbabilityQuantifier)) {
45
45
  const probability = calculateProbability(similarityVector);
46
- debug(`probability: ${probability} (Treshold: ${treshold})`);
47
- return returnResult({match: probability >= treshold, probability});
46
+ debug(`probability: ${probability} (Threshold: ${threshold})`);
47
+ return returnResult({match: probability >= threshold, probability});
48
48
  }
49
49
 
50
50
  debugData(`No feature yielded minimum probability amount of points (${minProbabilityQuantifier})`);
@@ -58,7 +58,7 @@ export default ({strategy, treshold = 0.9}, returnStrategy = false, localSetting
58
58
  function returnResult(result) {
59
59
  if (returnStrategy) {
60
60
  debug(`Returning detection strategy with the result`);
61
- const resultWithStrategy = {match: result.match, probability: result.probability, strategy: formatStrategy(strategy), treshold};
61
+ const resultWithStrategy = {match: result.match, probability: result.probability, strategy: formatStrategy(strategy), threshold};
62
62
  debugData(`${JSON.stringify(resultWithStrategy)}`);
63
63
  return resultWithStrategy;
64
64
  }