@natlibfi/marc-record-validators-melinda 11.3.1-alpha.1 → 11.3.2-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 (80) hide show
  1. package/dist/index.js +7 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/melindaCustomMergeFields.json +5120 -0
  4. package/dist/merge-fields/config.json +83 -0
  5. package/dist/merge-fields/controlSubfields.js +278 -0
  6. package/dist/merge-fields/controlSubfields.js.map +1 -0
  7. package/dist/merge-fields/counterpartField.js +674 -0
  8. package/dist/merge-fields/counterpartField.js.map +1 -0
  9. package/dist/merge-fields/index.js +76 -0
  10. package/dist/merge-fields/index.js.map +1 -0
  11. package/dist/merge-fields/mergableIndicator.js +95 -0
  12. package/dist/merge-fields/mergableIndicator.js.map +1 -0
  13. package/dist/merge-fields/mergableTag.js +33 -0
  14. package/dist/merge-fields/mergableTag.js.map +1 -0
  15. package/dist/merge-fields/mergeConstraints.js +1225 -0
  16. package/dist/merge-fields/mergeConstraints.js.map +1 -0
  17. package/dist/merge-fields/mergeField.js +190 -0
  18. package/dist/merge-fields/mergeField.js.map +1 -0
  19. package/dist/merge-fields/mergeIndicator.js +171 -0
  20. package/dist/merge-fields/mergeIndicator.js.map +1 -0
  21. package/dist/merge-fields/mergeOrAddPostprocess.js +57 -0
  22. package/dist/merge-fields/mergeOrAddPostprocess.js.map +1 -0
  23. package/dist/merge-fields/mergeOrAddSubfield.js +203 -0
  24. package/dist/merge-fields/mergeOrAddSubfield.js.map +1 -0
  25. package/dist/merge-fields/mergeSubfield.js +277 -0
  26. package/dist/merge-fields/mergeSubfield.js.map +1 -0
  27. package/dist/merge-fields/removeDuplicateSubfields.js +48 -0
  28. package/dist/merge-fields/removeDuplicateSubfields.js.map +1 -0
  29. package/dist/merge-fields/worldKnowledge.js +98 -0
  30. package/dist/merge-fields/worldKnowledge.js.map +1 -0
  31. package/dist/merge-fields.spec.js +51 -0
  32. package/dist/merge-fields.spec.js.map +1 -0
  33. package/dist/subfield6Utils.js +16 -1
  34. package/dist/subfield6Utils.js.map +1 -1
  35. package/dist/utils.js +108 -0
  36. package/dist/utils.js.map +1 -1
  37. package/package.json +6 -6
  38. package/src/index.js +3 -1
  39. package/src/melindaCustomMergeFields.json +5120 -0
  40. package/src/merge-fields/config.json +83 -0
  41. package/src/merge-fields/controlSubfields.js +307 -0
  42. package/src/merge-fields/counterpartField.js +736 -0
  43. package/src/merge-fields/index.js +69 -0
  44. package/src/merge-fields/mergableIndicator.js +90 -0
  45. package/src/merge-fields/mergableTag.js +89 -0
  46. package/src/merge-fields/mergeConstraints.js +309 -0
  47. package/src/merge-fields/mergeField.js +187 -0
  48. package/src/merge-fields/mergeIndicator.js +185 -0
  49. package/src/merge-fields/mergeOrAddPostprocess.js +56 -0
  50. package/src/merge-fields/mergeOrAddSubfield.js +218 -0
  51. package/src/merge-fields/mergeSubfield.js +306 -0
  52. package/src/merge-fields/removeDuplicateSubfields.js +50 -0
  53. package/src/merge-fields/worldKnowledge.js +104 -0
  54. package/src/merge-fields.spec.js +52 -0
  55. package/src/subfield6Utils.js +14 -1
  56. package/src/utils.js +119 -0
  57. package/test-fixtures/merge-fields/f01/expectedResult.json +11 -0
  58. package/test-fixtures/merge-fields/f01/metadata.json +5 -0
  59. package/test-fixtures/merge-fields/f01/record.json +13 -0
  60. package/test-fixtures/merge-fields/f02/expectedResult.json +14 -0
  61. package/test-fixtures/merge-fields/f02/metadata.json +6 -0
  62. package/test-fixtures/merge-fields/f02/record.json +16 -0
  63. package/test-fixtures/merge-fields/f03/expectedResult.json +17 -0
  64. package/test-fixtures/merge-fields/f03/metadata.json +7 -0
  65. package/test-fixtures/merge-fields/f03/record.json +23 -0
  66. package/test-fixtures/merge-fields/f04/expectedResult.json +14 -0
  67. package/test-fixtures/merge-fields/f04/metadata.json +5 -0
  68. package/test-fixtures/merge-fields/f04/record.json +19 -0
  69. package/test-fixtures/merge-fields/v01/expectedResult.json +6 -0
  70. package/test-fixtures/merge-fields/v01/metadata.json +5 -0
  71. package/test-fixtures/merge-fields/v01/record.json +13 -0
  72. package/test-fixtures/merge-fields/v02/expectedResult.json +4 -0
  73. package/test-fixtures/merge-fields/v02/metadata.json +5 -0
  74. package/test-fixtures/merge-fields/v02/record.json +13 -0
  75. package/test-fixtures/merge-fields/v03/expectedResult.json +6 -0
  76. package/test-fixtures/merge-fields/v03/metadata.json +6 -0
  77. package/test-fixtures/merge-fields/v03/record.json +16 -0
  78. package/test-fixtures/merge-fields/v04/expectedResult.json +4 -0
  79. package/test-fixtures/merge-fields/v04/metadata.json +6 -0
  80. package/test-fixtures/merge-fields/v04/record.json +16 -0
@@ -0,0 +1,674 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.baseIsSource = baseIsSource;
7
+ exports.canContainOptionalQualifier = canContainOptionalQualifier;
8
+ exports.getCounterpart = getCounterpart;
9
+ exports.splitToNameAndQualifier = splitToNameAndQualifier;
10
+ exports.splitToNameAndQualifierAndProcessName = splitToNameAndQualifierAndProcessName;
11
+ var _debug = _interopRequireDefault(require("debug"));
12
+ var _utils = require("../utils");
13
+ var _normalizeFieldForComparison = require("../normalizeFieldForComparison");
14
+ var _normalizeIdentifiers = require("../normalize-identifiers");
15
+ var _mergeConstraints = require("./mergeConstraints");
16
+ var _controlSubfields = require("./controlSubfields");
17
+ var _mergableIndicator = require("./mergableIndicator");
18
+ var _normalizeSubfieldValueForComparison = require("../normalizeSubfieldValueForComparison");
19
+ var _worldKnowledge = require("./worldKnowledge");
20
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
21
+ // For each incoming field that
22
+
23
+ // This should be done via our own normalizer:
24
+
25
+ const debug = (0, _debug.default)('@natlibfi/marc-record-validators-melinda:mergeField:counterpart');
26
+ //const debugData = debug.extend('data');
27
+ const debugDev = debug.extend('dev');
28
+ const irrelevantSubfieldsInNameAndTitlePartComparison = '5689';
29
+ const counterpartRegexps = {
30
+ // NB! tag is from source!
31
+ // Note that in the normal case, all source 1XX fields have been converted to 7XX fields.
32
+ '100': /^[17]00$/u,
33
+ '110': /^[17]10$/u,
34
+ '111': /^[17]11$/u,
35
+ '130': /^[17]30$/u,
36
+ '260': /^26[04]$/u,
37
+ '264': /^26[04]$/u,
38
+ '700': /^[17]00$/u,
39
+ '710': /^[17]10$/u,
40
+ '711': /^[17]11$/u,
41
+ '730': /^[17]30$/u,
42
+ // Hacks:
43
+ '940': /^[29]40$/u,
44
+ '973': /^[79]73$/u
45
+ };
46
+ const counterpartRegexpsSingle = {
47
+ // when base===source, never merge 1XX to 7XX, always 7XX to 1XX! Also, don't merge 264 to 260.
48
+ '260': /^26[04]$/u,
49
+ '700': /^[17]00$/u,
50
+ '110': /^[17]10$/u,
51
+ '111': /^[17]11$/u,
52
+ '130': /^[17]30$/u,
53
+ // Hacks:
54
+ '940': /^[29]40$/u,
55
+ '973': /^[79]73$/u
56
+ };
57
+
58
+ /*
59
+ function differentPublisherSubfields(field1, field2) {
60
+ if (field1.tag === '260' && field2.tag === '264' && field2.ind2 === '3') {
61
+ return true;
62
+ }
63
+ if (field1.tag === '264' && field1.ind2 === '3' && field2.tag === '260') {
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ */
69
+
70
+ function splitToNameAndQualifier(value) {
71
+ if (value.match(/^.* \([^()]+\)$/u)) {
72
+ const name = value.replace(/^(.*) \([^()]+\)$/u, '$1'); // eslint-disable-line prefer-named-capture-group
73
+ const qualifier = value.replace(/^.* (\([^()]+\))$/u, '$1'); // eslint-disable-line prefer-named-capture-group
74
+ return [name, qualifier];
75
+ }
76
+ return [value, undefined];
77
+ }
78
+ function splitToNameAndQualifierAndProcessName(name) {
79
+ //const nameOnly = name.replace(/(?: \([^)]+\)| abp?| Kustannus| Kustannus Oy|, kustannusosakeyhtiö| oyj?| ry)$/ugi, '');
80
+ const [qualifierlessName, qualifier] = splitToNameAndQualifier(name);
81
+ const [prefix, basename, suffix] = stripPrefixAndSuffix(qualifierlessName);
82
+ return {
83
+ name: getBestName(basename).toLowerCase(),
84
+ prefix,
85
+ suffix,
86
+ qualifier
87
+ };
88
+ function stripPrefixAndSuffix(companyName) {
89
+ const [nameOnly, suffix] = extractSuffix(companyName);
90
+ const [nameOnly2, prefix] = extractPrefix(nameOnly);
91
+ return [prefix, nameOnly2, suffix];
92
+ }
93
+ function extractSuffix(name) {
94
+ const nameOnly = name.replace(/(?: \([^)]+\)| abp?| Kustannus| Kustannus Oy|, kustannusosakeyhtiö| oyj?| ry)$/ugi, '');
95
+ if (nameOnly === name) {
96
+ return [name, undefined];
97
+ }
98
+ return [nameOnly, name.substring(nameOnly.length).replace(/^,? /u, '')];
99
+ }
100
+ function extractPrefix(name) {
101
+ const nameOnly = name.replace(/^(?:Ab|Kustannusosakeyhtiö|Kustannus Oy|Oy) /ugi, '');
102
+ if (nameOnly === name) {
103
+ return [name, undefined];
104
+ }
105
+ return [nameOnly, name.substring(0, name.length - nameOnly.length - 1)]; // -1 removes final space
106
+ }
107
+ function getBestName(name) {
108
+ const NAME = name.toUpperCase();
109
+ if (NAME === 'WSOY') {
110
+ return 'Werner Söderström osakeyhtiö';
111
+ }
112
+ if (NAME === 'NTAMO') {
113
+ return 'ntamo';
114
+ }
115
+ return name;
116
+ }
117
+ }
118
+ function canContainOptionalQualifier(tag, subfieldCode) {
119
+ // We have made 300$a NON-repeatable (against specs), as we newer want there to repeat (probably near-duplicates)
120
+ if (tag === '300' && subfieldCode === 'a') {
121
+ return true;
122
+ }
123
+ // 776$i is actually not needed for counterpart stuff (since it's repeatable), but it is needed in merge subfield stage.
124
+ if (tag === '776' && subfieldCode === 'i') {
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+ function withAndWithoutQualifierAgree(value1, value2, tag, subfieldCode) {
130
+ if (!canContainOptionalQualifier(tag, subfieldCode)) {
131
+ return false;
132
+ }
133
+ const [name1, qualifier1] = splitToNameAndQualifier(value1);
134
+ const [name2, qualifier2] = splitToNameAndQualifier(value2);
135
+
136
+ //nvdebug(`CN1: '${name1}', '${qualifier1}'`, debugDev);
137
+ //nvdebug(`CN2: '${name2}', '${qualifier2}'`, debugDev);
138
+
139
+ if (name1.toLowerCase() !== name2.toLowerCase()) {
140
+ return false;
141
+ }
142
+
143
+ // If either value does not have a qualifier, they are considered equals:
144
+ if (qualifier1 === undefined || qualifier2 === undefined || qualifier1.toLowerCase() === qualifier2.toLowerCase()) {
145
+ return true;
146
+ }
147
+ return false;
148
+ }
149
+ function corporateNamesAgree(value1, value2, tag, subfieldCode) {
150
+ if (subfieldCode !== 'a' || !['110', '610', '710', '810'].includes(tag)) {
151
+ return false;
152
+ }
153
+ const nameData1 = splitToNameAndQualifierAndProcessName(value1);
154
+ const nameData2 = splitToNameAndQualifierAndProcessName(value2);
155
+ (0, _utils.nvdebug)(`CN1: '${nameData1.name}', '${nameData1.qualifier}'`, debugDev);
156
+ (0, _utils.nvdebug)(`CN2: '${nameData2.name}', '${nameData2.qualifier}'`, debugDev);
157
+ if (nameData1.name !== nameData2.name) {
158
+ return false;
159
+ }
160
+ if (nameData1.qualifier && nameData2.qualifier && nameData1.qualifier !== nameData2.qualifier) {
161
+ return false;
162
+ }
163
+ // Currently all prefixes and suffixes are publisher information, so there's no point comparing them any further...
164
+
165
+ return true;
166
+
167
+ /*
168
+ function isKustantaja(nameData) {
169
+ if (nameData.suffix.match(/^(?:Kustannus|Kustannus oy|kustannusosakeyhtiö)$/iu)) {
170
+ return true;
171
+ }
172
+ if (nameData.prefix.match(/^Kustannus Oy$/i)) {
173
+ return true;
174
+ }
175
+ return false;
176
+ }
177
+ */
178
+ }
179
+ function pairableValue(tag, subfieldCode, value1, value2) {
180
+ // This function could just return true or false.
181
+ // I thought of preference when I wrote this, but preference implemented *here* (modularity). mergeFields.js should handle preference.
182
+ if (withAndWithoutQualifierAgree(value1, value2, tag, subfieldCode)) {
183
+ // 300$a "whatever" and "whatever (123 sivua)"
184
+ return value1;
185
+ }
186
+ if ((0, _normalizeSubfieldValueForComparison.partsAgree)(value1, value2, tag, subfieldCode) || corporateNamesAgree(value1, value2, tag, subfieldCode)) {
187
+ // Pure baseness: here we assume that base's value1 is better than source's value2.
188
+ return value1;
189
+ }
190
+ return undefined;
191
+ }
192
+ function counterpartExtraNormalize(tag, subfieldCode, value) {
193
+ /* eslint-disable prefer-named-capture-group, no-param-reassign */
194
+ // Remove trailing punctuation:
195
+ value = value.replace(/(\S)(?:,|\.|\?|!|\. -| *:| *;| =| \/)$/u, '$1');
196
+ // Remove brackets:
197
+ value = value.replace(/^\(([^()]+)\)$/u, '$1'); // Remove initial '(' and final ')' if both exist.
198
+ value = value.replace(/^\[([^[\]]+)\]$/u, '$1'); // Remove initial '[' and final ']' if both exist.
199
+ // Mainly for field 260$c:
200
+ value = (0, _utils.removeCopyright)(value);
201
+ value = value.replace(/http:\/\//ug, 'https://'); // MET-501: http vs https
202
+ value = (0, _worldKnowledge.normalizeForSamenessCheck)(tag, subfieldCode, value);
203
+
204
+ /* eslint-enable */
205
+ return value;
206
+ }
207
+ function uniqueKeyMatches(baseField, sourceField, forcedKeyString = null) {
208
+ // NB! Assume that field1 and field2 have same relevant subfields.
209
+ // What to do if if base
210
+ // const keySubfieldsAsString = forcedKeyString || getUniqueKeyFields(field1);
211
+ const keySubfieldsAsString = forcedKeyString || (0, _mergeConstraints.getMergeConstraintsForTag)(baseField.tag, 'key');
212
+ //return mandatorySubfieldComparison(baseField, sourceField, keySubfieldsAsString);
213
+ return optionalSubfieldComparison(baseField, sourceField, keySubfieldsAsString);
214
+ }
215
+ function optionalSubfieldComparison(originalBaseField, originalSourceField, keySubfieldsAsString) {
216
+ // Here "optional subfield" means a subfield, that needs not to be present, but if present, it must be identical...
217
+ // (Think of a better name...)
218
+ // We use clones here, since these changes done below are not intented to appear on the actual records.
219
+ const field1 = (0, _normalizeFieldForComparison.cloneAndNormalizeFieldForComparison)(originalBaseField);
220
+ const field2 = (0, _normalizeFieldForComparison.cloneAndNormalizeFieldForComparison)(originalSourceField);
221
+ if (keySubfieldsAsString === null) {
222
+ // does not currently happen
223
+ // If keySubfieldsAsString is undefined, (practically) everything is the string.
224
+ // When everything is the string, the strings need to be (practically) identical.
225
+ // (NB! Here order matters. We should probably make it matter everywhere.)
226
+ // (However, keySubfieldsAsString === '' will always succeed. Used by 040 at least.)
227
+ // NB! substring(6) skips "TAG II" (I=indicator. Thus we skip indicators)
228
+ return (0, _utils.fieldToString)(field1).substring(6) === (0, _utils.fieldToString)(field2).substring(6);
229
+ }
230
+ const subfieldArray = keySubfieldsAsString.split('');
231
+
232
+ // Long forgotten, but my educated guess about this: if 'key' is defined in merge constraints
233
+ // for this field, then at least one of the subfield codes in 'key' must be present in both fields.
234
+ // However, this is not necessarily right.
235
+ if (subfieldArray.length > 0 && !subfieldArray.some(sfCode => hasCommonNominator(sfCode))) {
236
+ return false;
237
+ }
238
+ return subfieldArray.every(subfieldCode => testOptionalSubfield(originalBaseField.tag, subfieldCode));
239
+ function hasCommonNominator(subfieldCode) {
240
+ //nvdebug(`hasCommonNominator(${subfieldCode})? '${fieldToString(originalBaseField)}' vs '${fieldToString(originalSourceField)}'`, debugDev);
241
+
242
+ // If base has $a and source has $b, there's no common nominator, thus fail...
243
+ const subfields1 = field1.subfields.filter(subfield => subfield.code === subfieldCode && (0, _worldKnowledge.valueCarriesMeaning)(field1.tag, subfield.code, subfield.value));
244
+ const subfields2 = field2.subfields.filter(subfield => subfield.code === subfieldCode && (0, _worldKnowledge.valueCarriesMeaning)(field2.tag, subfield.code, subfield.value));
245
+ return subfields1.length > 0 && subfields2.length > 0;
246
+ }
247
+ function testOptionalSubfield(tag, subfieldCode) {
248
+ // NB! Don't compare non-meaningful subfields
249
+ const subfields1 = field1.subfields.filter(subfield => subfield.code === subfieldCode && (0, _worldKnowledge.valueCarriesMeaning)(field1.tag, subfield.code, subfield.value));
250
+ const subfields2 = field2.subfields.filter(subfield => subfield.code === subfieldCode && (0, _worldKnowledge.valueCarriesMeaning)(field2.tag, subfield.code, subfield.value));
251
+
252
+ // If one side is empty, all is good
253
+ if (subfields1.length === 0 || subfields2.length === 0) {
254
+ return true;
255
+ }
256
+
257
+ //nvdebugSubfieldArray(subfields1, 'SF1', debugDev);
258
+ //nvdebugSubfieldArray(subfields2, 'SF2', debugDev);
259
+
260
+ // When pairing we can use stronger normalizations than the generic one:
261
+ const subfieldValues1 = subfields1.map(sf => counterpartExtraNormalize(tag, subfieldCode, sf.value));
262
+ const subfieldValues2 = subfields2.map(sf => counterpartExtraNormalize(tag, subfieldCode, sf.value));
263
+
264
+ //nvdebug(`SF1 NORM: ${subfieldValues1.join(' --')}`, debugDev);
265
+ //nvdebug(`SF2 NORM: ${subfieldValues2.join(' --')}`, debugDev);
266
+
267
+ // If one set is a subset of the other, all is probably good (how about 653$a, 505...)
268
+ if (subfieldValues1.every(val => subfieldValues2.includes(val)) || subfieldValues2.every(val => subfieldValues1.includes(val))) {
269
+ return true;
270
+ }
271
+ if (subfieldValues1.length === 1 && subfieldValues2.length === 1) {
272
+ return pairableValue(field1.tag, subfieldCode, subfieldValues1[0], subfieldValues2[0]) !== undefined;
273
+ }
274
+ return false;
275
+ }
276
+ }
277
+ function mandatorySubfieldComparison(originalField1, originalField2, keySubfieldsAsString) {
278
+ // NB! We use clones here, since these changes done below are not intented to appear on the actual records.
279
+ const field1 = (0, _normalizeFieldForComparison.cloneAndNormalizeFieldForComparison)(originalField1);
280
+ const field2 = (0, _normalizeFieldForComparison.cloneAndNormalizeFieldForComparison)(originalField2);
281
+ if (keySubfieldsAsString === null) {
282
+ // does not currently happen
283
+ // If keySubfieldsAsString is undefined, (practically) everything is the string.
284
+ // When everything is the string, the strings need to be (practically) identical.
285
+ // (NB! Here order matters. We should probably make it matter everywhere.)
286
+ // (However, keySubfieldsAsString === '' will always succeed. Used by 040 at least.)
287
+ return (0, _utils.fieldToString)(field1) === (0, _utils.fieldToString)(field2);
288
+ }
289
+ const subfieldArray = keySubfieldsAsString.split('');
290
+
291
+ //const differentSubfieldCodes = differentPublisherSubfields(originalField1, originalField2);
292
+
293
+ return subfieldArray.every(subfieldCode => mandatorySingleSubfieldComparison(subfieldCode));
294
+ function mandatorySingleSubfieldComparison(subfieldCode) {
295
+ //const otherSubfieldCode = getOtherSubfieldCode(subfieldCode);
296
+ const subfieldValues1 = field1.subfields.filter(subfield => subfield.code === subfieldCode).map(sf => sf.value);
297
+ const subfieldValues2 = field2.subfields.filter(subfield => subfield.code === subfieldCode).map(sf => sf.value);
298
+ // Assume that at least 1 instance must exist and that all instances must match
299
+ if (subfieldValues1.length !== subfieldValues2.length) {
300
+ debugDev(`mSC: Unique key: subfield ${subfieldCode} issues...`);
301
+ return false;
302
+ }
303
+ return subfieldValues1.every(value => subfieldValues2.includes(value));
304
+ }
305
+ }
306
+ function tagToRegexp(tag, internalMerge = false) {
307
+ if (internalMerge && tag in counterpartRegexpsSingle) {
308
+ return counterpartRegexpsSingle[tag];
309
+ }
310
+ if (!internalMerge && tag in counterpartRegexps) {
311
+ // eg. 700 looks for tag /^[17]00$/...
312
+ const regexp = counterpartRegexps[tag];
313
+ //nvdebug(`regexp for ${tag} found: ${regexp}`, debugDev);
314
+ return regexp;
315
+ }
316
+ //nvdebug(`WARNING: tagToRegexp(${tag}): no precompiled regexp found.`, debugDev);
317
+ return new RegExp(`^${tag}$`, 'u');
318
+ }
319
+ function areRequiredSubfieldsPresent(field) {
320
+ const subfieldString = (0, _mergeConstraints.getMergeConstraintsForTag)(field.tag, 'required');
321
+ if (subfieldString === null) {
322
+ return true;
323
+ } // nothing is required
324
+ const subfieldArray = subfieldString.split('');
325
+ return subfieldArray.every(sfcode => {
326
+ const result = (0, _utils.fieldHasSubfield)(field, sfcode);
327
+ if (!result) {
328
+ debugDev(`Required subfield ‡${sfcode} not found in '${(0, _utils.fieldToString)(field)}'!`);
329
+ return false;
330
+ }
331
+ return true;
332
+ });
333
+ }
334
+ function arePairedSubfieldsInBalance(field1, field2) {
335
+ const subfieldString = (0, _mergeConstraints.getMergeConstraintsForTag)(field1.tag, 'paired');
336
+ if (subfieldString === null) {
337
+ return true;
338
+ }
339
+ const subfieldArray = subfieldString.split('');
340
+ return subfieldArray.every(sfcode => (0, _utils.fieldHasNSubfields)(field1, sfcode) === (0, _utils.fieldHasNSubfields)(field2, sfcode));
341
+ }
342
+ function syntacticallyMergablePair(baseField, sourceField, config) {
343
+ // Indicators must typically be equal (there are exceptions such as non-filing characters though):
344
+ if (!(0, _mergableIndicator.mergableIndicator1)(baseField, sourceField, config)) {
345
+ (0, _utils.nvdebug)(`non-mergable (reason: indicator1): ${JSON.stringify(config)}`, debugDev);
346
+ return false;
347
+ }
348
+ if (!(0, _mergableIndicator.mergableIndicator2)(baseField, sourceField, config)) {
349
+ (0, _utils.nvdebug)(`non-mergable (reason: indicator2): ${JSON.stringify(config)}`, debugDev);
350
+ return false;
351
+ }
352
+ if (!(0, _controlSubfields.controlSubfieldsPermitMerge)(baseField, sourceField)) {
353
+ (0, _utils.nvdebug)('non-mergable (reason: control subfield)', debugDev);
354
+ return false;
355
+ }
356
+
357
+ // NB! field1.tag and field2.tag might differ (1XX vs 7XX). Therefore required subfields might theoretically differ as well.
358
+ // Note: Theoretically 260 $efg vs 264 with IND2=3 has already been handled by the preprocessor.
359
+ // Thus check both:
360
+ if (!areRequiredSubfieldsPresent(baseField) || !areRequiredSubfieldsPresent(sourceField)) {
361
+ (0, _utils.nvdebug)('non-mergable (reason: missing subfields)', debugDev);
362
+ return false;
363
+ }
364
+
365
+ // Stuff of Hacks! Eg. require that both fields either have or have not X00$t:
366
+ if (!arePairedSubfieldsInBalance(baseField, sourceField)) {
367
+ (0, _utils.nvdebug)('required subfield pair check failed.', debugDev);
368
+ return false;
369
+ }
370
+ return true;
371
+ }
372
+ function mergablePair(baseField, sourceField, config) {
373
+ if (!syntacticallyMergablePair(baseField, sourceField, config)) {
374
+ return false;
375
+ }
376
+
377
+ //debug('Test semantics...');
378
+ if (!semanticallyMergablePair(baseField, sourceField)) {
379
+ (0, _utils.nvdebug)('non-mergable (reason: semantics)', debugDev);
380
+ return false;
381
+ }
382
+ (0, _utils.nvdebug)(`MERGABLE PAIR:\n B: ${(0, _utils.fieldToString)(baseField)}\n S: ${(0, _utils.fieldToString)(sourceField)}`, debugDev);
383
+ return true;
384
+ }
385
+ function pairableAsteriIDs(baseField, sourceField) {
386
+ //nvdebug(`ASTERI1 ${fieldToString(baseField)}`, debugDev); // eslint-disable-line
387
+ //nvdebug(`ASTERI2 ${fieldToString(sourceField)}`, debugDev); // eslint-disable-line
388
+
389
+ // Check that relevant control subfield(s) exist in both records (as controlSubfieldsPermitMerge() doesn't check it):
390
+ const fin11a = getAsteriIDs(baseField);
391
+ if (fin11a.length === 0) {
392
+ return false;
393
+ }
394
+ const fin11b = getAsteriIDs(sourceField);
395
+ if (fin11b.length === 0) {
396
+ return false;
397
+ }
398
+ //nvdebug(`ASTERI WP3:\n${fin11a.join(", ")}\n${fin11b.join(", ")}`, debugDev); // eslint-disable-line
399
+
400
+ // Check that found control subfields agree. Use pre-existing generic function to reduce code.
401
+ // (NB! We could optimize and just return true here, as control subfield check is done elsewhere as well.
402
+ // However, explicitly checking them here makes the code more robust.)
403
+ if (!(0, _controlSubfields.controlSubfieldsPermitMerge)(baseField, sourceField)) {
404
+ return false;
405
+ }
406
+ //console.log(`ASTERI PAIR ${fieldToString(sourceField)}`); // eslint-disable-line
407
+ return true;
408
+
409
+ // NB! This boldly assumes that the default prefix for Asteri is '(FIN11)', not '(FI-ASTERI-N)' nor a finaf urn...
410
+ function getAsteriIDs(field) {
411
+ return field.subfields.filter(sf => sf.code === '0').map(sf => (0, _normalizeIdentifiers.normalizeControlSubfieldValue)(sf.value)).filter(val => val.substring(0, 7) === '(FIN11)');
412
+ }
413
+ }
414
+ function hasRepeatableSubfieldThatShouldBeTreatedAsNonRepeatable(field) {
415
+ if (field.tag === '260' || field.tag === '264') {
416
+ return ['a', 'b', 'c', 'e', 'f', 'g'].some(subfieldCode => (0, _utils.fieldHasMultipleSubfields)(field, subfieldCode));
417
+ }
418
+ if (field.tag === '382') {
419
+ return ['a', 'b', 'd', 'e', 'n', 'p'].some(subfieldCode => (0, _utils.fieldHasMultipleSubfields)(field, subfieldCode));
420
+ }
421
+ if (field.tag === '505') {
422
+ return ['t', 'r', 'g'].some(subfieldCode => (0, _utils.fieldHasMultipleSubfields)(field, subfieldCode));
423
+ }
424
+ return false;
425
+ }
426
+ function pairableName(baseField, sourceField) {
427
+ // 100$a$t: remove $t and everything after that
428
+ const reducedField1 = fieldToNamePart(baseField);
429
+ const reducedField2 = fieldToNamePart(sourceField);
430
+ const string1 = (0, _utils.fieldToString)(reducedField1);
431
+ const string2 = (0, _utils.fieldToString)(reducedField2);
432
+
433
+ //nvdebug(`IN: pairableName():\n '${string1}' vs\n '${string2}'`, debugDev);
434
+ if (string1 === string2) {
435
+ return true;
436
+ }
437
+
438
+ // Essentially these are too hard to handle with field-merge (eg. multi-505$g)
439
+ if (hasRepeatableSubfieldThatShouldBeTreatedAsNonRepeatable(reducedField1) || hasRepeatableSubfieldThatShouldBeTreatedAsNonRepeatable(reducedField2)) {
440
+ return false;
441
+ }
442
+
443
+ // Compare the remaining subsets...
444
+ // First check that name matches...
445
+ if (uniqueKeyMatches(reducedField1, reducedField2)) {
446
+ (0, _utils.nvdebug)(` name match: '${(0, _utils.fieldToString)(reducedField1)}'`, debugDev);
447
+ return true;
448
+ }
449
+
450
+ // However, name mismatch is not critical! If Asteri ID matches, it's still a match! *NOT* sure whether this a good idea.
451
+ // 2023-01-24 Disable this. Caretaker can fix these later on. Not a job for merge. We can't be sure that $0 pair is corrent, nor which version (base or source) to use.
452
+ // 2023-03-07: Enable this again!
453
+ if (pairableAsteriIDs(baseField, sourceField)) {
454
+ //nvdebug(` name match based on ASTERI $0'`, debugDev);
455
+ return true;
456
+ }
457
+ (0, _utils.nvdebug)(` name mismatch:`, debugDev);
458
+ (0, _utils.nvdebug)(` '${(0, _utils.fieldToString)(reducedField1)}' vs`, debugDev);
459
+ (0, _utils.nvdebug)(` '${(0, _utils.fieldToString)(reducedField2)}'`, debugDev);
460
+ return false;
461
+ }
462
+ function semanticallyMergablePair(baseField, sourceField) {
463
+ // On rare occasions a field contains also a title part. For these name part (= normally everything) and title part
464
+ // must be checked separately:
465
+ if (!titlePartsMatch(baseField, sourceField)) {
466
+ (0, _utils.nvdebug)(` ${baseField.tag} is unmergable: Title part mismatch.`, debugDev);
467
+ return false;
468
+ }
469
+
470
+ // Hmm... we should check lifespan here, $d YYYY
471
+
472
+ // Handle the field specific "unique key" (=set of fields that make the field unique
473
+ if (!pairableName(baseField, sourceField)) {
474
+ (0, _utils.nvdebug)('Unmergable: Name part mismatch', debugDev);
475
+ return false;
476
+ }
477
+ //debug(' Semantic checks passed! We are MERGABLE!');
478
+
479
+ return true;
480
+ }
481
+ function namePartThreshold(field) {
482
+ // Threshold is only applicaple to some tags..
483
+ if (!/[10]0$/u.test(field.tag)) {
484
+ return -1;
485
+ }
486
+ const t = field.subfields.findIndex(currSubfield => currSubfield.code === 't');
487
+ const u = t; // field.subfields.findIndex(currSubfield => currSubfield.code === 'u');
488
+ if (t === -1) {
489
+ return u;
490
+ }
491
+ if (u === -1) {
492
+ return t;
493
+ }
494
+ return t > u ? u : t;
495
+ }
496
+ function fieldToNamePart(field) {
497
+ const index = namePartThreshold(field);
498
+ const relevantSubfields = field.subfields.filter((sf, i) => i < index || index === -1).filter(sf => !irrelevantSubfieldsInNameAndTitlePartComparison.includes(sf.code));
499
+ const subsetField = {
500
+ 'tag': field.tag,
501
+ 'ind1': field.ind1,
502
+ 'ind2': field.ind2,
503
+ subfields: relevantSubfields
504
+ };
505
+
506
+ /*
507
+ if (index > -1) { // eslint-disable-line functional/no-conditional-statements
508
+ debugDev(`Name subset: ${fieldToString(subsetField)}`);
509
+ }
510
+ */
511
+
512
+ // Ummm... Sometimes $0 comes after $t but belongs to name part
513
+
514
+ return subsetField;
515
+ }
516
+ function fieldToTitlePart(field) {
517
+ // Take everything after 1st subfield $t...
518
+ const index = field.subfields.findIndex(currSubfield => currSubfield.code === 't');
519
+ const relevantSubfields = field.subfields.filter((sf, i) => i >= index).filter(sf => !irrelevantSubfieldsInNameAndTitlePartComparison.includes(sf.code));
520
+ const subsetField = {
521
+ 'tag': field.tag,
522
+ 'ind1': field.ind1,
523
+ 'ind2': field.ind2,
524
+ subfields: relevantSubfields
525
+ };
526
+ debugDev(`Title subset: ${(0, _utils.fieldToString)(subsetField)}`);
527
+ return subsetField;
528
+ }
529
+ function containsTitlePart(field) {
530
+ return fieldCanHaveTitlePart(field) && (0, _utils.fieldHasSubfield)(field, 't');
531
+ function fieldCanHaveTitlePart(field) {
532
+ return ['100', '110', '111', '700', '710', '711'].includes(field.tag);
533
+ }
534
+ }
535
+ function titlePartsMatch(field1, field2) {
536
+ if (!containsTitlePart(field1)) {
537
+ return !containsTitlePart(field2);
538
+ }
539
+ if (!containsTitlePart(field2)) {
540
+ return false;
541
+ }
542
+ debugDev(`TITLE PARTS NEED TO BE COMPARED`);
543
+
544
+ // 100$a$t: remove $t and everything after that
545
+ const subset1 = fieldToTitlePart(field1);
546
+ const subset2 = fieldToTitlePart(field2);
547
+ // Easter Egg, ffs. Hardcoded exception
548
+ return mandatorySubfieldComparison(subset1, subset2, 'dfhklmnoprstxvg');
549
+ }
550
+ function getAlternativeNamesFrom9XX(record, field) {
551
+ // Should we support 6XX and 8XX as well? Prolly not...
552
+ if (!field.tag.match(/^(?:100|110|111|600|610|611|700|710|711)$/u)) {
553
+ return [];
554
+ }
555
+ const tag = `9${field.tag.substring(1)}`;
556
+ const cands = record.get(tag).filter(f => (0, _utils.fieldHasSubfield)(f, 'a') && (0, _utils.fieldHasSubfield)(f, 'y'));
557
+ if (cands.length === 0) {
558
+ return [];
559
+ }
560
+ const punctuationlessField = (0, _normalizeFieldForComparison.cloneAndRemovePunctuation)(field);
561
+ const [name] = punctuationlessField.subfields.filter(sf => sf.code === 'a').map(sf => sf.value);
562
+ return cands.map(candField => getAltName(candField)).filter(val => val !== undefined);
563
+ function getAltName(altField) {
564
+ const [altA] = altField.subfields.filter(sf => sf.code === 'a').map(sf => sf.value);
565
+ const [altY] = altField.subfields.filter(sf => sf.code === 'y').map(sf => sf.value);
566
+ (0, _utils.nvdebug)(`Compare '${name}' vs '${altA}'/'${altY}'`, debugDev);
567
+ if (name === altA) {
568
+ return altY;
569
+ }
570
+ if (name === altY) {
571
+ return altA;
572
+ }
573
+ (0, _utils.nvdebug)(` miss`, debugDev);
574
+ return undefined;
575
+ }
576
+ }
577
+ function mergablePairWithAltName(normCandField, normalizedField, altName, config) {
578
+ // Replace source field $a name with alternative name and then compare:
579
+ const [a] = normalizedField.subfields.filter(sf => sf.code === 'a');
580
+ if (!a) {
581
+ return false;
582
+ }
583
+ a.value = altName; // eslint-disable-line functional/immutable-data
584
+
585
+ return mergablePair(normCandField, normalizedField, config);
586
+ }
587
+ function getCounterpartIndex(field, counterpartCands, altNames, config) {
588
+ const normalizedField = (0, _normalizeFieldForComparison.cloneAndNormalizeFieldForComparison)(field);
589
+ const normalizedCounterpartCands = counterpartCands.map(f => (0, _normalizeFieldForComparison.cloneAndNormalizeFieldForComparison)(f));
590
+ const index = normalizedCounterpartCands.findIndex(normCandField => mergablePair(normCandField, normalizedField, config));
591
+ if (index > -1) {
592
+ return index;
593
+ }
594
+ return normalizedCounterpartCands.findIndex(normCandField => altNames.some(altName => mergablePairWithAltName(normCandField, normalizedField, altName, config)));
595
+ }
596
+ function field264Exception(baseField, sourceRecord, sourceField, config) {
597
+ (0, _utils.nvdebug)('Field 264 exception as per MET-456', debugDev);
598
+ if (baseField.tag !== '264') {
599
+ return false;
600
+ }
601
+ if (sourceField.tag !== '264' || sourceRecord.get('264').length !== 1) {
602
+ return false;
603
+ }
604
+
605
+ // Don't worry about semantics:
606
+ return syntacticallyMergablePair(sourceField, baseField, config);
607
+ }
608
+ function getCounterpartCandidates(field, record) {
609
+ const counterpartCands = record.get(tagToRegexp(field.tag, record.internalMerge));
610
+
611
+ // MELKEHITYS-2969: copyright years should not merge with non-copyright years
612
+ if (field.tag === '260' && isNotCopyrightYear(field)) {
613
+ return counterpartCands.filter(candField => !isCopyrightField264(candField));
614
+ }
615
+ if (field.tag === '264' && isCopyrightField264(field)) {
616
+ // Copyright year
617
+ return counterpartCands.filter(candField => !isNotCopyrightYear(candField));
618
+ }
619
+ function isCopyrightField264(field) {
620
+ return field.tag === '264' && field.ind2 === '4';
621
+ }
622
+ function isNotCopyrightYear(field) {
623
+ if (field.tag === '264') {
624
+ return !isCopyrightField264(field);
625
+ }
626
+ // Field 260: copyright year does not contain $a or $b:
627
+ return !field.subfields.some(sf => sf.code === 'a' && sf.code === 'b');
628
+ }
629
+ return counterpartCands;
630
+ }
631
+ function baseIsSource(base, source) {
632
+ base.localTest = true; // eslint-disable-line functional/immutable-data
633
+ const result = source.localTest;
634
+ delete base.localTest; // eslint-disable-line functional/immutable-data
635
+ return result;
636
+ }
637
+ function getCounterpart(baseRecord, sourceRecord, field, config) {
638
+ // First get relevant candidate fields. Note that 1XX and corresponding 7XX are considered equal, and tags 260 and 264 are lumped together.
639
+ // (<= Note that self-merge behaves differently from two records here.)
640
+ // Hacks: 973 can merge with 773, 940 can merge with 240 (but not the other way around)
641
+ //nvdebug(`COUNTERPART FOR '${fieldToString(field)}'?`, debugDev);
642
+ const counterpartCands = getCounterpartCandidates(field, baseRecord).filter(f => !f.mergeCandidate);
643
+ if (!counterpartCands || counterpartCands.length === 0) {
644
+ //nvdebug(`No counterpart(s) found for ${fieldToString(field)}`, debugDev);
645
+ return null;
646
+ }
647
+ (0, _utils.nvdebug)(`Compare incoming '${(0, _utils.fieldToString)(field)}' with (up to) ${counterpartCands.length} existing field(s)`, debugDev);
648
+ const normalizedField = (0, _normalizeFieldForComparison.cloneAndNormalizeFieldForComparison)(field); // mainly strip punctuation here
649
+
650
+ (0, _utils.nvdebug)(`Norm to: '${(0, _utils.fieldToString)(normalizedField)}'`, debugDev);
651
+ const uniqueAlternativeNames = getUniqueAlernativeNames();
652
+ function getUniqueAlernativeNames() {
653
+ if (baseIsSource(baseRecord, sourceRecord)) {
654
+ return [];
655
+ }
656
+ // Try to look for alternative names from base and source record's 9XX fields:
657
+ const alternativeNames = getAlternativeNamesFrom9XX(baseRecord, field).concat(getAlternativeNamesFrom9XX(sourceRecord, field));
658
+ return alternativeNames.filter((name, i) => alternativeNames.indexOf(name) === i);
659
+ }
660
+
661
+ //nvdebug(` S: ${fieldToString(normalizedField)}`, debugDev);
662
+ // Then find (the index of) the first mathing candidate field and return it.
663
+ const index = getCounterpartIndex(normalizedField, counterpartCands, uniqueAlternativeNames, config);
664
+ if (index > -1) {
665
+ return counterpartCands[index];
666
+ }
667
+
668
+ // MET-456 exception
669
+ if (counterpartCands.length === 1 && field264Exception(counterpartCands[0], sourceRecord, field, config)) {
670
+ return counterpartCands[0];
671
+ }
672
+ return null;
673
+ }
674
+ //# sourceMappingURL=counterpartField.js.map